From 2a7c306616970037209d72052baf47dc567ceef7 Mon Sep 17 00:00:00 2001 From: konpoku Date: Sun, 16 Mar 2025 22:46:47 +0800 Subject: [PATCH 01/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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/19] 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;