From 2a7c306616970037209d72052baf47dc567ceef7 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 16 Mar 2025 22:46:47 +0800 Subject: [PATCH 01/29] feat: added weekly updater --- src/hasura/share.ts | 71 +++++++++ src/helpers/uuid.ts | 104 ++++++++++++ src/routes/weekly.ts | 366 ++++++++++++++++++++++++++++++++----------- 3 files changed, 450 insertions(+), 91 deletions(-) create mode 100644 src/helpers/uuid.ts diff --git a/src/hasura/share.ts b/src/hasura/share.ts index e6519ed2..71a9a549 100644 --- a/src/hasura/share.ts +++ b/src/hasura/share.ts @@ -1,6 +1,14 @@ import { gql } from "graphql-request"; import { client } from ".."; +import exp from "constants"; +type WeeklyPost = { + id: number; + title: string; + url: string; + date: Date; + } +export { WeeklyPost }; /** ============================================================================ ============================ QUERY FUNCTIONS =============================== @@ -555,3 +563,66 @@ export const delete_course_comment_likes = async(comment_uuid: string, user_uuid ); return delete_course_comment_likes_query?.delete_course_comment_likes_by_pk?.comment_uuid; } + +export const get_newest_weekly = async(): Promise => { + const get_newest_weekly_query: any = await client.request( + gql` + query MyQuery { + weekly_aggregate { + aggregate { + max { + date + } + } + } + } +` + ); + const date = get_newest_weekly_query?.weekly_aggregate?.aggregate?.max?.date + "T00:00:00.000+08:00"; + return new Date(date); +} +/** + * + * mutation MyMutation($date: date = "", $title: String = "", $url: String = "") { + insert_weekly(objects: {date: $date, title: $title, url: $url}) { + returning { + id + } + } +} + + */ +export const add_weekly_list = async(weekly_list: WeeklyPost[]): Promise => { + const add_weekly_list_query: any = await client.request( + gql` + mutation MyMutation($objects: [weekly_insert_input!]!) { + insert_weekly(objects: $objects) { + returning { + id + } + } + } +`, + { + objects: weekly_list + } + ); + return add_weekly_list_query?.insert_weekly?.returning; +} + +export const get_newest_weekly_id = async(): Promise => { + const get_newest_weekly_id_query: any = await client.request( + gql` + query MyQuery { + weekly_aggregate { + aggregate { + max { + id + } + } + } + } +` + ); + return get_newest_weekly_id_query?.weekly_aggregate?.aggregate?.max?.id; +} diff --git a/src/helpers/uuid.ts b/src/helpers/uuid.ts new file mode 100644 index 00000000..8d416438 --- /dev/null +++ b/src/helpers/uuid.ts @@ -0,0 +1,104 @@ +enum UUIDFormat { + CookieBase90, + FlickrBase58, + UUID25Base36 + } + + const constants = { + [UUIDFormat.CookieBase90]: + "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()*+-./:<=>?@[]^_`{|}~", + [UUIDFormat.FlickrBase58]: '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ', + [UUIDFormat.UUID25Base36]: '0123456789abcdefghijklmnopqrstuvwxyz' + } + + /** + * Calculate length for the shortened ID + * @param {number} alphabetLength + * @returns {number} + */ + const getShortIdLength = (alphabetLength: number): number => + Math.ceil(Math.log2(Math.pow(2, 128)) / Math.log2(alphabetLength)) + + /** + * Convert a hex string to a custom base string + * @param {string} hex + * @param {string} alphabet + * @returns {string} + */ + const hexToCustomBase = (hex: string, alphabet: string): string => { + const base = alphabet.length + let num = BigInt(`0x${hex}`) + let encoded = '' + + while (num > 0) { + encoded = alphabet[Number(num % BigInt(base))] + encoded + num = num / BigInt(base) + } + + return encoded + } + + interface PaddingParams { + shortIdLength: number + consistentLength: boolean + paddingChar: string + } + + /** + * Takes a UUID, strips the dashes, and translates to custom base + * @param {string} longId + * @param {string} alphabet + * @param {PaddingParams} [paddingParams] + * @returns {string} + */ + const shortenUUID = (longId: string, alphabet: string, paddingParams?: PaddingParams): string => { + const hex = longId.replace(/-/g, '') + const translated = hexToCustomBase(hex, alphabet) + + if (!paddingParams || !paddingParams.consistentLength) return translated + + return translated.padStart(paddingParams.shortIdLength, paddingParams.paddingChar) + } + + /** + * Generate a standard UUID + * @returns {string} + */ + const generateUUID = (): string => { + const hexDigits = '0123456789abcdef' + const s: string[] = Array(36).fill('') + + for (let i = 0; i < 36; i++) { + s[i] = hexDigits.charAt(Math.floor(Math.random() * 0x10)) + } + s[14] = '4' + s[19] = hexDigits.charAt((parseInt(s[19], 16) & 0x3) | 0x8) + s[8] = s[13] = s[18] = s[23] = '-' + + return s.join('') + } + + /** + * Generate a UUID in either standard or short format based on the provided format + * @param {UUIDFormat} [format] - Enum to specify the desired format + * @returns {string} + */ + const uuid = (format?: UUIDFormat): string => { + const standardUUID = generateUUID() + if (format === undefined) { + return standardUUID + } + + const useAlphabet = constants[format] + const shortIdLength = getShortIdLength(useAlphabet.length) + + const paddingParams: PaddingParams = { + shortIdLength, + consistentLength: true, + paddingChar: useAlphabet[0] + } + + return shortenUUID(standardUUID, useAlphabet, paddingParams) + } + + export { uuid, UUIDFormat } \ No newline at end of file diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 4c2e631c..721fc05e 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -1,81 +1,265 @@ import express from "express"; import { gql } from "graphql-request"; import { client } from ".."; - +import axios from "axios"; +import * as utils from "../helpers/utils"; +import * as fs from "fs/promises"; +import * as uuid from "../helpers/uuid"; +import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" +import { finished } from "stream"; const router = express.Router(); +const weixinSpider = async (headers: any, params: any, filename: string) => { + const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; + let fcontrol = 0; + try { + console.log("Spider Start") + const new_weekly_list: any[] = []; + let i: number = 0; + outerloop: + while (true) { + params["begin"] = (i * 5).toString(); + i++; + await new Promise(resolve => setTimeout(resolve, Math.random() * 9000 + 1000)); // 等待 1 到 10 秒之间的随机时间 + const response = await axios.get(url, { + headers, + params, + httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }) + }); + const data = response.data; + if (data.base_resp.ret === 200013) { + console.log(`Frequency control, stop at ${params["begin"]}`); + fcontrol = 1; + break; + } + if (!data.app_msg_list || data.app_msg_list.length === 0) { + console.log('All article parsed'); + break; + } + const newest_weekly_date = await get_newest_weekly(); + const newest_weekly_id = await get_newest_weekly_id(); + for (const item of data.app_msg_list) { + if (new Date(item.create_time * 1000) < newest_weekly_date) break outerloop; + if (item.title.includes("SAST Weekly")) { + let new_item: WeeklyPost = { + title: item.title, + url: item.link, + date: new Date(item.create_time * 1000), + id: newest_weekly_id + new_weekly_list.length + 1 + } + new_weekly_list.push(new_item); + } + } + } + //sort new_weekly_list by date + new_weekly_list.sort((a, b) => { + return a.date.getTime() - b.date.getTime(); + }); + if (new_weekly_list.length > 0) { + await add_weekly_list(new_weekly_list); + } + //using uuid as the files name + const base_directory = await utils.get_base_directory(); + if (!await utils.checkPathExists(`${base_directory}/weixinSpiderStatus`)) { + await fs.mkdir(`${base_directory}/weixinSpiderStatus`); + } + await fs.writeFile(`${base_directory}/weixinSpiderStatus/${filename}`, ""); + console.log("Spider finished"); + } + catch (error) { + console.error('Error fetching articles:', error); + } +} + +router.post("/check", async (req, res) => { + try { + const filename: string = req.body.filename; + const base_directory = await utils.get_base_directory(); + if (await utils.checkPathExists(`${base_directory}/weixinSpiderStatus/${filename}`)) { + return res.status(200).json({ finished: true }); + } else { + return res.status(200).json({ finished: false }); + } + } + catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } +}) + +router.post("/renew", async (req, res) => { + + try { + // 设置 headers + const headers = { + "Cookie": req.body.cookie, + "User-Agent": req.body.useragent + }; + const params = { + "token": req.body.token, + "lang": "zh_CN", + "f": "json", + "ajax": "1", + "action": "list_ex", + "begin": "1", + "count": "5", + "query": "", + "fakeid": "MzA5MjA5NjIxNg%3D%3D", + "type": "9", + } + const filename = uuid.uuid(); + weixinSpider(headers, params, filename); + return res.status(200).json({ "filename": filename }); + } catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } + //// 爬虫初始参数 + //const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; + //let begin = "0"; + //const params = { + // "token": req.body.token, + // "lang": "zh_CN", + // "f": "json", + // "ajax": "1", + // "action": "list_ex", + // "begin": "1", + // "count": "5", + // "query": "", + // "fakeid": "MzA5MjA5NjIxNg%3D%3D", + // "type": "9", + //}; + //// 用于频率控制 + //let fcontrol = 0; + //let j = 0; + // try { + // console.log("开始爬虫") + + // const app_msg_list: any[] = []; + // let i = Math.floor(app_msg_list.length / 5); + + // while (true) { + // params["begin"] = (i * 5).toString(); + // await new Promise(resolve => setTimeout(resolve, Math.random() * 9000 + 1000)); // 等待 1 到 10 秒之间的随机时间 + // try { + // const response = await axios.get(url, { + // headers, + // params, + // httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }) + // }); + // const data = response.data; + + // if (data.base_resp.ret === 200013) { + // console.log(`Frequency control, stop at ${params["begin"]}`); + // fcontrol = 1; + // break; + // } + + // if (!data.app_msg_list || data.app_msg_list.length === 0) { + // console.log('All article parsed'); + // break; + // } + + // app_msg_list.push(...data.app_msg_list); + + // const result = data.app_msg_list.map((item: any) => ({ + // title: item.title, + // link: item.link + // })); + // //todo: 获取数据库最新一条的id + // for (const { title, link } of result) { + // if (title.includes("SAST Weekly")) { + // //todo:插入数据库 + // } + // } + // j++; + // } catch (error) { + // console.error('Error fetching articles:', error); + // } + // } + + // if (!fcontrol) { + // return res.status(300).send("frequency control"); + // } else { + // return res.status(200).send("ok"); + // } + // } catch (error) { + // return res.status(500).send("error"); + // } + //} catch (err) { + // return res.status(500).send("500 Internal Server Error: " + err); + //} +}) router.get("/cover", async (req, res) => { - try { - if (!req.query.url) return res.status(400).send("400 Bad Request: no url provided!"); - const url: any = req.query.url; - const response = await fetch( - url, - { method: "GET"} - ); - if (response.ok) { - const text: string = await response.text(); - const match = text.match(/var msg_cdn_url = "(.*?)";/); - if (match && match[1]) - res.status(200).send(match[1]); - else throw(Error("capture failed!")); - } - else return res.status(500).send("500 Internal Server Error: fetch failed!"); - } catch (err) { - return res.status(500).send("500 Internal Server Error: " + err); + try { + if (!req.query.url) return res.status(400).send("400 Bad Request: no url provided!"); + const url: any = req.query.url; + const response = await fetch( + url, + { method: "GET" } + ); + if (response.ok) { + const text: string = await response.text(); + const match = text.match(/var msg_cdn_url = "(.*?)";/); + if (match && match[1]) + res.status(200).send(match[1]); + else throw (Error("capture failed!")); } + else return res.status(500).send("500 Internal Server Error: fetch failed!"); + } catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } }) const getTitle = async (url: string) => { - try { - const response = await fetch( - url, - { method: "GET"} - ); - if (response.ok) { - const text: string = await response.text(); - const match = text.match(/meta property="og:title" content=".*"/); - if (match == null) throw(Error("capture failed!")); - const title = match[0].slice(34, -1); - return title; - } - else throw(Error("fetch failed!")); - } catch (err: any) { - return err; + try { + const response = await fetch( + url, + { method: "GET" } + ); + if (response.ok) { + const text: string = await response.text(); + const match = text.match(/meta property="og:title" content=".*"/); + if (match == null) throw (Error("capture failed!")); + const title = match[0].slice(34, -1); + return title; } + else throw (Error("fetch failed!")); + } catch (err: any) { + return err; + } } router.post("/insert", async (req, res) => { - try { - if (!req.body.id || !req.body.url) return res.status(400).send("400 Bad Request: not enough params!"); - const title: string = await getTitle(req.body.url); - const QueryGreaterIds: any = await client.request( - gql` + try { + if (!req.body.id || !req.body.url) return res.status(400).send("400 Bad Request: not enough params!"); + const title: string = await getTitle(req.body.url); + const QueryGreaterIds: any = await client.request( + gql` query QueryGreaterIds($_id: Int) { weekly(where: {id: {_gt: $_id}}) { id } } `, - { _id: req.body.id } - ); - const sorted_ids = [...QueryGreaterIds.weekly]; - sorted_ids.sort((a: any, b: any) => { - return a.id - b.id; - }) - for (let i = sorted_ids.length - 1; i >= 0; i--) { - await client.request( - gql` + { _id: req.body.id } + ); + const sorted_ids = [...QueryGreaterIds.weekly]; + sorted_ids.sort((a: any, b: any) => { + return a.id - b.id; + }) + for (let i = sorted_ids.length - 1; i >= 0; i--) { + await client.request( + gql` mutation IncreaseIds($_id: Int) { update_weekly(where: {id: {_eq: $_id}}, _inc: {id: 1}) { affected_rows } } `, - { _id: sorted_ids[i].id } - ); - } - await client.request( - gql` + { _id: sorted_ids[i].id } + ); + } + await client.request( + gql` mutation Insert_Weekly_One($id: Int, $title: String, $url: String) { insert_weekly_one(object: {id: $id, title: $title, url: $url}) { id @@ -84,76 +268,76 @@ router.post("/insert", async (req, res) => { } } `, - { id: req.body.id + 1, title: title, url: req.body.url } - ); - return res.status(200).send("ok"); - } catch (err) { - return res.status(500).send("500 Internal Server Error: " + err); - } + { id: req.body.id + 1, title: title, url: req.body.url } + ); + return res.status(200).send("ok"); + } catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } }) router.post("/delete", async (req, res) => { - try { - if (!req.body.id) return res.status(400).send("400 Bad Request: not enough params!"); - const QueryGreaterIds: any = await client.request( - gql` + try { + if (!req.body.id) return res.status(400).send("400 Bad Request: not enough params!"); + const QueryGreaterIds: any = await client.request( + gql` query QueryGreaterIds($_id: Int) { weekly(where: {id: {_gt: $_id}}) { id } } `, - { _id: req.body.id } - ); - const sorted_ids = [...QueryGreaterIds.weekly]; - sorted_ids.sort((a: any, b: any) => { - return a.id - b.id; - }) - await client.request( - gql` + { _id: req.body.id } + ); + const sorted_ids = [...QueryGreaterIds.weekly]; + sorted_ids.sort((a: any, b: any) => { + return a.id - b.id; + }) + await client.request( + gql` mutation Delete_Weekly_One($_id: Int) { delete_weekly(where: {id: {_eq: $_id}}) { affected_rows } } `, - { _id: req.body.id } - ); - for (let i = 0; i < sorted_ids.length; i++) { - await client.request( - gql` + { _id: req.body.id } + ); + for (let i = 0; i < sorted_ids.length; i++) { + await client.request( + gql` mutation IncreaseIds($_id: Int) { update_weekly(where: {id: {_eq: $_id}}, _inc: {id: -1}) { affected_rows } } `, - { _id: sorted_ids[i].id } - ); - } - return res.status(200).send("ok"); - } catch (err) { - return res.status(500).send("500 Internal Server Error: " + err); + { _id: sorted_ids[i].id } + ); } + return res.status(200).send("ok"); + } catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } }) router.post("/init", async (req, res) => { - try { - if (!req.body.data) return res.status(400).send("400 Bad Request: no data provided!"); - await client.request( - gql` + try { + if (!req.body.data) return res.status(400).send("400 Bad Request: no data provided!"); + await client.request( + gql` mutation Init($objects: [weekly_insert_input!] = {}) { insert_weekly(objects: $objects) { affected_rows } } `, - { objects: req.body.data } - ); - return res.status(200).send("ok"); - } catch (err) { - return res.status(500).send("500 Internal Server Error: " + err); - } + { objects: req.body.data } + ); + return res.status(200).send("ok"); + } catch (err) { + return res.status(500).send("500 Internal Server Error: " + err); + } }) export default router; From 7a00b41d7a2fd919a448054fac99c2f01f9a2a0c Mon Sep 17 00:00:00 2001 From: konpoku Date: Mon, 24 Mar 2025 19:39:39 +0800 Subject: [PATCH 02/29] Implemented renew weekly failure return --- src/routes/weekly.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 721fc05e..1e026a96 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -6,11 +6,14 @@ import * as utils from "../helpers/utils"; import * as fs from "fs/promises"; import * as uuid from "../helpers/uuid"; import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" +import authenticate from "../middlewares/authenticate"; import { finished } from "stream"; +import { FAILSAFE_SCHEMA } from "js-yaml"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; let fcontrol = 0; + const base_directory = await utils.get_base_directory(); try { console.log("Spider Start") const new_weekly_list: any[] = []; @@ -58,7 +61,6 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { await add_weekly_list(new_weekly_list); } //using uuid as the files name - const base_directory = await utils.get_base_directory(); if (!await utils.checkPathExists(`${base_directory}/weixinSpiderStatus`)) { await fs.mkdir(`${base_directory}/weixinSpiderStatus`); } @@ -67,17 +69,20 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { } catch (error) { console.error('Error fetching articles:', error); + await fs.writeFile(`${base_directory}/weixinSpiderStatus/${filename}-failed`, ""); } } -router.post("/check", async (req, res) => { +router.post("/check", authenticate(["admin","counselor"]), async (req, res) => { try { const filename: string = req.body.filename; const base_directory = await utils.get_base_directory(); if (await utils.checkPathExists(`${base_directory}/weixinSpiderStatus/${filename}`)) { - return res.status(200).json({ finished: true }); + return res.status(200).json({ finished: true , failed: false}); + } else if (await utils.checkPathExists(`${base_directory}/weixinSpiderStatus/${filename}-failed`)) { + return res.status(200).json({ finished: false , failed: true}); } else { - return res.status(200).json({ finished: false }); + return res.status(200).json({ finished: false , failed: false}); } } catch (err) { @@ -85,7 +90,7 @@ router.post("/check", async (req, res) => { } }) -router.post("/renew", async (req, res) => { +router.post("/renew", authenticate(["admin","counselor"]), async (req, res) => { try { // 设置 headers From ab84bf853f13c48ad519babef136294b3cf71c53 Mon Sep 17 00:00:00 2001 From: konpoku Date: Mon, 24 Mar 2025 19:51:49 +0800 Subject: [PATCH 03/29] modified for lint check --- src/hasura/share.ts | 1 - src/routes/weekly.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/hasura/share.ts b/src/hasura/share.ts index 71a9a549..41584173 100644 --- a/src/hasura/share.ts +++ b/src/hasura/share.ts @@ -1,6 +1,5 @@ import { gql } from "graphql-request"; import { client } from ".."; -import exp from "constants"; type WeeklyPost = { id: number; diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 1e026a96..dcc76d28 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -7,8 +7,6 @@ import * as fs from "fs/promises"; import * as uuid from "../helpers/uuid"; import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" import authenticate from "../middlewares/authenticate"; -import { finished } from "stream"; -import { FAILSAFE_SCHEMA } from "js-yaml"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; @@ -18,8 +16,9 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { console.log("Spider Start") const new_weekly_list: any[] = []; let i: number = 0; + let failed: boolean = false; outerloop: - while (true) { + while (!failed) { params["begin"] = (i * 5).toString(); i++; await new Promise(resolve => setTimeout(resolve, Math.random() * 9000 + 1000)); // 等待 1 到 10 秒之间的随机时间 @@ -31,6 +30,7 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { const data = response.data; if (data.base_resp.ret === 200013) { console.log(`Frequency control, stop at ${params["begin"]}`); + failed = true; fcontrol = 1; break; } From 1fae2581be470279df919824db4dd63201241902 Mon Sep 17 00:00:00 2001 From: konpoku Date: Mon, 24 Mar 2025 19:52:12 +0800 Subject: [PATCH 04/29] modified for lint check --- src/routes/weekly.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index dcc76d28..79cd1cb0 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -7,31 +7,30 @@ import * as fs from "fs/promises"; import * as uuid from "../helpers/uuid"; import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" import authenticate from "../middlewares/authenticate"; +import { Agent } from "https"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; - let fcontrol = 0; + let fcontrol :boolean = false; const base_directory = await utils.get_base_directory(); try { console.log("Spider Start") const new_weekly_list: any[] = []; let i: number = 0; - let failed: boolean = false; outerloop: - while (!failed) { + while (!fcontrol) { params["begin"] = (i * 5).toString(); i++; await new Promise(resolve => setTimeout(resolve, Math.random() * 9000 + 1000)); // 等待 1 到 10 秒之间的随机时间 const response = await axios.get(url, { headers, params, - httpsAgent: new (require('https').Agent)({ rejectUnauthorized: false }) + httpsAgent: new Agent({ rejectUnauthorized: false }) }); const data = response.data; if (data.base_resp.ret === 200013) { console.log(`Frequency control, stop at ${params["begin"]}`); - failed = true; - fcontrol = 1; + fcontrol = true; break; } if (!data.app_msg_list || data.app_msg_list.length === 0) { @@ -43,7 +42,7 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { for (const item of data.app_msg_list) { if (new Date(item.create_time * 1000) < newest_weekly_date) break outerloop; if (item.title.includes("SAST Weekly")) { - let new_item: WeeklyPost = { + const new_item: WeeklyPost = { title: item.title, url: item.link, date: new Date(item.create_time * 1000), From 23a88d1f23dcdaa91e94b32d1adca90d007c66ee Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 25 Mar 2025 13:26:10 +0800 Subject: [PATCH 05/29] Removed admin --- src/routes/weekly.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 79cd1cb0..6dd3fafb 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -72,7 +72,7 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { } } -router.post("/check", authenticate(["admin","counselor"]), async (req, res) => { +router.post("/check", authenticate(["counselor"]), async (req, res) => { try { const filename: string = req.body.filename; const base_directory = await utils.get_base_directory(); @@ -89,7 +89,7 @@ router.post("/check", authenticate(["admin","counselor"]), async (req, res) => { } }) -router.post("/renew", authenticate(["admin","counselor"]), async (req, res) => { +router.post("/renew", authenticate(["counselor"]), async (req, res) => { try { // 设置 headers From d8dfbcae8a445ef1fd5743177711b6acb43bb890 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 20 Apr 2025 21:53:33 +0800 Subject: [PATCH 06/29] temporary commit --- src/routes/weekly.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 6dd3fafb..67a5ce3f 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -200,6 +200,7 @@ router.get("/cover", async (req, res) => { url, { method: "GET" } ); + console.log(response); if (response.ok) { const text: string = await response.text(); const match = text.match(/var msg_cdn_url = "(.*?)";/); From 39af8fc0cb1694651aa390b75d9794a2937c7844 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 20 Apr 2025 22:15:07 +0800 Subject: [PATCH 07/29] Fixed: weekly cover error --- src/routes/weekly.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 67a5ce3f..6ef51d70 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -8,6 +8,7 @@ import * as uuid from "../helpers/uuid"; import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" import authenticate from "../middlewares/authenticate"; import { Agent } from "https"; +import { Headers } from "cos-nodejs-sdk-v5"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; @@ -191,14 +192,26 @@ router.post("/renew", authenticate(["counselor"]), async (req, res) => { // return res.status(500).send("500 Internal Server Error: " + err); //} }) - router.get("/cover", async (req, res) => { try { if (!req.query.url) return res.status(400).send("400 Bad Request: no url provided!"); const url: any = req.query.url; + const useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; + const headers : HeadersInit = [ + ["User-Agent", useragent], + ["Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], + ["Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4"], + ["Accept-Encoding", "gzip, deflate, br"], + ["Connection", "keep-alive"], + ["Upgrade-Insecure-Requests", "1"], + ["Cache-Control", "max-age=0"] + ] const response = await fetch( url, - { method: "GET" } + { + method: "GET", + headers: headers, + }, ); console.log(response); if (response.ok) { From 71f0ee7c30642ad9c4480944350e677f472d0f23 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 20 Apr 2025 22:41:11 +0800 Subject: [PATCH 08/29] modified to pass the lint test --- src/routes/weekly.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 6ef51d70..2d456415 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -8,7 +8,6 @@ import * as uuid from "../helpers/uuid"; import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" import authenticate from "../middlewares/authenticate"; import { Agent } from "https"; -import { Headers } from "cos-nodejs-sdk-v5"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; From 8f961009971c65d673c3a613f5895b3a6ae2af96 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sat, 26 Apr 2025 21:57:46 +0800 Subject: [PATCH 09/29] fix: fixed weekly update and permission check --- src/hasura/share.ts | 25 ++++++++++++++++--------- src/routes/weekly.ts | 13 ++++++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/hasura/share.ts b/src/hasura/share.ts index 41584173..6fce4515 100644 --- a/src/hasura/share.ts +++ b/src/hasura/share.ts @@ -580,17 +580,24 @@ export const get_newest_weekly = async(): Promise => { const date = get_newest_weekly_query?.weekly_aggregate?.aggregate?.max?.date + "T00:00:00.000+08:00"; return new Date(date); } -/** - * - * mutation MyMutation($date: date = "", $title: String = "", $url: String = "") { - insert_weekly(objects: {date: $date, title: $title, url: $url}) { - returning { - id - } - } + +export const check_weekly_exist = async(date: Date): Promise => { + const check_weekly_exist_query: any = await client.request( + gql` + query MyQuery($targetDate: date!) { + weekly(where: {date: {_eq: $targetDate}}) { + date + } + } + ` + , + { + targetDate: date + } + ); + return check_weekly_exist_query?.weekly?.length > 0; } - */ export const add_weekly_list = async(weekly_list: WeeklyPost[]): Promise => { const add_weekly_list_query: any = await client.request( gql` diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 2d456415..9282c788 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -5,7 +5,7 @@ import axios from "axios"; import * as utils from "../helpers/utils"; import * as fs from "fs/promises"; import * as uuid from "../helpers/uuid"; -import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost } from "../hasura/share" +import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost, check_weekly_exist } from "../hasura/share" import authenticate from "../middlewares/authenticate"; import { Agent } from "https"; const router = express.Router(); @@ -42,6 +42,10 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { for (const item of data.app_msg_list) { if (new Date(item.create_time * 1000) < newest_weekly_date) break outerloop; if (item.title.includes("SAST Weekly")) { + const exist : boolean = await check_weekly_exist(new Date(item.create_time * 1000)); + if (exist) { + continue; + } const new_item: WeeklyPost = { title: item.title, url: item.link, @@ -52,7 +56,6 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { } } } - //sort new_weekly_list by date new_weekly_list.sort((a, b) => { return a.date.getTime() - b.date.getTime(); }); @@ -245,7 +248,7 @@ const getTitle = async (url: string) => { } } -router.post("/insert", async (req, res) => { +router.post("/insert",authenticate(["counselor"]), async (req, res) => { try { if (!req.body.id || !req.body.url) return res.status(400).send("400 Bad Request: not enough params!"); const title: string = await getTitle(req.body.url); @@ -293,7 +296,7 @@ router.post("/insert", async (req, res) => { } }) -router.post("/delete", async (req, res) => { +router.post("/delete",authenticate(["counselor"]), async (req, res) => { try { if (!req.body.id) return res.status(400).send("400 Bad Request: not enough params!"); const QueryGreaterIds: any = await client.request( @@ -338,7 +341,7 @@ router.post("/delete", async (req, res) => { } }) -router.post("/init", async (req, res) => { +router.post("/init",authenticate(["counselor"]), async (req, res) => { try { if (!req.body.data) return res.status(400).send("400 Bad Request: no data provided!"); await client.request( From a5d4b2257e703d4b7ac8d01ac3991a31801f9c90 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 11 May 2025 22:53:11 +0800 Subject: [PATCH 10/29] feat: added log for weekly update --- src/routes/weekly.ts | 307 +++++++++++++++++++++++-------------------- 1 file changed, 166 insertions(+), 141 deletions(-) diff --git a/src/routes/weekly.ts b/src/routes/weekly.ts index 9282c788..ddfd8056 100644 --- a/src/routes/weekly.ts +++ b/src/routes/weekly.ts @@ -5,27 +5,36 @@ import axios from "axios"; import * as utils from "../helpers/utils"; import * as fs from "fs/promises"; import * as uuid from "../helpers/uuid"; -import { get_newest_weekly, get_newest_weekly_id, add_weekly_list, WeeklyPost, check_weekly_exist } from "../hasura/share" +import { + get_newest_weekly, + get_newest_weekly_id, + add_weekly_list, + WeeklyPost, + check_weekly_exist, +} from "../hasura/share"; import authenticate from "../middlewares/authenticate"; import { Agent } from "https"; const router = express.Router(); const weixinSpider = async (headers: any, params: any, filename: string) => { const url = "https://mp.weixin.qq.com/cgi-bin/appmsg"; - let fcontrol :boolean = false; + let fcontrol: boolean = false; const base_directory = await utils.get_base_directory(); try { - console.log("Spider Start") + console.log("Spider Start"); const new_weekly_list: any[] = []; let i: number = 0; - outerloop: - while (!fcontrol) { + const newest_weekly_date = await get_newest_weekly(); + let newest_weekly_id = await get_newest_weekly_id(); + outerloop: while (!fcontrol) { params["begin"] = (i * 5).toString(); i++; - await new Promise(resolve => setTimeout(resolve, Math.random() * 9000 + 1000)); // 等待 1 到 10 秒之间的随机时间 + await new Promise((resolve) => + setTimeout(resolve, Math.random() * 9000 + 1000), + ); // 等待 1 到 10 秒之间的随机时间 const response = await axios.get(url, { headers, params, - httpsAgent: new Agent({ rejectUnauthorized: false }) + httpsAgent: new Agent({ rejectUnauthorized: false }), }); const data = response.data; if (data.base_resp.ret === 200013) { @@ -34,15 +43,23 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { break; } if (!data.app_msg_list || data.app_msg_list.length === 0) { - console.log('All article parsed'); + console.log("All article parsed"); break; } - const newest_weekly_date = await get_newest_weekly(); - const newest_weekly_id = await get_newest_weekly_id(); + console.log( + "Fetched " + + data.app_msg_list.length + + " articles after " + + new Date(data.app_msg_list[0].create_time * 1000).toLocaleString(), + ); for (const item of data.app_msg_list) { - if (new Date(item.create_time * 1000) < newest_weekly_date) break outerloop; + if (new Date(item.create_time * 1000) <= newest_weekly_date) + break outerloop; if (item.title.includes("SAST Weekly")) { - const exist : boolean = await check_weekly_exist(new Date(item.create_time * 1000)); + const exist: boolean = await check_weekly_exist( + new Date(item.create_time * 1000), + ); + if (exist) { continue; } @@ -50,71 +67,80 @@ const weixinSpider = async (headers: any, params: any, filename: string) => { title: item.title, url: item.link, date: new Date(item.create_time * 1000), - id: newest_weekly_id + new_weekly_list.length + 1 - } + id: newest_weekly_id + 1, + }; new_weekly_list.push(new_item); + if (new_weekly_list.length > 0) { + await add_weekly_list(new_weekly_list); + new_weekly_list.length = 0; + newest_weekly_id++; + } + console.log("Fetched: " + item.title); } } } - new_weekly_list.sort((a, b) => { - return a.date.getTime() - b.date.getTime(); - }); - if (new_weekly_list.length > 0) { - await add_weekly_list(new_weekly_list); - } - //using uuid as the files name - if (!await utils.checkPathExists(`${base_directory}/weixinSpiderStatus`)) { + if ( + !(await utils.checkPathExists(`${base_directory}/weixinSpiderStatus`)) + ) { await fs.mkdir(`${base_directory}/weixinSpiderStatus`); } await fs.writeFile(`${base_directory}/weixinSpiderStatus/${filename}`, ""); console.log("Spider finished"); + } catch (error) { + console.error("Error fetching articles:", error); + await fs.writeFile( + `${base_directory}/weixinSpiderStatus/${filename}-failed`, + "", + ); } - catch (error) { - console.error('Error fetching articles:', error); - await fs.writeFile(`${base_directory}/weixinSpiderStatus/${filename}-failed`, ""); - } -} +}; router.post("/check", authenticate(["counselor"]), async (req, res) => { try { const filename: string = req.body.filename; const base_directory = await utils.get_base_directory(); - if (await utils.checkPathExists(`${base_directory}/weixinSpiderStatus/${filename}`)) { - return res.status(200).json({ finished: true , failed: false}); - } else if (await utils.checkPathExists(`${base_directory}/weixinSpiderStatus/${filename}-failed`)) { - return res.status(200).json({ finished: false , failed: true}); + if ( + await utils.checkPathExists( + `${base_directory}/weixinSpiderStatus/${filename}`, + ) + ) { + return res.status(200).json({ finished: true, failed: false }); + } else if ( + await utils.checkPathExists( + `${base_directory}/weixinSpiderStatus/${filename}-failed`, + ) + ) { + return res.status(200).json({ finished: false, failed: true }); } else { - return res.status(200).json({ finished: false , failed: false}); + return res.status(200).json({ finished: false, failed: false }); } - } - catch (err) { + } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } -}) +}); router.post("/renew", authenticate(["counselor"]), async (req, res) => { - try { // 设置 headers const headers = { - "Cookie": req.body.cookie, - "User-Agent": req.body.useragent + Cookie: req.body.cookie, + "User-Agent": req.body.useragent, }; const params = { - "token": req.body.token, - "lang": "zh_CN", - "f": "json", - "ajax": "1", - "action": "list_ex", - "begin": "1", - "count": "5", - "query": "", - "fakeid": "MzA5MjA5NjIxNg%3D%3D", - "type": "9", - } + token: req.body.token, + lang: "zh_CN", + f: "json", + ajax: "1", + action: "list_ex", + begin: "1", + count: "5", + query: "", + fakeid: "MzA5MjA5NjIxNg%3D%3D", + type: "9", + }; const filename = uuid.uuid(); weixinSpider(headers, params, filename); - return res.status(200).json({ "filename": filename }); + return res.status(200).json({ filename: filename }); } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } @@ -193,172 +219,171 @@ router.post("/renew", authenticate(["counselor"]), async (req, res) => { //} catch (err) { // return res.status(500).send("500 Internal Server Error: " + err); //} -}) +}); router.get("/cover", async (req, res) => { try { - if (!req.query.url) return res.status(400).send("400 Bad Request: no url provided!"); + if (!req.query.url) + return res.status(400).send("400 Bad Request: no url provided!"); const url: any = req.query.url; - const useragent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; - const headers : HeadersInit = [ + const useragent = + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3"; + const headers: HeadersInit = [ ["User-Agent", useragent], - ["Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], + [ + "Accept", + "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + ], ["Accept-Language", "zh-CN,zh;q=0.8,en-US;q=0.6,en;q=0.4"], ["Accept-Encoding", "gzip, deflate, br"], ["Connection", "keep-alive"], ["Upgrade-Insecure-Requests", "1"], - ["Cache-Control", "max-age=0"] - ] - const response = await fetch( - url, - { - method: "GET", - headers: headers, - }, - ); - console.log(response); + ["Cache-Control", "max-age=0"], + ]; + const response = await fetch(url, { + method: "GET", + headers: headers, + }); if (response.ok) { const text: string = await response.text(); const match = text.match(/var msg_cdn_url = "(.*?)";/); - if (match && match[1]) - res.status(200).send(match[1]); - else throw (Error("capture failed!")); - } - else return res.status(500).send("500 Internal Server Error: fetch failed!"); + if (match && match[1]) res.status(200).send(match[1]); + else throw Error("capture failed!"); + } else + return res.status(500).send("500 Internal Server Error: fetch failed!"); } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } -}) +}); const getTitle = async (url: string) => { try { - const response = await fetch( - url, - { method: "GET" } - ); + const response = await fetch(url, { method: "GET" }); if (response.ok) { const text: string = await response.text(); const match = text.match(/meta property="og:title" content=".*"/); - if (match == null) throw (Error("capture failed!")); + if (match == null) throw Error("capture failed!"); const title = match[0].slice(34, -1); return title; - } - else throw (Error("fetch failed!")); + } else throw Error("fetch failed!"); } catch (err: any) { return err; } -} +}; -router.post("/insert",authenticate(["counselor"]), async (req, res) => { +router.post("/insert", authenticate(["counselor"]), async (req, res) => { try { - if (!req.body.id || !req.body.url) return res.status(400).send("400 Bad Request: not enough params!"); + if (!req.body.id || !req.body.url) + return res.status(400).send("400 Bad Request: not enough params!"); const title: string = await getTitle(req.body.url); const QueryGreaterIds: any = await client.request( gql` - query QueryGreaterIds($_id: Int) { - weekly(where: {id: {_gt: $_id}}) { - id - } - } - `, - { _id: req.body.id } + query QueryGreaterIds($_id: Int) { + weekly(where: { id: { _gt: $_id } }) { + id + } + } + `, + { _id: req.body.id }, ); const sorted_ids = [...QueryGreaterIds.weekly]; sorted_ids.sort((a: any, b: any) => { return a.id - b.id; - }) + }); for (let i = sorted_ids.length - 1; i >= 0; i--) { await client.request( gql` - mutation IncreaseIds($_id: Int) { - update_weekly(where: {id: {_eq: $_id}}, _inc: {id: 1}) { - affected_rows - } - } - `, - { _id: sorted_ids[i].id } + mutation IncreaseIds($_id: Int) { + update_weekly(where: { id: { _eq: $_id } }, _inc: { id: 1 }) { + affected_rows + } + } + `, + { _id: sorted_ids[i].id }, ); } await client.request( gql` - mutation Insert_Weekly_One($id: Int, $title: String, $url: String) { - insert_weekly_one(object: {id: $id, title: $title, url: $url}) { - id - title - url - } - } - `, - { id: req.body.id + 1, title: title, url: req.body.url } + mutation Insert_Weekly_One($id: Int, $title: String, $url: String) { + insert_weekly_one(object: { id: $id, title: $title, url: $url }) { + id + title + url + } + } + `, + { id: req.body.id + 1, title: title, url: req.body.url }, ); return res.status(200).send("ok"); } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } -}) +}); -router.post("/delete",authenticate(["counselor"]), async (req, res) => { +router.post("/delete", authenticate(["counselor"]), async (req, res) => { try { - if (!req.body.id) return res.status(400).send("400 Bad Request: not enough params!"); + if (!req.body.id) + return res.status(400).send("400 Bad Request: not enough params!"); const QueryGreaterIds: any = await client.request( gql` - query QueryGreaterIds($_id: Int) { - weekly(where: {id: {_gt: $_id}}) { - id - } - } - `, - { _id: req.body.id } + query QueryGreaterIds($_id: Int) { + weekly(where: { id: { _gt: $_id } }) { + id + } + } + `, + { _id: req.body.id }, ); const sorted_ids = [...QueryGreaterIds.weekly]; sorted_ids.sort((a: any, b: any) => { return a.id - b.id; - }) + }); await client.request( gql` - mutation Delete_Weekly_One($_id: Int) { - delete_weekly(where: {id: {_eq: $_id}}) { - affected_rows - } - } - `, - { _id: req.body.id } + mutation Delete_Weekly_One($_id: Int) { + delete_weekly(where: { id: { _eq: $_id } }) { + affected_rows + } + } + `, + { _id: req.body.id }, ); for (let i = 0; i < sorted_ids.length; i++) { await client.request( gql` - mutation IncreaseIds($_id: Int) { - update_weekly(where: {id: {_eq: $_id}}, _inc: {id: -1}) { - affected_rows - } - } - `, - { _id: sorted_ids[i].id } + mutation IncreaseIds($_id: Int) { + update_weekly(where: { id: { _eq: $_id } }, _inc: { id: -1 }) { + affected_rows + } + } + `, + { _id: sorted_ids[i].id }, ); } return res.status(200).send("ok"); } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } -}) +}); -router.post("/init",authenticate(["counselor"]), async (req, res) => { +router.post("/init", authenticate(["counselor"]), async (req, res) => { try { - if (!req.body.data) return res.status(400).send("400 Bad Request: no data provided!"); + if (!req.body.data) + return res.status(400).send("400 Bad Request: no data provided!"); await client.request( gql` - mutation Init($objects: [weekly_insert_input!] = {}) { - insert_weekly(objects: $objects) { - affected_rows - } - } - `, - { objects: req.body.data } + mutation Init($objects: [weekly_insert_input!] = {}) { + insert_weekly(objects: $objects) { + affected_rows + } + } + `, + { objects: req.body.data }, ); return res.status(200).send("ok"); } catch (err) { return res.status(500).send("500 Internal Server Error: " + err); } -}) +}); export default router; From 8e012615aa7af2bfb527692b6ad667b9004a0840 Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 17 Jun 2025 22:28:23 +0800 Subject: [PATCH 11/29] updated contest team member limit --- src/hasura/contest.ts | 1759 ++++++++++++++++++++++++----------------- src/routes/team.ts | 436 ++++++---- 2 files changed, 1301 insertions(+), 894 deletions(-) diff --git a/src/hasura/contest.ts b/src/hasura/contest.ts index b29a5bd8..ac91e667 100644 --- a/src/hasura/contest.ts +++ b/src/hasura/contest.ts @@ -1,7 +1,6 @@ import { gql } from "graphql-request"; import { client } from ".."; - export interface TeamInfo { team_id: string; team_name: string; @@ -31,12 +30,11 @@ export const get_contest_name: any = async (contest_id: string) => { `, { contest_id: contest_id, - } + }, ); return query_contest_name.contest[0]?.name ?? null; }; - /** * query contest_id from contest_name * @param {string} contest_name @@ -53,12 +51,11 @@ export const get_contest_id: any = async (contest_name: string) => { `, { contest_name: contest_name, - } + }, ); return query_contest_id.contest[0]?.id ?? null; }; - /** * query contest settings from contest_id * @param {string} contest_id @@ -68,7 +65,7 @@ export const get_contest_settings: any = async (contest_id: string) => { const query_contest_settings: any = await client.request( gql` query get_contest_settings($contest_id: uuid!) { - contest(where: {id: {_eq: $contest_id}}) { + contest(where: { id: { _eq: $contest_id } }) { arena_switch code_upload_switch playback_switch @@ -80,31 +77,43 @@ export const get_contest_settings: any = async (contest_id: string) => { `, { contest_id: contest_id, - } + }, ); return { arena_switch: query_contest_settings.contest[0]?.arena_switch ?? false, - code_upload_switch: query_contest_settings.contest[0]?.code_upload_switch ?? false, - playback_switch: query_contest_settings.contest[0]?.playback_switch ?? false, - playground_switch: query_contest_settings.contest[0]?.playground_switch ?? false, + code_upload_switch: + query_contest_settings.contest[0]?.code_upload_switch ?? false, + playback_switch: + query_contest_settings.contest[0]?.playback_switch ?? false, + playground_switch: + query_contest_settings.contest[0]?.playground_switch ?? false, stream_switch: query_contest_settings.contest[0]?.stream_switch ?? false, team_switch: query_contest_settings.contest[0]?.team_switch ?? false, }; }; - /** * query team_id from user_uuid and contest_id * @param {string} user_uuid * @param {string} contest_id * @returns {string} team_id */ -export const get_team_from_user: any = async (user_uuid: string, contest_id: string) => { +export const get_team_from_user: any = async ( + user_uuid: string, + contest_id: string, +) => { const query_team_id: any = await client.request( gql` query get_team_id($user_uuid: uuid!, $contest_id: uuid!) { - contest_team_member(where: {_and: {contest_team: {contest_id: {_eq: $contest_id}}, user_uuid: {_eq: $user_uuid}}}) { + contest_team_member( + where: { + _and: { + contest_team: { contest_id: { _eq: $contest_id } } + user_uuid: { _eq: $user_uuid } + } + } + ) { team_id } } @@ -112,12 +121,11 @@ export const get_team_from_user: any = async (user_uuid: string, contest_id: str { user_uuid: user_uuid, contest_id: contest_id, - } + }, ); return query_team_id.contest_team_member[0]?.team_id ?? null; }; - /** * query team_id from code_id * @param {string} code_id @@ -127,19 +135,18 @@ export const get_team_from_code: any = async (code_id: string) => { const query_team_id: any = await client.request( gql` query get_team_id($code_id: uuid!) { - contest_team_code(where: {code_id: {_eq: $code_id}}) { + contest_team_code(where: { code_id: { _eq: $code_id } }) { team_id } } `, { code_id: code_id, - } + }, ); return query_team_id.contest_team_code[0]?.team_id ?? null; }; - /** * query compile_status from code_id * @param {string} code_id @@ -149,7 +156,7 @@ export const get_compile_status: any = async (code_id: string) => { const query_compile_status: any = await client.request( gql` query get_compile_status($code_id: uuid!) { - contest_team_code(where: {code_id: {_eq: $code_id}}) { + contest_team_code(where: { code_id: { _eq: $code_id } }) { compile_status language } @@ -157,15 +164,15 @@ export const get_compile_status: any = async (code_id: string) => { `, { code_id: code_id, - } + }, ); return { - compile_status: query_compile_status.contest_team_code[0]?.compile_status ?? null, - language: query_compile_status.contest_team_code[0]?.language ?? null - } + compile_status: + query_compile_status.contest_team_code[0]?.compile_status ?? null, + language: query_compile_status.contest_team_code[0]?.language ?? null, + }; }; - /** * query contest_score from team_id, contest_id and round_id * @param {string} team_id @@ -173,12 +180,17 @@ export const get_compile_status: any = async (code_id: string) => { * @param {string} round_id * @returns {number} score */ -export const get_team_contest_score: any = async (team_id: string, round_id: string) => { +export const get_team_contest_score: any = async ( + team_id: string, + round_id: string, +) => { const query_contest_score: any = await client.request( gql` query getTeamScore($team_id: uuid!, $round_id: uuid!) { contest_team_by_pk(team_id: $team_id) { - contest_team_rooms_aggregate(where: {contest_room: {round_id: {_eq: $round_id}}}) { + contest_team_rooms_aggregate( + where: { contest_room: { round_id: { _eq: $round_id } } } + ) { aggregate { sum { score @@ -190,13 +202,15 @@ export const get_team_contest_score: any = async (team_id: string, round_id: str `, { team_id: team_id, - round_id: round_id - } + round_id: round_id, + }, + ); + return ( + query_contest_score?.contest_team_by_pk?.contest_team_rooms_aggregate + ?.aggregate?.sum?.score ?? 0 ); - return query_contest_score?.contest_team_by_pk?.contest_team_rooms_aggregate?.aggregate?.sum?.score ?? 0; }; - /** * query arena_score from team_id, contest_id * @param {string} team_id @@ -209,7 +223,9 @@ export const get_team_arena_score: any = async (team_id: string) => { gql` query getTeamScore($team_id: uuid!) { contest_team_by_pk(team_id: $team_id) { - contest_team_rooms_aggregate(where: {contest_room: {round_id: {_is_null: true}}}) { + contest_team_rooms_aggregate( + where: { contest_room: { round_id: { _is_null: true } } } + ) { aggregate { sum { score @@ -221,34 +237,46 @@ export const get_team_arena_score: any = async (team_id: string) => { `, { team_id: team_id, - } + }, + ); + return ( + query_arena_score?.contest_team_by_pk?.contest_team_rooms_aggregate + ?.aggregate?.sum?.score ?? 0 ); - return query_arena_score?.contest_team_by_pk?.contest_team_rooms_aggregate?.aggregate?.sum?.score ?? 0; }; - /** * query manager_uuid from user_uuid and contest_id * @param {string} user_uuid * @param {string} contest_id * @returns {string} manager_uuid */ -export const get_maneger_from_user: any = async (user_uuid: string, contest_id: string) => { +export const get_maneger_from_user: any = async ( + user_uuid: string, + contest_id: string, +) => { const query_if_manager: any = await client.request( gql` query query_is_manager($contest_id: uuid!, $user_uuid: uuid!) { - contest_manager(where: {_and: {contest_id: {_eq: $contest_id}, user_uuid: {_eq: $user_uuid}}}) { + contest_manager( + where: { + _and: { + contest_id: { _eq: $contest_id } + user_uuid: { _eq: $user_uuid } + } + } + ) { user_uuid } } `, { contest_id: contest_id, - user_uuid: user_uuid - } + user_uuid: user_uuid, + }, ); return query_if_manager.contest_manager[0]?.user_uuid ?? null; -} +}; /** * get max game time from contest_id in seconds @@ -259,18 +287,17 @@ export const get_game_time: any = async (contest_id: string) => { const game_time: any = await client.request( gql` query get_game_time($contest_id: uuid!) { - contest(where: {id: {_eq: $contest_id}}) { + contest(where: { id: { _eq: $contest_id } }) { game_time } } `, { contest_id: contest_id, - } + }, ); return game_time.contest[0].game_time ?? null; -} - +}; /** * get server docker memory limit from contest_id (in GB) @@ -281,18 +308,17 @@ export const get_server_memory_limit: any = async (contest_id: string) => { const server_memory_limit: any = await client.request( gql` query get_server_memory_limit($contest_id: uuid!) { - contest(where: {id: {_eq: $contest_id}}) { + contest(where: { id: { _eq: $contest_id } }) { server_memory_limit } } `, { contest_id: contest_id, - } + }, ); return server_memory_limit.contest[0].server_memory_limit ?? null; -} - +}; /** * get client docker memory limit from contest_id (in GB) @@ -303,18 +329,17 @@ export const get_client_memory_limit: any = async (contest_id: string) => { const client_memory_limit: any = await client.request( gql` query get_client_memory_limit($contest_id: uuid!) { - contest(where: {id: {_eq: $contest_id}}) { + contest(where: { id: { _eq: $contest_id } }) { client_memory_limit } } `, { contest_id: contest_id, - } + }, ); return client_memory_limit.contest[0].client_memory_limit ?? null; -} - +}; /** * query language and contest_id from code_id @@ -325,7 +350,7 @@ export const query_code: any = async (code_id: string) => { const query_all_from_code: any = await client.request( gql` query get_all_from_code($code_id: uuid!) { - contest_team_code(where: {code_id: {_eq: $code_id}}) { + contest_team_code(where: { code_id: { _eq: $code_id } }) { team_id language compile_status @@ -340,18 +365,22 @@ export const query_code: any = async (code_id: string) => { `, { code_id: code_id, - } + }, ); return { - contest_id: query_all_from_code.contest_team_code[0]?.contest_team?.contest_id ?? null, - contest_name: query_all_from_code.contest_team_code[0]?.contest_team?.contest?.name ?? null, + contest_id: + query_all_from_code.contest_team_code[0]?.contest_team?.contest_id ?? + null, + contest_name: + query_all_from_code.contest_team_code[0]?.contest_team?.contest?.name ?? + null, team_id: query_all_from_code.contest_team_code[0]?.team_id ?? null, language: query_all_from_code.contest_team_code[0]?.language ?? null, - compile_status: query_all_from_code.contest_team_code[0]?.compile_status ?? null + compile_status: + query_all_from_code.contest_team_code[0]?.compile_status ?? null, }; -} - +}; /** * Count the number of active rooms that a team is in @@ -359,11 +388,26 @@ export const query_code: any = async (code_id: string) => { * @param {string} team_id * @returns {number} count */ -export const count_room_team: any = async (contest_id: string, team_id: string) => { +export const count_room_team: any = async ( + contest_id: string, + team_id: string, +) => { const count_room_from_team: any = await client.request( gql` query count_room($contest_id: uuid!, $team_id: uuid!) { - contest_room_team_aggregate(where: {_and: {team_id: {_eq: $team_id}, contest_room: {_and: {contest_id: {_eq: $contest_id}, status: {_in: [ "Waiting", "Running" ]}}}}}) { + contest_room_team_aggregate( + where: { + _and: { + team_id: { _eq: $team_id } + contest_room: { + _and: { + contest_id: { _eq: $contest_id } + status: { _in: ["Waiting", "Running"] } + } + } + } + } + ) { aggregate { count } @@ -372,32 +416,28 @@ export const count_room_team: any = async (contest_id: string, team_id: string) `, { contest_id: contest_id, - team_id: team_id - } + team_id: team_id, + }, ); return count_room_from_team.contest_room_team_aggregate.aggregate.count; -} - +}; /** * Get all the exposed ports * @returns {[{port}]} [{port}] */ export const get_exposed_ports: any = async () => { - const query_exposed_ports: any = await client.request( - gql` - query get_exposed_ports { - contest_room(where: {port: {_is_null: false}}) { - port - } + const query_exposed_ports: any = await client.request(gql` + query get_exposed_ports { + contest_room(where: { port: { _is_null: false } }) { + port } - ` - ); - const result = query_exposed_ports.contest_room + } + `); + const result = query_exposed_ports.contest_room; return result; -} - +}; /** * Get the exposed port by room id @@ -407,18 +447,17 @@ export const get_exposed_port_by_room: any = async (room_id: string) => { const query_exposed_port_by_room: any = await client.request( gql` query get_exposed_port_by_room($room_id: uuid!) { - contest_room(where: {room_id: {_eq: $room_id}}) { + contest_room(where: { room_id: { _eq: $room_id } }) { port } } `, { - room_id: room_id - } + room_id: room_id, + }, ); - return query_exposed_port_by_room.contest_room[0]?.port -} - + return query_exposed_port_by_room.contest_room[0]?.port; +}; /** * query player_label from contest_id and team_label @@ -426,24 +465,35 @@ export const get_exposed_port_by_room: any = async (room_id: string) => { * @param {string} team_label * @returns {string[]} player_labels */ -export const get_players_label: any = async (contest_id: string, team_label: string) => { +export const get_players_label: any = async ( + contest_id: string, + team_label: string, +) => { const query_players_label: any = await client.request( gql` query get_players_label($contest_id: uuid!, $team_label: String!) { - contest_player(where: {_and: {contest_id: {_eq: $contest_id}, team_label: {_eq: $team_label}}}) { + contest_player( + where: { + _and: { + contest_id: { _eq: $contest_id } + team_label: { _eq: $team_label } + } + } + ) { player_label } } `, { contest_id: contest_id, - team_label: team_label - } + team_label: team_label, + }, ); - return query_players_label.contest_player.map((player: any) => player.player_label); -} - + return query_players_label.contest_player.map( + (player: any) => player.player_label, + ); +}; /** * query code_id and role from team_id and player_label @@ -451,11 +501,18 @@ export const get_players_label: any = async (contest_id: string, team_label: str * @param {string} player_label * @returns {object} {code_id, role} */ -export const get_player_code: any = async (team_id: string, player_label: string) => { +export const get_player_code: any = async ( + team_id: string, + player_label: string, +) => { const query_code_id: any = await client.request( gql` query get_code_id($team_id: uuid!, $player_label: String!) { - contest_team_player(where: {_and: {team_id: {_eq: $team_id}, player: {_eq: $player_label}}}) { + contest_team_player( + where: { + _and: { team_id: { _eq: $team_id }, player: { _eq: $player_label } } + } + ) { code_id role } @@ -463,16 +520,15 @@ export const get_player_code: any = async (team_id: string, player_label: string `, { team_id: team_id, - player_label: player_label - } + player_label: player_label, + }, ); return { code_id: query_code_id.contest_team_player[0]?.code_id ?? null, - role: query_code_id.contest_team_player[0]?.role ?? null - } -} - + role: query_code_id.contest_team_player[0]?.role ?? null, + }; +}; /** * query round info from round_id @@ -483,7 +539,7 @@ export const get_round_info: any = async (round_id: string) => { const query_round_info: any = await client.request( gql` query get_round_info($round_id: uuid!) { - contest_round(where: {round_id: {_eq: $round_id}}) { + contest_round(where: { round_id: { _eq: $round_id } }) { contest_id map_id } @@ -491,15 +547,14 @@ export const get_round_info: any = async (round_id: string) => { `, { round_id: round_id, - } + }, ); return { contest_id: query_round_info.contest_round[0]?.contest_id ?? null, - map_id: query_round_info.contest_round[0]?.map_id ?? null - } -} - + map_id: query_round_info.contest_round[0]?.map_id ?? null, + }; +}; /** * query contest player info from contest_id @@ -510,7 +565,10 @@ export const get_contest_players: any = async (contest_id: string) => { const query_players: any = await client.request( gql` query get_players($contest_id: uuid!) { - contest_player(where: {contest_id: {_eq: $contest_id}}, order_by: {team_label: asc}) { + contest_player( + where: { contest_id: { _eq: $contest_id } } + order_by: { team_label: asc } + ) { team_label player_label } @@ -518,15 +576,18 @@ export const get_contest_players: any = async (contest_id: string) => { `, { contest_id: contest_id, - } + }, ); return { - team_labels: query_players.contest_player.map((player: any) => player.team_label), - players_labels: query_players.contest_player.map((player: any) => player.player_label) - } -} - + team_labels: query_players.contest_player.map( + (player: any) => player.team_label, + ), + players_labels: query_players.contest_player.map( + (player: any) => player.player_label, + ), + }; +}; /** * query all team_ids from contest_id @@ -537,19 +598,21 @@ export const get_all_teams: any = async (contest_id: string) => { const query_teams: any = await client.request( gql` query get_teams($contest_id: uuid!) { - contest_team(where: {contest_id: {_eq: $contest_id}}, order_by: {team_id: asc}) { + contest_team( + where: { contest_id: { _eq: $contest_id } } + order_by: { team_id: asc } + ) { team_id } } `, { contest_id: contest_id, - } + }, ); return query_teams.contest_team.map((team: any) => team.team_id); -} - +}; /** * query room_id from team_id, team_label and round_id @@ -558,11 +621,27 @@ export const get_all_teams: any = async (contest_id: string) => { * @param {string} round_id * @returns {string[]} room_id */ -export const get_room_id: any = async (team_id: string, team_label: string, round_id: string) => { +export const get_room_id: any = async ( + team_id: string, + team_label: string, + round_id: string, +) => { const query_room_id: any = await client.request( gql` - query get_room_id($team_id: uuid!, $team_label: String!, $round_id: uuid!) { - contest_room_team(where: {_and: {team_id: {_eq: $team_id}, team_label: {_eq: $team_label}, contest_room: {round_id: {_eq: $round_id}}}}) { + query get_room_id( + $team_id: uuid! + $team_label: String! + $round_id: uuid! + ) { + contest_room_team( + where: { + _and: { + team_id: { _eq: $team_id } + team_label: { _eq: $team_label } + contest_room: { round_id: { _eq: $round_id } } + } + } + ) { room_id } } @@ -570,36 +649,32 @@ export const get_room_id: any = async (team_id: string, team_label: string, roun { team_id: team_id, team_label: team_label, - round_id: round_id - } + round_id: round_id, + }, ); return query_room_id.contest_room_team.map((room: any) => room.room_id); -} - +}; /** * query all maps * @returns {object} {contest_list, map_list} */ export const get_all_maps: any = async () => { - const query_maps: any = await client.request( - gql` - query get_maps { - contest_map(order_by: {contest_id: asc}) { - contest_id - map_id - } + const query_maps: any = await client.request(gql` + query get_maps { + contest_map(order_by: { contest_id: asc }) { + contest_id + map_id } - ` - ); + } + `); return { contest_list: query_maps.contest_map.map((map: any) => map.contest_id), - map_list: query_maps.contest_map.map((map: any) => map.map_id) - } -} - + map_list: query_maps.contest_map.map((map: any) => map.map_id), + }; +}; /** * get map name @@ -610,18 +685,17 @@ export const get_map_name: any = async (map_id: string) => { const query_map: any = await client.request( gql` query get_map_name($map_id: uuid!) { - contest_map(where: {map_id: {_eq: $map_id}}){ + contest_map(where: { map_id: { _eq: $map_id } }) { filename } } `, { - map_id: map_id - } + map_id: map_id, + }, ); return query_map.contest_map[0].filename ?? null; -} - +}; /** * get room_info by room_id @@ -632,7 +706,7 @@ export const get_room_info: any = async (room_id: string) => { const query_room_info: any = await client.request( gql` query get_room_info($room_id: uuid!) { - contest_room(where: {room_id: {_eq: $room_id}}) { + contest_room(where: { room_id: { _eq: $room_id } }) { contest { name } @@ -641,53 +715,79 @@ export const get_room_info: any = async (room_id: string) => { } `, { - room_id: room_id - } + room_id: room_id, + }, ); return { contest_name: query_room_info.contest_room[0]?.contest?.name ?? null, - round_id: query_room_info.contest_room[0]?.round_id ?? null - } -} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + round_id: query_room_info.contest_room[0]?.round_id ?? null, + }; +}; +/** + * Get the headcount of a contest team + * @param {string} team_id The ID of the team + * @returns {Promise} The headcount of the team + */ +export const get_team_membercount: any = async (team_id: string) => { + const get_team_membercount: any = await client.request( + gql` + query MyQuery($team_id: uuid!) { + contest_team(where: { team_id: { _eq: $team_id } }) { + contest_team_members_aggregate { + aggregate { + count + } + } + } + } + `, + { + team_id: team_id, + }, + ); + return ( + get_team_membercount.contest_team[0]?.contest_team_members_aggregate + .aggregate?.count ?? 0 + ); +}; +export const get_team_member_limit: any = async (team_id: string) => { + const get_team_member_limit: any = await client.request( + gql` + query MyQuery($_eq: uuid!) { + contest_team(where: { team_id: { _eq: $_eq } }) { + contest { + max_teammember_limit + } + } + } + `, + { + _eq: team_id, + }, + ); + return ( + get_team_member_limit.contest_team[0]?.contest?.max_teammember_limit ?? 0 + ); +}; +export const get_contest_member_limit: any = async (contest_id: string) => { + const get_contest_member_limit: any = await client.request( + gql` + query MyQuery($_eq: uuid!) { + contest(where: { id: { _eq: $_eq } }) { + max_teammember_limit + } + } + `, + { + _eq: contest_id, + }, + ); + console.log(get_contest_member_limit); + return get_contest_member_limit.contest[0]?.max_teammember_limit ?? 0; +}; /** ============================================================================ @@ -702,11 +802,21 @@ export const get_room_info: any = async (room_id: string) => { * @param {string} map_id * @returns {string} room_id */ -export const insert_room: any = async (contest_id: string, status: string, map_id: string) => { +export const insert_room: any = async ( + contest_id: string, + status: string, + map_id: string, +) => { const insert_room: any = await client.request( gql` - mutation insert_room($contest_id: uuid!, $status: String!, $map_id: uuid!) { - insert_contest_room_one(object: {contest_id: $contest_id, status: $status, map_id: $map_id}) { + mutation insert_room( + $contest_id: uuid! + $status: String! + $map_id: uuid! + ) { + insert_contest_room_one( + object: { contest_id: $contest_id, status: $status, map_id: $map_id } + ) { room_id } } @@ -714,13 +824,12 @@ export const insert_room: any = async (contest_id: string, status: string, map_i { contest_id: contest_id, status: status, - map_id: map_id - } + map_id: map_id, + }, ); return insert_room.insert_contest_room_one?.room_id ?? null; -} - +}; /** * insert a new competition room @@ -730,11 +839,28 @@ export const insert_room: any = async (contest_id: string, status: string, map_i * @param {string} round_id * @returns {string} room_id */ -export const insert_room_competition: any = async (contest_id: string, status: string, map_id: string, round_id: string) => { +export const insert_room_competition: any = async ( + contest_id: string, + status: string, + map_id: string, + round_id: string, +) => { const insert_room: any = await client.request( gql` - mutation insert_room($contest_id: uuid!, $status: String!, $map_id: uuid!, $round_id: uuid!) { - insert_contest_room_one(object: {contest_id: $contest_id, status: $status, map_id: $map_id, round_id: $round_id}) { + mutation insert_room( + $contest_id: uuid! + $status: String! + $map_id: uuid! + $round_id: uuid! + ) { + insert_contest_room_one( + object: { + contest_id: $contest_id + status: $status + map_id: $map_id + round_id: $round_id + } + ) { room_id } } @@ -743,13 +869,12 @@ export const insert_room_competition: any = async (contest_id: string, status: s contest_id: contest_id, status: status, map_id: map_id, - round_id: round_id - } + round_id: round_id, + }, ); return insert_room.insert_contest_room_one?.room_id ?? null; -} - +}; /** * Insert room_teams @@ -760,7 +885,13 @@ export const insert_room_competition: any = async (contest_id: string, status: s * @param {string[][]} player_codes * @returns {number} affected_rows */ -export const insert_room_teams: any = async (room_id: string, team_ids: Array, team_labels: Array, player_roles: Array>, player_codes: Array>) => { +export const insert_room_teams: any = async ( + room_id: string, + team_ids: Array, + team_labels: Array, + player_roles: Array>, + player_codes: Array>, +) => { const insert_room_teams: any = await client.request( gql` mutation insert_room_teams($objects: [contest_room_team_insert_input!]!) { @@ -775,13 +906,13 @@ export const insert_room_teams: any = async (room_id: string, team_ids: Array} */ -export const add_contest_map:any = async(contest_id:string, name:string, filename:string, team_labels:string) => { - const add_contest_map:any = await client.request( +export const add_contest_map: any = async ( + contest_id: string, + name: string, + filename: string, + team_labels: string, +) => { + const add_contest_map: any = await client.request( gql` - mutation AddContestMap( - $contest_id: uuid! - $name: String! - $filename: String! - $team_labels: String! - ) { - insert_contest_map_one( - object: { - contest_id: $contest_id - name: $name - filename: $filename - team_labels: $team_labels + mutation AddContestMap( + $contest_id: uuid! + $name: String! + $filename: String! + $team_labels: String! + ) { + insert_contest_map_one( + object: { + contest_id: $contest_id + name: $name + filename: $filename + team_labels: $team_labels + } + ) { + map_id + } } - ) { - map_id - } - } `, { contest_id: contest_id, name: name, filename: filename, - team_labels: team_labels - }); + team_labels: team_labels, + }, + ); return add_contest_map.insert_contest_map_one?.map_id ?? undefined; -} +}; /** * @@ -829,36 +966,41 @@ export const add_contest_map:any = async(contest_id:string, name:string, filenam * @param {string} contest_id * @returns {Promise} */ -export const add_contest_notice:any = async(title:string,content:string,files:string,contest_id:string) => { - const add_contest_notice:any = await client.request( +export const add_contest_notice: any = async ( + title: string, + content: string, + files: string, + contest_id: string, +) => { + const add_contest_notice: any = await client.request( gql` - mutation AddContestNotice( - $title: String! - $content: String! - $files: String - $contest_id: uuid! - ) { - insert_contest_notice_one( - object: { - title: $title - content: $content - files: $files - contest_id: $contest_id - } + mutation AddContestNotice( + $title: String! + $content: String! + $files: String + $contest_id: uuid! ) { - id + insert_contest_notice_one( + object: { + title: $title + content: $content + files: $files + contest_id: $contest_id + } + ) { + id + } } - } `, { - title:title, - content:content, - files:files, - contest_id:contest_id - } - ) - return add_contest_notice.insert_contest_notice_one?.id?? undefined; -} + title: title, + content: content, + files: files, + contest_id: contest_id, + }, + ); + return add_contest_notice.insert_contest_notice_one?.id ?? undefined; +}; /** * @@ -868,35 +1010,41 @@ export const add_contest_notice:any = async(title:string,content:string,files:st * @param {string} roles_available * @returns {Promise} */ -export const add_contest_player:any = async(contest_id:string,team_label:string,player_label:string,roles_available:string) =>{ - const add_contest_player:any = await client.request( +export const add_contest_player: any = async ( + contest_id: string, + team_label: string, + player_label: string, + roles_available: string, +) => { + const add_contest_player: any = await client.request( gql` - mutation AddContestPlayer( - $contest_id: uuid! - $team_label: String! - $player_label: String! - $roles_available: String! - ) { - insert_contest_player_one( - object: { - contest_id: $contest_id - team_label: $team_label - player_label: $player_label - roles_available: $roles_available - } + mutation AddContestPlayer( + $contest_id: uuid! + $team_label: String! + $player_label: String! + $roles_available: String! ) { - team_label + insert_contest_player_one( + object: { + contest_id: $contest_id + team_label: $team_label + player_label: $player_label + roles_available: $roles_available + } + ) { + team_label + } } - } `, { contest_id: contest_id, team_label: team_label, player_label: player_label, - roles_available: roles_available - }); - return add_contest_player.insert_contest_player_one?.team_label?? undefined; -} + roles_available: roles_available, + }, + ); + return add_contest_player.insert_contest_player_one?.team_label ?? undefined; +}; /** * @@ -905,24 +1053,33 @@ export const add_contest_player:any = async(contest_id:string,team_label:string, * @param {string} map_id ID * @returns {Promise} ID */ -export const add_contest_round:any = async(contest_id:string,name:string,map_id:string)=>{ - const add_contest_round:any = await client.request( +export const add_contest_round: any = async ( + contest_id: string, + name: string, + map_id: string, +) => { + const add_contest_round: any = await client.request( gql` - mutation AddContestRound($contest_id: uuid!, $name: String!, $map_id: uuid) { - insert_contest_round_one( - object: { contest_id: $contest_id, name: $name, map_id: $map_id } + mutation AddContestRound( + $contest_id: uuid! + $name: String! + $map_id: uuid ) { - round_id + insert_contest_round_one( + object: { contest_id: $contest_id, name: $name, map_id: $map_id } + ) { + round_id + } } - } - `, - { - contest_id: contest_id, - name: name, - map_id: map_id - }); - return add_contest_round.insert_contest_round_one?.round_id?? undefined; -} + `, + { + contest_id: contest_id, + name: name, + map_id: map_id, + }, + ); + return add_contest_round.insert_contest_round_one?.round_id ?? undefined; +}; /** * @@ -932,34 +1089,40 @@ export const add_contest_round:any = async(contest_id:string,name:string,map_id: * @param {string} compile_status * @returns {Promise} ID */ -export const add_team_code:any = async(team_id:string,code_name:string,language:string,compile_status:string) =>{ - const add_team_code:any = await client.request( +export const add_team_code: any = async ( + team_id: string, + code_name: string, + language: string, + compile_status: string, +) => { + const add_team_code: any = await client.request( gql` - mutation AddTeamCode( - $team_id: uuid! - $code_name: String! - $language: String! - $compile_status: String - ) { - insert_contest_team_code_one( - object: { - team_id: $team_id - code_name: $code_name - language: $language - compile_status: $compile_status - } + mutation AddTeamCode( + $team_id: uuid! + $code_name: String! + $language: String! + $compile_status: String ) { - code_id + insert_contest_team_code_one( + object: { + team_id: $team_id + code_name: $code_name + language: $language + compile_status: $compile_status + } + ) { + code_id + } } - } `, { team_id: team_id, code_name: code_name, compile_status: compile_status, - }); - return add_team_code.insert_contest_team_code_one?.code_id?? undefined; - } + }, + ); + return add_team_code.insert_contest_team_code_one?.code_id ?? undefined; +}; /** * @@ -967,24 +1130,38 @@ export const add_team_code:any = async(team_id:string,code_name:string,language: * @param {string} player * @returns {Promise} */ -export const add_team_player:any = async(team_id:string,player:string) =>{ - const add_team_player:any = await client.request( - gql` - mutation AddTeamPlayer($team_id: uuid!, $player: String!) { - insert_contest_team_player_one( - object: { team_id: $team_id, player: $player } - ) { - player - } + +export class TeamPlayerLimitError extends Error { + constructor(message: string) { + super(message); + this.name = "TeamPlayerLimitError"; } - `, - { - team_id:team_id, - player:player - }); - return add_team_player.insert_contest_team_player_one?.player?? undefined; } +export const add_team_player: any = async (team_id: string, player: string) => { + const limit = get_team_member_limit(team_id); + const member_count = await get_team_membercount(team_id); + if (member_count >= limit && limit > 0) { + throw new TeamPlayerLimitError(`Team member limit reached: ${limit}`); + } + const add_team_player: any = await client.request( + gql` + mutation AddTeamPlayer($team_id: uuid!, $player: String!) { + insert_contest_team_player_one( + object: { team_id: $team_id, player: $player } + ) { + player + } + } + `, + { + team_id: team_id, + player: player, + }, + ); + return add_team_player.insert_contest_team_player_one?.player ?? undefined; +}; + /** * * @param {string} team_name @@ -994,38 +1171,46 @@ export const add_team_player:any = async(team_id:string,player:string) =>{ * @param {string} contest_id ID * @returns {Promise} ID */ -export const add_team:any = async(team_name:string,team_intro:string,team_leader_uuid:string,invited_code:string,contest_id:string) =>{ - const add_team:any = await client.request( +export const add_team: any = async ( + team_name: string, + team_intro: string, + team_leader_uuid: string, + invited_code: string, + contest_id: string, +) => { + const add_team: any = await client.request( gql` - mutation AddTeam( - $team_name: String! - $team_intro: String = "" # 此处的intro可以为NULL - $team_leader_uuid: uuid! # team_leader的uuid - $invited_code: String! - $contest_id: uuid! # 是必填的项 - ) { - insert_contest_team_one( - object: { - team_name: $team_name - team_intro: $team_intro - team_leader_uuid: $team_leader_uuid - invited_code: $invited_code - contest_id: $contest_id - contest_team_members: { data: { user_uuid: $team_leader_uuid } } - } + mutation AddTeam( + $team_name: String! + $team_intro: String = "" # 此处的intro可以为NULL + $team_leader_uuid: uuid! # team_leader的uuid + $invited_code: String! + $contest_id: uuid! # 是必填的项 ) { - team_id + insert_contest_team_one( + object: { + team_name: $team_name + team_intro: $team_intro + team_leader_uuid: $team_leader_uuid + invited_code: $invited_code + contest_id: $contest_id + contest_team_members: { data: { user_uuid: $team_leader_uuid } } + } + ) { + team_id + } } - }`, + `, { team_name: team_name, team_intro: team_intro, team_leader_uuid: team_leader_uuid, invited_code: invited_code, - contest_id: contest_id - }); - return add_team.insert_contest_team_one?.team_id ?? undefined; - } + contest_id: contest_id, + }, + ); + return add_team.insert_contest_team_one?.team_id ?? undefined; +}; /** * @@ -1033,23 +1218,27 @@ export const add_team:any = async(team_name:string,team_intro:string,team_leader * @param {string} user_uuid UUID * @returns {Promise} ID */ -export const add_team_member:any = async(team_id:string,user_uuid:string) =>{ - const add_team_member:any = await client.request( +export const add_team_member: any = async ( + team_id: string, + user_uuid: string, +) => { + const add_team_member: any = await client.request( gql` - mutation AddTeamMember($team_id: uuid!, $user_uuid: uuid!) { - insert_contest_team_member_one( - object: { team_id: $team_id, user_uuid: $user_uuid } - ) { - team_id + mutation AddTeamMember($team_id: uuid!, $user_uuid: uuid!) { + insert_contest_team_member_one( + object: { team_id: $team_id, user_uuid: $user_uuid } + ) { + team_id + } } - } `, { team_id: team_id, - user_uuid: user_uuid - }); - return add_team_member.insert_contest_team_member_one?.team_id?? undefined; - } + user_uuid: user_uuid, + }, + ); + return add_team_member.insert_contest_team_member_one?.team_id ?? undefined; +}; /** * @@ -1060,56 +1249,45 @@ export const add_team_member:any = async(team_id:string,user_uuid:string) =>{ * @param {string} description * @returns {Promise} */ -export const add_contest_time:any = async(contest_id:string,event:string,start:Date,end:Date,description:string) =>{ - const add_contest_time:any = await client.request( +export const add_contest_time: any = async ( + contest_id: string, + event: string, + start: Date, + end: Date, + description: string, +) => { + const add_contest_time: any = await client.request( gql` - mutation AddContestTime( - $contest_id: uuid! - $event: String! - $start: timestamptz! - $end: timestamptz! - $description: String - ) { - insert_contest_time_one( - object: { - contest_id: $contest_id - event: $event - start: $start - end: $end - description: $description - } + mutation AddContestTime( + $contest_id: uuid! + $event: String! + $start: timestamptz! + $end: timestamptz! + $description: String ) { - event + insert_contest_time_one( + object: { + contest_id: $contest_id + event: $event + start: $start + end: $end + description: $description + } + ) { + event + } } - }`, + `, { contest_id: contest_id, event: event, start: new Date(start), end: new Date(end), - description: description - }); - return add_contest_time.insert_contest_time_one?.event?? undefined; - } - - - - - - - - - - - - - - - - - - - + description: description, + }, + ); + return add_contest_time.insert_contest_time_one?.event ?? undefined; +}; /** ============================================================================ @@ -1123,11 +1301,20 @@ export const add_contest_time:any = async(contest_id:string,event:string,start:D * @param {string} compile_status * @returns {number} affected_rows */ -export const update_compile_status: any = async (code_id: string, compile_status: string) => { +export const update_compile_status: any = async ( + code_id: string, + compile_status: string, +) => { const update_compile_status: any = await client.request( gql` - mutation update_compile_status($code_id: uuid!, $compile_status: String!) { - update_contest_team_code(where: {code_id: {_eq: $code_id}}, _set: {compile_status: $compile_status}) { + mutation update_compile_status( + $code_id: uuid! + $compile_status: String! + ) { + update_contest_team_code( + where: { code_id: { _eq: $code_id } } + _set: { compile_status: $compile_status } + ) { affected_rows } } @@ -1135,12 +1322,11 @@ export const update_compile_status: any = async (code_id: string, compile_status { code_id: code_id, compile_status: compile_status, - } + }, ); return update_compile_status.update_contest_team_code.affected_rows; -} - +}; /** * update room status and port @@ -1149,11 +1335,22 @@ export const update_compile_status: any = async (code_id: string, compile_status * @param {number} port * @returns {number} affected_rows */ -export const update_room_status_and_port: any = async (room_id: string, status: string, port: number | null) => { +export const update_room_status_and_port: any = async ( + room_id: string, + status: string, + port: number | null, +) => { const update_room_status: any = await client.request( gql` - mutation update_room_status($room_id: uuid!, $status: String!, $port: Int) { - update_contest_room(where: {room_id: {_eq: $room_id}}, _set: {status: $status, port: $port}) { + mutation update_room_status( + $room_id: uuid! + $status: String! + $port: Int + ) { + update_contest_room( + where: { room_id: { _eq: $room_id } } + _set: { status: $status, port: $port } + ) { affected_rows } } @@ -1161,13 +1358,12 @@ export const update_room_status_and_port: any = async (room_id: string, status: { room_id: room_id, status: status, - port: port - } + port: port, + }, ); return update_room_status.update_contest_room.affected_rows; -} - +}; /** * update room status @@ -1175,24 +1371,29 @@ export const update_room_status_and_port: any = async (room_id: string, status: * @param {string} status * @returns {number} affected_rows */ -export const update_room_status: any = async (room_id: string, status: string) => { +export const update_room_status: any = async ( + room_id: string, + status: string, +) => { const update_room_status: any = await client.request( gql` mutation update_room_status($room_id: uuid!, $status: String!) { - update_contest_room(where: {room_id: {_eq: $room_id}}, _set: {status: $status}) { + update_contest_room( + where: { room_id: { _eq: $room_id } } + _set: { status: $status } + ) { affected_rows } } `, { room_id: room_id, - status: status - } + status: status, + }, ); return update_room_status.update_contest_room.affected_rows; -} - +}; /** * update room port @@ -1200,24 +1401,29 @@ export const update_room_status: any = async (room_id: string, status: string) = * @param {number} port * @returns {number} affected_rows */ -export const update_room_port: any = async (room_id: string, port: number | null) => { +export const update_room_port: any = async ( + room_id: string, + port: number | null, +) => { const update_room_status: any = await client.request( gql` mutation update_room_status($room_id: uuid!, $port: Int) { - update_contest_room(where: {room_id: {_eq: $room_id}}, _set: {port: $port}) { + update_contest_room( + where: { room_id: { _eq: $room_id } } + _set: { port: $port } + ) { affected_rows } } `, { room_id: room_id, - port: port - } + port: port, + }, ); return update_room_status.update_contest_room.affected_rows; -} - +}; /** * update room_team score @@ -1225,11 +1431,24 @@ export const update_room_port: any = async (room_id: string, port: number | null * @param {string} team_id * @param {number} score */ -export const update_room_team_score: any = async (room_id: string, team_id: string, score: number) => { +export const update_room_team_score: any = async ( + room_id: string, + team_id: string, + score: number, +) => { const update_room_team_score: any = await client.request( gql` - mutation update_room_team_score($room_id: uuid!, $team_id: uuid!, $score: Int!) { - update_contest_room_team(where: {_and: {room_id: {_eq: $room_id}, team_id: {_eq: $team_id}}}, _set: {score: $score}) { + mutation update_room_team_score( + $room_id: uuid! + $team_id: uuid! + $score: Int! + ) { + update_contest_room_team( + where: { + _and: { room_id: { _eq: $room_id }, team_id: { _eq: $team_id } } + } + _set: { score: $score } + ) { affected_rows } } @@ -1237,13 +1456,12 @@ export const update_room_team_score: any = async (room_id: string, team_id: stri { room_id: room_id, team_id: team_id, - score: score - } + score: score, + }, ); return update_room_team_score.update_contest_room_team.affected_rows; -} - +}; /** * update room_team player roles @@ -1251,14 +1469,24 @@ export const update_room_team_score: any = async (room_id: string, team_id: stri * @param {string[]} team_ids * @param {string[][]} player_roles */ -export const update_room_team_player_roles: any = async (room_id: string, team_ids: string[], player_roles: string[][]) => { +export const update_room_team_player_roles: any = async ( + room_id: string, + team_ids: string[], + player_roles: string[][], +) => { let totalAffectedRows = 0; for (let i = 0; i < team_ids.length; i++) { const mutation = gql` - mutation update_team_player_roles($room_id: uuid!, $team_id: uuid!, $player_roles: String!) { + mutation update_team_player_roles( + $room_id: uuid! + $team_id: uuid! + $player_roles: String! + ) { update_contest_room_team( - where: { _and: { room_id: { _eq: $room_id }, team_id: { _eq: $team_id } } }, + where: { + _and: { room_id: { _eq: $room_id }, team_id: { _eq: $team_id } } + } _set: { player_roles: $player_roles } ) { affected_rows @@ -1285,23 +1513,32 @@ export const update_room_team_player_roles: any = async (room_id: string, team_i * @param {string} created_at * @returns {string} created_at */ -export const update_room_created_at: any = async (room_id: string, created_at: string) => { +export const update_room_created_at: any = async ( + room_id: string, + created_at: string, +) => { const update_room_created_at: any = await client.request( gql` - mutation update_room_created_at($room_id: uuid!, $created_at: timestamptz = "") { - update_contest_room_by_pk(pk_columns: {room_id: $room_id}, _set: {created_at: $created_at}) { + mutation update_room_created_at( + $room_id: uuid! + $created_at: timestamptz = "" + ) { + update_contest_room_by_pk( + pk_columns: { room_id: $room_id } + _set: { created_at: $created_at } + ) { created_at } } `, { room_id: room_id, - created_at: created_at - } + created_at: created_at, + }, ); return update_room_created_at.update_contest_room_by_pk.created_at; -} +}; /** * Updates the contest information. @@ -1313,11 +1550,19 @@ export const update_room_created_at: any = async (room_id: string, created_at: s * @param {Date} end_date The new end date of the contest (timestamp). * @returns {string} The ID of the updated contest. */ -export const update_contest_info:any = async(contest_id: string, updateFields:Partial<{fullname: string; description: string; start_date: Date;end_date: Date}>) => { - - const setFields:{[key:string]:any} = {}; +export const update_contest_info: any = async ( + contest_id: string, + updateFields: Partial<{ + fullname: string; + description: string; + start_date: Date; + end_date: Date; + }>, +) => { + const setFields: { [key: string]: any } = {}; if (updateFields.fullname) setFields.fullname = updateFields.fullname; - if (updateFields.description) setFields.description = updateFields.description; + if (updateFields.description) + setFields.description = updateFields.description; if (updateFields.start_date) setFields.start_date = updateFields.start_date; if (updateFields.end_date) setFields.end_date = updateFields.end_date; @@ -1327,12 +1572,12 @@ export const update_contest_info:any = async(contest_id: string, updateFields:Pa } const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join(', '); + .map((key) => `${key}: $${key}`) + .join(", "); const variableString = Object.keys(setFields) - .map(key => `$${key}: String`) - .join(', '); + .map((key) => `$${key}: String`) + .join(", "); const mutation = gql` mutation UpdateContest($contest_id: uuid!, ${variableString}) { @@ -1346,22 +1591,22 @@ export const update_contest_info:any = async(contest_id: string, updateFields:Pa } `; - const variables:{[key:string]:any} = { - contest_id:contest_id - } - if(setFields.fullname) variables.fullname = setFields.fullname; - if(setFields.description) variables.description = setFields.description; - if(setFields.start_date) variables.start_date = setFields.start_date; - if(setFields.end_date) variables.end_date = setFields.end_date; + const variables: { [key: string]: any } = { + contest_id: contest_id, + }; + if (setFields.fullname) variables.fullname = setFields.fullname; + if (setFields.description) variables.description = setFields.description; + if (setFields.start_date) variables.start_date = setFields.start_date; + if (setFields.end_date) variables.end_date = setFields.end_date; try { const response: any = await client.request(mutation, variables); return response.update_contest_by_pk?.id ?? undefined; } catch (error) { - console.error('Error updating contest info', error); + console.error("Error updating contest info", error); throw error; } -} +}; /** * Updates the contest switches. @@ -1375,32 +1620,40 @@ export const update_contest_info:any = async(contest_id: string, updateFields:Pa * @param {boolean} playback_switch The new state of the playback switch. * @returns {string} The ID of the updated contest. */ -export const update_contest_switch:any = async(contest_id: string, team_switch: boolean, code_upload_switch: boolean, arena_switch: boolean, playground_switch: boolean, stream_switch: boolean, playback_switch:boolean) => { - const update_contest_switch:any = await client.request( +export const update_contest_switch: any = async ( + contest_id: string, + team_switch: boolean, + code_upload_switch: boolean, + arena_switch: boolean, + playground_switch: boolean, + stream_switch: boolean, + playback_switch: boolean, +) => { + const update_contest_switch: any = await client.request( gql` - mutation UpdateContestSwitch( - $contest_id: uuid! - $team_switch: Boolean! - $code_upload_switch: Boolean! - $arena_switch: Boolean! - $playground_switch: Boolean! - $stream_switch: Boolean! - $playback_switch: Boolean! - ) { - update_contest_by_pk( - pk_columns: { id: $contest_id } - _set: { - team_switch: $team_switch - code_upload_switch: $code_upload_switch - arena_switch: $arena_switch - playground_switch: $playground_switch - stream_switch: $stream_switch - playback_switch: $playback_switch - } + mutation UpdateContestSwitch( + $contest_id: uuid! + $team_switch: Boolean! + $code_upload_switch: Boolean! + $arena_switch: Boolean! + $playground_switch: Boolean! + $stream_switch: Boolean! + $playback_switch: Boolean! ) { - id + update_contest_by_pk( + pk_columns: { id: $contest_id } + _set: { + team_switch: $team_switch + code_upload_switch: $code_upload_switch + arena_switch: $arena_switch + playground_switch: $playground_switch + stream_switch: $stream_switch + playback_switch: $playback_switch + } + ) { + id + } } - } `, { contest_id: contest_id, @@ -1409,12 +1662,12 @@ export const update_contest_switch:any = async(contest_id: string, team_switch: arena_switch: arena_switch, playground_switch: playground_switch, stream_switch: stream_switch, - playback_switch: playback_switch - } + playback_switch: playback_switch, + }, ); - return update_contest_switch.update_contest_by_pk?.id?? undefined; -} + return update_contest_switch.update_contest_by_pk?.id ?? undefined; +}; /** * Updates the contest map. @@ -1425,26 +1678,34 @@ export const update_contest_switch:any = async(contest_id: string, team_switch: * @param {string} team_labels The new team labels of the map. * @returns {string} The ID of the updated map. */ -export const update_contest_map:any = async(map_id:string, updateFields: Partial<{ name: string; filename: string; team_labels: string }>) => { +export const update_contest_map: any = async ( + map_id: string, + updateFields: Partial<{ + name: string; + filename: string; + team_labels: string; + }>, +) => { const setFields: any = {}; - if(updateFields.name) setFields.name = updateFields.name; - if(updateFields.filename) setFields.filename = updateFields.filename; - if(updateFields.team_labels) setFields.team_labels = updateFields.team_labels; + if (updateFields.name) setFields.name = updateFields.name; + if (updateFields.filename) setFields.filename = updateFields.filename; + if (updateFields.team_labels) + setFields.team_labels = updateFields.team_labels; - if(Object.keys(setFields).length === 0 ){ - console.error("At least update one feature"); - return undefined; - } + if (Object.keys(setFields).length === 0) { + console.error("At least update one feature"); + return undefined; + } - const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join(', '); + const setString = Object.keys(setFields) + .map((key) => `${key}: $${key}`) + .join(", "); - const variableString = Object.keys(setFields) - .map(key => `$${key}: String`) - .join(', '); + const variableString = Object.keys(setFields) + .map((key) => `$${key}: String`) + .join(", "); - const mutation = gql` + const mutation = gql` mutation UpdateContestMap($map_id: uuid!, ${variableString}) { update_contest_map_by_pk(pk_columns: { map_id: $map_id }, _set: { ${setString} }) { map_id @@ -1455,29 +1716,29 @@ export const update_contest_map:any = async(map_id:string, updateFields: Partial } `; -/* const variables = { + /* const variables = { map_id, ...setFields } 本以为这样可以,但是不能,因为setFields是一个对象,而variables是一个数组 */ - const variables:{[key:string]:any} = { - map_id: map_id - } + const variables: { [key: string]: any } = { + map_id: map_id, + }; - if(setFields.name) variables.name = setFields.name; - if(setFields.filename) variables.filename = setFields.filename; - if(setFields.team_labels) variables.team_labels = setFields.team_labels; + if (setFields.name) variables.name = setFields.name; + if (setFields.filename) variables.filename = setFields.filename; + if (setFields.team_labels) variables.team_labels = setFields.team_labels; - try { - const response:any = await client.request(mutation,variables); - return response.update_contest_map_by_pk?.map_id?? undefined; - }catch(error){ - console.error('Error updating contest map', error); - throw(error); - } -} + try { + const response: any = await client.request(mutation, variables); + return response.update_contest_map_by_pk?.map_id ?? undefined; + } catch (error) { + console.error("Error updating contest map", error); + throw error; + } +}; /** * Updates the contest notice. @@ -1488,7 +1749,10 @@ export const update_contest_map:any = async(map_id:string, updateFields: Partial * @param {string} files Optional The new files of the notice. * @returns {string} The ID of the updated notice. */ -export const update_contest_notice: any = async (id: string, updateFields: Partial<{ title: string; content: string; files: string }>) => { +export const update_contest_notice: any = async ( + id: string, + updateFields: Partial<{ title: string; content: string; files: string }>, +) => { const setFields: any = {}; if (updateFields.title) setFields.title = updateFields.title; if (updateFields.content) setFields.content = updateFields.content; @@ -1500,12 +1764,12 @@ export const update_contest_notice: any = async (id: string, updateFields: Parti } const variableString = Object.keys(setFields) - .map(key=>`$${key}`) - .join(', '); + .map((key) => `$${key}`) + .join(", "); const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join(', '); + .map((key) => `${key}: $${key}`) + .join(", "); const mutation = gql` mutation UpdateContestNotice($id: uuid!, ${variableString}) { @@ -1518,19 +1782,18 @@ export const update_contest_notice: any = async (id: string, updateFields: Parti } `; - const variables:{[key:string]:any} = { - id:id - } - if(setFields.title) variables.title = setFields.title; - if(setFields.content) variables.content = setFields.content; - if(setFields.files) variables.files = setFields.files; - + const variables: { [key: string]: any } = { + id: id, + }; + if (setFields.title) variables.title = setFields.title; + if (setFields.content) variables.content = setFields.content; + if (setFields.files) variables.files = setFields.files; try { const response: any = await client.request(mutation, variables); return response.update_contest_notice_by_pk?.id ?? undefined; } catch (error) { - console.error('Error updating contest notice', error); + console.error("Error updating contest notice", error); throw error; } }; @@ -1544,9 +1807,15 @@ export const update_contest_notice: any = async (id: string, updateFields: Parti * @param {string} roles_available Optional The new roles available for the player. * @returns {string} The team label of the updated player. */ -export const update_contest_player: any = async (contest_id: string, player_label: string, team_label:string,updateFields: Partial<{roles_available: string }>) => { +export const update_contest_player: any = async ( + contest_id: string, + player_label: string, + team_label: string, + updateFields: Partial<{ roles_available: string }>, +) => { const setFields: any = {}; - if (updateFields.roles_available) setFields.roles_available = updateFields.roles_available; + if (updateFields.roles_available) + setFields.roles_available = updateFields.roles_available; if (Object.keys(setFields).length === 0) { console.error("At least update one feature"); @@ -1554,12 +1823,12 @@ export const update_contest_player: any = async (contest_id: string, player_labe } const variableString = Object.keys(setFields) - .map(key => `$${key}: String`) - .join(', '); + .map((key) => `$${key}: String`) + .join(", "); const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join(', '); + .map((key) => `${key}: $${key}`) + .join(", "); const mutation = gql` mutation UpdateContestPlayer($contest_id: uuid!, $player_label: String!, $team_label:String!,${variableString}) { @@ -1571,19 +1840,20 @@ export const update_contest_player: any = async (contest_id: string, player_labe } `; - const variables:{[key:string]:any} = { - contest_id:contest_id, - player_label:player_label, - team_label:team_label - } + const variables: { [key: string]: any } = { + contest_id: contest_id, + player_label: player_label, + team_label: team_label, + }; - if(setFields.roles_available) variables.roles_available = setFields.roles_available; + if (setFields.roles_available) + variables.roles_available = setFields.roles_available; try { const response: any = await client.request(mutation, variables); return response.update_contest_player_by_pk?.team_label ?? undefined; } catch (error) { - console.error('Error updating contest player', error); + console.error("Error updating contest player", error); throw error; } }; @@ -1595,26 +1865,31 @@ export const update_contest_player: any = async (contest_id: string, player_labe * @param {string} name The new name of the round. * @returns {string} The ID of the updated round. */ -export const update_contest_round_name:any = async(round_id:string,name:string) => { - const update_contest_round_name:any = await client.request( +export const update_contest_round_name: any = async ( + round_id: string, + name: string, +) => { + const update_contest_round_name: any = await client.request( gql` - mutation UpdateContestRoundName($round_id: uuid!, $name: String!) { - update_contest_round_by_pk( - pk_columns: { round_id: $round_id } - _set: { name: $name } - ) { - round_id + mutation UpdateContestRoundName($round_id: uuid!, $name: String!) { + update_contest_round_by_pk( + pk_columns: { round_id: $round_id } + _set: { name: $name } + ) { + round_id + } } - } `, { round_id: round_id, - name: name - } + name: name, + }, ); - return update_contest_round_name.update_contest_round_by_pk?.round_id?? undefined; -} + return ( + update_contest_round_name.update_contest_round_by_pk?.round_id ?? undefined + ); +}; /** * Updates the team code name. @@ -1623,26 +1898,31 @@ export const update_contest_round_name:any = async(round_id:string,name:string) * @param {string} code_name The new name of the code. * @returns {string} The ID of the updated code. */ -export const update_team_code_name:any = async(code_id:string,code_name:string) => { - const update_team_code_name:any = await client.request( +export const update_team_code_name: any = async ( + code_id: string, + code_name: string, +) => { + const update_team_code_name: any = await client.request( gql` - mutation UpdateTeamCodeName($code_id: uuid!, $code_name: String!) { - update_contest_team_code_by_pk( - pk_columns: { code_id: $code_id } - _set: { code_name: $code_name } - ) { - code_id + mutation UpdateTeamCodeName($code_id: uuid!, $code_name: String!) { + update_contest_team_code_by_pk( + pk_columns: { code_id: $code_id } + _set: { code_name: $code_name } + ) { + code_id + } } - } `, { code_id: code_id, - code_name: code_name - } + code_name: code_name, + }, ); - return update_team_code_name.update_contest_team_code_by_pk?.code_id?? undefined; -} + return ( + update_team_code_name.update_contest_team_code_by_pk?.code_id ?? undefined + ); +}; /** * Update a team player's information @@ -1652,32 +1932,39 @@ export const update_team_code_name:any = async(code_id:string,code_name:string) * @param {string} role The role of the player * @returns {Promise} The updated player's information */ -export const update_team_player:any = async(team_id:string,player:string,code_id:string,role:string) =>{ - const update_team_player:any = await client.request( +export const update_team_player: any = async ( + team_id: string, + player: string, + code_id: string, + role: string, +) => { + const update_team_player: any = await client.request( gql` - mutation UpdateTeamPlayer( - $team_id: uuid! - $player: String! - $code_id: uuid - $role: String - ) { - update_contest_team_player_by_pk( - pk_columns: { team_id: $team_id, player: $player } - _set: { code_id: $code_id, role: $role } + mutation UpdateTeamPlayer( + $team_id: uuid! + $player: String! + $code_id: uuid + $role: String ) { - player + update_contest_team_player_by_pk( + pk_columns: { team_id: $team_id, player: $player } + _set: { code_id: $code_id, role: $role } + ) { + player + } } - } `, { team_id: team_id, player: player, code_id: code_id, - role: role - }); - return update_team_player.update_contest_team_player_by_pk?.player?? undefined; - } - + role: role, + }, + ); + return ( + update_team_player.update_contest_team_player_by_pk?.player ?? undefined + ); +}; /** * Update a team's information @@ -1687,7 +1974,10 @@ export const update_team_player:any = async(team_id:string,player:string,code_id * @returns {Promise} The updated team's ID */ -export const update_team:any = async(team_id:string,updateFields:Partial<{ team_name: string; team_intro: string}>) =>{ +export const update_team: any = async ( + team_id: string, + updateFields: Partial<{ team_name: string; team_intro: string }>, +) => { const setFields: any = {}; if (updateFields.team_name) setFields.team_name = updateFields.team_name; if (updateFields.team_intro) setFields.team_intro = updateFields.team_intro; @@ -1698,14 +1988,14 @@ export const update_team:any = async(team_id:string,updateFields:Partial<{ team_ } const variableString = Object.keys(setFields) - .map(key => `$${key}: String`) - .join(',') + .map((key) => `$${key}: String`) + .join(","); const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join(',') + .map((key) => `${key}: $${key}`) + .join(","); - const mutation = gql` + const mutation = gql` mutation UpdateTeam( $team_id: uuid!, ${variableString} @@ -1718,21 +2008,20 @@ export const update_team:any = async(team_id:string,updateFields:Partial<{ team_ } } `; - const variables:{[key:string]:any}= { - team_id:team_id - } - if(setFields.team_name) variables.team_name = setFields.team_name; - if(setFields.team_intro) variables.team_intro = setFields.team_intro; - + const variables: { [key: string]: any } = { + team_id: team_id, + }; + if (setFields.team_name) variables.team_name = setFields.team_name; + if (setFields.team_intro) variables.team_intro = setFields.team_intro; try { const response: any = await client.request(mutation, variables); return response.update_contest_team_by_pk?.team_id ?? undefined; } catch (error) { - console.error('Error updating contest player', error); + console.error("Error updating contest player", error); throw error; } -} +}; /** * Update contest time information @@ -1744,54 +2033,40 @@ export const update_team:any = async(team_id:string,updateFields:Partial<{ team_ * @returns {Promise} The updated event information */ -export const update_contest_time:any = async(contest_id:string,event:string,start:Date,end:Date,description:string) =>{ - const update_contest_time:any = await client.request( +export const update_contest_time: any = async ( + contest_id: string, + event: string, + start: Date, + end: Date, + description: string, +) => { + const update_contest_time: any = await client.request( gql` - mutation UpdateContestTime( - $contest_id: uuid! - $event: String! - $start: timestamptz! - $end: timestamptz! - $description: String - ) { - update_contest_time_by_pk( - pk_columns: { contest_id: $contest_id, event: $event } - _set: { start: $start, end: $end, description: $description } + mutation UpdateContestTime( + $contest_id: uuid! + $event: String! + $start: timestamptz! + $end: timestamptz! + $description: String ) { - event + update_contest_time_by_pk( + pk_columns: { contest_id: $contest_id, event: $event } + _set: { start: $start, end: $end, description: $description } + ) { + event + } } - } `, { contest_id: contest_id, event: event, start: new Date(start), end: new Date(end), - description: description - }); - return update_contest_time.update_contest_time_by_pk?.event?? undefined; - } - - - - - - - - - - - - - - - - - - - - - + description: description, + }, + ); + return update_contest_time.update_contest_time_by_pk?.event ?? undefined; +}; /** ============================================================================ @@ -1799,7 +2074,6 @@ export const update_contest_time:any = async(contest_id:string,event:string,star ============================================================================ */ - /** * delete contest room * @param {string} room_id @@ -1809,19 +2083,18 @@ export const delete_room: any = async (room_id: string) => { const delete_room: any = await client.request( gql` mutation delete_room($room_id: uuid!) { - delete_contest_room(where: {room_id: {_eq: $room_id}}) { + delete_contest_room(where: { room_id: { _eq: $room_id } }) { affected_rows } } `, { - room_id: room_id - } + room_id: room_id, + }, ); return delete_room.delete_contest_room.affected_rows; -} - +}; /** * delete contest room team @@ -1832,25 +2105,25 @@ export const delete_room_team: any = async (room_id: string) => { const delete_room_team: any = await client.request( gql` mutation delete_room_team($room_id: uuid!) { - delete_contest_room_team(where: {room_id: {_eq: $room_id}}) { + delete_contest_room_team(where: { room_id: { _eq: $room_id } }) { affected_rows } } `, { - room_id: room_id - } + room_id: room_id, + }, ); return delete_room_team.delete_contest_room_team.affected_rows; -} +}; /** * Delete a contest * @param {string} contest_id The ID of the contest to be deleted * @returns {Promise} The number of affected rows */ -export const delete_contest:any = async (contest_id: string) => { +export const delete_contest: any = async (contest_id: string) => { const delete_contest: any = await client.request( gql` mutation delete_contest($contest_id: uuid!) { @@ -1860,12 +2133,12 @@ export const delete_contest:any = async (contest_id: string) => { } `, { - contest_id: contest_id - } + contest_id: contest_id, + }, ); - return delete_contest.delete_contest?.affected_rows?? undefined; -} + return delete_contest.delete_contest?.affected_rows ?? undefined; +}; /** * Delete a contest map @@ -1873,7 +2146,7 @@ export const delete_contest:any = async (contest_id: string) => { * @returns {Promise} The ID of the deleted map */ -export const delete_contest_map:any = async (map_id: string) => { +export const delete_contest_map: any = async (map_id: string) => { const delete_contest_map: any = await client.request( gql` mutation delete_contest_map($map_id: uuid!) { @@ -1883,12 +2156,12 @@ export const delete_contest_map:any = async (map_id: string) => { } `, { - map_id: map_id - } + map_id: map_id, + }, ); - return delete_contest_map.delete_contest_map_by_pk?.map_id?? undefined; -} + return delete_contest_map.delete_contest_map_by_pk?.map_id ?? undefined; +}; /** * Delete a contest notice @@ -1896,21 +2169,21 @@ export const delete_contest_map:any = async (map_id: string) => { * @returns {Promise} The ID of the deleted notice */ -export const delete_contest_notice:any = async(id:string) =>{ - const delete_contest_notice:any = await client.request( +export const delete_contest_notice: any = async (id: string) => { + const delete_contest_notice: any = await client.request( gql` - mutation DeleteContestNotice($id: uuid!) { - delete_contest_notice_by_pk(id: $id) { - id + mutation DeleteContestNotice($id: uuid!) { + delete_contest_notice_by_pk(id: $id) { + id + } } - } `, { - id:id - } + id: id, + }, ); - return delete_contest_notice.delete_contest_notice_by_pk?.id?? undefined; -} + return delete_contest_notice.delete_contest_notice_by_pk?.id ?? undefined; +}; /** * Delete a contest player @@ -1920,31 +2193,37 @@ export const delete_contest_notice:any = async(id:string) =>{ * @returns {Promise} The label of the deleted team */ -export const delete_contest_player:any = async(contest_id:string,team_label:string,player_label:string) =>{ - const delete_contest_player:any = await client.request( +export const delete_contest_player: any = async ( + contest_id: string, + team_label: string, + player_label: string, +) => { + const delete_contest_player: any = await client.request( gql` - mutation DeleteContestPlayer( - $contest_id: uuid! - $team_label: String! - $player_label: String! - ) { - delete_contest_player_by_pk( - contest_id: $contest_id - team_label: $team_label - player_label: $player_label + mutation DeleteContestPlayer( + $contest_id: uuid! + $team_label: String! + $player_label: String! ) { - team_label + delete_contest_player_by_pk( + contest_id: $contest_id + team_label: $team_label + player_label: $player_label + ) { + team_label + } } - } `, { contest_id: contest_id, team_label: team_label, - player_label: player_label - }); - return delete_contest_player.delete_contest_player_by_pk?.team_label?? undefined; -} - + player_label: player_label, + }, + ); + return ( + delete_contest_player.delete_contest_player_by_pk?.team_label ?? undefined + ); +}; /** * Delete a contest round @@ -1952,20 +2231,21 @@ export const delete_contest_player:any = async(contest_id:string,team_label:stri * @returns {Promise} The ID of the deleted round */ -export const delete_contest_round:any = async(round_id:string) => { - const delete_contest_round:any = await client.request( +export const delete_contest_round: any = async (round_id: string) => { + const delete_contest_round: any = await client.request( gql` - mutation DeleteContestRound($round_id: uuid!) { - delete_contest_round_by_pk(round_id: $round_id) { - round_id + mutation DeleteContestRound($round_id: uuid!) { + delete_contest_round_by_pk(round_id: $round_id) { + round_id + } } - } `, { - round_id: round_id - }); - return delete_contest_round.delete_contest_round_by_pk?.round_id?? undefined; -} + round_id: round_id, + }, + ); + return delete_contest_round.delete_contest_round_by_pk?.round_id ?? undefined; +}; /** * Delete a team code @@ -1973,22 +2253,21 @@ export const delete_contest_round:any = async(round_id:string) => { * @returns {Promise} The ID of the deleted code */ -export const delete_team_code:any = async(code_id:string)=>{ - const delete_team_code:any = await client.request( +export const delete_team_code: any = async (code_id: string) => { + const delete_team_code: any = await client.request( gql` - mutation DeleteTeamCode($code_id: uuid!) { - delete_contest_team_code_by_pk(code_id: $code_id) { - code_id + mutation DeleteTeamCode($code_id: uuid!) { + delete_contest_team_code_by_pk(code_id: $code_id) { + code_id + } } - } `, { - code_id: code_id - - }); - return delete_team_code.delete_contest_team_code_by_pk?.code_id?? undefined; -} - + code_id: code_id, + }, + ); + return delete_team_code.delete_contest_team_code_by_pk?.code_id ?? undefined; +}; /** * Delete a team @@ -1996,22 +2275,21 @@ export const delete_team_code:any = async(code_id:string)=>{ * @returns {Promise} The ID of the deleted team */ - -export const delete_team:any = async(team_id:string) => { - const delete_team:any = await client.request( +export const delete_team: any = async (team_id: string) => { + const delete_team: any = await client.request( gql` - mutation DeleteTeam($team_id: uuid!) { - delete_contest_team_by_pk(team_id: $team_id) { - team_id + mutation DeleteTeam($team_id: uuid!) { + delete_contest_team_by_pk(team_id: $team_id) { + team_id + } } - } `, { - team_id: team_id - }); - return delete_team.delete_contest_team_by_pk?.team_id?? undefined; -} - + team_id: team_id, + }, + ); + return delete_team.delete_contest_team_by_pk?.team_id ?? undefined; +}; /** * Delete a team member @@ -2020,21 +2298,30 @@ export const delete_team:any = async(team_id:string) => { * @returns {Promise} The ID of the team */ -export const delete_team_member:any = async(user_uuid:string,team_id:string) => { - const delete_team_member:any = await client.request( +export const delete_team_member: any = async ( + user_uuid: string, + team_id: string, +) => { + const delete_team_member: any = await client.request( gql` - mutation DeleteTeamMember($user_uuid: uuid!, $team_id: uuid!) { - delete_contest_team_member_by_pk(user_uuid: $user_uuid, team_id: $team_id) { - team_id + mutation DeleteTeamMember($user_uuid: uuid!, $team_id: uuid!) { + delete_contest_team_member_by_pk( + user_uuid: $user_uuid + team_id: $team_id + ) { + team_id + } } - } `, { user_uuid: user_uuid, - team_id: team_id - }); - return delete_team_member.delete_contest_team_member_by_pk?.team_id?? undefined; -} + team_id: team_id, + }, + ); + return ( + delete_team_member.delete_contest_team_member_by_pk?.team_id ?? undefined + ); +}; /** * Delete contest time @@ -2043,18 +2330,22 @@ export const delete_team_member:any = async(user_uuid:string,team_id:string) => * @returns {Promise} The event of the deleted contest time */ -export const delete_contest_time:any = async(contest_id:string,event:string) => { - const delete_contest_time:any = await client.request( +export const delete_contest_time: any = async ( + contest_id: string, + event: string, +) => { + const delete_contest_time: any = await client.request( gql` - mutation DeleteContestTime($contest_id: uuid!, $event: String!) { - delete_contest_time_by_pk(contest_id: $contest_id, event: $event) { - event + mutation DeleteContestTime($contest_id: uuid!, $event: String!) { + delete_contest_time_by_pk(contest_id: $contest_id, event: $event) { + event + } } - } `, { contest_id: contest_id, - event: event - }); - return delete_contest_time.delete_contest_time_by_pk?.event?? undefined; - } + event: event, + }, + ); + return delete_contest_time.delete_contest_time_by_pk?.event ?? undefined; +}; diff --git a/src/routes/team.ts b/src/routes/team.ts index 871149db..76a029fd 100644 --- a/src/routes/team.ts +++ b/src/routes/team.ts @@ -4,213 +4,329 @@ import * as ContHasFunc from "../hasura/contest"; const router = express.Router(); +router.post("/member_limit", authenticate(), async (req, res) => { + try { + const { contest_id } = req.body; + if (!contest_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const limit = await ContHasFunc.get_contest_member_limit(contest_id); + res.status(200).json({ limit: limit }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); + // used in codepage.tsx router.post("/add_team_code", authenticate(["student"]), async (req, res) => { - try { - const { team_id, code_name, language, compile_status, user_id, contest_id } = req.body; - if (!team_id || !code_name || !language || !compile_status) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - - // 获取团队信息 - const teamInfo = await ContHasFunc.get_team_from_user(user_id, contest_id); - if (!teamInfo) { - return res.status(404).json({ error: "404 Not Found: Team not found" }); - } - // 判断是否是团队成员 - if(teamInfo.team_id !== team_id){ - return res.status(403).json({ error: "403 Forbidden: You are not in this team" }); - } - - const code_id = await ContHasFunc.add_team_code(team_id, code_name, language, compile_status); - res.status(200).json({ code_id:code_id,message:"Code added successfully" }); - } catch (err:any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { + team_id, + code_name, + language, + compile_status, + user_id, + contest_id, + } = req.body; + if (!team_id || !code_name || !language || !compile_status) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + + // 获取团队信息 + const teamInfo = await ContHasFunc.get_team_from_user(user_id, contest_id); + if (!teamInfo) { + return res.status(404).json({ error: "404 Not Found: Team not found" }); + } + // 判断是否是团队成员 + if (teamInfo.team_id !== team_id) { + return res + .status(403) + .json({ error: "403 Forbidden: You are not in this team" }); } -}); + const code_id = await ContHasFunc.add_team_code( + team_id, + code_name, + language, + compile_status, + ); + res + .status(200) + .json({ code_id: code_id, message: "Code added successfully" }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); // used in joinpage.tsx router.post("/add_team_player", authenticate(["student"]), async (req, res) => { - try { - const { team_id, player } = req.body; - if (!team_id || !player) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const player_result = await ContHasFunc.add_team_player(team_id, player); - res.status(200).json({ player: player_result,message:"Team Player Added Successfully" }); - } catch (err:any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { team_id, player } = req.body; + if (!team_id || !player) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); } + const player_result = await ContHasFunc.add_team_player(team_id, player); + res.status(200).json({ + player: player_result, + message: "Team Player Added Successfully", + }); + } catch (err: any) { + if (err.name === "TeamPlayerLimitError") { + return res.status(551).json({ error: "551 Exceeded Limit" }); + } + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); router.post("/add_team", authenticate(["student"]), async (req, res) => { - try { - const { team_name, team_intro, team_leader_uuid, invited_code, contest_id } = req.body; - if (!team_name || !team_intro || !invited_code || !contest_id) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - else if(!team_leader_uuid){ - return res.status(400).json({ error: "400 Bad Request: Missing Team Leader UUID" }); - } - // else if(!isValid(contest_id)){ - //} - const team_id = await ContHasFunc.add_team(team_name, team_intro, team_leader_uuid, invited_code, contest_id); - res.status(200).json({ team_id: team_id,message:"Team Added Successfully" }); - } catch (err:any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { + team_name, + team_intro, + team_leader_uuid, + invited_code, + contest_id, + } = req.body; + if (!team_name || !team_intro || !invited_code || !contest_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } else if (!team_leader_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing Team Leader UUID" }); } + // else if(!isValid(contest_id)){ + //} + const team_id = await ContHasFunc.add_team( + team_name, + team_intro, + team_leader_uuid, + invited_code, + contest_id, + ); + res + .status(200) + .json({ team_id: team_id, message: "Team Added Successfully" }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); router.post("/add_team_member", authenticate(["student"]), async (req, res) => { - try { - const { team_id, user_uuid } = req.body; - if (!team_id || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const team_id_result = await ContHasFunc.add_team_member(team_id, user_uuid); - res.status(200).json({ message:"Team Member Added Successfully",team_id: team_id_result }); - } catch (err:any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { team_id, user_uuid } = req.body; + if (!team_id || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const team_id_result = await ContHasFunc.add_team_member( + team_id, + user_uuid, + ); + res.status(200).json({ + message: "Team Member Added Successfully", + team_id: team_id_result, + }); + } catch (err: any) { + if (err.name === "TeamPlayerLimitError") { + return res.status(650).json({ error: "65: Team member limit reached" }); } + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); - // used in codepage.tsx -router.post("/update_team_code_name", authenticate(["student"]), async (req, res) => { +router.post( + "/update_team_code_name", + authenticate(["student"]), + async (req, res) => { try { - const { code_id, code_name } = req.body; - if (!code_id || !code_name) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const update_team_code_name = await ContHasFunc.update_team_code_name(code_id, code_name); - res.status(200).json({ code_id: update_team_code_name.code_id,message:"Code Name Updated Successfully" }); + const { code_id, code_name } = req.body; + if (!code_id || !code_name) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const update_team_code_name = await ContHasFunc.update_team_code_name( + code_id, + code_name, + ); + res.status(200).json({ + code_id: update_team_code_name.code_id, + message: "Code Name Updated Successfully", + }); } catch (err: any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); - + }, +); // used in codepage.tsx -router.post("/update_team_player", authenticate(["student"]), async (req, res) => { +router.post( + "/update_team_player", + authenticate(["student"]), + async (req, res) => { try { - const { team_id, player, code_id, role } = req.body; - if (!team_id || !player || !code_id || !role) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const update_team_player = await ContHasFunc.update_team_player(team_id, player, code_id, role); - res.status(200).json({ player: update_team_player.player,message:"Player Updated Successfully" }); + const { team_id, player, code_id, role } = req.body; + if (!team_id || !player || !code_id || !role) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const update_team_player = await ContHasFunc.update_team_player( + team_id, + player, + code_id, + role, + ); + res.status(200).json({ + player: update_team_player.player, + message: "Player Updated Successfully", + }); } catch (err: any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // used in managepage.tsx router.post("/update_team", authenticate(["student"]), async (req, res) => { - try { - const { team_id, ...update_Fields } = req.body; - if (!team_id) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters(team_id)" }); - } - const update_team = await ContHasFunc.update_team(team_id, update_Fields); - res.status(200).json({ message:"Team Updated Successfully",team_id: update_team.team_id }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { team_id, ...update_Fields } = req.body; + if (!team_id) { + return res.status(400).json({ + error: "400 Bad Request: Missing required parameters(team_id)", + }); } + const update_team = await ContHasFunc.update_team(team_id, update_Fields); + res.status(200).json({ + message: "Team Updated Successfully", + team_id: update_team.team_id, + }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); - - //used in codepage.tsx -router.post("/delete_team_code", authenticate(["student"]), async (req, res) => { +router.post( + "/delete_team_code", + authenticate(["student"]), + async (req, res) => { try { - const { code_id } = req.body; - if (!code_id) { - return res.status(400).send("400 Bad Request: Missing required parameters"); - } - const delete_team_code = await ContHasFunc.delete_team_code(code_id); - res.status(200).json({ code_id: delete_team_code,message:"Code Deleted Successfully" }); + const { code_id } = req.body; + if (!code_id) { + return res + .status(400) + .send("400 Bad Request: Missing required parameters"); + } + const delete_team_code = await ContHasFunc.delete_team_code(code_id); + res.status(200).json({ + code_id: delete_team_code, + message: "Code Deleted Successfully", + }); } catch (err: any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // used in managepage.tsx router.post("/delete_team", authenticate(["student"]), async (req, res) => { - try { - const { team_id } = req.body; - if (!team_id) { - return res.status(400).send("400 Bad Request: Missing required parameters"); - } - const delete_team = await ContHasFunc.delete_team(team_id); - res.status(200).json({ team_id: delete_team, message:"Team Deleted Successfully" }); - } catch (err: any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { team_id } = req.body; + if (!team_id) { + return res + .status(400) + .send("400 Bad Request: Missing required parameters"); } + const delete_team = await ContHasFunc.delete_team(team_id); + res + .status(200) + .json({ team_id: delete_team, message: "Team Deleted Successfully" }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); // used in managepage.tsx -router.post("/delete_team_member", authenticate(["student"]), async (req, res) => { +router.post( + "/delete_team_member", + authenticate(["student"]), + async (req, res) => { try { - const { user_uuid, team_id } = req.body; - if (!user_uuid || !team_id) { - return res.status(400).send("400 Bad Request: Missing required parameters"); - } - const delete_team_member = await ContHasFunc.delete_team_member(user_uuid, team_id); - res.status(200).json({ team_id: delete_team_member,message:"Team Member Deleted Successfully" }); + const { user_uuid, team_id } = req.body; + if (!user_uuid || !team_id) { + return res + .status(400) + .send("400 Bad Request: Missing required parameters"); + } + const delete_team_member = await ContHasFunc.delete_team_member( + user_uuid, + team_id, + ); + res.status(200).json({ + team_id: delete_team_member, + message: "Team Member Deleted Successfully", + }); } catch (err: any) { - - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); -export default router; \ No newline at end of file +export default router; From 8795da07279281177c0a22dd336fd5eb8a4085be Mon Sep 17 00:00:00 2001 From: konpoku Date: Wed, 18 Jun 2025 00:34:36 +0800 Subject: [PATCH 12/29] debugged for team member limit check --- src/hasura/contest.ts | 14 +++++++------- src/routes/team.ts | 19 ++++++++----------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/hasura/contest.ts b/src/hasura/contest.ts index ac91e667..9902830e 100644 --- a/src/hasura/contest.ts +++ b/src/hasura/contest.ts @@ -1139,11 +1139,6 @@ export class TeamPlayerLimitError extends Error { } export const add_team_player: any = async (team_id: string, player: string) => { - const limit = get_team_member_limit(team_id); - const member_count = await get_team_membercount(team_id); - if (member_count >= limit && limit > 0) { - throw new TeamPlayerLimitError(`Team member limit reached: ${limit}`); - } const add_team_player: any = await client.request( gql` mutation AddTeamPlayer($team_id: uuid!, $player: String!) { @@ -1222,7 +1217,12 @@ export const add_team_member: any = async ( team_id: string, user_uuid: string, ) => { - const add_team_member: any = await client.request( + const limit = await get_team_member_limit(team_id); + const member_count = await get_team_membercount(team_id); + if (member_count >= limit && limit > 0) { + return false; + } + await client.request( gql` mutation AddTeamMember($team_id: uuid!, $user_uuid: uuid!) { insert_contest_team_member_one( @@ -1237,7 +1237,7 @@ export const add_team_member: any = async ( user_uuid: user_uuid, }, ); - return add_team_member.insert_contest_team_member_one?.team_id ?? undefined; + return true; }; /** diff --git a/src/routes/team.ts b/src/routes/team.ts index 76a029fd..19bad07c 100644 --- a/src/routes/team.ts +++ b/src/routes/team.ts @@ -85,9 +85,6 @@ router.post("/add_team_player", authenticate(["student"]), async (req, res) => { message: "Team Player Added Successfully", }); } catch (err: any) { - if (err.name === "TeamPlayerLimitError") { - return res.status(551).json({ error: "551 Exceeded Limit" }); - } res.status(500).json({ error: "500 Internal Server Error", message: err.message, @@ -137,23 +134,23 @@ router.post("/add_team", authenticate(["student"]), async (req, res) => { router.post("/add_team_member", authenticate(["student"]), async (req, res) => { try { - const { team_id, user_uuid } = req.body; + const team_id = req.body.team_id; + const user_uuid = req.body.user_uuid; if (!team_id || !user_uuid) { return res .status(400) .json({ error: "400 Bad Request: Missing required parameters" }); } - const team_id_result = await ContHasFunc.add_team_member( - team_id, - user_uuid, - ); - res.status(200).json({ + const result = await ContHasFunc.add_team_member(team_id, user_uuid); + if (!result) { + return res.status(551).json({ error: "551: Team member limit reached" }); + } + return res.status(200).json({ message: "Team Member Added Successfully", - team_id: team_id_result, }); } catch (err: any) { if (err.name === "TeamPlayerLimitError") { - return res.status(650).json({ error: "65: Team member limit reached" }); + return res.status(551).json({ error: "551: Team member limit reached" }); } res.status(500).json({ error: "500 Internal Server Error", From 40068397528613df3f7579111cb17990a9c18063 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sat, 15 Nov 2025 22:37:21 +0800 Subject: [PATCH 13/29] feat(api): add user profile update logic --- src/hasura/user.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/routes/user.ts | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/hasura/user.ts diff --git a/src/hasura/user.ts b/src/hasura/user.ts new file mode 100644 index 00000000..e161c426 --- /dev/null +++ b/src/hasura/user.ts @@ -0,0 +1,38 @@ +import { gql } from "graphql-request"; +import { client } from ".."; + +export const mutation_update_user_profile = async (uuid: string, className?: string, department?: string, realname?: string, student_no?: string, username?: string) => { + const variables: any = { }; + if (className !== undefined) variables.class = className; + if (department !== undefined) variables.department = department; + if (realname !== undefined) variables.realname = realname; + if (student_no !== undefined) variables.student_no = student_no; + if (username !== undefined) variables.username = username; + const query: any = await client.request( + gql` + mutation UpdateProfile( + $uuid: uuid! + $class: String + $department: String + $realname: String + $student_no: String + $username: String + ) { + update_users_by_pk( + pk_columns: { uuid: $uuid } + _set: { + class: $class + department: $department + username: $username + student_no: $student_no + realname: $realname + } + ) { + updated_at + } + } + `, + { uuid, ...variables} + ); + return query.update_users_by_pk.updated_at; +} diff --git a/src/routes/user.ts b/src/routes/user.ts index 9d0009e3..289f3227 100644 --- a/src/routes/user.ts +++ b/src/routes/user.ts @@ -12,6 +12,9 @@ import authenticate, { import * as validator from "../helpers/validate"; import { client } from ".."; import { sendMessageVerifyCode } from "../helpers/short_message"; +import { mutation_update_user_profile } from "../hasura/user"; + + const router = express.Router(); @@ -675,10 +678,43 @@ router.post("/edit-profile", authenticate(), async (req, res) => { } catch (err) { console.error(err); return res.status(500).send(err); + }; +}); + +router.post("/update", authenticate(), async(req, res) => { + /** + * @route POST /user/update + * @description 更新用户资料(除email/phone/password外的其他字段) + * @body {className?: string, department?: string, realname?: string, student_no?: string, username?: string} + * @returns 更改状态 + */ + const updates = req.body; + if (!updates.className && !updates.department && !updates.realname && !updates.student_no && !updates.username) { + return res.status(422).send("422 Unprocessable Entity: Missing fields to update"); + } + const className = updates.className; + const department = updates.department; + const realname = updates.realname; + const student_no = updates.student_no; + const username = updates.username; + if (Object.keys(updates).length === 0) { + return res.status(422).send("422 Unprocessable Entity: No fields to update"); + } + try { + const result = mutation_update_user_profile(req.auth.user.uuid, className, department, realname, student_no, username); + if (result) { + return res.status(200).send(result); + } else { + return res.status(500).send("500 Internal Server Error: Failed to update user profile"); + } + } catch (err) { + console.error(err); + return res.status(500).send(err); } }); -router.post("/delete", authenticate(), async (req, res) => { + +router.post("/delete", authenticate(), async(req, res) => { /** * @route POST /user/delete * @description 删除用户。先验证请求中的验证码与`verificationToken`中的是否一致,再删除`hasura`中的数据列 From b921ceb5369417d216f0c1642c4c5bfb28b6db2c Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 16 Dec 2025 01:04:01 +0800 Subject: [PATCH 14/29] added LLM backend --- package.json | 2 + src/app.ts | 6 +- src/env.ts | 2 + src/hasura/llm.ts | 118 ++++++++++++++++++++++++++++++++++++++++ src/helpers/llm_cron.ts | 37 +++++++++++++ src/helpers/redis.ts | 15 +++++ src/index.ts | 6 +- yarn.lock | 62 +++++++++++++++++++++ 8 files changed, 244 insertions(+), 4 deletions(-) create mode 100644 src/env.ts create mode 100644 src/hasura/llm.ts create mode 100644 src/helpers/llm_cron.ts create mode 100644 src/helpers/redis.ts diff --git a/package.json b/package.json index b9f7422e..981c4ae9 100644 --- a/package.json +++ b/package.json @@ -26,12 +26,14 @@ "graphql": "16.12.0", "graphql-request": "6.1.0", "html-to-text": "9.0.5", + "ioredis": "^5.3.2", "isemail": "3.2.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.2", "morgan": "1.10.1", "node-cron": "4.2.1", "nodemailer": "7.0.11", + "openai": "^6.10.0", "qcloud-cos-sts": "3.1.3", "unisms": "0.0.6", "web-push": "3.6.7" diff --git a/src/app.ts b/src/app.ts index c560f11c..945bffe4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,6 +20,7 @@ import chatRoute from "./routes/chat"; import mentorRoute from "./routes/mentor"; import noticeRoute from "./routes/notice"; import courseRouter from "./routes/course"; +import llmRouter from "./routes/llm"; const app = express(); @@ -37,7 +38,7 @@ app.use( callback(new Error("Not allowed by CORS")); } }, - }) + }), ); app.use(logger(process.env.NODE_ENV === "production" ? "combined" : "dev")); @@ -48,9 +49,10 @@ app.use("/static", staticRouter); app.use("/user", userRouter); app.use("/emails", emailRouter); app.use("/weekly", weeklyRouter); +app.use("/llm", llmRouter); app.use("/docs", docsRouter); app.use("/application", applicationRouter); -app.use("/files",fileRouter); +app.use("/files", fileRouter); app.use("/code", codeRouter); // app.use("/contest", contestRouter); // app.use("/room", roomRouter); diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..7ff27f7c --- /dev/null +++ b/src/env.ts @@ -0,0 +1,2 @@ +import dotenv from "dotenv"; +dotenv.config(); diff --git a/src/hasura/llm.ts b/src/hasura/llm.ts new file mode 100644 index 00000000..7b280b79 --- /dev/null +++ b/src/hasura/llm.ts @@ -0,0 +1,118 @@ +import { gql } from "graphql-request"; +import { client } from ".."; + +export const get_user_llm_usage = async (student_no: string) => { + const query: any = await client.request( + gql` + query GetUserLlmUsage($student_no: String!) { + user_llm_usage_by_pk(student_no: $student_no) { + student_no + total_tokens_used + token_limit + } + } + `, + { student_no }, + ); + return query.user_llm_usage_by_pk; +}; + +export const init_user_llm_usage = async ( + student_no: string, + token_limit: number = 0, +) => { + const query: any = await client.request( + gql` + mutation InitUserLlmUsage($student_no: String!, $token_limit: bigint!) { + insert_user_llm_usage_one( + object: { student_no: $student_no, token_limit: $token_limit } + on_conflict: { constraint: user_llm_usage_pkey, update_columns: [] } + ) { + student_no + token_limit + total_tokens_used + } + } + `, + { student_no, token_limit }, + ); + return query.insert_user_llm_usage_one; +}; + +export const update_user_llm_usage = async ( + student_no: string, + tokens_to_add: number, +) => { + const query: any = await client.request( + gql` + mutation UpdateUserLlmUsage( + $student_no: String! + $tokens_to_add: bigint! + $updated_at: timestamptz! + ) { + update_user_llm_usage_by_pk( + pk_columns: { student_no: $student_no } + _inc: { total_tokens_used: $tokens_to_add } + _set: { last_updated_at: $updated_at } + ) { + total_tokens_used + token_limit + } + } + `, + { + student_no, + tokens_to_add, + updated_at: new Date().toISOString(), + }, + ); + return query.update_user_llm_usage_by_pk; +}; + +export const set_user_llm_usage = async ( + student_no: string, + total_usage: number, +) => { + const query: any = await client.request( + gql` + mutation SetUserLlmUsage( + $student_no: String! + $total_usage: bigint! + $updated_at: timestamptz! + ) { + update_user_llm_usage_by_pk( + pk_columns: { student_no: $student_no } + _set: { + total_tokens_used: $total_usage + last_updated_at: $updated_at + } + ) { + total_tokens_used + token_limit + } + } + `, + { + student_no, + total_usage, + updated_at: new Date().toISOString(), + }, + ); + return query.update_user_llm_usage_by_pk; +}; + +export const get_llm_model_config = async (model_value: string) => { + const query: any = await client.request( + gql` + query GetLlmModelConfig($model_value: String!) { + llm_list(where: { value: { _eq: $model_value } }) { + name + value + deepthinkingmodel + } + } + `, + { model_value }, + ); + return query.llm_list[0]; +}; diff --git a/src/helpers/llm_cron.ts b/src/helpers/llm_cron.ts new file mode 100644 index 00000000..cc9f6a93 --- /dev/null +++ b/src/helpers/llm_cron.ts @@ -0,0 +1,37 @@ +import cron from "node-cron"; +import redis from "./redis"; +import { set_user_llm_usage } from "../hasura/llm"; + +export const llm_cron = () => { + // Run every 5 minutes + cron.schedule("*/5 * * * *", async () => { + console.log("Starting LLM usage sync..."); + let cursor = "0"; + do { + // Scan for usage keys + const result = await redis.scan( + cursor, + "MATCH", + "llm_usage:*", + "COUNT", + "100", + ); + cursor = result[0]; + const keys = result[1]; + + for (const key of keys) { + try { + const studentNo = key.split(":")[1]; + const usageStr = await redis.get(key); + if (usageStr) { + const usage = parseInt(usageStr); + await set_user_llm_usage(studentNo, usage); + } + } catch (e) { + console.error(`Failed to sync usage for key ${key}:`, e); + } + } + } while (cursor !== "0"); + console.log("LLM usage sync completed."); + }); +}; diff --git a/src/helpers/redis.ts b/src/helpers/redis.ts new file mode 100644 index 00000000..82051f8d --- /dev/null +++ b/src/helpers/redis.ts @@ -0,0 +1,15 @@ +import Redis from "ioredis"; + +const redisUrl = process.env.REDIS_URL || "redis://localhost:6379"; + +const redis = new Redis(redisUrl); + +redis.on("error", (err) => { + console.error("Redis connection error:", err); +}); + +redis.on("connect", () => { + console.log("Connected to Redis"); +}); + +export default redis; diff --git a/src/index.ts b/src/index.ts index 9446e5cd..60c619f2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ +import "./env"; import Debug from "debug"; -import dotenv from "dotenv"; import http from "http"; import app from "./app"; import { GraphQLClient } from "graphql-request"; import { queue_element } from "./helpers/docker_queue"; +import { llm_cron } from "./helpers/llm_cron"; // import docker_cron from "./helpers/docker_queue"; -dotenv.config(); const debug = Debug("eesast-api"); const normalizePort: (val: string) => number | boolean = (val) => { @@ -36,6 +36,8 @@ export const docker_queue: queue_element[] = []; // weekly_cron(); // weekly_init(); +llm_cron(); + const port = normalizePort(process.env.PORT || "28888"); app.set("port", port); diff --git a/yarn.lock b/yarn.lock index 47e977cc..b72d0dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -619,6 +619,11 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/retry/-/retry-0.4.3.tgz#c2b9d2e374ee62c586d3adbea87199b1d7a7a6ba" integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== +"@ioredis/commands@1.4.0": + version "1.4.0" + resolved "https://registry.npmmirror.com/@ioredis/commands/-/commands-1.4.0.tgz#9f657d51cdd5d2fdb8889592aa4a355546151f25" + integrity sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ== + "@jridgewell/resolve-uri@^3.0.3": version "3.1.2" resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" @@ -1817,6 +1822,11 @@ cliui@^8.0.1: strip-ansi "^6.0.1" wrap-ansi "^7.0.0" +cluster-key-slot@^1.1.0: + version "1.1.2" + resolved "https://registry.npmmirror.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" + integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== + color-convert@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" @@ -2030,6 +2040,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== +denque@^2.1.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" + integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== + depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" @@ -2789,6 +2804,21 @@ inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0 resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +ioredis@^5.3.2: + version "5.8.2" + resolved "https://registry.npmmirror.com/ioredis/-/ioredis-5.8.2.tgz#c7a228a26cf36f17a5a8011148836877780e2e14" + integrity sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q== + dependencies: + "@ioredis/commands" "1.4.0" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.1.0" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ipaddr.js@1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" @@ -3051,11 +3081,21 @@ lodash.camelcase@^4.3.0: resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" integrity sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA== +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.npmmirror.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w== +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.npmmirror.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isboolean@^3.0.3: version "3.0.3" resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6" @@ -3361,6 +3401,11 @@ onetime@^7.0.0: dependencies: mimic-function "^5.0.0" +openai@^6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/openai/-/openai-6.10.0.tgz#3f52d2ad7b6b2288124d064b0eb737c914d1f3ea" + integrity sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A== + optionator@^0.9.3: version "0.9.4" resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.4.tgz#7ea1c1a5d91d764fb282139c88fe11e182a3a734" @@ -3618,6 +3663,18 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.npmmirror.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.npmmirror.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== + dependencies: + redis-errors "^1.0.0" + request@^2.88.2: version "2.88.2" resolved "https://registry.yarnpkg.com/request/-/request-2.88.2.tgz#d73c918731cb5a87da047e207234146f664d12b3" @@ -3889,6 +3946,11 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.npmmirror.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + statuses@2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" From 6176b38f8bdb15807e5ac96819e36053a9ea8f15 Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 16 Dec 2025 01:04:30 +0800 Subject: [PATCH 15/29] debug: LLM route added --- src/routes/llm.ts | 390 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 390 insertions(+) create mode 100644 src/routes/llm.ts diff --git a/src/routes/llm.ts b/src/routes/llm.ts new file mode 100644 index 00000000..f38608ea --- /dev/null +++ b/src/routes/llm.ts @@ -0,0 +1,390 @@ +import express from "express"; +import OpenAI from "openai"; +import jwt from "jsonwebtoken"; +import fs from "fs"; +import path from "path"; +import redis from "../helpers/redis"; +import { + get_user_llm_usage, + init_user_llm_usage, + get_llm_model_config, +} from "../hasura/llm"; + +const router = express.Router(); + +// Configuration +const PUBLIC_KEY_PATH = + process.env.LLM_PUBLIC_KEY_PATH || + path.join(__dirname, "../../public_key.pem"); +const JWT_SECRET = process.env.LLM_JWT_SECRET || "eesast_llm_session_secret"; +const SESSION_EXPIRY = "12h"; // Session token expiry + +// Helper to get global quota dynamically +const getGlobalQuota = async () => { + try { + const redisDefault = await redis.get("llm_global_limit"); + if (redisDefault) return parseInt(redisDefault); + } catch (e) { + console.error("Failed to get global limit from Redis", e); + } + return parseInt(process.env.LLM_DEFAULT_LIMIT || "500000"); +}; + +// Helper to read public key +const getPublicKey = () => { + try { + if (process.env.LLM_PUBLIC_KEY) { + return process.env.LLM_PUBLIC_KEY; + } + if (fs.existsSync(PUBLIC_KEY_PATH)) { + return fs.readFileSync(PUBLIC_KEY_PATH, "utf8"); + } + console.warn("LLM Public Key not found at " + PUBLIC_KEY_PATH); + return null; + } catch (e) { + console.error("Error reading public key:", e); + return null; + } +}; + +// Middleware to verify LLM Session Token +const verifySession = async ( + req: express.Request, + res: express.Response, + next: express.NextFunction, +) => { + const authHeader = req.headers.authorization; + if (!authHeader || !authHeader.startsWith("Bearer ")) { + return res + .status(401) + .json({ error: "Missing or invalid Authorization header" }); + } + + const token = authHeader.split(" ")[1]; + + try { + const decoded = jwt.verify(token, JWT_SECRET) as any; + const studentNo = decoded.sub; + const iat = decoded.iat; + + // Check if token is invalidated by a newer login + const minIat = await redis.get(`llm_min_iat:${studentNo}`); + if (minIat && iat && iat < parseInt(minIat)) { + return res + .status(401) + .json({ error: "Session expired (logged in elsewhere)" }); + } + + // Attach user info to request + (req as any).llmUser = { + studentNo: studentNo, + email: decoded.email, + role: decoded.role, + }; + + next(); + } catch (err) { + return res.status(401).json({ error: "Invalid session token" }); + } +}; + +// 1. Verify Access Key and Exchange for Session Token +router.post("/verify", async (req, res) => { + const { accessKey } = req.body; + + if (!accessKey) { + return res.status(400).json({ error: "Access Key is required" }); + } + + const publicKey = getPublicKey(); + if (!publicKey) { + return res + .status(500) + .json({ error: "Server configuration error (Public Key missing)" }); + } + + try { + // Verify RSA signature + const decoded = jwt.verify(accessKey, publicKey, { + algorithms: ["RS256"], + }) as any; + const { sub: studentNo, jti, exp, email, quota } = decoded; + + // Check Replay Attack (Redis) + const isUsed = await redis.get(`used_key:${jti}`); + if (isUsed) { + return res + .status(403) + .json({ error: "Access Key has already been used" }); + } + + // Mark Key as used (expire at key's expiration time) + const ttl = exp ? exp - Math.floor(Date.now() / 1000) : 3600 * 24 * 7; + if (ttl > 0) { + await redis.set(`used_key:${jti}`, "1", "EX", ttl); + } + + // Invalidate old sessions + const now = Math.floor(Date.now() / 1000); + await redis.set(`llm_min_iat:${studentNo}`, now); + + // Initialize Quota if not exists + // We use '0' in DB to indicate "Follow Global Limit" + + // Try to get from DB first + let dbUser = await get_user_llm_usage(studentNo); + + if (!dbUser) { + // Create in DB if not exists + // If Access Key has specific quota, use it. Otherwise use 0 (Global). + const limit = quota || 0; + dbUser = await init_user_llm_usage(studentNo, limit); + } + + // Sync DB limit to Redis + // If DB limit is > 0, it's a custom limit -> Set Redis key + // If DB limit is 0, it's global -> Delete Redis key (so /chat falls back to global) + const dbLimit = dbUser?.token_limit || 0; + + if (dbLimit > 0) { + await redis.set(`llm_limit:${studentNo}`, dbLimit); + } else { + // If quota was provided in Access Key but DB update failed/race condition, use quota + if (quota && quota > 0) { + await redis.set(`llm_limit:${studentNo}`, quota); + } else { + await redis.del(`llm_limit:${studentNo}`); + } + } + + // Sync usage from DB to Redis if Redis is empty (e.g. after restart) + const currentUsage = await redis.get(`llm_usage:${studentNo}`); + if (!currentUsage && dbUser) { + await redis.set(`llm_usage:${studentNo}`, dbUser.total_tokens_used); + } + + // Issue Session Token + const sessionToken = jwt.sign( + { + sub: studentNo, + email: email, + role: "student", + type: "llm_session", + }, + JWT_SECRET, + { expiresIn: SESSION_EXPIRY }, + ); + + res.json({ + token: sessionToken, + user: { + studentNo, + email, + }, + }); + } catch (err) { + console.error("Access Key Verification Failed:", err); + return res.status(403).json({ error: "Invalid or expired Access Key" }); + } +}); + +// 2. Chat Endpoint +router.post("/chat", verifySession, async (req, res) => { + const { messages, model } = req.body; + const user = (req as any).llmUser; + const studentNo = user.studentNo; + + if (!messages || !Array.isArray(messages)) { + return res.status(400).json({ error: "Messages array is required" }); + } + + // Concurrency Control + const activeKey = `llm_active:${studentNo}`; + const activeCount = await redis.incr(activeKey); + + // Set TTL for safety (e.g., 5 mins) + if (activeCount === 1) { + await redis.expire(activeKey, 300); + } + + if (activeCount > 1) { + await redis.decr(activeKey); + return res + .status(429) + .json({ + error: + "Too many concurrent requests. Please wait for the previous request to finish.", + }); + } + + // Rate Limiting (e.g., 1 request per 3 seconds) + const rateLimitKey = `llm_rate_limit:${studentNo}`; + const isRateLimited = await redis.get(rateLimitKey); + if (isRateLimited) { + await redis.decr(activeKey); // Release concurrency lock + return res + .status(429) + .json({ error: "Request too frequent. Please wait a few seconds." }); + } + // Set rate limit flag for 3 seconds + await redis.set(rateLimitKey, "1", "EX", 3); + + // Setup AbortController for client disconnect + const controller = new AbortController(); + req.on("close", () => { + controller.abort(); + }); + + try { + // Quota Check + const usageKey = `llm_usage:${studentNo}`; + const limitKey = `llm_limit:${studentNo}`; + + const [usageStr, limitStr] = await Promise.all([ + redis.get(usageKey), + redis.get(limitKey), + ]); + + const usage = parseInt(usageStr || "0"); + let limit = parseInt(limitStr || "0"); + + // If no custom limit in Redis, use Global Limit + if (!limitStr) { + limit = await getGlobalQuota(); + } + + if (usage >= limit) { + throw new Error("QUOTA_EXCEEDED"); + } + + let apiKey = process.env.LLM_API_KEY; + let baseURL = process.env.LLM_API_URL; + + // Special configuration for Qwen3-Max + if (model === "Qwen3-Max") { + if (process.env.QWEN_API_KEY) { + apiKey = process.env.QWEN_API_KEY; + if (process.env.QWEN_API_URL) { + baseURL = process.env.QWEN_API_URL; + } + } + } + + if (!apiKey) { + // Mock response for testing + res.json({ + choices: [ + { + message: { + role: "assistant", + content: "Mock: Backend configured but no API Key.", + }, + }, + ], + }); + return; + } + + // Check for deep thinking configuration + let enableThinking = false; + try { + const modelConfig = await get_llm_model_config(model || "gpt-3.5-turbo"); + if (modelConfig && modelConfig.deepthinkingmodel === "enabled") { + enableThinking = true; + } + } catch (e) { + console.warn("Failed to fetch model config:", e); + } + + const client = new OpenAI({ + apiKey: apiKey, + baseURL: baseURL, + }); + + const requestOptions: any = { + model: model || "gpt-3.5-turbo", + messages: messages, + stream: true, + stream_options: { include_usage: true }, // Request usage stats + }; + + if (enableThinking) { + requestOptions.enable_thinking = true; + } + + const stream = (await client.chat.completions.create(requestOptions, { + signal: controller.signal, + })) as any; + + res.setHeader("Content-Type", "text/event-stream"); + res.setHeader("Cache-Control", "no-cache"); + res.setHeader("Connection", "keep-alive"); + + let totalTokens = 0; + + for await (const chunk of stream) { + // Handle usage if provided in the last chunk + if (chunk.usage) { + totalTokens = chunk.usage.total_tokens; + } + + const delta = chunk.choices[0]?.delta; + if (delta) { + const reasoning = delta.reasoning_content; + const content = delta.content; + if (content || reasoning) { + res.write(`data: ${JSON.stringify({ content, reasoning })}\n\n`); + } + } + } + + // If usage was not returned by stream (some providers don't), estimate it + if (totalTokens === 0) { + // Rough estimation: 1 char ~= 0.5 token (very rough, but better than nothing) + // In production, use a tokenizer library like tiktoken + const inputLen = messages.reduce( + (acc: number, m: any) => acc + (m.content?.length || 0), + 0, + ); + // We don't have the full output content easily here without buffering, + // but we can assume some average or just count input for now if stream usage is missing. + // For now, let's just count input * 1.5 as a fallback + totalTokens = Math.ceil(inputLen * 0.7); + } + + // Update Usage in Redis + if (totalTokens > 0) { + await redis.incrby(usageKey, totalTokens); + } + + res.write("data: [DONE]\n\n"); + res.end(); + } catch (error: any) { + if (error.message === "QUOTA_EXCEEDED") { + res + .status(402) + .json({ error: "Token quota exceeded. Please contact admin." }); + } else { + console.error("LLM API Error:", error); + // If headers sent, we can't send JSON error, just end stream + if (res.headersSent) { + res.write( + `data: ${JSON.stringify({ error: "Internal Server Error" })}\n\n`, + ); + res.end(); + } else { + res + .status(500) + .json({ + error: "Failed to fetch from LLM provider", + details: error.message, + }); + } + } + } finally { + // Release Concurrency Lock + await redis.decr(activeKey); + } +}); + +export default router; From 335a942b12b7f00df9fcbd0aeb42020f9fde08e1 Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 16 Dec 2025 01:09:51 +0800 Subject: [PATCH 16/29] debug: fixed public key handling --- src/routes/llm.ts | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/routes/llm.ts b/src/routes/llm.ts index f38608ea..f3e36b5b 100644 --- a/src/routes/llm.ts +++ b/src/routes/llm.ts @@ -34,7 +34,7 @@ const getGlobalQuota = async () => { const getPublicKey = () => { try { if (process.env.LLM_PUBLIC_KEY) { - return process.env.LLM_PUBLIC_KEY; + return process.env.LLM_PUBLIC_KEY.replace(/\\n/g, "\n"); } if (fs.existsSync(PUBLIC_KEY_PATH)) { return fs.readFileSync(PUBLIC_KEY_PATH, "utf8"); @@ -209,12 +209,10 @@ router.post("/chat", verifySession, async (req, res) => { if (activeCount > 1) { await redis.decr(activeKey); - return res - .status(429) - .json({ - error: - "Too many concurrent requests. Please wait for the previous request to finish.", - }); + return res.status(429).json({ + error: + "Too many concurrent requests. Please wait for the previous request to finish.", + }); } // Rate Limiting (e.g., 1 request per 3 seconds) @@ -373,12 +371,10 @@ router.post("/chat", verifySession, async (req, res) => { ); res.end(); } else { - res - .status(500) - .json({ - error: "Failed to fetch from LLM provider", - details: error.message, - }); + res.status(500).json({ + error: "Failed to fetch from LLM provider", + details: error.message, + }); } } } finally { From bb713f03868a48277bcb8ce197fd00a0e91f16ca Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 16 Dec 2025 21:50:03 +0800 Subject: [PATCH 17/29] fixed several bugs in LLM api --- src/hasura/llm.ts | 21 +++++++++++++++++---- src/routes/llm.ts | 19 +++++++++++++++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/hasura/llm.ts b/src/hasura/llm.ts index 7b280b79..ced176fe 100644 --- a/src/hasura/llm.ts +++ b/src/hasura/llm.ts @@ -20,21 +20,34 @@ export const get_user_llm_usage = async (student_no: string) => { export const init_user_llm_usage = async ( student_no: string, token_limit: number = 0, + email?: string, ) => { const query: any = await client.request( gql` - mutation InitUserLlmUsage($student_no: String!, $token_limit: bigint!) { + mutation InitUserLlmUsage( + $student_no: String! + $token_limit: bigint! + $email: String + ) { insert_user_llm_usage_one( - object: { student_no: $student_no, token_limit: $token_limit } - on_conflict: { constraint: user_llm_usage_pkey, update_columns: [] } + object: { + student_no: $student_no + token_limit: $token_limit + email: $email + } + on_conflict: { + constraint: user_llm_usage_pkey + update_columns: [email] + } ) { student_no token_limit total_tokens_used + email } } `, - { student_no, token_limit }, + { student_no, token_limit, email }, ); return query.insert_user_llm_usage_one; }; diff --git a/src/routes/llm.ts b/src/routes/llm.ts index f3e36b5b..326cfe4d 100644 --- a/src/routes/llm.ts +++ b/src/routes/llm.ts @@ -27,7 +27,7 @@ const getGlobalQuota = async () => { } catch (e) { console.error("Failed to get global limit from Redis", e); } - return parseInt(process.env.LLM_DEFAULT_LIMIT || "500000"); + return parseInt(process.env.LLM_DEFAULT_LIMIT || "5000000"); }; // Helper to read public key @@ -138,7 +138,14 @@ router.post("/verify", async (req, res) => { // Create in DB if not exists // If Access Key has specific quota, use it. Otherwise use 0 (Global). const limit = quota || 0; - dbUser = await init_user_llm_usage(studentNo, limit); + dbUser = await init_user_llm_usage(studentNo, limit, email); + } else if (email && dbUser.email !== email) { + // Update email if it's different (and we have a new one) + // We can reuse init_user_llm_usage because of on_conflict update_columns: [email] + // But we should be careful not to reset token_limit if we don't want to. + // However, init_user_llm_usage currently takes token_limit. + // Let's just call it with the existing limit to update the email. + dbUser = await init_user_llm_usage(studentNo, dbUser.token_limit, email); } // Sync DB limit to Redis @@ -258,8 +265,12 @@ router.post("/chat", verifySession, async (req, res) => { let apiKey = process.env.LLM_API_KEY; let baseURL = process.env.LLM_API_URL; - // Special configuration for Qwen3-Max - if (model === "Qwen3-Max") { + // Special configuration for Qwen models + if ( + model && + (model.toLowerCase().includes("qwen") || + model.toLowerCase().includes("qwq")) + ) { if (process.env.QWEN_API_KEY) { apiKey = process.env.QWEN_API_KEY; if (process.env.QWEN_API_URL) { From 65f79b2c9a084926a10baa87b405b09d35ea7e31 Mon Sep 17 00:00:00 2001 From: konpoku Date: Tue, 16 Dec 2025 23:17:38 +0800 Subject: [PATCH 18/29] fixed access key usage logging issues --- src/hasura/llm.ts | 38 ++++++++++++++++++++++++++++++++++++++ src/routes/llm.ts | 23 +++++++++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/hasura/llm.ts b/src/hasura/llm.ts index ced176fe..0ba36810 100644 --- a/src/hasura/llm.ts +++ b/src/hasura/llm.ts @@ -129,3 +129,41 @@ export const get_llm_model_config = async (model_value: string) => { ); return query.llm_list[0]; }; + +export const log_access_key_usage = async ( + student_no: string, + jti: string, + email?: string, +) => { + const query: any = await client.request( + gql` + mutation LogAccessKeyUsage( + $student_no: String! + $jti: String! + $email: String + ) { + insert_access_key_log_one( + object: { student_no: $student_no, jti: $jti, email: $email } + ) { + id + } + } + `, + { student_no, jti, email }, + ); + return query.insert_access_key_log_one; +}; + +export const check_access_key_usage = async (jti: string) => { + const query: any = await client.request( + gql` + query CheckAccessKeyUsage($jti: String!) { + access_key_log(where: { jti: { _eq: $jti } }) { + id + } + } + `, + { jti }, + ); + return query.access_key_log.length > 0; +}; diff --git a/src/routes/llm.ts b/src/routes/llm.ts index 326cfe4d..fbcfba81 100644 --- a/src/routes/llm.ts +++ b/src/routes/llm.ts @@ -8,6 +8,8 @@ import { get_user_llm_usage, init_user_llm_usage, get_llm_model_config, + log_access_key_usage, + check_access_key_usage, } from "../hasura/llm"; const router = express.Router(); @@ -118,12 +120,33 @@ router.post("/verify", async (req, res) => { .json({ error: "Access Key has already been used" }); } + // Check Replay Attack (DB - Persistence) + const isUsedDB = await check_access_key_usage(jti); + if (isUsedDB) { + // Restore Redis state for performance + const ttl = exp ? exp - Math.floor(Date.now() / 1000) : 3600 * 24 * 7; + if (ttl > 0) { + await redis.set(`used_key:${jti}`, "1", "EX", ttl); + } + return res + .status(403) + .json({ error: "Access Key has already been used" }); + } + // Mark Key as used (expire at key's expiration time) const ttl = exp ? exp - Math.floor(Date.now() / 1000) : 3600 * 24 * 7; if (ttl > 0) { await redis.set(`used_key:${jti}`, "1", "EX", ttl); } + // Log Access Key usage to DB + try { + await log_access_key_usage(studentNo, jti, email); + } catch (e) { + console.error("Failed to log access key usage:", e); + // Don't block login if logging fails, but it's good to know + } + // Invalidate old sessions const now = Math.floor(Date.now() / 1000); await redis.set(`llm_min_iat:${studentNo}`, now); From f07141a86a2d47571850e9b9bed1030517ef364c Mon Sep 17 00:00:00 2001 From: konpoku Date: Sat, 27 Dec 2025 15:38:52 +0800 Subject: [PATCH 19/29] modified contest permission check and README.md --- README.md | 13 +-- src/routes/team.ts | 208 +++++++++++++++++++++------------------------ 2 files changed, 104 insertions(+), 117 deletions(-) diff --git a/README.md b/README.md index ab11f9eb..b8c5d5d5 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,9 @@ EESAST 后端 API - 用户验证 - 静态文件权限管理 +- 数据库访问 -**其余逻辑均使用 Hasura** +**正在进行数据库逻辑向后端迁移工作** ## API 接口 @@ -22,17 +23,19 @@ EESAST 后端 API - node 16 / npm - yarn - TypeScript -- MongoDB +- Redis + +### Before Dev + +需要在测试端安装和启动Redis +具体见[Redis安装文档](https://redis.io/docs/latest/operate/oss_and_stack/install/archive/install-redis/) ### 工具 - VSCode 扩展 - - Prettier - ESLint -- MongoDB Compass Community - - Postman ### 脚本 diff --git a/src/routes/team.ts b/src/routes/team.ts index 19bad07c..ee86b34a 100644 --- a/src/routes/team.ts +++ b/src/routes/team.ts @@ -24,7 +24,7 @@ router.post("/member_limit", authenticate(), async (req, res) => { }); // used in codepage.tsx -router.post("/add_team_code", authenticate(["student"]), async (req, res) => { +router.post("/add_team_code", authenticate(), async (req, res) => { try { const { team_id, @@ -71,7 +71,7 @@ router.post("/add_team_code", authenticate(["student"]), async (req, res) => { }); // used in joinpage.tsx -router.post("/add_team_player", authenticate(["student"]), async (req, res) => { +router.post("/add_team_player", authenticate(), async (req, res) => { try { const { team_id, player } = req.body; if (!team_id || !player) { @@ -132,7 +132,7 @@ router.post("/add_team", authenticate(["student"]), async (req, res) => { } }); -router.post("/add_team_member", authenticate(["student"]), async (req, res) => { +router.post("/add_team_member", authenticate(), async (req, res) => { try { const team_id = req.body.team_id; const user_uuid = req.body.user_uuid; @@ -161,69 +161,61 @@ router.post("/add_team_member", authenticate(["student"]), async (req, res) => { }); // used in codepage.tsx -router.post( - "/update_team_code_name", - authenticate(["student"]), - async (req, res) => { - try { - const { code_id, code_name } = req.body; - if (!code_id || !code_name) { - return res - .status(400) - .json({ error: "400 Bad Request: Missing required parameters" }); - } - const update_team_code_name = await ContHasFunc.update_team_code_name( - code_id, - code_name, - ); - res.status(200).json({ - code_id: update_team_code_name.code_id, - message: "Code Name Updated Successfully", - }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === "development" ? err.stack : undefined, - }); +router.post("/update_team_code_name", authenticate([]), async (req, res) => { + try { + const { code_id, code_name } = req.body; + if (!code_id || !code_name) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); } - }, -); + const update_team_code_name = await ContHasFunc.update_team_code_name( + code_id, + code_name, + ); + res.status(200).json({ + code_id: update_team_code_name.code_id, + message: "Code Name Updated Successfully", + }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); // used in codepage.tsx -router.post( - "/update_team_player", - authenticate(["student"]), - async (req, res) => { - try { - const { team_id, player, code_id, role } = req.body; - if (!team_id || !player || !code_id || !role) { - return res - .status(400) - .json({ error: "400 Bad Request: Missing required parameters" }); - } - const update_team_player = await ContHasFunc.update_team_player( - team_id, - player, - code_id, - role, - ); - res.status(200).json({ - player: update_team_player.player, - message: "Player Updated Successfully", - }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === "development" ? err.stack : undefined, - }); +router.post("/update_team_player", authenticate(), async (req, res) => { + try { + const { team_id, player, code_id, role } = req.body; + if (!team_id || !player || !code_id || !role) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); } - }, -); + const update_team_player = await ContHasFunc.update_team_player( + team_id, + player, + code_id, + role, + ); + res.status(200).json({ + player: update_team_player.player, + message: "Player Updated Successfully", + }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); // used in managepage.tsx -router.post("/update_team", authenticate(["student"]), async (req, res) => { +router.post("/update_team", authenticate(), async (req, res) => { try { const { team_id, ...update_Fields } = req.body; if (!team_id) { @@ -247,35 +239,31 @@ router.post("/update_team", authenticate(["student"]), async (req, res) => { //used in codepage.tsx -router.post( - "/delete_team_code", - authenticate(["student"]), - async (req, res) => { - try { - const { code_id } = req.body; - if (!code_id) { - return res - .status(400) - .send("400 Bad Request: Missing required parameters"); - } - const delete_team_code = await ContHasFunc.delete_team_code(code_id); - res.status(200).json({ - code_id: delete_team_code, - message: "Code Deleted Successfully", - }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === "development" ? err.stack : undefined, - }); +router.post("/delete_team_code", authenticate(), async (req, res) => { + try { + const { code_id } = req.body; + if (!code_id) { + return res + .status(400) + .send("400 Bad Request: Missing required parameters"); } - }, -); + const delete_team_code = await ContHasFunc.delete_team_code(code_id); + res.status(200).json({ + code_id: delete_team_code, + message: "Code Deleted Successfully", + }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); // used in managepage.tsx -router.post("/delete_team", authenticate(["student"]), async (req, res) => { +router.post("/delete_team", authenticate(), async (req, res) => { try { const { team_id } = req.body; if (!team_id) { @@ -297,33 +285,29 @@ router.post("/delete_team", authenticate(["student"]), async (req, res) => { }); // used in managepage.tsx -router.post( - "/delete_team_member", - authenticate(["student"]), - async (req, res) => { - try { - const { user_uuid, team_id } = req.body; - if (!user_uuid || !team_id) { - return res - .status(400) - .send("400 Bad Request: Missing required parameters"); - } - const delete_team_member = await ContHasFunc.delete_team_member( - user_uuid, - team_id, - ); - res.status(200).json({ - team_id: delete_team_member, - message: "Team Member Deleted Successfully", - }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === "development" ? err.stack : undefined, - }); +router.post("/delete_team_member", authenticate(), async (req, res) => { + try { + const { user_uuid, team_id } = req.body; + if (!user_uuid || !team_id) { + return res + .status(400) + .send("400 Bad Request: Missing required parameters"); } - }, -); + const delete_team_member = await ContHasFunc.delete_team_member( + user_uuid, + team_id, + ); + res.status(200).json({ + team_id: delete_team_member, + message: "Team Member Deleted Successfully", + }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); export default router; From b96b886ca471088ce533751432a96e36785751b9 Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Wed, 25 Feb 2026 21:20:51 +0800 Subject: [PATCH 20/29] feat: add endpoints for managing team software code and improve validation --- src/hasura/contest.ts | 241 +++++++++++++++++++++++++++++++++++++- src/routes/competition.ts | 227 +++++++++++++++++++++++++++++++++++ 2 files changed, 467 insertions(+), 1 deletion(-) diff --git a/src/hasura/contest.ts b/src/hasura/contest.ts index 9902830e..f2a90750 100644 --- a/src/hasura/contest.ts +++ b/src/hasura/contest.ts @@ -123,7 +123,89 @@ export const get_team_from_user: any = async ( contest_id: contest_id, }, ); - return query_team_id.contest_team_member[0]?.team_id ?? null; + if (query_team_id.contest_team_member.length !== 0) { + return query_team_id.contest_team_member[0]?.team_id ?? null; + } + const query_team_from_leader: any = await client.request( + gql` + query get_team_from_leader($user_uuid: uuid!, $contest_id: uuid!) { + contest_team( + where: { + team_leader_uuid: { _eq: $user_uuid } + contest_id: { _eq: $contest_id } + } + ) { + team_id + } + } + `, + { + user_uuid: user_uuid, + contest_id: contest_id, + }, + ); + if (query_team_from_leader.contest_team.length !== 0) { + return query_team_from_leader.contest_team[0]?.id ?? null; + } + return null; +}; + +/** + * Check if user is in a team + * @param {string} user_uuid + * @param {string} team_id + * @returns {boolean} true if user is in the team, false otherwise + */ +export const check_user_team: any = async ( + user_uuid: string, + team_id: string, +) => { + // Check if user is a team member + const query_member: any = await client.request( + gql` + query check_user_team_member($user_uuid: uuid!, $team_id: uuid!) { + contest_team_member( + where: { + _and: { user_uuid: { _eq: $user_uuid }, team_id: { _eq: $team_id } } + } + ) { + user_uuid + } + } + `, + { + user_uuid: user_uuid, + team_id: team_id, + }, + ); + if (query_member.contest_team_member.length !== 0) { + return true; + } + // Check if user is the team leader + const query_leader: any = await client.request( + gql` + query check_user_team_leader($user_uuid: uuid!, $team_id: uuid!) { + contest_team( + where: { + _and: { + team_leader_uuid: { _eq: $user_uuid } + team_id: { _eq: $team_id } + } + } + ) { + team_id + } + } + `, + { + user_uuid: user_uuid, + team_id: team_id, + }, + ); + if (query_leader.contest_team.length !== 0) { + return true; + } + return false; }; /** @@ -1240,6 +1322,163 @@ export const add_team_member: any = async ( return true; }; +export const add_team_software_code: any = async ( + contest_id: string, + team_id: string, + code_url: string, +) => { + await client.request( + gql` + mutation add_team_software_code( + $url: String! + $team_id: uuid! + $contest_id: uuid! + ) { + insert_contest_team_software_code( + objects: { URL: $url, team_id: $team_id, contest_id: $contest_id } + ) { + returning { + created_at + } + } + } + `, + { + team_id: team_id, + url: code_url, + contest_id: contest_id, + }, + ); +}; + +export const update_team_software_code: any = async ( + team_id: string, + code_url: string, + submission_count: number, +) => { + await client.request( + gql` + mutation change_team_software_code( + $url: String! + $teamId: uuid! + $count: Int! + ) { + update_contest_team_software_code( + where: { team_id: { _eq: $teamId } } + _set: { URL: $url, submission_count: $count, updated_at: "now()" } + ) { + affected_rows + } + } + `, + { + teamId: team_id, + url: code_url, + count: submission_count, + }, + ); +}; + +export const get_team_software_code_one: any = async (team_id: string) => { + const query_team_software_code: any = await client.request( + gql` + query get_team_software_code($team_id: uuid!) { + contest_team_software_code(where: { team_id: { _eq: $team_id } }) { + URL + updated_at + submission_count + } + } + `, + { + team_id: team_id, + }, + ); + return query_team_software_code.contest_team_software_code.map( + (code: any) => ({ + URL: code.URL, + updated_at: code.updated_at, + submission_count: code.submission_count, + }), + )[0]; +}; + +export const get_all_team_software_code_by_contest: any = async ( + contest_id: string, +) => { + const query_all_team_software_code_by_contest: any = await client.request( + gql` + query get_all_team_software_code_by_contest($contest_id: uuid!) { + contest_team_software_code( + where: { contest_team: { contest_id: { _eq: $contest_id } } } + ) { + URL + created_at + updated_at + submission_count + contest_team { + team_name + team_leader { + realname + student_no + } + contest_team_members { + user { + realname + student_no + } + } + } + } + } + `, + { + contest_id: contest_id, + }, + ); + return query_all_team_software_code_by_contest.contest_team_software_code.map( + (code: any) => ({ + URL: code.URL, + created_at: code.created_at, + updated_at: code.updated_at, + submission_count: code.submission_count, + team_name: code.contest_team.team_name, + team_leader: { + realname: code.contest_team.team_leader.realname, + student_no: code.contest_team.team_leader.student_no, + }, + members: (code.contest_team.contest_team_members || []).map( + (member: any) => ({ + realname: member.user.realname, + student_no: member.user.student_no, + }), + ), + }), + ); +}; + +export const get_team_software_submission_count: any = async ( + team_id: string, +) => { + const query_team_software_submission_count: any = await client.request( + gql` + query team_software_submission_count($teamId: uuid!) { + contest_team_software_code(where: { team_id: { _eq: $teamId } }) { + submission_count + } + } + `, + { + teamId: team_id, + }, + ); + return ( + query_team_software_submission_count.contest_team_software_code.map( + (code: any) => code.submission_count, + )[0] ?? 0 + ); +}; + /** * * @param {uuid} contest_id ID diff --git a/src/routes/competition.ts b/src/routes/competition.ts index 6ea5eaee..cb707b3a 100644 --- a/src/routes/competition.ts +++ b/src/routes/competition.ts @@ -1169,4 +1169,231 @@ router.get("/playback/:room_id", async (req, res) => { } }); +/** + * Add team software code + * @param {string} contest_id - Contest ID + * @param {string} team_id - Team ID + * @param {string} code_url - Code URL + */ +router.post("/add_team_software_code", authenticate(), async (req, res) => { + try { + const user_uuid = req.auth.user.uuid; + const { contest_id, team_id, code_url } = req.body; + + // Input validation + if (!contest_id || !team_id || !code_url) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(contest_id) || !uuidRegex.test(team_id)) { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid UUID format" }); + } + + // Validate URL format + try { + new URL(code_url); + } catch { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid URL format" }); + } + + // Permission check: verify user is a member of the team + const userTeam = await ContHasFunc.check_user_team(user_uuid, team_id); + if (!userTeam) { + return res + .status(403) + .json({ error: "403 Forbidden: You are not a member of this team" }); + } + + // Add team software code + await ContHasFunc.add_team_software_code(contest_id, team_id, code_url); + res.status(200).json({ message: "Team software code added successfully" }); + } catch (err: any) { + console.error(err); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); + +/** + * Update team software code + * @param {string} team_id - Team ID + * @param {string} code_url - Code URL + * @param {number} submission_count - Current submission count + */ +router.post("/update_team_software_code", authenticate(), async (req, res) => { + try { + const user_uuid = req.auth.user.uuid; + const { team_id, code_url } = req.body; + + // Input validation + if (!team_id || !code_url) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(team_id)) { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid UUID format" }); + } + + // Validate URL format + try { + new URL(code_url); + } catch { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid URL format" }); + } + + // Validate submission_count is a non-negative number + + // Permission check: verify user is a member of the team + const is_member = await ContHasFunc.check_user_team(user_uuid, team_id); + if (!is_member) { + return res + .status(403) + .json({ error: "403 Forbidden: You are not a member of this team" }); + } + + const submission_count: number = + (await ContHasFunc.get_team_software_submission_count(team_id)) + 1; + + // Update team software code + await ContHasFunc.update_team_software_code( + team_id, + code_url, + submission_count, + ); + res + .status(200) + .json({ message: "Team software code updated successfully" }); + } catch (err: any) { + console.error(err); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); + +/** + * Get team software code by team ID + * @param {string} team_id - Team ID + */ +router.post("/get_team_software_code_one", authenticate(), async (req, res) => { + try { + const user_uuid = req.auth.user.uuid; + const { team_id } = req.body; + + // Input validation + if (!team_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing team_id" }); + } + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(team_id)) { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid UUID format" }); + } + + // Permission check: verify user is a member of the team or a manager + // For now, we'll allow team members to view their own code + //const teams = await ContHasFunc.get_team_from_user(user_uuid, team_id); + const isMember = await ContHasFunc.check_user_team(user_uuid, team_id); + if (!isMember) { + return res.status(403).json({ + error: "403 Forbidden: You are not authorized to view this team's code", + }); + } + + // Get team software code + const code = await ContHasFunc.get_team_software_code_one(team_id); + res.status(200).json({ data: code }); + } catch (err: any) { + console.error(err); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); + +/** + * Get all team software code by contest ID + * @param {string} contest_id - Contest ID + */ +router.post( + "/get_all_team_software_code_by_contest", + authenticate(), + async (req, res) => { + try { + const user_uuid = req.auth.user.uuid; + const { contest_id } = req.body; + + // Input validation + if (!contest_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing contest_id" }); + } + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(contest_id)) { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid UUID format" }); + } + + // Permission check: verify user is a manager of the contest + const is_manager = await ContHasFunc.get_maneger_from_user( + user_uuid, + contest_id, + ); + if (!is_manager) { + return res.status(403).json({ + error: "403 Forbidden: Only contest managers can view all team codes", + }); + } + + // Get all team software codes + const codes = + await ContHasFunc.get_all_team_software_code_by_contest(contest_id); + res.status(200).json({ data: codes }); + } catch (err: any) { + console.error(err); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } + }, +); + export default router; From d75a4cadb2f6051e6983713dba3c0e9e5d9612a4 Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Sun, 1 Mar 2026 14:31:12 +0800 Subject: [PATCH 21/29] added semester management --- src/hasura/mentor.ts | 360 +++++++++++++++++++++++++------------- src/routes/application.ts | 85 +++++++++ 2 files changed, 321 insertions(+), 124 deletions(-) diff --git a/src/hasura/mentor.ts b/src/hasura/mentor.ts index 24779f2c..6ef16955 100644 --- a/src/hasura/mentor.ts +++ b/src/hasura/mentor.ts @@ -7,28 +7,26 @@ export interface Freshman_Insert_Input { uuid: string; } export const get_mentor_info_list = async () => { - const query: any = await client.request( - gql` - query GetMentorInfoList { - mentor_info(order_by: { available: desc, max_applicants: desc }) { - achievement - available - background - field - intro - max_applicants - mentor_uuid - user { - department - email - realname - } + const query: any = await client.request(gql` + query GetMentorInfoList { + mentor_info(order_by: { available: desc, max_applicants: desc }) { + achievement + available + background + field + intro + max_applicants + mentor_uuid + user { + department + email + realname } } - ` - ); + } + `); return query.mentor_info; -} +}; export const get_mentor_info = async (uuid: string) => { const query: any = await client.request( @@ -50,13 +48,15 @@ export const get_mentor_info = async (uuid: string) => { } } `, - { uuid: uuid } + { uuid: uuid }, ); return query.mentor_info_by_pk ?? null; -} - +}; -export const get_mentor_applications_count = async (uuid: string, year: number) => { +export const get_mentor_applications_count = async ( + uuid: string, + year: number, +) => { const query: any = await client.request( gql` query GetMentorApplicationsCount($uuid: uuid!, $year: Int!) { @@ -69,13 +69,15 @@ export const get_mentor_applications_count = async (uuid: string, year: number) } } `, - { uuid: uuid, year: year } + { uuid: uuid, year: year }, ); return query.mentor_application_aggregate?.aggregate?.count ?? 0; -} - +}; -export const get_mentor_applications_approved_count = async (uuid: string, year: number) => { +export const get_mentor_applications_approved_count = async ( + uuid: string, + year: number, +) => { const query: any = await client.request( gql` query GetMentorApplicationsApprovedCount($uuid: uuid!, $year: Int!) { @@ -93,12 +95,17 @@ export const get_mentor_applications_approved_count = async (uuid: string, year: } } `, - { uuid: uuid, year: year } + { uuid: uuid, year: year }, ); return query.mentor_application_aggregate?.aggregate?.count ?? 0; -} +}; -export const insert_mentor_application = async (mentor_uuid: string, student_uuid: string, year: number, statement: string) => { +export const insert_mentor_application = async ( + mentor_uuid: string, + student_uuid: string, + year: number, + statement: string, +) => { const query: any = await client.request( gql` mutation InsertMentorApplication( @@ -123,11 +130,11 @@ export const insert_mentor_application = async (mentor_uuid: string, student_uui mentor_uuid: mentor_uuid, student_uuid: student_uuid, year: year, - statement: statement - } + statement: statement, + }, ); return query.insert_mentor_application_one?.id ?? null; -} +}; //newly added export const insert_mentor_info = async (mentor_uuid: string) => { const query: any = await client.request( @@ -141,41 +148,53 @@ export const insert_mentor_info = async (mentor_uuid: string) => { } } `, - { mentor_uuid: mentor_uuid } + { mentor_uuid: mentor_uuid }, ); return query.insert_mentor_info_one?.updated_at ?? null; -} +}; -export const update_mentor_info_available = async (uuid: string, available: boolean) => { +export const update_mentor_info_available = async ( + uuid: string, + available: boolean, +) => { const query: any = await client.request( gql` - mutation UpdateMentorInfoAvailable($uuid: uuid!, $available: Boolean!) { - update_mentor_info_by_pk( - pk_columns: { mentor_uuid: $uuid } - _set: { available: $available } - ) { - available + mutation UpdateMentorInfoAvailable($uuid: uuid!, $available: Boolean!) { + update_mentor_info_by_pk( + pk_columns: { mentor_uuid: $uuid } + _set: { available: $available } + ) { + available + } } - } `, - { uuid: uuid, available: available } + { uuid: uuid, available: available }, ); return query.update_mentor_info_by_pk?.available ?? null; -} +}; -export const update_mentor_info_description = async (mentor_uuid: string,updateFields: Partial<{achievement: string, background: string, field: string, intro: string}>) => { - if(Object.keys(updateFields).length === 0) return null; +export const update_mentor_info_description = async ( + mentor_uuid: string, + updateFields: Partial<{ + achievement: string; + background: string; + field: string; + intro: string; + }>, +) => { + if (Object.keys(updateFields).length === 0) return null; const setFields: any = {}; - if(updateFields.achievement) setFields.achievement = updateFields.achievement; - if(updateFields.background) setFields.background = updateFields.background; - if(updateFields.field) setFields.field = updateFields.field; - if(updateFields.intro) setFields.intro = updateFields.intro; + if (updateFields.achievement) + setFields.achievement = updateFields.achievement; + if (updateFields.background) setFields.background = updateFields.background; + if (updateFields.field) setFields.field = updateFields.field; + if (updateFields.intro) setFields.intro = updateFields.intro; const variablesString = Object.keys(setFields) - .map(key => `$${key} : String`) - .join('\n'); + .map((key) => `$${key} : String`) + .join("\n"); const setString = Object.keys(setFields) - .map(key => `${key}: $${key}`) - .join('\n'); + .map((key) => `${key}: $${key}`) + .join("\n"); const mutation = gql` mutation UpdateMentorInfoDescription($mentor_uuid: uuid! ${variablesString}) { @@ -187,23 +206,26 @@ export const update_mentor_info_description = async (mentor_uuid: string,updateF } `; console.log(mutation); - const variables: {[key: string]: any} = { + const variables: { [key: string]: any } = { mentor_uuid: mentor_uuid, achievement: updateFields.achievement, background: updateFields.background, field: updateFields.field, - intro: updateFields.intro + intro: updateFields.intro, }; try { const response: any = await client.request(mutation, variables); return response.update_mentor_info_by_pk?.mentor_uuid ?? null; } catch (error) { - console.error('Error updating mentor info', error); + console.error("Error updating mentor info", error); throw error; } -} +}; -export const update_mentor_application_status = async (id: string, status: string) => { +export const update_mentor_application_status = async ( + id: string, + status: string, +) => { const query: any = await client.request( gql` mutation UpdateMentorApplicationStatus($id: uuid!, $status: String!) { @@ -215,15 +237,21 @@ export const update_mentor_application_status = async (id: string, status: strin } } `, - { id: id, status: status } + { id: id, status: status }, ); return query.update_mentor_application_by_pk?.status ?? null; -} +}; -export const update_mentor_application_statement = async (id: string, statement: string) => { +export const update_mentor_application_statement = async ( + id: string, + statement: string, +) => { const query: any = await client.request( gql` - mutation UpdateMentorApplicationStatement($id: uuid!, $statement: String!) { + mutation UpdateMentorApplicationStatement( + $id: uuid! + $statement: String! + ) { update_mentor_application_by_pk( pk_columns: { id: $id } _set: { statement: $statement } @@ -232,10 +260,10 @@ export const update_mentor_application_statement = async (id: string, statement: } } `, - { id: id, statement: statement } + { id: id, statement: statement }, ); return query.update_mentor_application_by_pk?.statement ?? null; -} +}; export const delete_mentor_application = async (id: string) => { const query: any = await client.request( @@ -246,51 +274,54 @@ export const delete_mentor_application = async (id: string) => { } } `, - { id: id } + { id: id }, ); return query.delete_mentor_application_by_pk?.id ?? null; -} -export const update_mentor_application_chat_status = async (id: string, chat_status: boolean) => { - try{ - const query: any = await client.request( - gql` - mutation UpdateMentorApplicationChatStatus($id: uuid!, $chat_status: Boolean!) { - update_mentor_application_by_pk( - pk_columns: { id: $id } - _set: { chat_status: $chat_status } +}; +export const update_mentor_application_chat_status = async ( + id: string, + chat_status: boolean, +) => { + try { + const query: any = await client.request( + gql` + mutation UpdateMentorApplicationChatStatus( + $id: uuid! + $chat_status: Boolean! ) { - chat_status + update_mentor_application_by_pk( + pk_columns: { id: $id } + _set: { chat_status: $chat_status } + ) { + chat_status + } } -} - `, - { id: id, chat_status: chat_status } - ); - return query.update_mentor_application_by_pk?.chat_status ?? null; -} - catch(e){ + `, + { id: id, chat_status: chat_status }, + ); + return query.update_mentor_application_by_pk?.chat_status ?? null; + } catch (e) { console.log(e); console.log("error"); } -} -export const insert_mentor_application_schedule = - async ( - year: number, - start_A: Date, - start_B: Date, - start_C: Date, - start_D: Date, - start_E: Date, - end_A: Date, - end_B: Date, - end_C: Date, - end_D: Date, - end_E: Date - ) => { +}; +export const insert_mentor_application_schedule = async ( + year: number, + start_A: Date, + start_B: Date, + start_C: Date, + start_D: Date, + start_E: Date, + end_A: Date, + end_B: Date, + end_C: Date, + end_D: Date, + end_E: Date, +) => { console.log(end_E); const query: any = await client.request( gql` - -mutation InsertMentorApplicationSchedule( + mutation InsertMentorApplicationSchedule( $year: Int! $start_A: timestamptz! $start_B: timestamptz! @@ -316,22 +347,23 @@ mutation InsertMentorApplicationSchedule( end_C: $end_C end_D: $end_D end_E: $end_E - } - on_conflict: { - constraint: mentor_time_pkey - update_columns: [ - start_A - start_B - start_C - start_D - start_E - end_A - end_B - end_C - end_D - end_E - ]}) - { + } + on_conflict: { + constraint: mentor_time_pkey + update_columns: [ + start_A + start_B + start_C + start_D + start_E + end_A + end_B + end_C + end_D + end_E + ] + } + ) { activateIn } } @@ -347,11 +379,11 @@ mutation InsertMentorApplicationSchedule( end_B: end_B, end_C: end_C, end_D: end_D, - end_E: end_E - } + end_E: end_E, + }, ); return query.insert_mentor_time_one?.activateIn ?? null; -} +}; //export const insert_freshman_info_list = async (fresman_data: Array) => { // const query: any = await client.request( @@ -374,10 +406,14 @@ mutation InsertMentorApplicationSchedule( //} // -export const insert_freshman_info_list = async (fresman_data: Freshman_Insert_Input) => { +export const insert_freshman_info_list = async ( + fresman_data: Freshman_Insert_Input, +) => { const query: any = await client.request( gql` - mutation InsertFreshmanInfoList($freshmanData: [freshman_insert_input!]!) { + mutation InsertFreshmanInfoList( + $freshmanData: [freshman_insert_input!]! + ) { insert_freshman( objects: $freshmanData on_conflict: { @@ -389,7 +425,83 @@ export const insert_freshman_info_list = async (fresman_data: Freshman_Insert_In } } `, - {freshmanData: fresman_data } + { freshmanData: fresman_data }, ); return query.insert_freshman?.affected_rows ?? 0; -} +}; + +export const get_current_semester = async () => { + const query: any = await client.request(gql` + query fetch_current_semester { + semester(where: { is_current: { _eq: true } }) { + semester + } + } + `); + + return query.semester?.[0]?.semester ?? null; +}; + +export const get_semester_list = async () => { + const query: any = await client.request(gql` + query fetch_semester_list { + semester(order_by: { semester: desc }) { + semester + is_current + } + } + `); + return query.semester ?? []; +}; + +export const add_semester = async (semester: string) => { + const query: any = await client.request( + gql` + mutation add_semester($semester: String!) { + insert_semester_one( + object: { semester: $semester, is_current: false } + on_conflict: { constraint: semester_pkey, update_columns: [] } + ) { + semester + } + } + `, + { semester }, + ); + return query.insert_semester_one?.semester ?? null; +}; + +export const delete_semester = async (semester: string) => { + const query: any = await client.request( + gql` + mutation delete_semester($semester: String!) { + delete_semester_by_pk(semester: $semester) { + semester + } + } + `, + { semester }, + ); + return query.delete_semester_by_pk?.semester ?? null; +}; + +export const set_current_semester = async (semester: string) => { + const query: any = await client.request( + gql` + mutation set_current_semester($semester: String!) { + reset_current: update_semester(where: {}, _set: { is_current: false }) { + affected_rows + } + set_current: update_semester( + where: { semester: { _eq: $semester } } + _set: { is_current: true } + ) { + affected_rows + } + } + `, + { semester: semester }, + ); + + return query.set_current?.affected_rows ?? null; +}; diff --git a/src/routes/application.ts b/src/routes/application.ts index 317f4041..cf2c36be 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -3,6 +3,13 @@ import express from "express"; import { gql } from "graphql-request"; import { client } from ".."; import * as HnrHasFunc from "../hasura/honor"; +import { + get_current_semester, + set_current_semester, + get_semester_list, + add_semester, + delete_semester, +} from "../hasura/mentor"; import authenticate from "../middlewares/authenticate"; import { sendEmail } from "../helpers/email"; import { @@ -2313,4 +2320,82 @@ router.post( }, ); +router.get("/semester", async (req, res) => { + try { + const curr_semester = await get_current_semester(); + if (!curr_semester) { + return res.status(500).send("Error: No semester found"); + } + return res.status(200).send(curr_semester); + } catch (err) { + console.log(err); + return res.status(500); + } +}); + +router.post("/set_semester", authenticate(["counselor"]), async (req, res) => { + try { + const semester: string = req.body.semester; + if (!semester) { + return res.status(400).send("Error: Invalid semester"); + } + const result = await set_current_semester(semester); + if (!result) { + return res.status(500).send("Error: Set semester failed"); + } + return res.status(200).json({ affected_rows: result }); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } +}); + +router.get("/semester_list", authenticate(["counselor"]), async (req, res) => { + try { + const list = await get_semester_list(); + return res.status(200).json(list); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } +}); + +router.post("/add_semester", authenticate(["counselor"]), async (req, res) => { + try { + const semester: string = req.body.semester; + if (!semester) { + return res.status(400).send("Error: Invalid semester"); + } + const result = await add_semester(semester); + if (!result) { + return res.status(500).send("Error: Add semester failed"); + } + return res.status(200).json({ semester: result }); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } +}); + +router.delete( + "/delete_semester", + authenticate(["counselor"]), + async (req, res) => { + try { + const semester: string = req.body.semester; + if (!semester) { + return res.status(400).send("Error: Invalid semester"); + } + const result = await delete_semester(semester); + if (!result) { + return res.status(500).send("Error: Delete semester failed"); + } + return res.status(200).json({ semester: result }); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + export default router; From de2551213fa7f35a78e86accc58583ea28e3a1fd Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Mon, 2 Mar 2026 22:55:40 +0800 Subject: [PATCH 22/29] feat: update member chat --- src/routes/application.ts | 417 ++++++++++++++++++++++++++++++++++++++ src/routes/static.ts | 156 +++++++++----- 2 files changed, 525 insertions(+), 48 deletions(-) diff --git a/src/routes/application.ts b/src/routes/application.ts index cf2c36be..b666eb0e 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -2398,4 +2398,421 @@ router.delete( }, ); +// ==================== 积极分子谈话记录(新系统:基于 member_chat_record 表) ==================== + +// 学生查询自己的所有积极分子谈话记录(同时返回 is_member 和当前学期) +router.get( + "/info/mentor/my_member_chats", + authenticate(["student"]), + async (req, res) => { + try { + const user_uuid: string = req.auth.user.uuid; + + // 通过已通过的申请查询其导师是否参与积极分子谈话 + const appl_query: any = await client.request( + gql` + query GetApprovedMentorForStudent($student_uuid: uuid!) { + mentor_application( + where: { + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + limit: 1 + ) { + mentor_uuid + } + } + `, + { student_uuid: user_uuid }, + ); + let is_member: boolean = false; + const approved_mentor_uuid = + appl_query.mentor_application?.[0]?.mentor_uuid; + if (approved_mentor_uuid) { + const mentor_check: any = await client.request( + gql` + query GetMentorIsMember($mentor_uuid: uuid!) { + mentor_info_by_pk(mentor_uuid: $mentor_uuid) { + is_member + } + } + `, + { mentor_uuid: approved_mentor_uuid }, + ); + is_member = mentor_check.mentor_info_by_pk?.is_member ?? false; + } + + // 获取当前学期 + const current_semester = await get_current_semester(); + + // 查询该学生所有谈话记录,按时间倒序 + const records_query: any = await client.request( + gql` + query GetMemberChatRecords($user_id: uuid!) { + member_chat_record( + where: { user_id: { _eq: $user_id } } + order_by: { updated_at: desc } + ) { + id + updated_at + user_id + semester + member_chat_confirm + } + } + `, + { user_id: user_uuid }, + ); + + return res.status(200).json({ + is_member, + current_semester, + records: records_query.member_chat_record ?? [], + }); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 学生提交积极分子谈话记录(对当前学期插入/更新记录,文件已由前端上传至 COS) +router.post( + "/info/mentor/member_chat_submit", + authenticate(["student"]), + async (req, res) => { + try { + const user_uuid: string = req.auth.user.uuid; + + // 权限:查询已通过申请的导师是否参与积极分子谈话 + const appl_query: any = await client.request( + gql` + query GetApprovedMentorForStudent($student_uuid: uuid!) { + mentor_application( + where: { + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + limit: 1 + ) { + mentor_uuid + } + } + `, + { student_uuid: user_uuid }, + ); + const approved_mentor_uuid = + appl_query.mentor_application?.[0]?.mentor_uuid; + let is_member: boolean = false; + if (approved_mentor_uuid) { + const mentor_check: any = await client.request( + gql` + query GetMentorIsMember($mentor_uuid: uuid!) { + mentor_info_by_pk(mentor_uuid: $mentor_uuid) { + is_member + } + } + `, + { mentor_uuid: approved_mentor_uuid }, + ); + is_member = mentor_check.mentor_info_by_pk?.is_member ?? false; + } + if (!is_member) { + return res.status(403).send("Error: Not a member"); + } + + // 获取当前学期 + const current_semester = await get_current_semester(); + if (!current_semester) { + return res.status(400).send("Error: No current semester"); + } + console.log(`Current semester: ${current_semester}`); + // 查询当前学期是否已有记录 + const existing_query: any = await client.request( + gql` + query CheckExistingRecord($user_id: uuid!, $semester: String!) { + member_chat_record( + where: { + user_id: { _eq: $user_id } + semester: { _eq: $semester } + } + ) { + id + member_chat_confirm + } + } + `, + { user_id: user_uuid, semester: current_semester }, + ); + const existing = existing_query.member_chat_record?.[0]; + + if (existing?.member_chat_confirm) { + return res + .status(400) + .send("Error: Chat already confirmed for this semester"); + } + + let result: any; + if (existing) { + // 已有记录:更新 updated_at(触发器会自动更新,这里显式写入时间保证及时性) + const update_mut: any = await client.request( + gql` + mutation UpdateMemberChatRecord($id: uuid!, $now: timestamptz!) { + update_member_chat_record_by_pk( + pk_columns: { id: $id } + _set: { updated_at: $now } + ) { + id + updated_at + user_id + semester + member_chat_confirm + } + } + `, + { id: existing.id, now: new Date().toISOString() }, + ); + result = update_mut.update_member_chat_record_by_pk; + } else { + // 新增记录 + const insert_mut: any = await client.request( + gql` + mutation InsertMemberChatRecord( + $user_id: uuid! + $semester: String! + ) { + insert_member_chat_record_one( + object: { user_id: $user_id, semester: $semester } + ) { + id + updated_at + user_id + semester + member_chat_confirm + } + } + `, + { user_id: user_uuid, semester: current_semester }, + ); + result = insert_mut.insert_member_chat_record_one; + } + + if (!result) { + return res.status(500).send("Error: Submit failed"); + } + return res.status(200).json(result); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 导师/辅导员确认积极分子谈话记录 +router.post( + "/info/mentor/member_chat_confirm", + authenticate(["teacher", "counselor"]), + async (req, res) => { + try { + const record_id: string = req.body.record_id; + const operator_uuid: string = req.auth.user.uuid; + const operator_role: string = req.auth.user.role; + if (!record_id) { + return res.status(400).send("Error: Missing record_id"); + } + + // 查询记录是否存在 + const record_query: any = await client.request( + gql` + query GetMemberChatRecord($id: uuid!) { + member_chat_record_by_pk(id: $id) { + id + user_id + semester + member_chat_confirm + } + } + `, + { id: record_id }, + ); + if (!record_query.member_chat_record_by_pk) { + return res.status(404).send("Error: Record not found"); + } + if (record_query.member_chat_record_by_pk.member_chat_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需额外验证该学生是自己已通过的学生 + if (operator_role === "teacher") { + const student_uuid = record_query.member_chat_record_by_pk.user_id; + const appl_check: any = await client.request( + gql` + query CheckTeacherStudent( + $mentor_uuid: uuid! + $student_uuid: uuid! + ) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + } + } + `, + { mentor_uuid: operator_uuid, student_uuid }, + ); + if (appl_check.mentor_application.length === 0) { + return res.status(403).send("Error: Not your student"); + } + } + + const update_mut: any = await client.request( + gql` + mutation ConfirmMemberChatRecord($id: uuid!) { + update_member_chat_record_by_pk( + pk_columns: { id: $id } + _set: { member_chat_confirm: true } + ) { + id + member_chat_confirm + semester + user_id + } + } + `, + { id: record_id }, + ); + + if (!update_mut.update_member_chat_record_by_pk) { + return res.status(500).send("Error: Confirm failed"); + } + return res.status(200).json(update_mut.update_member_chat_record_by_pk); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 辅导员获取所有积极分子谈话记录 +router.get( + "/info/mentor/all_member_chats", + authenticate(["counselor"]), + async (req, res) => { + try { + const query: any = await client.request(gql` + query GetAllMemberChatRecords { + member_chat_record(order_by: { updated_at: desc }) { + id + updated_at + user_id + semester + member_chat_confirm + user { + realname + student_no + department + class + } + } + } + `); + return res.status(200).json(query.member_chat_record ?? []); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 老师获取自己指导的学生的积极分子谈话记录(仅已通过申请的学生) +router.get( + "/info/mentor/my_students_member_chats", + authenticate(["teacher"]), + async (req, res) => { + try { + const mentor_uuid: string = req.auth.user.uuid; + + // 先检查该老师是否参与积极分子谈话 + const mentor_check: any = await client.request( + gql` + query CheckTeacherIsMember($mentor_uuid: uuid!) { + mentor_info_by_pk(mentor_uuid: $mentor_uuid) { + is_member + } + } + `, + { mentor_uuid }, + ); + if (!mentor_check.mentor_info_by_pk?.is_member) { + return res.status(200).json([]); + } + + // 再查该老师所有已通过申请的学生 uuid + const applications_query: any = await client.request( + gql` + query GetApprovedStudents($mentor_uuid: uuid!) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + year + student { + realname + student_no + department + class + } + } + } + `, + { mentor_uuid }, + ); + + const approved = applications_query?.mentor_application ?? []; + if (approved.length === 0) { + return res.status(200).json([]); + } + + const student_uuids: string[] = approved.map((a: any) => a.student_uuid); + + // 再查这些学生的积极分子谈话记录 + const records_query: any = await client.request( + gql` + query GetStudentsMemberChatRecords($student_uuids: [uuid!]!) { + member_chat_record( + where: { user_id: { _in: $student_uuids } } + order_by: { updated_at: desc } + ) { + id + updated_at + user_id + semester + member_chat_confirm + user { + realname + student_no + department + class + } + } + } + `, + { student_uuids }, + ); + + return res.status(200).json(records_query.member_chat_record ?? []); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + export default router; diff --git a/src/routes/static.ts b/src/routes/static.ts index 6c358f21..be55c098 100644 --- a/src/routes/static.ts +++ b/src/routes/static.ts @@ -22,20 +22,18 @@ const viewActions = [ "name/cos:HeadObject", "name/cos:GetObject", "name/cos:GetBucket", -] +]; let user_uuid: string, role: string; -router.get("/*",authenticate(), async (req, res, next) => { - try{ +router.get("/*", authenticate(), async (req, res, next) => { + try { user_uuid = req.auth.user.uuid; role = req.auth.user.role; // admin gets all permissions, otherwise throw to next route. - if (role == 'admin') { + if (role == "admin") { const sts = await getSTS(generalActions, "*"); - return res.status(200).send(sts); //返回sts密钥 - } - else - next('route'); + return res.status(200).send(sts); //返回sts密钥 + } else next("route"); } catch (err) { return res.status(500).send(err); } @@ -43,8 +41,11 @@ router.get("/*",authenticate(), async (req, res, next) => { //info router.get("/upload/*", async (req, res) => { - try{ - const sts = await getSTS(role === "counselor" ? generalActions : viewActions, `upload/*`); + try { + const sts = await getSTS( + role === "counselor" ? generalActions : viewActions, + `upload/*`, + ); return res.status(200).send(sts); } catch (err) { return res.status(500).send(err); @@ -53,47 +54,100 @@ router.get("/upload/*", async (req, res) => { //avatar router.get("/avatar/*", async (req, res) => { - try{ - const sts = await getSTS(role === "anonymous" ? viewActions : generalActions , `avatar/*`); + try { + const sts = await getSTS( + role === "anonymous" ? viewActions : generalActions, + `avatar/*`, + ); return res.status(200).send(sts); } catch (err) { return res.status(500).send(err); } }); +router.get("/chat_record/:user_uuid/member/:semester/*", async (req, res) => { + try { + const { user_uuid: target_uuid, semester } = req.params; + if (role === "counselor") { + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/member/${semester}/*`, + ); + return res.status(200).send(sts); + } else if (role === "student" && user_uuid === target_uuid) { + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/member/${semester}/*`, + ); + return res.status(200).send(sts); + } else if (role === "teacher") { + // 验证该学生是否为此老师已通过的学生 + const applications: any = await client.request( + gql` + query CheckTeacherStudent($mentor_uuid: uuid!, $student_uuid: uuid!) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + } + } + `, + { mentor_uuid: user_uuid, student_uuid: target_uuid }, + ); + if (applications.mentor_application.length === 0) { + return res.status(401).send("当前用户没有权限访问该谈话记录"); + } + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/member/${semester}/*`, + ); + return res.status(200).send(sts); + } else { + return res.status(401).send("当前用户没有权限访问该谈话记录"); + } + } catch (err) { + return res.status(500).send(err); + } +}); + router.get("/chat_record/:application_id/*", async (req, res) => { - try{ - if (role == 'student' || role == 'teacher') { + try { + if (role == "student" || role == "teacher") { const application_id = req.params.application_id; const applications: any = await client.request( gql` query query_if_in_application($application_id: uuid) { - mentor_application(where: {id: {_eq: $application_id}}) { + mentor_application(where: { id: { _eq: $application_id } }) { mentor_uuid student_uuid } } `, - { application_id: application_id } + { application_id: application_id }, ); if (applications.mentor_application.length == 0) return res.status(404).send("未查找到该申请"); const application = applications.mentor_application[0]; - if ((role == 'student' && user_uuid == application.student_uuid) || - (role == 'teacher' && user_uuid == application.mentor_uuid) - ) { - const sts = await getSTS(generalActions, `chat_record/${application_id}/*`); - return res.status(200).send(sts); - } - else { + if ( + (role == "student" && user_uuid == application.student_uuid) || + (role == "teacher" && user_uuid == application.mentor_uuid) + ) { + const sts = await getSTS( + generalActions, + `chat_record/${application_id}/*`, + ); + return res.status(200).send(sts); + } else { return res.status(401).send("当前用户没有该申请的权限"); } - } - else if (role === "counselor") { + } else if (role === "counselor") { const sts = await getSTS(generalActions, `chat_record/*`); return res.status(200).send(sts); - } - else { + } else { return res.status(401).send("401 Unauthorized"); } } catch (err) { @@ -106,15 +160,22 @@ const checkContestManager = async (name: string) => { const query_if_manager: any = await client.request( gql` query query_is_manager($name: String, $user_uuid: uuid) { - contest_manager(where: {_and: {contest: {name: {_eq: $name}}, user_uuid: {_eq: $user_uuid}}}) { + contest_manager( + where: { + _and: { + contest: { name: { _eq: $name } } + user_uuid: { _eq: $user_uuid } + } + } + ) { user_uuid } } `, { name: name, - user_uuid: user_uuid - } + user_uuid: user_uuid, + }, ); return query_if_manager.contest_manager.length != 0; }; @@ -125,7 +186,7 @@ const checkTeamMember = async (name: string, team_id: string) => { contest_team( where: { _and: [ - { contest: {name: {_eq: $name}} } + { contest: { name: { _eq: $name } } } { team_id: { _eq: $team_id } } { contest_team_members: { user_uuid: { _eq: $user_uuid } } } ] @@ -138,22 +199,21 @@ const checkTeamMember = async (name: string, team_id: string) => { { name: name, team_id: team_id, - user_uuid: user_uuid - } + user_uuid: user_uuid, + }, ); return query_in_team.contest_team.length != 0; }; //首先检验比赛管理员权限 router.get("/:name/*", async (req, res, next) => { - try{ + try { const name = req.params.name; if (await checkContestManager(name)) { const sts = await getSTS(generalActions, `${name}/*`); return res.status(200).send(sts); - } - else { - next('route'); + } else { + next("route"); } } catch (err) { return res.status(500).send(err); @@ -161,10 +221,11 @@ router.get("/:name/*", async (req, res, next) => { }); router.get("/:name/code/:team_id/*", async (req, res) => { - try{ + try { const name = req.params.name; const team_id = req.params.team_id; - if (!await checkTeamMember(name, team_id)) return res.status(401).send("当前用户不在队伍中"); + if (!(await checkTeamMember(name, team_id))) + return res.status(401).send("当前用户不在队伍中"); const sts = await getSTS(generalActions, `${name}/code/${team_id}/*`); return res.status(200).send(sts); } catch (err) { @@ -173,7 +234,7 @@ router.get("/:name/code/:team_id/*", async (req, res) => { }); router.get("/:name/notice/*", async (req, res) => { - try{ + try { const name = req.params.name; const sts = await getSTS(viewActions, `${name}/notice/*`); return res.status(200).send(sts); @@ -183,7 +244,7 @@ router.get("/:name/notice/*", async (req, res) => { }); router.get("/:name/competition/*", async (req, res) => { - try{ + try { const name = req.params.name; const sts = await getSTS(viewActions, `${name}/competition/*`); return res.status(200).send(sts); @@ -193,7 +254,7 @@ router.get("/:name/competition/*", async (req, res) => { }); router.get("/:name/map/*", async (req, res) => { - try{ + try { const name = req.params.name; const sts = await getSTS(viewActions, `${name}/map/*`); return res.status(200).send(sts); @@ -203,7 +264,7 @@ router.get("/:name/map/*", async (req, res) => { }); router.get("/:name/arena/*", async (req, res) => { - try{ + try { const name = req.params.name; const sts = await getSTS(viewActions, `${name}/arena/*`); return res.status(200).send(sts); @@ -213,14 +274,13 @@ router.get("/:name/arena/*", async (req, res) => { }); router.get("/public/WebGL/:name/*", async (req, res, next) => { - try{ + try { const name = req.params.name; if (await checkContestManager(name)) { const sts = await getSTS(generalActions, `public/WebGL/${name}/*`); return res.status(200).send(sts); - } - else { - next('route'); + } else { + next("route"); } } catch (err) { return res.status(500).send(err); @@ -228,7 +288,7 @@ router.get("/public/WebGL/:name/*", async (req, res, next) => { }); router.get("/public/*", async (req, res) => { - try{ + try { const sts = await getSTS(viewActions, `public/*`); return res.status(200).send(sts); } catch (err) { From 8107bf4143e883cf83bc1d146a6431d9775c7d90 Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Tue, 3 Mar 2026 10:53:26 +0800 Subject: [PATCH 23/29] feat: added new mentor talk --- src/routes/application.ts | 345 ++++++++++++++++++++++++++++++++++++++ src/routes/static.ts | 51 ++++++ 2 files changed, 396 insertions(+) diff --git a/src/routes/application.ts b/src/routes/application.ts index b666eb0e..3235c346 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -2815,4 +2815,349 @@ router.get( }, ); +// ==================== 普通谈话记录(新系统:基于 mentor_talk_record 表) ==================== + +// 学生查询自己的所有谈话记录(同时返回 in_freshman 和当前学期) +router.get( + "/info/mentor/my_talk_records", + authenticate(["student"]), + async (req, res) => { + try { + const user_uuid: string = req.auth.user.uuid; + const student_no: string = req.auth.user.student_no; + + // 检查是否在 freshman 表中 + const freshman_query: any = await client.request( + gql` + query CheckIsFreshman($student_no: String!) { + freshman(where: { student_no: { _eq: $student_no } }) { + student_no + } + } + `, + { student_no }, + ); + const in_freshman: boolean = (freshman_query.freshman?.length ?? 0) > 0; + + const current_semester = await get_current_semester(); + + const records_query: any = await client.request( + gql` + query GetMentorTalkRecords($user_id: uuid!) { + mentor_talk_record( + where: { user_id: { _eq: $user_id } } + order_by: { updated_at: desc } + ) { + id + updated_at + user_id + semester + mentor_talk_confirm + } + } + `, + { user_id: user_uuid }, + ); + + return res.status(200).json({ + in_freshman, + current_semester, + records: records_query.mentor_talk_record ?? [], + }); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 学生提交谈话记录 +router.post( + "/info/mentor/talk_submit", + authenticate(["student"]), + async (req, res) => { + try { + const user_uuid: string = req.auth.user.uuid; + const student_no: string = req.auth.user.student_no; + + // 权限:是否在 freshman 表中 + const freshman_query: any = await client.request( + gql` + query CheckIsFreshman($student_no: String!) { + freshman(where: { student_no: { _eq: $student_no } }) { + student_no + } + } + `, + { student_no }, + ); + if ((freshman_query.freshman?.length ?? 0) === 0) { + return res.status(403).send("Error: Not a freshman"); + } + + const current_semester = await get_current_semester(); + if (!current_semester) { + return res.status(400).send("Error: No current semester"); + } + + const existing_query: any = await client.request( + gql` + query CheckExistingTalkRecord($user_id: uuid!, $semester: String!) { + mentor_talk_record( + where: { + user_id: { _eq: $user_id } + semester: { _eq: $semester } + } + ) { + id + mentor_talk_confirm + } + } + `, + { user_id: user_uuid, semester: current_semester }, + ); + const existing = existing_query.mentor_talk_record?.[0]; + + if (existing?.mentor_talk_confirm) { + return res + .status(400) + .send("Error: Talk already confirmed for this semester"); + } + + let result: any; + if (existing) { + const update_mut: any = await client.request( + gql` + mutation UpdateMentorTalkRecord($id: uuid!, $now: timestamptz!) { + update_mentor_talk_record_by_pk( + pk_columns: { id: $id } + _set: { updated_at: $now } + ) { + id + updated_at + user_id + semester + mentor_talk_confirm + } + } + `, + { id: existing.id, now: new Date().toISOString() }, + ); + result = update_mut.update_mentor_talk_record_by_pk; + } else { + const insert_mut: any = await client.request( + gql` + mutation InsertMentorTalkRecord( + $user_id: uuid! + $semester: String! + ) { + insert_mentor_talk_record_one( + object: { user_id: $user_id, semester: $semester } + ) { + id + updated_at + user_id + semester + mentor_talk_confirm + } + } + `, + { user_id: user_uuid, semester: current_semester }, + ); + result = insert_mut.insert_mentor_talk_record_one; + } + + if (!result) { + return res.status(500).send("Error: Submit failed"); + } + return res.status(200).json(result); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 导师/辅导员确认谈话记录 +router.post( + "/info/mentor/talk_confirm", + authenticate(["teacher", "counselor"]), + async (req, res) => { + try { + const record_id: string = req.body.record_id; + const operator_uuid: string = req.auth.user.uuid; + const operator_role: string = req.auth.user.role; + if (!record_id) { + return res.status(400).send("Error: Missing record_id"); + } + + const record_query: any = await client.request( + gql` + query GetMentorTalkRecord($id: uuid!) { + mentor_talk_record_by_pk(id: $id) { + id + user_id + semester + mentor_talk_confirm + } + } + `, + { id: record_id }, + ); + if (!record_query.mentor_talk_record_by_pk) { + return res.status(404).send("Error: Record not found"); + } + if (record_query.mentor_talk_record_by_pk.mentor_talk_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需验证该学生是自己已通过的学生 + if (operator_role === "teacher") { + const student_uuid = record_query.mentor_talk_record_by_pk.user_id; + const appl_check: any = await client.request( + gql` + query CheckTeacherStudent( + $mentor_uuid: uuid! + $student_uuid: uuid! + ) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + } + } + `, + { mentor_uuid: operator_uuid, student_uuid }, + ); + if (appl_check.mentor_application.length === 0) { + return res.status(403).send("Error: Not your student"); + } + } + + const update_mut: any = await client.request( + gql` + mutation ConfirmMentorTalkRecord($id: uuid!) { + update_mentor_talk_record_by_pk( + pk_columns: { id: $id } + _set: { mentor_talk_confirm: true } + ) { + id + mentor_talk_confirm + semester + user_id + } + } + `, + { id: record_id }, + ); + + if (!update_mut.update_mentor_talk_record_by_pk) { + return res.status(500).send("Error: Confirm failed"); + } + return res.status(200).json(update_mut.update_mentor_talk_record_by_pk); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 辅导员获取所有谈话记录 +router.get( + "/info/mentor/all_talk_records", + authenticate(["counselor"]), + async (req, res) => { + try { + const query: any = await client.request(gql` + query GetAllMentorTalkRecords { + mentor_talk_record(order_by: { updated_at: desc }) { + id + updated_at + user_id + semester + mentor_talk_confirm + user { + realname + student_no + department + class + } + } + } + `); + return res.status(200).json(query.mentor_talk_record ?? []); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + +// 导师获取自己学生的谈话记录(仅已通过申请的学生) +router.get( + "/info/mentor/my_students_talk_records", + authenticate(["teacher"]), + async (req, res) => { + try { + const mentor_uuid: string = req.auth.user.uuid; + + const applications_query: any = await client.request( + gql` + query GetApprovedStudentsForTalk($mentor_uuid: uuid!) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + } + } + `, + { mentor_uuid }, + ); + + const approved = applications_query?.mentor_application ?? []; + if (approved.length === 0) { + return res.status(200).json([]); + } + + const student_uuids: string[] = approved.map((a: any) => a.student_uuid); + + const records_query: any = await client.request( + gql` + query GetStudentsTalkRecords($student_uuids: [uuid!]!) { + mentor_talk_record( + where: { user_id: { _in: $student_uuids } } + order_by: { updated_at: desc } + ) { + id + updated_at + user_id + semester + mentor_talk_confirm + user { + realname + student_no + department + class + } + } + } + `, + { student_uuids }, + ); + + return res.status(200).json(records_query.mentor_talk_record ?? []); + } catch (err) { + console.log(err); + return res.status(500).send("Internal server error"); + } + }, +); + export default router; diff --git a/src/routes/static.ts b/src/routes/static.ts index be55c098..6686ea1d 100644 --- a/src/routes/static.ts +++ b/src/routes/static.ts @@ -114,6 +114,57 @@ router.get("/chat_record/:user_uuid/member/:semester/*", async (req, res) => { } }); +router.get("/chat_record/:user_uuid/talk/:semester/*", async (req, res) => { + try { + const { user_uuid: target_uuid, semester } = req.params; + if (role === "counselor") { + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/talk/${semester}/*`, + ); + return res.status(200).send(sts); + } else if (role === "student" && user_uuid === target_uuid) { + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/talk/${semester}/*`, + ); + return res.status(200).send(sts); + } else if (role === "teacher") { + const applications: any = await client.request( + gql` + query CheckTeacherStudentForTalk( + $mentor_uuid: uuid! + $student_uuid: uuid! + ) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid + } + } + `, + { mentor_uuid: user_uuid, student_uuid: target_uuid }, + ); + if (applications.mentor_application.length === 0) { + return res.status(401).send("当前用户没有权限访问该谈话记录"); + } + const sts = await getSTS( + generalActions, + `chat_record/${target_uuid}/talk/${semester}/*`, + ); + return res.status(200).send(sts); + } else { + return res.status(401).send("当前用户没有权限访问该谈话记录"); + } + } catch (err) { + return res.status(500).send(err); + } +}); + router.get("/chat_record/:application_id/*", async (req, res) => { try { if (role == "student" || role == "teacher") { From 4a93f66ecf0bf485a8ad34b76285bdaaf45fbc67 Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Tue, 3 Mar 2026 11:34:39 +0800 Subject: [PATCH 24/29] feat: make chat system compatible --- src/routes/application.ts | 529 ++++++++++++++++++++++++++++++++------ 1 file changed, 457 insertions(+), 72 deletions(-) diff --git a/src/routes/application.ts b/src/routes/application.ts index 3235c346..618a2261 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -2445,7 +2445,7 @@ router.get( // 获取当前学期 const current_semester = await get_current_semester(); - // 查询该学生所有谈话记录,按时间倒序 + // 查询新系统数据(member_chat_record 表) const records_query: any = await client.request( gql` query GetMemberChatRecords($user_id: uuid!) { @@ -2464,10 +2464,47 @@ router.get( { user_id: user_uuid }, ); + // 查询旧系统数据(mentor_application 表) + const old_records_query: any = await client.request( + gql` + query GetOldMemberChatRecords($student_uuid: uuid!) { + mentor_application( + where: { + student_uuid: { _eq: $student_uuid } + member_chat_status: { _eq: true } + } + order_by: { member_chat_time: desc } + ) { + id + member_chat_time + member_chat_confirm + } + } + `, + { student_uuid: user_uuid }, + ); + + // 合并旧数据(学期为空字符串) + const legacy_records = (old_records_query.mentor_application ?? []).map( + (app: any) => ({ + id: app.id, + updated_at: app.member_chat_time, + user_id: user_uuid, + semester: "", // 旧系统标记 + member_chat_confirm: app.member_chat_confirm ?? false, + application_id: app.id, // 用于下载 + }), + ); + + const all_records = [ + ...legacy_records, + ...(records_query.member_chat_record ?? []), + ]; + return res.status(200).json({ is_member, current_semester, - records: records_query.member_chat_record ?? [], + records: all_records, }); } catch (err) { console.log(err); @@ -2635,61 +2672,131 @@ router.post( `, { id: record_id }, ); - if (!record_query.member_chat_record_by_pk) { - return res.status(404).send("Error: Record not found"); - } - if (record_query.member_chat_record_by_pk.member_chat_confirm) { - return res.status(400).send("Error: Already confirmed"); - } - // 导师需额外验证该学生是自己已通过的学生 - if (operator_role === "teacher") { - const student_uuid = record_query.member_chat_record_by_pk.user_id; - const appl_check: any = await client.request( - gql` - query CheckTeacherStudent( - $mentor_uuid: uuid! - $student_uuid: uuid! - ) { - mentor_application( - where: { - mentor_uuid: { _eq: $mentor_uuid } - student_uuid: { _eq: $student_uuid } - status: { _eq: "approved" } + // 新系统数据处理 + if (record_query.member_chat_record_by_pk) { + if (record_query.member_chat_record_by_pk.member_chat_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需额外验证该学生是自己已通过的学生 + if (operator_role === "teacher") { + const student_uuid = record_query.member_chat_record_by_pk.user_id; + const appl_check: any = await client.request( + gql` + query CheckTeacherStudent( + $mentor_uuid: uuid! + $student_uuid: uuid! + ) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid } + } + `, + { mentor_uuid: operator_uuid, student_uuid }, + ); + if (appl_check.mentor_application.length === 0) { + return res.status(403).send("Error: Not your student"); + } + } + + const update_mut: any = await client.request( + gql` + mutation ConfirmMemberChatRecord($id: uuid!) { + update_member_chat_record_by_pk( + pk_columns: { id: $id } + _set: { member_chat_confirm: true } ) { - student_uuid + id + member_chat_confirm + semester + user_id } } `, - { mentor_uuid: operator_uuid, student_uuid }, + { id: record_id }, ); - if (appl_check.mentor_application.length === 0) { - return res.status(403).send("Error: Not your student"); + + if (!update_mut.update_member_chat_record_by_pk) { + return res.status(500).send("Error: Confirm failed"); } + return res.status(200).json(update_mut.update_member_chat_record_by_pk); } - const update_mut: any = await client.request( + // 旧系统数据处理:尝试从 mentor_application 表查询 + const old_record_query: any = await client.request( gql` - mutation ConfirmMemberChatRecord($id: uuid!) { - update_member_chat_record_by_pk( + query GetOldMemberChatRecord($id: uuid!) { + mentor_application_by_pk(id: $id) { + id + student_uuid + mentor_uuid + status + is_member + member_chat_status + member_chat_confirm + } + } + `, + { id: record_id }, + ); + + if (!old_record_query.mentor_application_by_pk) { + return res.status(404).send("Error: Record not found"); + } + + const old_app = old_record_query.mentor_application_by_pk; + if ( + old_app.status !== "approved" || + !old_app.is_member || + !old_app.member_chat_status + ) { + return res.status(404).send("Error: Not a valid member chat record"); + } + if (old_app.member_chat_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需验证该学生是自己的 + if ( + operator_role === "teacher" && + old_app.mentor_uuid !== operator_uuid + ) { + return res.status(403).send("Error: Not your student"); + } + + // 更新旧系统字段 + const update_old_mut: any = await client.request( + gql` + mutation ConfirmOldMemberChatRecord($id: uuid!) { + update_mentor_application_by_pk( pk_columns: { id: $id } _set: { member_chat_confirm: true } ) { id member_chat_confirm - semester - user_id + student_uuid } } `, { id: record_id }, ); - if (!update_mut.update_member_chat_record_by_pk) { + if (!update_old_mut.update_mentor_application_by_pk) { return res.status(500).send("Error: Confirm failed"); } - return res.status(200).json(update_mut.update_member_chat_record_by_pk); + return res.status(200).json({ + id: record_id, + member_chat_confirm: true, + semester: "", + user_id: old_app.student_uuid, + }); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); @@ -2703,6 +2810,7 @@ router.get( authenticate(["counselor"]), async (req, res) => { try { + // 查询新系统数据 const query: any = await client.request(gql` query GetAllMemberChatRecords { member_chat_record(order_by: { updated_at: desc }) { @@ -2720,7 +2828,47 @@ router.get( } } `); - return res.status(200).json(query.member_chat_record ?? []); + + // 查询旧系统数据 + const old_query: any = await client.request(gql` + query GetAllOldMemberChatRecords { + mentor_application( + where: { member_chat_status: { _eq: true } } + order_by: { member_chat_time: desc } + ) { + id + student_uuid + member_chat_time + member_chat_confirm + student { + realname + student_no + department + class + } + } + } + `); + + // 转换旧数据格式 + const legacy_records = (old_query.mentor_application ?? []).map( + (app: any) => ({ + id: app.id, + updated_at: app.member_chat_time, + user_id: app.student_uuid, + semester: "", + member_chat_confirm: app.member_chat_confirm ?? false, + application_id: app.id, + user: app.student, + }), + ); + + const all_records = [ + ...legacy_records, + ...(query.member_chat_record ?? []), + ]; + + return res.status(200).json(all_records); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); @@ -2782,7 +2930,7 @@ router.get( const student_uuids: string[] = approved.map((a: any) => a.student_uuid); - // 再查这些学生的积极分子谈话记录 + // 查询新系统数据(member_chat_record 表) const records_query: any = await client.request( gql` query GetStudentsMemberChatRecords($student_uuids: [uuid!]!) { @@ -2807,7 +2955,53 @@ router.get( { student_uuids }, ); - return res.status(200).json(records_query.member_chat_record ?? []); + // 查询旧系统数据(mentor_application 表) + const old_applications_query: any = await client.request( + gql` + query GetOldMemberChatApplications($mentor_uuid: uuid!) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + status: { _eq: "approved" } + member_chat_status: { _eq: true } + } + order_by: { member_chat_time: desc } + ) { + id + student_uuid + member_chat_time + member_chat_confirm + student { + realname + student_no + department + class + } + } + } + `, + { mentor_uuid }, + ); + + // 转换旧数据格式 + const legacy_records = ( + old_applications_query.mentor_application ?? [] + ).map((app: any) => ({ + id: app.id, + updated_at: app.member_chat_time, + user_id: app.student_uuid, + semester: "", // 旧系统标记 + member_chat_confirm: app.member_chat_confirm ?? false, + application_id: app.id, // 用于下载 + user: app.student, + })); + + const all_records = [ + ...legacy_records, + ...(records_query.member_chat_record ?? []), + ]; + + return res.status(200).json(all_records); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); @@ -2841,6 +3035,7 @@ router.get( const current_semester = await get_current_semester(); + // 查询新系统数据(mentor_talk_record 表) const records_query: any = await client.request( gql` query GetMentorTalkRecords($user_id: uuid!) { @@ -2859,10 +3054,47 @@ router.get( { user_id: user_uuid }, ); + // 查询旧系统数据(mentor_application 表) + const old_records_query: any = await client.request( + gql` + query GetOldMentorTalkRecords($student_uuid: uuid!) { + mentor_application( + where: { + student_uuid: { _eq: $student_uuid } + chat_status: { _eq: true } + } + order_by: { chat_time: desc } + ) { + id + chat_time + chat_confirm + } + } + `, + { student_uuid: user_uuid }, + ); + + // 合并旧数据(学期为空字符串) + const legacy_records = (old_records_query.mentor_application ?? []).map( + (app: any) => ({ + id: app.id, + updated_at: app.chat_time, + user_id: user_uuid, + semester: "", // 旧系统标记 + mentor_talk_confirm: app.chat_confirm ?? false, + application_id: app.id, // 用于下载 + }), + ); + + const all_records = [ + ...legacy_records, + ...(records_query.mentor_talk_record ?? []), + ]; + return res.status(200).json({ in_freshman, current_semester, - records: records_query.mentor_talk_record ?? [], + records: all_records, }); } catch (err) { console.log(err); @@ -3004,61 +3236,126 @@ router.post( `, { id: record_id }, ); - if (!record_query.mentor_talk_record_by_pk) { - return res.status(404).send("Error: Record not found"); - } - if (record_query.mentor_talk_record_by_pk.mentor_talk_confirm) { - return res.status(400).send("Error: Already confirmed"); - } - // 导师需验证该学生是自己已通过的学生 - if (operator_role === "teacher") { - const student_uuid = record_query.mentor_talk_record_by_pk.user_id; - const appl_check: any = await client.request( - gql` - query CheckTeacherStudent( - $mentor_uuid: uuid! - $student_uuid: uuid! - ) { - mentor_application( - where: { - mentor_uuid: { _eq: $mentor_uuid } - student_uuid: { _eq: $student_uuid } - status: { _eq: "approved" } + // 新系统数据处理 + if (record_query.mentor_talk_record_by_pk) { + if (record_query.mentor_talk_record_by_pk.mentor_talk_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需验证该学生是自己已通过的学生 + if (operator_role === "teacher") { + const student_uuid = record_query.mentor_talk_record_by_pk.user_id; + const appl_check: any = await client.request( + gql` + query CheckTeacherStudent( + $mentor_uuid: uuid! + $student_uuid: uuid! + ) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + student_uuid: { _eq: $student_uuid } + status: { _eq: "approved" } + } + ) { + student_uuid } + } + `, + { mentor_uuid: operator_uuid, student_uuid }, + ); + if (appl_check.mentor_application.length === 0) { + return res.status(403).send("Error: Not your student"); + } + } + + const update_mut: any = await client.request( + gql` + mutation ConfirmMentorTalkRecord($id: uuid!) { + update_mentor_talk_record_by_pk( + pk_columns: { id: $id } + _set: { mentor_talk_confirm: true } ) { - student_uuid + id + mentor_talk_confirm + semester + user_id } } `, - { mentor_uuid: operator_uuid, student_uuid }, + { id: record_id }, ); - if (appl_check.mentor_application.length === 0) { - return res.status(403).send("Error: Not your student"); + + if (!update_mut.update_mentor_talk_record_by_pk) { + return res.status(500).send("Error: Confirm failed"); } + return res.status(200).json(update_mut.update_mentor_talk_record_by_pk); } - const update_mut: any = await client.request( + // 旧系统数据处理:尝试从 mentor_application 表查询 + const old_record_query: any = await client.request( gql` - mutation ConfirmMentorTalkRecord($id: uuid!) { - update_mentor_talk_record_by_pk( + query GetOldTalkRecord($id: uuid!) { + mentor_application_by_pk(id: $id) { + id + student_uuid + mentor_uuid + status + chat_status + chat_confirm + } + } + `, + { id: record_id }, + ); + + if (!old_record_query.mentor_application_by_pk) { + return res.status(404).send("Error: Record not found"); + } + + const old_app = old_record_query.mentor_application_by_pk; + if (old_app.status !== "approved" || !old_app.chat_status) { + return res.status(404).send("Error: Not a valid talk record"); + } + if (old_app.chat_confirm) { + return res.status(400).send("Error: Already confirmed"); + } + + // 导师需验证该学生是自己的 + if ( + operator_role === "teacher" && + old_app.mentor_uuid !== operator_uuid + ) { + return res.status(403).send("Error: Not your student"); + } + + // 更新旧系统字段 + const update_old_mut: any = await client.request( + gql` + mutation ConfirmOldTalkRecord($id: uuid!) { + update_mentor_application_by_pk( pk_columns: { id: $id } - _set: { mentor_talk_confirm: true } + _set: { chat_confirm: true } ) { id - mentor_talk_confirm - semester - user_id + chat_confirm + student_uuid } } `, { id: record_id }, ); - if (!update_mut.update_mentor_talk_record_by_pk) { + if (!update_old_mut.update_mentor_application_by_pk) { return res.status(500).send("Error: Confirm failed"); } - return res.status(200).json(update_mut.update_mentor_talk_record_by_pk); + return res.status(200).json({ + id: record_id, + mentor_talk_confirm: true, + semester: "", + user_id: old_app.student_uuid, + }); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); @@ -3072,6 +3369,7 @@ router.get( authenticate(["counselor"]), async (req, res) => { try { + // 查询新系统数据 const query: any = await client.request(gql` query GetAllMentorTalkRecords { mentor_talk_record(order_by: { updated_at: desc }) { @@ -3089,7 +3387,47 @@ router.get( } } `); - return res.status(200).json(query.mentor_talk_record ?? []); + + // 查询旧系统数据 + const old_query: any = await client.request(gql` + query GetAllOldTalkRecords { + mentor_application( + where: { chat_status: { _eq: true } } + order_by: { chat_time: desc } + ) { + id + student_uuid + chat_time + chat_confirm + student { + realname + student_no + department + class + } + } + } + `); + + // 转换旧数据格式 + const legacy_records = (old_query.mentor_application ?? []).map( + (app: any) => ({ + id: app.id, + updated_at: app.chat_time, + user_id: app.student_uuid, + semester: "", + mentor_talk_confirm: app.chat_confirm ?? false, + application_id: app.id, + user: app.student, + }), + ); + + const all_records = [ + ...legacy_records, + ...(query.mentor_talk_record ?? []), + ]; + + return res.status(200).json(all_records); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); @@ -3128,6 +3466,7 @@ router.get( const student_uuids: string[] = approved.map((a: any) => a.student_uuid); + // 查询新系统数据(mentor_talk_record 表) const records_query: any = await client.request( gql` query GetStudentsTalkRecords($student_uuids: [uuid!]!) { @@ -3152,7 +3491,53 @@ router.get( { student_uuids }, ); - return res.status(200).json(records_query.mentor_talk_record ?? []); + // 查询旧系统数据(mentor_application 表) + const old_applications_query: any = await client.request( + gql` + query GetOldTalkApplications($mentor_uuid: uuid!) { + mentor_application( + where: { + mentor_uuid: { _eq: $mentor_uuid } + status: { _eq: "approved" } + chat_status: { _eq: true } + } + order_by: { chat_time: desc } + ) { + id + student_uuid + chat_time + chat_confirm + student { + realname + student_no + department + class + } + } + } + `, + { mentor_uuid }, + ); + + // 转换旧数据格式 + const legacy_records = ( + old_applications_query.mentor_application ?? [] + ).map((app: any) => ({ + id: app.id, + updated_at: app.chat_time, + user_id: app.student_uuid, + semester: "", // 旧系统标记 + mentor_talk_confirm: app.chat_confirm ?? false, + application_id: app.id, // 用于下载 + user: app.student, + })); + + const all_records = [ + ...legacy_records, + ...(records_query.mentor_talk_record ?? []), + ]; + + return res.status(200).json(all_records); } catch (err) { console.log(err); return res.status(500).send("Internal server error"); From bd8092a3f2d521977a665437900f5b3dba9d328f Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Tue, 3 Mar 2026 16:24:54 +0800 Subject: [PATCH 25/29] fixed: fixed member check error --- src/routes/application.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/routes/application.ts b/src/routes/application.ts index 618a2261..8cd01713 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -2441,6 +2441,24 @@ router.get( ); is_member = mentor_check.mentor_info_by_pk?.is_member ?? false; } + if (is_member) { + const freshman_check: any = await client.request( + gql` + query GetFreshmanIsMember($student_uuid: uuid!) { + freshman(where: { uuid: { _eq: $student_uuid } }) { + is_member + } + } + `, + { student_uuid: user_uuid }, + ); + if (freshman_check.freshman.length > 0) { + is_member = freshman_check.freshman[0].is_member ?? false; + } else { + // 如果没有找到对应的 freshman 记录,默认不是成员(或根据实际需求调整) + is_member = false; + } + } // 获取当前学期 const current_semester = await get_current_semester(); From 9bbf9de466eda47de0b904b84721bfdb50f16ceb Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Tue, 3 Mar 2026 19:47:56 +0800 Subject: [PATCH 26/29] fix: old member check bug --- src/routes/application.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/routes/application.ts b/src/routes/application.ts index 8cd01713..fab9f7e3 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -2428,6 +2428,7 @@ router.get( let is_member: boolean = false; const approved_mentor_uuid = appl_query.mentor_application?.[0]?.mentor_uuid; + // 先判断导师是否参与积极分子谈话 if (approved_mentor_uuid) { const mentor_check: any = await client.request( gql` @@ -2441,6 +2442,7 @@ router.get( ); is_member = mentor_check.mentor_info_by_pk?.is_member ?? false; } + //再判断学生是否是积极分子 if (is_member) { const freshman_check: any = await client.request( gql` @@ -2459,6 +2461,28 @@ router.get( is_member = false; } } + //最后在旧系统(mentor_application)中查询是否是积极分子 + if (!is_member) { + const old_member_check: any = await client.request( + gql` + query GetOldMemberStatus($student_uuid: uuid!) { + mentor_application( + where: { + student_uuid: { _eq: $student_uuid } + is_member: { _eq: true } + } + limit: 1 + ) { + is_member + } + } + `, + { student_uuid: user_uuid }, + ); + if (old_member_check.mentor_application.length > 0) { + is_member = old_member_check.mentor_application[0].is_member ?? false; + } + } // 获取当前学期 const current_semester = await get_current_semester(); @@ -2489,7 +2513,7 @@ router.get( mentor_application( where: { student_uuid: { _eq: $student_uuid } - member_chat_status: { _eq: true } + is_member: { _eq: true } } order_by: { member_chat_time: desc } ) { From 6ac4aa3ff2f42e7945f4dc69b67b1ba4c27a5ab9 Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Sun, 8 Mar 2026 19:59:36 +0800 Subject: [PATCH 27/29] added software contest deadline checks --- src/hasura/contest.ts | 37 ++++++++++++++++++++++++ src/routes/competition.ts | 60 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 95 insertions(+), 2 deletions(-) diff --git a/src/hasura/contest.ts b/src/hasura/contest.ts index f2a90750..96f6aa71 100644 --- a/src/hasura/contest.ts +++ b/src/hasura/contest.ts @@ -2588,3 +2588,40 @@ export const delete_contest_time: any = async ( ); return delete_contest_time.delete_contest_time_by_pk?.event ?? undefined; }; + +export const get_software_contest_deadline: any = async ( + contest_id: string, +) => { + const query_software_contest_deadline: any = await client.request( + gql` + query MyQuery($_eq: uuid = "") { + contest_soft_code_deadline(where: { contest_id: { _eq: $_eq } }) { + deadline + } + } + `, + { + _eq: contest_id, + }, + ); + return ( + query_software_contest_deadline.contest_soft_code_deadline[0]?.deadline ?? + undefined + ); +}; + +export const get_contest_id_from_team_id: any = async (team_id: string) => { + const query_contest_id_from_team_id: any = await client.request( + gql` + query MyQuery($team: uuid = "") { + contest_team(where: { team_id: { _eq: $team } }) { + contest_id + } + } + `, + { + team: team_id, + }, + ); + return query_contest_id_from_team_id.contest_team[0]?.contest_id ?? undefined; +}; diff --git a/src/routes/competition.ts b/src/routes/competition.ts index cb707b3a..e0dee329 100644 --- a/src/routes/competition.ts +++ b/src/routes/competition.ts @@ -1204,7 +1204,15 @@ router.post("/add_team_software_code", authenticate(), async (req, res) => { .status(400) .json({ error: "400 Bad Request: Invalid URL format" }); } - + // Deadline check: verify current time is before contest deadline + const deadline = + await ContHasFunc.get_software_contest_deadline(contest_id); + const isBeforeDeadline = deadline ? new Date() < new Date(deadline) : false; + if (!isBeforeDeadline) { + return res.status(403).json({ + error: "403 Forbidden: Contest submission deadline has passed", + }); + } // Permission check: verify user is a member of the team const userTeam = await ContHasFunc.check_user_team(user_uuid, team_id); if (!userTeam) { @@ -1262,7 +1270,16 @@ router.post("/update_team_software_code", authenticate(), async (req, res) => { .json({ error: "400 Bad Request: Invalid URL format" }); } - // Validate submission_count is a non-negative number + // Deadline check: verify current time is before contest deadline + const contest_id = await ContHasFunc.get_contest_id_from_team_id(team_id); + const deadline = + await ContHasFunc.get_software_contest_deadline(contest_id); + const isBeforeDeadline = deadline ? new Date() < new Date(deadline) : false; + if (!isBeforeDeadline) { + return res.status(403).json({ + error: "403 Forbidden: Contest submission deadline has passed", + }); + } // Permission check: verify user is a member of the team const is_member = await ContHasFunc.check_user_team(user_uuid, team_id); @@ -1396,4 +1413,43 @@ router.post( }, ); +router.post("/get_software_contest_ddl", authenticate(), async (req, res) => { + try { + // const user_uuid = req.auth.user.uuid; + const { contest_id } = req.body; + + // Input validation + if (!contest_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing contest_id" }); + } + + // Validate UUID format + const uuidRegex = + /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; + if (!uuidRegex.test(contest_id)) { + return res + .status(400) + .json({ error: "400 Bad Request: Invalid UUID format" }); + } + + // Permission check: verify user is a manager of the contest + const result = await ContHasFunc.get_software_contest_deadline(contest_id); + if (result === null) { + return res + .status(404) + .json({ error: "404 Not Found: Contest not found" }); + } + const { deadline, current_time } = result; + res.status(200).json({ data: { deadline, current_time } }); + } catch (err: any) { + console.error(err); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } +}); export default router; From 3a94f32dcfadcf534b6a8755f6735ef7ce27aeae Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Mon, 16 Mar 2026 22:14:54 +0800 Subject: [PATCH 28/29] fixed yarn lock --- .github/workflows/deploy.yml | 6 +- package.json | 15 +- src/app.ts | 12 + src/hasura/share.ts | 1024 ++++++++++++++++++------------- src/middlewares/authenticate.ts | 62 +- src/routes/application.ts | 99 ++- src/routes/notice.ts | 95 ++- src/routes/share.ts | 773 +++++++++++++++-------- 8 files changed, 1319 insertions(+), 767 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 2df8e561..53661775 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,9 +2,9 @@ name: deploy on: push: - branches: [ master ] + branches: [master] pull_request: - branches: [ master ] + branches: [master] jobs: test: @@ -16,7 +16,7 @@ jobs: - name: Use Node.js ${{matrix.node-version}} uses: actions/setup-node@v6 with: - node-version: "24.13.1" + node-version: "24.14.0" - name: get yarn cache path id: yarn-cache-dir-path diff --git a/package.json b/package.json index 045fda12..dc37f315 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ }, "dependencies": { "async-mutex": "0.5.0", - "axios": "1.13.5", + "axios": "1.13.6", "bcrypt": "6.0.0", "cors": "2.8.6", "cos-nodejs-sdk-v5": "2.15.4", @@ -23,6 +23,7 @@ "dockerode": "4.0.9", "dotenv": "17.3.1", "express": "4.22.1", + "express-rate-limit": "^8.3.1", "graphql": "16.12.0", "graphql-request": "6.1.0", "html-to-text": "9.0.5", @@ -32,7 +33,7 @@ "jsonwebtoken": "9.0.2", "morgan": "1.10.1", "node-cron": "4.2.1", - "nodemailer": "8.0.1", + "nodemailer": "8.0.2", "openai": "6.22.0", "qcloud-cos-sts": "3.1.3", "unisms": "0.0.6", @@ -48,20 +49,20 @@ "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.7", "@types/morgan": "1.9.10", - "@types/node": "24.10.4", + "@types/node": "24.12.0", "@types/node-cron": "3.0.11", "@types/nodemailer": "7.0.11", "@types/web-push": "3.6.4", "cross-env": "10.1.0", - "eslint": "9.39.3", + "eslint": "9.39.4", "eslint-config-prettier": "10.1.8", "husky": "9.1.7", - "lint-staged": "16.2.7", - "nodemon": "3.1.13", + "lint-staged": "16.3.3", + "nodemon": "3.1.14", "prettier": "3.8.1", "ts-node": "10.9.2", "typescript": "5.9.3", - "typescript-eslint": "8.56.0" + "typescript-eslint": "8.57.0" }, "lint-staged": { "src/**/*.ts": [ diff --git a/src/app.ts b/src/app.ts index 945bffe4..263f5bff 100644 --- a/src/app.ts +++ b/src/app.ts @@ -20,7 +20,9 @@ import chatRoute from "./routes/chat"; import mentorRoute from "./routes/mentor"; import noticeRoute from "./routes/notice"; import courseRouter from "./routes/course"; +import shareRouter from "./routes/share"; import llmRouter from "./routes/llm"; +import rateLimit from "express-rate-limit"; const app = express(); @@ -29,6 +31,14 @@ const whitelist = ? ["https://eesast.com", "https://docs.eesast.com", "http://localhost:3000"] : ["http://localhost:3000"]; +const globalRateLimiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1 minutes + max: 1000, // limit each IP to 1000 requests per windowMs + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers + message: "Too many requests from this IP, please try again later.", +}); + app.use( cors({ origin: function (origin, callback) { @@ -45,6 +55,7 @@ app.use(logger(process.env.NODE_ENV === "production" ? "combined" : "dev")); app.use(express.json()); app.use(express.urlencoded({ extended: true })); +app.use(globalRateLimiter); app.use("/static", staticRouter); app.use("/user", userRouter); app.use("/emails", emailRouter); @@ -65,4 +76,5 @@ app.use("/chat", chatRoute); app.use("/mentor", mentorRoute); app.use("/notice", noticeRoute); app.use("/course", courseRouter); +app.use("/share", shareRouter); export default app; diff --git a/src/hasura/share.ts b/src/hasura/share.ts index 6fce4515..aa879e41 100644 --- a/src/hasura/share.ts +++ b/src/hasura/share.ts @@ -2,11 +2,11 @@ import { gql } from "graphql-request"; import { client } from ".."; type WeeklyPost = { - id: number; - title: string; - url: string; - date: Date; - } + id: number; + title: string; + url: string; + date: Date; +}; export { WeeklyPost }; /** ============================================================================ @@ -14,10 +14,6 @@ export { WeeklyPost }; ============================================================================ */ - - - - /** ============================================================================ ============================ INSERT FUNCTIONS ============================== @@ -35,51 +31,61 @@ export { WeeklyPost }; * @param {string} code - Course code * @returns {Promise} - The UUID of the new course */ -export const add_course = async(year: number, type: string, semester: string, professor: string, name: string, language: string, fullname: string, code: string): Promise => { - const add_course_query: { insert_course?: { returning: { uuid: string }[] } } = await client.request( - gql` - mutation AddCourse( - $year: Int!, - $type: String!, - $semester: String!, - $professor: String!, - $name: String!, - $language: String!, - $fullname: String!, - $code: String! +export const add_course = async ( + year: number, + type: string, + semester: string, + professor: string, + name: string, + language: string, + fullname: string, + code: string, +): Promise => { + const add_course_query: { + insert_course?: { returning: { uuid: string }[] }; + } = await client.request( + gql` + mutation AddCourse( + $year: Int! + $type: String! + $semester: String! + $professor: String! + $name: String! + $language: String! + $fullname: String! + $code: String! + ) { + insert_course( + objects: { + code: $code + fullname: $fullname + language: $language + name: $name + professor: $professor + semester: $semester + type: $type + year: $year + } ) { - insert_course( - objects: { - code: $code, - fullname: $fullname, - language: $language, - name: $name, - professor: $professor, - semester: $semester, - type: $type, - year: $year - } - ) { - returning { - uuid - } - } - }`, - { - year: year, - type: type, - semester: semester, - professor: professor, - name: name, - language: language, - fullname: fullname, - code: code + returning { + uuid + } } - ); - return add_course_query?.insert_course?.returning[0]?.uuid; -} - - + } + `, + { + year: year, + type: type, + semester: semester, + professor: professor, + name: name, + language: language, + fullname: fullname, + code: code, + }, + ); + return add_course_query?.insert_course?.returning[0]?.uuid; +}; /** * Add course info @@ -88,22 +94,35 @@ export const add_course = async(year: number, type: string, semester: string, pr * @param {string} course_id - The UUID of the course * @returns {Promise} - The UUID of the added course */ -export const add_course_info = async(key: string, value: string, course_id: string): Promise => { - const add_course_info_query: { insert_course_info_one?: { course_id: string } } = await client.request( - gql` - mutation AddCourseInfo($key: String!, $value: String!, $course_id: uuid!) { - insert_course_info_one(object: {key: $key, value: $value, course_id: $course_id}) { - course_id - } - }`, - { - key: key, - value: value, - course_id: course_id +export const add_course_info = async ( + key: string, + value: string, + course_id: string, +): Promise => { + const add_course_info_query: { + insert_course_info_one?: { course_id: string }; + } = await client.request( + gql` + mutation AddCourseInfo( + $key: String! + $value: String! + $course_id: uuid! + ) { + insert_course_info_one( + object: { key: $key, value: $value, course_id: $course_id } + ) { + course_id } - ); - return add_course_info_query?.insert_course_info_one?.course_id; -} + } + `, + { + key: key, + value: value, + course_id: course_id, + }, + ); + return add_course_info_query?.insert_course_info_one?.course_id; +}; /** * Add course rating @@ -117,47 +136,61 @@ export const add_course_info = async(key: string, value: string, course_id: stri * @param {string} user_uuid - The UUID of the user * @returns {Promise} - The creation timestamp of the new rating */ -export const add_course_rating = async(dim1: number, dim2: number, dim3: number, dim4: number, dim5: number, dim6: number, course_id: string, user_uuid: string): Promise => { - const add_course_rating_query: { insert_course_rating_one?: { created_at: string } } = await client.request( - gql` - mutation AddCourseRating( - $dim1: Int!, - $dim2: Int!, - $dim3: Int!, - $dim4: Int!, - $dim5: Int!, - $dim6: Int!, - $course_id: uuid!, - $user_uuid: uuid! +export const add_course_rating = async ( + dim1: number, + dim2: number, + dim3: number, + dim4: number, + dim5: number, + dim6: number, + course_id: string, + user_uuid: string, +): Promise => { + const add_course_rating_query: { + insert_course_rating_one?: { created_at: string }; + } = await client.request( + gql` + mutation AddCourseRating( + $dim1: Int! + $dim2: Int! + $dim3: Int! + $dim4: Int! + $dim5: Int! + $dim6: Int! + $course_id: uuid! + $user_uuid: uuid! + ) { + insert_course_rating_one( + object: { + dim1: $dim1 + dim2: $dim2 + dim3: $dim3 + dim4: $dim4 + dim5: $dim5 + dim6: $dim6 + course_id: $course_id + user_uuid: $user_uuid + } ) { - insert_course_rating_one(object: { - dim1: $dim1, - dim2: $dim2, - dim3: $dim3, - dim4: $dim4, - dim5: $dim5, - dim6: $dim6, - course_id: $course_id, - user_uuid: $user_uuid - }) { - created_at - } - }`, - { - dim1: dim1, - dim2: dim2, - dim3: dim3, - dim4: dim4, - dim5: dim5, - dim6: dim6, - course_id: course_id, - user_uuid: user_uuid + created_at } - ); - return add_course_rating_query?.insert_course_rating_one?.created_at; -} + } + `, + { + dim1: dim1, + dim2: dim2, + dim3: dim3, + dim4: dim4, + dim5: dim5, + dim6: dim6, + course_id: course_id, + user_uuid: user_uuid, + }, + ); + return add_course_rating_query?.insert_course_rating_one?.created_at; +}; - /** +/** * Add a course comment * @param {string} comment - The content of the comment * @param {string} user_uuid - The UUID of the user @@ -165,35 +198,43 @@ export const add_course_rating = async(dim1: number, dim2: number, dim3: number, * @param {string} parent_uuid - The UUID of the parent comment * @returns {Promise} - The UUID of the new comment */ -export const add_course_comment_one = async(comment: string, user_uuid: string, course_uuid: string, parent_uuid: string): Promise => { - const add_course_comment_one_query: { insert_course_comment_one?: { uuid: string } } = await client.request( - gql` - mutation AddCourseCommentOne( - $comment: String! - $user_uuid: uuid! - $course_uuid: uuid! - $parent_uuid: uuid +export const add_course_comment_one = async ( + comment: string, + user_uuid: string, + course_uuid: string, + parent_uuid: string, +): Promise => { + const add_course_comment_one_query: { + insert_course_comment_one?: { uuid: string }; + } = await client.request( + gql` + mutation AddCourseCommentOne( + $comment: String! + $user_uuid: uuid! + $course_uuid: uuid! + $parent_uuid: uuid + ) { + insert_course_comment_one( + object: { + comment: $comment + course_id: $course_uuid + user_uuid: $user_uuid + parent_uuid: $parent_uuid + } ) { - insert_course_comment_one( - object: { - comment: $comment - course_id: $course_uuid - user_uuid: $user_uuid - parent_uuid: $parent_uuid - } - ) { - uuid - } - }`, + uuid + } + } + `, { - comment: comment, - user_uuid: user_uuid, - course_uuid: course_uuid, - parent_uuid: parent_uuid - } - ); - return add_course_comment_one_query?.insert_course_comment_one?.uuid; -} + comment: comment, + user_uuid: user_uuid, + course_uuid: course_uuid, + parent_uuid: parent_uuid, + }, + ); + return add_course_comment_one_query?.insert_course_comment_one?.uuid; +}; /** * Add stars to a course comment @@ -201,25 +242,34 @@ export const add_course_comment_one = async(comment: string, user_uuid: string, * @param {string} user_uuid - The UUID of the user * @returns {Promise} - The UUID of the comment */ -export const add_course_comment_stars = async(comment_uuid: string, user_uuid: string): Promise => { - const add_course_comment_stars_query: { insert_course_comment_stars_one?: { comment_uuid: string } } = await client.request( - gql` - mutation AddCourseCommentStars($comment_uuid: uuid!, $user_uuid: uuid!) { - insert_course_comment_stars_one( - object: { comment_uuid: $comment_uuid, user_uuid: $user_uuid } - on_conflict: { constraint: course_comment_star_pkey, update_columns: [] } - ) { - comment_uuid - } - }`, - { - comment_uuid: comment_uuid, - user_uuid: user_uuid +export const add_course_comment_stars = async ( + comment_uuid: string, + user_uuid: string, +): Promise => { + const add_course_comment_stars_query: { + insert_course_comment_stars_one?: { comment_uuid: string }; + } = await client.request( + gql` + mutation AddCourseCommentStars($comment_uuid: uuid!, $user_uuid: uuid!) { + insert_course_comment_stars_one( + object: { comment_uuid: $comment_uuid, user_uuid: $user_uuid } + on_conflict: { + constraint: course_comment_star_pkey + update_columns: [] + } + ) { + comment_uuid } - ); - return add_course_comment_stars_query?.insert_course_comment_stars_one?.comment_uuid; -} - + } + `, + { + comment_uuid: comment_uuid, + user_uuid: user_uuid, + }, + ); + return add_course_comment_stars_query?.insert_course_comment_stars_one + ?.comment_uuid; +}; /** * Add likes to a course comment @@ -227,24 +277,34 @@ export const add_course_comment_stars = async(comment_uuid: string, user_uuid: s * @param {string} user_uuid - The UUID of the user * @returns {Promise} - The UUID of the comment */ -export const add_course_comment_likes = async(comment_uuid: string, user_uuid: string): Promise => { - const add_course_comment_likes_query: { insert_course_comment_likes_one?: { comment_uuid: string } } = await client.request( - gql` - mutation AddCourseCommentLikes($comment_uuid: uuid!, $user_uuid: uuid!) { - insert_course_comment_likes_one( - object: { comment_uuid: $comment_uuid, user_uuid: $user_uuid } - on_conflict: { constraint: course_comment_likes_pkey, update_columns: [] } - ) { - comment_uuid - } - }`, - { - comment_uuid: comment_uuid, - user_uuid: user_uuid +export const add_course_comment_likes = async ( + comment_uuid: string, + user_uuid: string, +): Promise => { + const add_course_comment_likes_query: { + insert_course_comment_likes_one?: { comment_uuid: string }; + } = await client.request( + gql` + mutation AddCourseCommentLikes($comment_uuid: uuid!, $user_uuid: uuid!) { + insert_course_comment_likes_one( + object: { comment_uuid: $comment_uuid, user_uuid: $user_uuid } + on_conflict: { + constraint: course_comment_likes_pkey + update_columns: [] + } + ) { + comment_uuid } - ); - return add_course_comment_likes_query?.insert_course_comment_likes_one?.comment_uuid; -} + } + `, + { + comment_uuid: comment_uuid, + user_uuid: user_uuid, + }, + ); + return add_course_comment_likes_query?.insert_course_comment_likes_one + ?.comment_uuid; +}; /** ============================================================================ @@ -265,75 +325,103 @@ export const add_course_comment_likes = async(comment_uuid: string, user_uuid: s * @param {number} year - Year of the course * @returns {Promise} - The UUID of the updated course */ -export const update_course = async(code: string, uuid: string, fullname: string, language: string, name: string, professor: string, semester: string, type: string, year: number): Promise => { - const update_course_query: { update_course_by_pk?: { uuid: string } } = await client.request( - gql` +export const update_course = async ( + code: string, + uuid: string, + fullname: string, + language: string, + name: string, + professor: string, + semester: string, + type: string, + year: number, +): Promise => { + const update_course_query: { update_course_by_pk?: { uuid: string } } = + await client.request( + gql` mutation UpdateCourse( - $code: String!, - $uuid: uuid!, - $fullname: String!, - $language: String!, - $name: String!, - $professor: String!, - $semester: String!, - $type: String!, - $year: Int! + $code: String! + $uuid: uuid! + $fullname: String! + $language: String! + $name: String! + $professor: String! + $semester: String! + $type: String! + $year: Int! ) { - update_course_by_pk( - pk_columns: {uuid: $uuid}, - _set: { - code: $code, - fullname: $fullname, - language: $language, - name: $name, - professor: $professor, - semester: $semester, - type: $type, - year: $year - } - ) { - uuid + update_course_by_pk( + pk_columns: { uuid: $uuid } + _set: { + code: $code + fullname: $fullname + language: $language + name: $name + professor: $professor + semester: $semester + type: $type + year: $year } - }`, - { - code: code, - uuid: uuid, - fullname: fullname, - language: language, - name: name, - professor: professor, - semester: semester, - type: type, - year: year + ) { + uuid + } } + `, + { + code: code, + uuid: uuid, + fullname: fullname, + language: language, + name: name, + professor: professor, + semester: semester, + type: type, + year: year, + }, ); - return update_course_query?.update_course_by_pk?.uuid; -} - + return update_course_query?.update_course_by_pk?.uuid; +}; /** - * Update course info + * Update course info key and value * @param {string} course_id - The UUID of the course - * @param {string} key - The key to update - * @param {string} value - The new value - * @returns {Promise} - The UUID of the updated course + * @param {string} old_key - The current key + * @param {string} new_key - The new key to set + * @param {string} value - The new value to set + * @returns {Promise} - The number of affected rows */ -export const update_course_info = async(course_id: string, key: string, value: string): Promise => { - const update_course_info_query: { update_course_info_by_pk?: { course_id: string } } = await client.request( - gql` - mutation UpdateCourseInfo($course_id: uuid!, $key: String!, $value: String!) { - update_course_info_by_pk(pk_columns: {course_id: $course_id, key: $key}, _set: {value: $value}) { - course_id - } - }`, - { - course_id: course_id, - key: key, - value: value +export const update_course_info = async ( + course_id: string, + old_key: string, + new_key: string, + value: string, +): Promise => { + const update_query: { update_course_info?: { affected_rows: number } } = + await client.request( + gql` + mutation UpdateCourseInfoAtomic( + $course_id: uuid! + $old_key: String! + $new_key: String! + $value: String! + ) { + update_course_info( + where: { course_id: { _eq: $course_id }, key: { _eq: $old_key } } + _set: { key: $new_key, value: $value } + ) { + affected_rows + } } + `, + { + course_id: course_id, + old_key: old_key, + new_key: new_key, + value: value, + }, ); - return update_course_info_query?.update_course_info_by_pk?.course_id; -} + return update_query?.update_course_info?.affected_rows; +}; /** * Update course rating @@ -347,74 +435,87 @@ export const update_course_info = async(course_id: string, key: string, value: s * @param {number} dim6 - Dimension 6 rating * @returns {Promise} - The update timestamp of the rating */ -export const update_course_rating = async(course_id: string, user_uuid: string, dim1: number, dim2: number, dim3: number, dim4: number, dim5: number, dim6: number): Promise => { - const update_course_rating_query: { update_course_rating_by_pk?: { updated_at: string } } = await client.request( - gql` - mutation UpdateCourseRating( - $course_id: uuid!, - $user_uuid: uuid!, - $dim1: Int!, - $dim2: Int!, - $dim3: Int!, - $dim4: Int!, - $dim5: Int!, - $dim6: Int! +export const update_course_rating = async ( + course_id: string, + user_uuid: string, + dim1: number, + dim2: number, + dim3: number, + dim4: number, + dim5: number, + dim6: number, +): Promise => { + const update_course_rating_query: { + update_course_rating_by_pk?: { updated_at: string }; + } = await client.request( + gql` + mutation UpdateCourseRating( + $course_id: uuid! + $user_uuid: uuid! + $dim1: Int! + $dim2: Int! + $dim3: Int! + $dim4: Int! + $dim5: Int! + $dim6: Int! + ) { + update_course_rating_by_pk( + pk_columns: { course_id: $course_id, user_uuid: $user_uuid } + _set: { + dim1: $dim1 + dim2: $dim2 + dim3: $dim3 + dim4: $dim4 + dim5: $dim5 + dim6: $dim6 + } ) { - update_course_rating_by_pk( - pk_columns: { - course_id: $course_id, - user_uuid: $user_uuid - }, - _set: { - dim1: $dim1, - dim2: $dim2, - dim3: $dim3, - dim4: $dim4, - dim5: $dim5, - dim6: $dim6 - } - ) { - updated_at - } - }`, - { - course_id: course_id, - user_uuid: user_uuid, - dim1: dim1, - dim2: dim2, - dim3: dim3, - dim4: dim4, - dim5: dim5, - dim6: dim6 + updated_at } - ); - return update_course_rating_query?.update_course_rating_by_pk?.updated_at; -} + } + `, + { + course_id: course_id, + user_uuid: user_uuid, + dim1: dim1, + dim2: dim2, + dim3: dim3, + dim4: dim4, + dim5: dim5, + dim6: dim6, + }, + ); + return update_course_rating_query?.update_course_rating_by_pk?.updated_at; +}; - /** +/** * Update a course comment * @param {string} comment - The new content of the comment * @param {string} uuid - The UUID of the comment * @returns {Promise} - The UUID of the updated comment */ -export const update_course_comment = async (comment: string, uuid: string): Promise => { - const update_course_comment_query: any = await client.request( - gql` - mutation UpdateCourseComment($comment: String!, $uuid: uuid!) { - update_course_comment_by_pk( - pk_columns: { uuid: $uuid } - _set: { comment: $comment } - ) { - uuid - } - }`, - { - uuid: uuid, - comment: comment +export const update_course_comment = async ( + comment: string, + uuid: string, +): Promise => { + const update_course_comment_query: any = await client.request( + gql` + mutation UpdateCourseComment($comment: String!, $uuid: uuid!) { + update_course_comment_by_pk( + pk_columns: { uuid: $uuid } + _set: { comment: $comment } + ) { + uuid } - ); - return update_course_comment_query?.update_course_comment_by_pk?.uuid; -} + } + `, + { + uuid: uuid, + comment: comment, + }, + ); + return update_course_comment_query?.update_course_comment_by_pk?.uuid; +}; /** ============================================================================ @@ -422,50 +523,58 @@ export const update_course_comment = async (comment: string, uuid: string): Prom ============================================================================ */ - /** * Delete course * @param {string} uuid - Course UUID * @returns {Promise} - The UUID of the deleted course */ -export const delete_course = async(uuid: string): Promise => { - const delete_course_query: { delete_course_by_pk?: { uuid: string } } = await client.request( - gql` +export const delete_course = async ( + uuid: string, +): Promise => { + const delete_course_query: { delete_course_by_pk?: { uuid: string } } = + await client.request( + gql` mutation DeleteCourse($uuid: uuid!) { - delete_course_by_pk(uuid: $uuid) { - uuid - } - }`, - { - uuid: uuid + delete_course_by_pk(uuid: $uuid) { + uuid + } } + `, + { + uuid: uuid, + }, ); - return delete_course_query?.delete_course_by_pk?.uuid; -} - + return delete_course_query?.delete_course_by_pk?.uuid; +}; - /** +/** * Delete course info * @param {string} course_id - The UUID of the course * @param {string} key - The key to delete * @returns {Promise<{ course_id: string, key: string } | undefined>} - The UUID and key of the deleted course info */ -export const delete_course_info = async(course_id: string, key: string): Promise<{ course_id: string, key: string } | undefined> => { - const delete_course_info_query: { delete_course_info_by_pk?: { course_id: string, key: string } } = await client.request( - gql` - mutation MyMutation3($course_id: uuid!, $key: String!) { - delete_course_info_by_pk(course_id: $course_id, key: $key) { - course_id - key - } - }`, - { - course_id: course_id, - key: key +export const delete_course_info = async ( + course_id: string, + key: string, +): Promise<{ course_id: string; key: string } | undefined> => { + const delete_course_info_query: { + delete_course_info_by_pk?: { course_id: string; key: string }; + } = await client.request( + gql` + mutation MyMutation3($course_id: uuid!, $key: String!) { + delete_course_info_by_pk(course_id: $course_id, key: $key) { + course_id + key } - ); - return delete_course_info_query?.delete_course_info_by_pk; -} + } + `, + { + course_id: course_id, + key: key, + }, + ); + return delete_course_info_query?.delete_course_info_by_pk; +}; /** * Delete course rating @@ -473,45 +582,59 @@ export const delete_course_info = async(course_id: string, key: string): Promise * @param {string} user_uuid - The UUID of the user * @returns {Promise<{ course_id: string, user_uuid: string } | undefined>} - The UUIDs of the deleted course rating */ -export const delete_course_rating = async(course_id: string, user_uuid: string): Promise<{ course_id: string, user_uuid: string } | undefined> => { - const delete_course_rating_query: { delete_course_rating_by_pk?: { course_id: string, user_uuid: string } } = await client.request( - gql` - mutation DeleteCourseRating($course_id: uuid!, $user_uuid: uuid!) { - delete_course_rating_by_pk(course_id: $course_id, user_uuid: $user_uuid) { - course_id - user_uuid - } - }`, - { - course_id: course_id, - user_uuid: user_uuid +export const delete_course_rating = async ( + course_id: string, + user_uuid: string, +): Promise<{ course_id: string; user_uuid: string } | undefined> => { + const delete_course_rating_query: { + delete_course_rating_by_pk?: { course_id: string; user_uuid: string }; + } = await client.request( + gql` + mutation DeleteCourseRating($course_id: uuid!, $user_uuid: uuid!) { + delete_course_rating_by_pk( + course_id: $course_id + user_uuid: $user_uuid + ) { + course_id + user_uuid } - ); - return delete_course_rating_query?.delete_course_rating_by_pk; -} + } + `, + { + course_id: course_id, + user_uuid: user_uuid, + }, + ); + return delete_course_rating_query?.delete_course_rating_by_pk; +}; /** * Soft delete a course comment * @param {string} uuid - The UUID of the comment * @returns {Promise} - The UUID of the deleted comment */ -export const delete_course_comment_one = async (uuid: string): Promise => { - const delete_course_comment_one_query: { update_course_comment_by_pk?: { uuid: string } } = await client.request( - gql` - mutation DeleteCourseCommentOne($uuid: uuid!) { - update_course_comment_by_pk( - pk_columns: { uuid: $uuid } - _set: { deleted: true } - ) { - uuid - } - }`, - { - uuid: uuid +export const delete_course_comment_one = async ( + uuid: string, +): Promise => { + const delete_course_comment_one_query: { + update_course_comment_by_pk?: { uuid: string }; + } = await client.request( + gql` + mutation DeleteCourseCommentOne($uuid: uuid!) { + update_course_comment_by_pk( + pk_columns: { uuid: $uuid } + _set: { deleted: true } + ) { + uuid } - ); - return delete_course_comment_one_query?.update_course_comment_by_pk?.uuid; -} + } + `, + { + uuid: uuid, + }, + ); + return delete_course_comment_one_query?.update_course_comment_by_pk?.uuid; +}; /** * Delete stars from a course comment @@ -519,24 +642,34 @@ export const delete_course_comment_one = async (uuid: string): Promise} - The UUID of the comment */ -export const delete_course_comment_stars = async (comment_uuid: string, user_uuid: string): Promise => { - const delete_course_comment_stars_query: { delete_course_comment_stars_by_pk?: { comment_uuid: string } } = await client.request( - gql` - mutation DeleteCourseCommentStars($comment_uuid: uuid!, $user_uuid: uuid!) { - delete_course_comment_stars_by_pk( - comment_uuid: $comment_uuid - user_uuid: $user_uuid - ) { - comment_uuid - } - }`, - { - comment_uuid: comment_uuid, - user_uuid: user_uuid +export const delete_course_comment_stars = async ( + comment_uuid: string, + user_uuid: string, +): Promise => { + const delete_course_comment_stars_query: { + delete_course_comment_stars_by_pk?: { comment_uuid: string }; + } = await client.request( + gql` + mutation DeleteCourseCommentStars( + $comment_uuid: uuid! + $user_uuid: uuid! + ) { + delete_course_comment_stars_by_pk( + comment_uuid: $comment_uuid + user_uuid: $user_uuid + ) { + comment_uuid } - ); - return delete_course_comment_stars_query?.delete_course_comment_stars_by_pk?.comment_uuid; -} + } + `, + { + comment_uuid: comment_uuid, + user_uuid: user_uuid, + }, + ); + return delete_course_comment_stars_query?.delete_course_comment_stars_by_pk + ?.comment_uuid; +}; /** * Delete likes from a course comment @@ -544,91 +677,100 @@ export const delete_course_comment_stars = async (comment_uuid: string, user_uui * @param {string} user_uuid - The UUID of the user * @returns {Promise} - The UUID of the comment */ -export const delete_course_comment_likes = async(comment_uuid: string, user_uuid: string): Promise => { - const delete_course_comment_likes_query: { delete_course_comment_likes_by_pk?: { comment_uuid: string } } = await client.request( - gql` - mutation DeleteCourseCommentLikes($comment_uuid: uuid!, $user_uuid: uuid!) { - delete_course_comment_likes_by_pk( - comment_uuid: $comment_uuid - user_uuid: $user_uuid - ) { - comment_uuid - } - }`, - { - comment_uuid: comment_uuid, - user_uuid: user_uuid - } - ); - return delete_course_comment_likes_query?.delete_course_comment_likes_by_pk?.comment_uuid; -} - -export const get_newest_weekly = async(): Promise => { - const get_newest_weekly_query: any = await client.request( - gql` - query MyQuery { - weekly_aggregate { - aggregate { - max { - date - } - } - } +export const delete_course_comment_likes = async ( + comment_uuid: string, + user_uuid: string, +): Promise => { + const delete_course_comment_likes_query: { + delete_course_comment_likes_by_pk?: { comment_uuid: string }; + } = await client.request( + gql` + mutation DeleteCourseCommentLikes( + $comment_uuid: uuid! + $user_uuid: uuid! + ) { + delete_course_comment_likes_by_pk( + comment_uuid: $comment_uuid + user_uuid: $user_uuid + ) { + comment_uuid } -` - ); - const date = get_newest_weekly_query?.weekly_aggregate?.aggregate?.max?.date + "T00:00:00.000+08:00"; - return new Date(date); -} - -export const check_weekly_exist = async(date: Date): Promise => { - const check_weekly_exist_query: any = await client.request( - gql` - query MyQuery($targetDate: date!) { - weekly(where: {date: {_eq: $targetDate}}) { + } + `, + { + comment_uuid: comment_uuid, + user_uuid: user_uuid, + }, + ); + return delete_course_comment_likes_query?.delete_course_comment_likes_by_pk + ?.comment_uuid; +}; + +export const get_newest_weekly = async (): Promise => { + const get_newest_weekly_query: any = await client.request(gql` + query MyQuery { + weekly_aggregate { + aggregate { + max { date } } - ` - , - { - targetDate: date + } + } + `); + const date = + get_newest_weekly_query?.weekly_aggregate?.aggregate?.max?.date + + "T00:00:00.000+08:00"; + return new Date(date); +}; + +export const check_weekly_exist = async (date: Date): Promise => { + const check_weekly_exist_query: any = await client.request( + gql` + query MyQuery($targetDate: date!) { + weekly(where: { date: { _eq: $targetDate } }) { + date } - ); - return check_weekly_exist_query?.weekly?.length > 0; -} - -export const add_weekly_list = async(weekly_list: WeeklyPost[]): Promise => { - const add_weekly_list_query: any = await client.request( - gql` - mutation MyMutation($objects: [weekly_insert_input!]!) { - insert_weekly(objects: $objects) { - returning { - id - } - } + } + `, + { + targetDate: date, + }, + ); + return check_weekly_exist_query?.weekly?.length > 0; +}; + +export const add_weekly_list = async ( + weekly_list: WeeklyPost[], +): Promise => { + const add_weekly_list_query: any = await client.request( + gql` + mutation MyMutation($objects: [weekly_insert_input!]!) { + insert_weekly(objects: $objects) { + returning { + id } -`, - { - objects: weekly_list } - ); - return add_weekly_list_query?.insert_weekly?.returning; -} - -export const get_newest_weekly_id = async(): Promise => { - const get_newest_weekly_id_query: any = await client.request( - gql` - query MyQuery { - weekly_aggregate { - aggregate { - max { - id - } - } + } + `, + { + objects: weekly_list, + }, + ); + return add_weekly_list_query?.insert_weekly?.returning; +}; + +export const get_newest_weekly_id = async (): Promise => { + const get_newest_weekly_id_query: any = await client.request(gql` + query MyQuery { + weekly_aggregate { + aggregate { + max { + id } - } -` - ); - return get_newest_weekly_id_query?.weekly_aggregate?.aggregate?.max?.id; -} + } + } + } + `); + return get_newest_weekly_id_query?.weekly_aggregate?.aggregate?.max?.id; +}; diff --git a/src/middlewares/authenticate.ts b/src/middlewares/authenticate.ts index 4a251e57..e53314c5 100644 --- a/src/middlewares/authenticate.ts +++ b/src/middlewares/authenticate.ts @@ -3,7 +3,6 @@ import jwt from "jsonwebtoken"; import { gql } from "graphql-request"; import { client } from ".."; - export interface JwtUserPayload { uuid: string; role: string; @@ -64,18 +63,21 @@ const anonymous_user: UserInfo = { * Middleware: validate user authorizations; reject if necessary */ const authenticate: ( - acceptableRoles?: string[] + acceptableRoles?: string[], ) => (req: Request, res: Response, next: NextFunction) => Response | void = ( - acceptableRoles + acceptableRoles, ) => { return (req: Request, res: Response, next: NextFunction) => { const authHeader = req.get("Authorization"); - if(!authHeader) { - if (!acceptableRoles || acceptableRoles.length === 0 || acceptableRoles.includes("anonymous")) { + if (!authHeader) { + if ( + !acceptableRoles || + acceptableRoles.length === 0 || + acceptableRoles.includes("anonymous") + ) { req.auth = { user: anonymous_user }; return next(); - } - else { + } else { return res.status(401).send("401 Unauthorized: Missing Token"); } } @@ -93,32 +95,40 @@ const authenticate: ( const users: any = await client.request( gql` query MyQuery($uuid: uuid!) { - users(where: {uuid: {_eq: $uuid}}) { - uuid, - username, - password, - role, - realname, - email, - phone, - student_no, - department, - class, - tsinghua_email, - github_id, + users(where: { uuid: { _eq: $uuid } }) { + uuid + username + password + role + realname + email + phone + student_no + department + class + tsinghua_email + github_id } } `, { - uuid: payload.uuid - } - ) + uuid: payload.uuid, + }, + ); const user = users.users[0]; req.auth = { user: user ?? anonymous_user }; - if (!acceptableRoles || acceptableRoles.length === 0 || acceptableRoles.includes(user.role)) { + // console.log(user); + // console.log(acceptableRoles); + + if (user === undefined || user?.role === undefined) { + return res.status(401).send("401 Unauthorized: Permission denied"); + } else if ( + !acceptableRoles || + acceptableRoles.length === 0 || + acceptableRoles.includes(user.role) + ) { return next(); - } - else { + } else { return res.status(401).send("401 Unauthorized: Permission denied"); } } catch (err) { diff --git a/src/routes/application.ts b/src/routes/application.ts index fab9f7e3..533f0c59 100644 --- a/src/routes/application.ts +++ b/src/routes/application.ts @@ -1,5 +1,6 @@ import bcrypt from "bcrypt"; -import express from "express"; +import express, { Request } from "express"; +import rateLimit from "express-rate-limit"; import { gql } from "graphql-request"; import { client } from ".."; import * as HnrHasFunc from "../hasura/honor"; @@ -24,6 +25,71 @@ import * as validator from "../helpers/validate"; const router = express.Router(); +const queryLimiter = rateLimit({ + windowMs: 1 * 60 * 1000, // 1分钟 + max: 40, // 每分钟最多20次查询 + message: { error: "查询过于频繁,请1分钟后再试" }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req: Request) => (req as any).auth?.user?.uuid as string, + skip: (req: Request) => { + const role = (req as any).auth?.user?.role; + return role === "counselor" || role === "teacher"; + }, +}); + +// const submitLimiter = rateLimit({ +// windowMs: 10 * 60 * 1000, +// max: 3, +// message: { error: "提交尝试过多,请10分钟后再试" }, +// keyGenerator: (req) => (req as any).auth?.user?.uuid as string, +// skip: (req) => { +// const role = (req as any).auth?.user?.role; +// return role === "counselor" || role === "teacher"; +// }, +// }); + +const requestCounts = new Map(); + +function checkRateLimit( + user_uuid: string, + role: string, + windowMs: number, + maxRequests: number, +): { allowed: boolean; message?: string; currentCount?: number } { + // 跳过辅导员和老师 + if (role === "counselor" || role === "teacher") { + return { allowed: true }; + } + + const now = Date.now(); + + if (!requestCounts.has(user_uuid)) { + requestCounts.set(user_uuid, { count: 0, lastReset: now }); + } + + const userStats = requestCounts.get(user_uuid)!; + + // 检查是否需要重置 + if (now - userStats.lastReset > windowMs) { + userStats.count = 0; + userStats.lastReset = now; + } + + userStats.count++; + console.log(`用户 ${user_uuid} 在本分钟内已进行 ${userStats.count} 次查询`); + + if (userStats.count >= maxRequests) { + return { + allowed: false, + message: "查询过于频繁,请1分钟后再试", + currentCount: userStats.count, + }; + } + + return { allowed: true }; +} + interface IMentor { uuid: string; // 导师uuid name: string; // 导师姓名 @@ -496,6 +562,15 @@ router.get( const role: string = req.auth.user.role; if (role === "student") { + const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20); + if (!limitResult.allowed) { + return res.status(429).json({ + error: limitResult.message, + currentCount: limitResult.currentCount, + limit: 20, + }); + } + const application_query: any = await client.request( gql` query MyQuery($student_uuid: uuid!) { @@ -851,6 +926,7 @@ router.get( // 获取新生信息 router.get( "/info/mentor/freshmen", + queryLimiter, authenticate(["student", "counselor"]), async (req, res) => { try { @@ -860,6 +936,7 @@ router.get( if (role === "student") { const student_no: string = req.auth.user.student_no; const realname: string = req.auth.user.realname; + console.log(student_no); const freshman_query: any = await client.request( gql` query MyQuery( @@ -1340,6 +1417,16 @@ router.post( const id: string = req.body.id; const statement: string = req.body.statement; const user_uuid: string = req.auth.user.uuid; + const role: string = req.auth.user.role; + + const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20); + if (!limitResult.allowed) { + return res.status(429).json({ + error: limitResult.message, + currentCount: limitResult.currentCount, + limit: 20, + }); + } if (statement.length === 0) { return res.status(400).send("Error: Invalid statement"); @@ -1971,6 +2058,16 @@ router.put( const user_uuid: string = req.auth.user.uuid; const student_no: string = req.auth.user.student_no; const realname: string = req.auth.user.realname; + const role: string = req.auth.user.role; + + const limitResult = checkRateLimit(user_uuid, role, 1 * 60 * 1000, 20); + if (!limitResult.allowed) { + return res.status(429).json({ + error: limitResult.message, + currentCount: limitResult.currentCount, + limit: 20, + }); + } if (statement.length === 0) { return res.status(400).send("Error: Invalid statement"); diff --git a/src/routes/notice.ts b/src/routes/notice.ts index e7666c2a..b84d83ca 100644 --- a/src/routes/notice.ts +++ b/src/routes/notice.ts @@ -3,29 +3,37 @@ import authenticate from "../middlewares/authenticate"; import * as noticeFunc from "../hasura/notice"; const router = express.Router(); //TODO: Optional parameters should be implemented. -router.post("/update", authenticate(["counselor","root"]), async (req, res) => { - try { - const title: string = req.body.title; - const content: string = req.body.content; - const files: string = req.body.files; - const notice_type: string = req.body.notice_type; - const notice_id = req.body.id; - const updateFields : any = {}; - if (title) updateFields.title = title; - if (content) updateFields.content = content; - if (files) updateFields.files = files; - if (notice_type) updateFields.notice_type = notice_type; - console.log(updateFields); - if(!notice_id || updateFields.length === 0) - { - return res.status(422).send("422 Unprocessable Entity: Missing credentials"); +router.post( + "/update", + authenticate(["counselor", "root"]), + async (req, res) => { + try { + const title: string = req.body.title; + const content: string = req.body.content; + const files: string = req.body.files; + const notice_type: string = req.body.notice_type; + const notice_id = req.body.id; + const updateFields: any = {}; + if (title) updateFields.title = title; + if (content) updateFields.content = content; + if (files) updateFields.files = files; + if (notice_type) updateFields.notice_type = notice_type; + console.log(updateFields); + if (!notice_id || updateFields.length === 0) { + return res + .status(422) + .send("422 Unprocessable Entity: Missing credentials"); + } + const notice_uuid: string = await noticeFunc.update_notice( + notice_id, + updateFields, + ); + return res.status(200).send(notice_uuid); + } catch (err) { + return res.status(500).send("Internal Server Error"); } - const notice_uuid: string = await noticeFunc.update_notice(notice_id,updateFields); - return res.status(200).send(notice_uuid); - } catch (err) { - return res.status(500).send("Internal Server Error"); - } -}); + }, +); //router.post("/update", authenticate(["counselor","root"]), async (req, res) => { // try { @@ -44,17 +52,48 @@ router.post("/update", authenticate(["counselor","root"]), async (req, res) => { // } //}); -router.post("/delete", authenticate(["counselor","root"]), async (req, res) => { +//newly added +router.post("/add", authenticate(["counselor", "root"]), async (req, res) => { try { - const id: string = req.body.id; - if (!id) { - return res.status(422).send("422 Unprocessable Entity: Missing credentials"); + const title: string = req.body.title; + const content: string = req.body.content; + const files: string = req.body.files; + const notice_type: string = req.body.notice_type; + + if (!title || !content || !files || !notice_type) { + return res + .status(422) + .send("422 Unprocessable Entity: Missing credentials"); } - const notice_id = await noticeFunc.delete_notice(id); + const notice_id: string = await noticeFunc.add_notice( + title, + content, + files, + notice_type, + ); return res.status(200).send(notice_id); } catch (err) { return res.status(500).send("Internal Server Error"); } }); -export default router; \ No newline at end of file +router.post( + "/delete", + authenticate(["counselor", "root"]), + async (req, res) => { + try { + const id: string = req.body.id; + if (!id) { + return res + .status(422) + .send("422 Unprocessable Entity: Missing credentials"); + } + const notice_id = await noticeFunc.delete_notice(id); + return res.status(200).send(notice_id); + } catch (err) { + return res.status(500).send("Internal Server Error"); + } + }, +); + +export default router; diff --git a/src/routes/share.ts b/src/routes/share.ts index cd8db60a..1782dc62 100644 --- a/src/routes/share.ts +++ b/src/routes/share.ts @@ -6,343 +6,594 @@ const router = express.Router(); // 添加课程 router.post("/add_course", authenticate(["admin"]), async (req, res) => { - try { - const { year, type, semester, professor, name, language, fullname, code } = req.body; - if (!year || !type || !semester || !professor || !name || !language || !fullname || !code) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const course_uuid = await ContHasFunc.add_course(year, type, semester, professor, name, language, fullname, code); - if (!course_uuid) { - return res.status(404).json({ message: "Failed to add course" }); - } - res.status(200).json({ course_uuid: course_uuid, message: "Course added successfully" }); - } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + try { + const { year, type, semester, professor, name, language, fullname, code } = + req.body; + if ( + !year || + !type || + !semester || + !professor || + !name || + !language || + !fullname || + !code + ) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const course_uuid = await ContHasFunc.add_course( + year, + type, + semester, + professor, + name, + language, + fullname, + code, + ); + if (!course_uuid) { + return res.status(404).json({ message: "Failed to add course" }); } + res + .status(200) + .json({ course_uuid: course_uuid, message: "Course added successfully" }); + } catch (err: any) { + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); + } }); // 添加课程信息 -router.post("/add_course_info", authenticate(["admin"]), async (req, res) => { +router.post( + "/add_course_info", + authenticate(["counselor"]), + async (req, res) => { try { - const { key, value, course_id } = req.body; - if (!key || !value || !course_id) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const course_info_id = await ContHasFunc.add_course_info(key, value, course_id); - if (!course_info_id) { - return res.status(404).json({ message: "Failed to add course info" }); - } - res.status(200).json({ course_info_id: course_info_id, message: "Course info added successfully" }); + const { key, value, course_id } = req.body; + if (!key || !value || !course_id) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const course_info_id = await ContHasFunc.add_course_info( + key, + value, + course_id, + ); + if (!course_info_id) { + return res.status(404).json({ message: "Failed to add course info" }); + } + res.status(200).json({ + course_info_id: course_info_id, + message: "Course info added successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 添加课程评分 -router.post("/add_course_rating", authenticate(["student"]), async (req, res) => { +router.post( + "/add_course_rating", + authenticate(["student"]), + async (req, res) => { try { - const { dim1, dim2, dim3, dim4, dim5, dim6, course_id, user_uuid } = req.body; - if (!dim1 || !dim2 || !dim3 || !dim4 || !dim5 || !dim6 || !course_id || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const created_at = await ContHasFunc.add_course_rating(dim1, dim2, dim3, dim4, dim5, dim6, course_id, user_uuid); - if (!created_at) { - return res.status(404).json({ message: "Failed to add course rating" }); - } - res.status(200).json({ created_at: created_at, message: "Course rating added successfully" }); + const { dim1, dim2, dim3, dim4, dim5, dim6, course_id, user_uuid } = + req.body; + if ( + !dim1 || + !dim2 || + !dim3 || + !dim4 || + !dim5 || + !dim6 || + !course_id || + !user_uuid + ) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const created_at = await ContHasFunc.add_course_rating( + dim1, + dim2, + dim3, + dim4, + dim5, + dim6, + course_id, + user_uuid, + ); + if (!created_at) { + return res.status(404).json({ message: "Failed to add course rating" }); + } + res.status(200).json({ + created_at: created_at, + message: "Course rating added successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); - + }, +); // 添加课程评论 -router.post("/add_course_comment", authenticate(["student"]), async (req, res) => { +router.post( + "/add_course_comment", + authenticate(["student"]), + async (req, res) => { try { - const { comment, user_uuid, course_uuid, parent_uuid } = req.body; - if (!comment || !user_uuid || !course_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const comment_uuid = await ContHasFunc.add_course_comment_one(comment, user_uuid, course_uuid, parent_uuid); - if(!comment_uuid){ - // HasFunc return undefined/null - return res.status(404).json({ message: "Course not found" }); - } - res.status(200).json({ comment_uuid: comment_uuid, message: "Comment added successfully" }); + const { comment, user_uuid, course_uuid, parent_uuid } = req.body; + if (!comment || !user_uuid || !course_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const comment_uuid = await ContHasFunc.add_course_comment_one( + comment, + user_uuid, + course_uuid, + parent_uuid, + ); + if (!comment_uuid) { + // HasFunc return undefined/null + return res.status(404).json({ message: "Course not found" }); + } + res.status(200).json({ + comment_uuid: comment_uuid, + message: "Comment added successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 添加课程评论星标 -router.post("/add_course_comment_stars", authenticate(["student"]), async (req, res) => { +router.post( + "/add_course_comment_stars", + authenticate(["student"]), + async (req, res) => { try { - const { comment_uuid, user_uuid } = req.body; - if (!comment_uuid || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.add_course_comment_stars(comment_uuid, user_uuid); - if(!result){ - return res.status(404).json({message:"Comment not found"}); - } - res.status(200).json({ comment_uuid: result, message: "Comment star added successfully" }); + const { comment_uuid, user_uuid } = req.body; + if (!comment_uuid || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.add_course_comment_stars( + comment_uuid, + user_uuid, + ); + if (!result) { + return res.status(404).json({ message: "Comment not found" }); + } + res.status(200).json({ + comment_uuid: result, + message: "Comment star added successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 添加课程评论点赞 -router.post("/add_course_comment_likes", authenticate(["student"]), async (req, res) => { +router.post( + "/add_course_comment_likes", + authenticate(["student"]), + async (req, res) => { try { - const { comment_uuid, user_uuid } = req.body; - if (!comment_uuid || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.add_course_comment_likes(comment_uuid, user_uuid); - if(!result){ - return res.status(404).json({message: "Comment not found" }); - } - res.status(200).json({ comment_uuid: result, message: "Comment like added successfully" }); + const { comment_uuid, user_uuid } = req.body; + if (!comment_uuid || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.add_course_comment_likes( + comment_uuid, + user_uuid, + ); + if (!result) { + return res.status(404).json({ message: "Comment not found" }); + } + res.status(200).json({ + comment_uuid: result, + message: "Comment like added successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); - + }, +); // 更新课程 -router.post("/update_course", authenticate(["admin", "professor"]), async (req, res) => { +router.post( + "/update_course", + authenticate(["admin", "professor"]), + async (req, res) => { try { - const { code, uuid, fullname, language, name, professor, semester, type, year } = req.body; - if (!code || !uuid || !fullname || !language || !name || !professor || !semester || !type || !year) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.update_course(code, uuid, fullname, language, name, professor, semester, type, year); - if (!result) { - return res.status(404).json({ message: "Course not found" }); - } - res.status(200).json({ uuid: result, message: "Course updated successfully" }); + const { + code, + uuid, + fullname, + language, + name, + professor, + semester, + type, + year, + } = req.body; + if ( + !code || + !uuid || + !fullname || + !language || + !name || + !professor || + !semester || + !type || + !year + ) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.update_course( + code, + uuid, + fullname, + language, + name, + professor, + semester, + type, + year, + ); + if (!result) { + return res.status(404).json({ message: "Course not found" }); + } + res + .status(200) + .json({ uuid: result, message: "Course updated successfully" }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 更新课程信息 -router.post("/update_course_info", authenticate(["admin", "professor"]), async (req, res) => { +router.post( + "/update_course_info", + authenticate(["counselor", "professor"]), + async (req, res) => { try { - const { course_id, key, value } = req.body; - if (!course_id || !key || !value) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.update_course_info(course_id, key, value); - if (!result) { - return res.status(404).json({ message: "Course info not found" }); - } - res.status(200).json({ course_id: result, message: "Course info updated successfully" }); + const { course_id, old_key, new_key, value } = req.body; + if (!course_id || !old_key || !new_key) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const affected_rows = await ContHasFunc.update_course_info( + course_id, + old_key, + new_key, + value, + ); + if (affected_rows === 0) { + return res + .status(404) + .json({ message: "Course info not found or no changes made" }); + } + res.status(200).json({ message: "Course info updated successfully" }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + // 捕获主键冲突错误(例如 new_key 已经存在) + if (err.message && err.message.includes("Uniqueness violation")) { + return res.status(409).json({ error: "Key already exists" }); + } + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + }); } -}); + }, +); // 更新课程评分 -router.post("/update_course_rating", authenticate(["student"]), async (req, res) => { +router.post( + "/update_course_rating", + authenticate(["student"]), + async (req, res) => { try { - const { course_id, user_uuid, dim1, dim2, dim3, dim4, dim5, dim6 } = req.body; - if (!course_id || !user_uuid || dim1 === undefined || dim2 === undefined || dim3 === undefined || dim4 === undefined || dim5 === undefined || dim6 === undefined) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.update_course_rating(course_id, user_uuid, dim1, dim2, dim3, dim4, dim5, dim6); - if (!result) { - return res.status(404).json({ message: "Course rating not found" }); - } - res.status(200).json({ updated_at: result, message: "Course rating updated successfully" }); + const { course_id, user_uuid, dim1, dim2, dim3, dim4, dim5, dim6 } = + req.body; + if ( + !course_id || + !user_uuid || + dim1 === undefined || + dim2 === undefined || + dim3 === undefined || + dim4 === undefined || + dim5 === undefined || + dim6 === undefined + ) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.update_course_rating( + course_id, + user_uuid, + dim1, + dim2, + dim3, + dim4, + dim5, + dim6, + ); + if (!result) { + return res.status(404).json({ message: "Course rating not found" }); + } + res.status(200).json({ + updated_at: result, + message: "Course rating updated successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); - + }, +); // 更新课程评论 -router.post("/update_course_comment", authenticate(["student"]), async (req, res) => { +router.post( + "/update_course_comment", + authenticate(["student"]), + async (req, res) => { try { - const { comment, uuid } = req.body; - if (!comment || !uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.update_course_comment(comment, uuid); - if(!result){ - return res.status(404).json({message:"Comment not found"}); - } - res.status(200).json({ uuid: result, message: "Comment updated successfully" }); + const { comment, uuid } = req.body; + if (!comment || !uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.update_course_comment(comment, uuid); + if (!result) { + return res.status(404).json({ message: "Comment not found" }); + } + res + .status(200) + .json({ uuid: result, message: "Comment updated successfully" }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); - + }, +); // 删除课程 -router.post("/delete_course", authenticate(["admin", "professor"]), async (req, res) => { +router.post( + "/delete_course", + authenticate(["admin", "professor"]), + async (req, res) => { try { - const { uuid } = req.body; - if (!uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course(uuid); - if (!result) { - return res.status(404).json({ message: "Course not found" }); - } - res.status(200).json({ uuid: result, message: "Course deleted successfully" }); + const { uuid } = req.body; + if (!uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course(uuid); + if (!result) { + return res.status(404).json({ message: "Course not found" }); + } + res + .status(200) + .json({ uuid: result, message: "Course deleted successfully" }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 删除课程信息 -router.post("/delete_course_info", authenticate(["admin", "professor"]), async (req, res) => { +router.post( + "/delete_course_info", + authenticate(["counselor", "professor"]), + async (req, res) => { try { - const { course_id, key } = req.body; - if (!course_id || !key) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course_info(course_id, key); - if (!result) { - return res.status(404).json({ message: "Course info not found" }); - } - res.status(200).json({ course_id: result.course_id, key: result.key, message: "Course info deleted successfully" }); + const { course_id, key } = req.body; + if (!course_id || !key) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course_info(course_id, key); + if (!result) { + return res.status(404).json({ message: "Course info not found" }); + } + res.status(200).json({ + course_id: result.course_id, + key: result.key, + message: "Course info deleted successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 删除课程评分 -router.post("/delete_course_rating", authenticate(["student"]), async (req, res) => { +router.post( + "/delete_course_rating", + authenticate(["student"]), + async (req, res) => { try { - const { course_id, user_uuid } = req.body; - if (!course_id || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course_rating(course_id, user_uuid); - if (!result) { - return res.status(404).json({ message: "Course rating not found" }); - } - res.status(200).json({ course_id: result.course_id, user_uuid: result.user_uuid, message: "Course rating deleted successfully" }); + const { course_id, user_uuid } = req.body; + if (!course_id || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course_rating( + course_id, + user_uuid, + ); + if (!result) { + return res.status(404).json({ message: "Course rating not found" }); + } + res.status(200).json({ + course_id: result.course_id, + user_uuid: result.user_uuid, + message: "Course rating deleted successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 删除课程评论 -router.delete("/delete_course_comment", authenticate(["student"]), async (req, res) => { +router.delete( + "/delete_course_comment", + authenticate(["student"]), + async (req, res) => { try { - const { uuid } = req.body; - if (!uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course_comment_one(uuid); - if(!result){ - res.status(404).json({message: "Comment not found" }); - } - res.status(200).json({ uuid: result, message: "Comment deleted successfully" }); + const { uuid } = req.body; + if (!uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course_comment_one(uuid); + if (!result) { + res.status(404).json({ message: "Comment not found" }); + } + res + .status(200) + .json({ uuid: result, message: "Comment deleted successfully" }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 删除课程评论星标 -router.delete("/delete_course_comment_stars", authenticate(["student"]), async (req, res) => { +router.delete( + "/delete_course_comment_stars", + authenticate(["student"]), + async (req, res) => { try { - const { comment_uuid, user_uuid } = req.body; - if (!comment_uuid || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course_comment_stars(comment_uuid, user_uuid); - if(!result){ - res.status(404).json({message: "Comment not found" }); - } - res.status(200).json({ comment_uuid: result, message: "Comment star deleted successfully" }); + const { comment_uuid, user_uuid } = req.body; + if (!comment_uuid || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course_comment_stars( + comment_uuid, + user_uuid, + ); + if (!result) { + res.status(404).json({ message: "Comment not found" }); + } + res.status(200).json({ + comment_uuid: result, + message: "Comment star deleted successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); // 删除课程评论点赞 -router.delete("/delete_course_comment_likes", authenticate(["student"]), async (req, res) => { +router.delete( + "/delete_course_comment_likes", + authenticate(["student"]), + async (req, res) => { try { - const { comment_uuid, user_uuid } = req.body; - if (!comment_uuid || !user_uuid) { - return res.status(400).json({ error: "400 Bad Request: Missing required parameters" }); - } - const result = await ContHasFunc.delete_course_comment_likes(comment_uuid, user_uuid); - if(!result){ - res.status(404).json({message: "Comment not found" }); - } - res.status(200).json({ comment_uuid: result, message: "Comment like deleted successfully" }); + const { comment_uuid, user_uuid } = req.body; + if (!comment_uuid || !user_uuid) { + return res + .status(400) + .json({ error: "400 Bad Request: Missing required parameters" }); + } + const result = await ContHasFunc.delete_course_comment_likes( + comment_uuid, + user_uuid, + ); + if (!result) { + res.status(404).json({ message: "Comment not found" }); + } + res.status(200).json({ + comment_uuid: result, + message: "Comment like deleted successfully", + }); } catch (err: any) { - res.status(500).json({ - error: "500 Internal Server Error", - message: err.message, - stack: process.env.NODE_ENV === 'development' ? err.stack : undefined - }); + res.status(500).json({ + error: "500 Internal Server Error", + message: err.message, + stack: process.env.NODE_ENV === "development" ? err.stack : undefined, + }); } -}); + }, +); export default router; From 6727711138a96f0f978b7cacb5f1126dff5d617e Mon Sep 17 00:00:00 2001 From: konpoku <543329382@qq.com> Date: Mon, 16 Mar 2026 22:23:21 +0800 Subject: [PATCH 29/29] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dyarn.lock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yarn.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/yarn.lock b/yarn.lock index d55b957d..b50f7c4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,10 +1746,10 @@ graphql-request@6.1.0: "@graphql-typed-document-node/core" "^3.2.0" cross-fetch "^3.1.5" -graphql@16.13.1: - version "16.13.1" - resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.13.1.tgz#38ae5c76fbc4a009e0004dca6c76c370a1da7b54" - integrity sha512-gGgrVCoDKlIZ8fIqXBBb0pPKqDgki0Z/FSKNiQzSGj2uEYHr1tq5wmBegGwJx6QB5S5cM0khSBpi/JFHMCvsmQ== +graphql@16.12.0: + version "16.12.0" + resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.12.0.tgz#28cc2462435b1ac3fdc6976d030cef83a0c13ac7" + integrity sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ== har-schema@^2.0.0: version "2.0.0"