refactor(api): 请求体解析收成 readJson / parseBody
54 处 handler 都手写同一段: const parsed = X.safeParse(await c.req.json().catch(() => null)) if (!parsed.success) return failure(c, 400, "invalid-request", ...) 漏写 .catch 就是一个畸形请求打出 500。 - http.ts 加 readJson(读 + 校验)和 parseBody(再加上 400)。parseBody 不给 message 时回 zod 的第一条 issue —— 后台表单原来就是这么写的,fallback 文案 「Invalid payload」/「参数错误」只在 issues 为空时才用得上,实际走不到 - 51 处换成 parseBody;另 3 处失败时带别的错误码或和路径参数一起判,换成 readJson - 错误码、状态码、文案、判断顺序都不变 实跑核过:非 JSON 请求体回原来的固定文案,公告标题为空回 zod issue。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c43da01ff2
commit
f2ca34fd4b
21 files changed
+228
-426
No files matched your search
@@ -1,4 +1,5 @@
|
|||||||
import type { Context } from "hono"
|
import type { Context } from "hono"
|
||||||
|
import type { z } from "zod"
|
||||||
|
|
||||||
export function success<T>(c: Context, data: T, status = 200) {
|
export function success<T>(c: Context, data: T, status = 200) {
|
||||||
return c.json({ data }, status as 200)
|
return c.json({ data }, status as 200)
|
||||||
@@ -12,3 +13,42 @@ export function failure(
|
|||||||
) {
|
) {
|
||||||
return c.json({ error: { code, message } }, status)
|
return c.json({ error: { code, message } }, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读 JSON 请求体并按契约校验。body 不是合法 JSON 时按 `null` 交给 schema,
|
||||||
|
* 由 schema 统一判失败 —— 原来每个 handler 都手写一遍 `c.req.json().catch(() => null)`,
|
||||||
|
* 漏写 `.catch` 就是一个畸形请求打出 500。
|
||||||
|
*/
|
||||||
|
export async function readJson<S extends z.ZodType>(c: Context, schema: S) {
|
||||||
|
return schema.safeParse(await c.req.json().catch(() => null))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `readJson` + 校验失败时的 400。绝大多数入参端点就是这个形状:
|
||||||
|
*
|
||||||
|
* const parsed = await parseBody(c, schema, "标签名不能为空")
|
||||||
|
* if (!parsed.success) return parsed.response
|
||||||
|
*
|
||||||
|
* 不给 `message` 就回 zod 的第一条 issue(后台表单要告诉老师具体哪一栏不对)。
|
||||||
|
* 失败时要带别的错误码、或者和路径参数一起判的,用 `readJson` 自己写。
|
||||||
|
*/
|
||||||
|
export async function parseBody<S extends z.ZodType>(
|
||||||
|
c: Context,
|
||||||
|
schema: S,
|
||||||
|
message?: string,
|
||||||
|
): Promise<
|
||||||
|
{ success: true; data: z.output<S> } | { success: false; response: Response }
|
||||||
|
> {
|
||||||
|
const parsed = await readJson(c, schema)
|
||||||
|
if (parsed.success) return { success: true, data: parsed.data }
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
response: failure(
|
||||||
|
c,
|
||||||
|
400,
|
||||||
|
"invalid-request",
|
||||||
|
// issues 不会是空的,?? 只是给类型一个交代
|
||||||
|
message ?? parsed.error.issues[0]?.message ?? "参数错误",
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@ import { onlineUserIds } from "../auth/presence"
|
|||||||
import { optionalAuth, requireAuth, type AppEnv } from "../auth/middleware"
|
import { optionalAuth, requireAuth, type AppEnv } from "../auth/middleware"
|
||||||
import { config } from "../config"
|
import { config } from "../config"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { JudgeStatus } from "../judge/status"
|
||||||
import { getBooleanOption } from "../services/options"
|
import { getBooleanOption } from "../services/options"
|
||||||
import { getUserProfileById } from "../services/profile"
|
import { getUserProfileById } from "../services/profile"
|
||||||
@@ -70,11 +70,12 @@ export const accountRoutes = new Hono<AppEnv>()
|
|||||||
const REGISTER_PER_IP: AttemptRule = { limit: 100, windowSeconds: 60 * 60 }
|
const REGISTER_PER_IP: AttemptRule = { limit: 100, windowSeconds: 60 * 60 }
|
||||||
|
|
||||||
accountRoutes.post("/users", async (c) => {
|
accountRoutes.post("/users", async (c) => {
|
||||||
const parsed = registerRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
registerRequestSchema,
|
||||||
|
"Invalid registration payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid registration payload")
|
|
||||||
if (!(await getBooleanOption("allow_register", true))) {
|
if (!(await getBooleanOption("allow_register", true))) {
|
||||||
return failure(
|
return failure(
|
||||||
c,
|
c,
|
||||||
@@ -173,11 +174,12 @@ accountRoutes.get("/profiles/:username", optionalAuth, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
accountRoutes.put("/me/profile", requireAuth, async (c) => {
|
accountRoutes.put("/me/profile", requireAuth, async (c) => {
|
||||||
const parsed = updateProfileRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
updateProfileRequestSchema,
|
||||||
|
"Invalid profile payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid profile payload")
|
|
||||||
const values = Object.fromEntries(
|
const values = Object.fromEntries(
|
||||||
Object.entries(parsed.data).map(([key, value]) => [
|
Object.entries(parsed.data).map(([key, value]) => [
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireAuth, type AppEnv } from "../auth/middleware"
|
import { requireAuth, type AppEnv } from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { objectValue } from "./helpers"
|
import { objectValue } from "./helpers"
|
||||||
|
|
||||||
export const achievementRoutes = new Hono<AppEnv>()
|
export const achievementRoutes = new Hono<AppEnv>()
|
||||||
@@ -177,11 +177,12 @@ achievementRoutes.get("/achievements/pending", requireAuth, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
achievementRoutes.post("/achievements/pending/read", requireAuth, async (c) => {
|
achievementRoutes.post("/achievements/pending/read", requireAuth, async (c) => {
|
||||||
const parsed = markAchievementsReadSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
markAchievementsReadSchema,
|
||||||
|
"Invalid achievement ids",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid achievement ids")
|
|
||||||
if (parsed.data.ids.length > 0) {
|
if (parsed.data.ids.length > 0) {
|
||||||
await db
|
await db
|
||||||
.update(schema.userAchievement)
|
.update(schema.userAchievement)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import { isUserOnline, onlineUserIds } from "../../auth/presence"
|
|||||||
import { revokeUserSessions } from "../../auth/session"
|
import { revokeUserSessions } from "../../auth/session"
|
||||||
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { queryInteger, sampleUser } from "../helpers"
|
import { queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
export const adminAccountRoutes = new Hono<AppEnv>()
|
export const adminAccountRoutes = new Hono<AppEnv>()
|
||||||
@@ -252,17 +252,8 @@ adminAccountRoutes.get("/users/:id", requireSuperAdmin, async (c) => {
|
|||||||
|
|
||||||
adminAccountRoutes.put("/users/:id", requireSuperAdmin, async (c) => {
|
adminAccountRoutes.put("/users/:id", requireSuperAdmin, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateUserRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateUserRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const data = parsed.data
|
const data = parsed.data
|
||||||
const [existing] = await selectUser(id)
|
const [existing] = await selectUser(id)
|
||||||
if (!existing) return failure(c, 404, "user-not-found", "User does not exist")
|
if (!existing) return failure(c, 404, "user-not-found", "User does not exist")
|
||||||
@@ -350,17 +341,8 @@ adminAccountRoutes.put("/users/:id", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => {
|
adminAccountRoutes.post("/users", requireSuperAdmin, async (c) => {
|
||||||
const parsed = importUsersRequestSchema.safeParse(
|
const parsed = await parseBody(c, importUsersRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const rows = parsed.data.users
|
const rows = parsed.data.users
|
||||||
type Prepared = {
|
type Prepared = {
|
||||||
username: string
|
username: string
|
||||||
@@ -544,11 +526,8 @@ function isForeignKeyViolation(error: unknown) {
|
|||||||
class UserHasSubmissionsError extends Error {}
|
class UserHasSubmissionsError extends Error {}
|
||||||
|
|
||||||
adminAccountRoutes.delete("/users", requireSuperAdmin, async (c) => {
|
adminAccountRoutes.delete("/users", requireSuperAdmin, async (c) => {
|
||||||
const parsed = deleteUsersRequestSchema.safeParse(
|
const parsed = await parseBody(c, deleteUsersRequestSchema, "ids is required")
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success)
|
|
||||||
return failure(c, 400, "invalid-request", "ids is required")
|
|
||||||
const me = c.get("user")!.id
|
const me = c.get("user")!.id
|
||||||
if (parsed.data.ids.includes(me)) {
|
if (parsed.data.ids.includes(me)) {
|
||||||
return failure(
|
return failure(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import {
|
import {
|
||||||
ACHIEVEMENT_METRICS,
|
ACHIEVEMENT_METRICS,
|
||||||
findMetric,
|
findMetric,
|
||||||
@@ -72,17 +72,8 @@ adminAchievementRoutes.get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
adminAchievementRoutes.post("/achievements", requireSuperAdmin, async (c) => {
|
adminAchievementRoutes.post("/achievements", requireSuperAdmin, async (c) => {
|
||||||
const parsed = createAchievementRequestSchema.safeParse(
|
const parsed = await parseBody(c, createAchievementRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!findMetric(parsed.data.metric))
|
if (!findMetric(parsed.data.metric))
|
||||||
return failure(c, 400, "invalid-metric", "指标不存在")
|
return failure(c, 400, "invalid-metric", "指标不存在")
|
||||||
|
|
||||||
@@ -111,17 +102,8 @@ adminAchievementRoutes.put(
|
|||||||
requireSuperAdmin,
|
requireSuperAdmin,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateAchievementRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateAchievementRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!findMetric(parsed.data.metric))
|
if (!findMetric(parsed.data.metric))
|
||||||
return failure(c, 400, "invalid-metric", "指标不存在")
|
return failure(c, 400, "invalid-metric", "指标不存在")
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { queryInteger, sampleUser } from "../helpers"
|
import { queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
export const adminAnnouncementRoutes = new Hono<AppEnv>()
|
export const adminAnnouncementRoutes = new Hono<AppEnv>()
|
||||||
@@ -78,17 +78,8 @@ adminAnnouncementRoutes.get("/announcements", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminAnnouncementRoutes.post("/announcements", requireSuperAdmin, async (c) => {
|
adminAnnouncementRoutes.post("/announcements", requireSuperAdmin, async (c) => {
|
||||||
const parsed = createAnnouncementRequestSchema.safeParse(
|
const parsed = await parseBody(c, createAnnouncementRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(schema.announcement)
|
.insert(schema.announcement)
|
||||||
@@ -126,17 +117,8 @@ adminAnnouncementRoutes.put(
|
|||||||
requireSuperAdmin,
|
requireSuperAdmin,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateAnnouncementRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateAnnouncementRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(schema.announcement)
|
.update(schema.announcement)
|
||||||
.set({ ...parsed.data, lastUpdateTime: new Date().toISOString() })
|
.set({ ...parsed.data, lastUpdateTime: new Date().toISOString() })
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
import { config } from "../../config"
|
import { config } from "../../config"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { publishConfigUpdate } from "../../events"
|
import { publishConfigUpdate } from "../../events"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { getWebsiteOptions } from "../../services/options"
|
import { getWebsiteOptions } from "../../services/options"
|
||||||
import { todayStart } from "../../time"
|
import { todayStart } from "../../time"
|
||||||
import { queryInteger } from "../helpers"
|
import { queryInteger } from "../helpers"
|
||||||
@@ -77,17 +77,8 @@ adminConfRoutes.get("/website", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminConfRoutes.post("/website", requireSuperAdmin, async (c) => {
|
adminConfRoutes.post("/website", requireSuperAdmin, async (c) => {
|
||||||
const parsed = updateWebsiteConfigRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateWebsiteConfigRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const entries = (
|
const entries = (
|
||||||
Object.entries(OPTION_KEYS) as [keyof typeof OPTION_KEYS, string][]
|
Object.entries(OPTION_KEYS) as [keyof typeof OPTION_KEYS, string][]
|
||||||
).map(([field, key]) => ({ field, key, value: parsed.data[field] }))
|
).map(([field, key]) => ({ field, key, value: parsed.data[field] }))
|
||||||
@@ -131,11 +122,12 @@ adminConfRoutes.get("/judge-servers", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminConfRoutes.put("/judge-servers/:id", requireSuperAdmin, async (c) => {
|
adminConfRoutes.put("/judge-servers/:id", requireSuperAdmin, async (c) => {
|
||||||
const parsed = updateJudgeServerRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
updateJudgeServerRequestSchema,
|
||||||
|
"isDisabled is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "isDisabled is required")
|
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(schema.judgeServer)
|
.update(schema.judgeServer)
|
||||||
.set({ isDisabled: parsed.data.isDisabled })
|
.set({ isDisabled: parsed.data.isDisabled })
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { Hono } from "hono"
|
|||||||
import { requireTeacher, type AppEnv } from "../../auth/middleware"
|
import { requireTeacher, type AppEnv } from "../../auth/middleware"
|
||||||
import type { AuthUser } from "../../auth/session"
|
import type { AuthUser } from "../../auth/session"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { contestStatus } from "../../services/contest"
|
import { contestStatus } from "../../services/contest"
|
||||||
import { objectValue, queryInteger, sampleUser } from "../helpers"
|
import { objectValue, queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
@@ -120,17 +120,8 @@ adminContestRoutes.get("/contests/:id", requireTeacher, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminContestRoutes.post("/contests", requireTeacher, async (c) => {
|
adminContestRoutes.post("/contests", requireTeacher, async (c) => {
|
||||||
const parsed = createContestRequestSchema.safeParse(
|
const parsed = await parseBody(c, createContestRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const error = validatePayload(parsed.data)
|
const error = validatePayload(parsed.data)
|
||||||
if (error) return failure(c, 400, "invalid-contest", error)
|
if (error) return failure(c, 400, "invalid-contest", error)
|
||||||
|
|
||||||
@@ -157,17 +148,8 @@ adminContestRoutes.post("/contests", requireTeacher, async (c) => {
|
|||||||
|
|
||||||
adminContestRoutes.put("/contests/:id", requireTeacher, async (c) => {
|
adminContestRoutes.put("/contests/:id", requireTeacher, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateContestRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateContestRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [existing] = await selectContest(id)
|
const [existing] = await selectContest(id)
|
||||||
if (!existing || !ownedBy(c.get("user")!, existing.contest)) {
|
if (!existing || !ownedBy(c.get("user")!, existing.contest)) {
|
||||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||||
@@ -386,17 +368,8 @@ adminContestRoutes.put(
|
|||||||
requireTeacher,
|
requireTeacher,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const contestId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const contestId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateAcmHelperRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateAcmHelperRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [contest] = await db
|
const [contest] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.contest)
|
.from(schema.contest)
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { Hono } from "hono"
|
|||||||
import { requireProblemPermission, type AppEnv } from "../../auth/middleware"
|
import { requireProblemPermission, type AppEnv } from "../../auth/middleware"
|
||||||
import type { AuthUser } from "../../auth/session"
|
import type { AuthUser } from "../../auth/session"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { astRulesError, pickAstRules } from "../../judge/ast"
|
import { astRulesError, pickAstRules } from "../../judge/ast"
|
||||||
import { buildSqlDisplay } from "../../judge/sql"
|
import { buildSqlDisplay } from "../../judge/sql"
|
||||||
import { completeChat } from "../../services/ai"
|
import { completeChat } from "../../services/ai"
|
||||||
@@ -432,17 +432,8 @@ adminProblemRoutes.get("/problems/:id", requireProblemPermission, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminProblemRoutes.post("/problems", requireProblemPermission, async (c) => {
|
adminProblemRoutes.post("/problems", requireProblemPermission, async (c) => {
|
||||||
const parsed = createProblemRequestSchema.safeParse(
|
const parsed = await parseBody(c, createProblemRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const checked = commonChecks(parsed.data)
|
const checked = commonChecks(parsed.data)
|
||||||
if ("error" in checked)
|
if ("error" in checked)
|
||||||
return failure(c, 400, "invalid-problem", checked.error)
|
return failure(c, 400, "invalid-problem", checked.error)
|
||||||
@@ -495,17 +486,8 @@ adminProblemRoutes.post("/problems", requireProblemPermission, async (c) => {
|
|||||||
|
|
||||||
adminProblemRoutes.put("/problems/:id", requireProblemPermission, async (c) => {
|
adminProblemRoutes.put("/problems/:id", requireProblemPermission, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateProblemRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateProblemRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.problem)
|
.from(schema.problem)
|
||||||
@@ -702,17 +684,8 @@ adminProblemRoutes.post(
|
|||||||
) {
|
) {
|
||||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||||
}
|
}
|
||||||
const parsed = createProblemRequestSchema.safeParse(
|
const parsed = await parseBody(c, createProblemRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const checked = commonChecks(parsed.data)
|
const checked = commonChecks(parsed.data)
|
||||||
if ("error" in checked)
|
if ("error" in checked)
|
||||||
return failure(c, 400, "invalid-problem", checked.error)
|
return failure(c, 400, "invalid-problem", checked.error)
|
||||||
@@ -774,12 +747,12 @@ adminProblemRoutes.post(
|
|||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = makeProblemPublicRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
makeProblemPublicRequestSchema,
|
||||||
|
"displayId 不能为空",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "displayId 不能为空")
|
|
||||||
|
|
||||||
const [problem] = await db
|
const [problem] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.problem)
|
.from(schema.problem)
|
||||||
@@ -856,17 +829,8 @@ adminProblemRoutes.post(
|
|||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const contestId = queryInteger(c.req.param("contestId"), 0, { min: 1 })
|
const contestId = queryInteger(c.req.param("contestId"), 0, { min: 1 })
|
||||||
const parsed = addContestProblemRequestSchema.safeParse(
|
const parsed = await parseBody(c, addContestProblemRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [contest] = await db
|
const [contest] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(schema.contest)
|
.from(schema.contest)
|
||||||
@@ -1055,17 +1019,8 @@ adminProblemRoutes.post(
|
|||||||
"/sql-test-cases/preview",
|
"/sql-test-cases/preview",
|
||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const parsed = sqlPreviewRequestSchema.safeParse(
|
const parsed = await parseBody(c, sqlPreviewRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const outcome = await buildSqlDisplay(
|
const outcome = await buildSqlDisplay(
|
||||||
parsed.data.initSql,
|
parsed.data.initSql,
|
||||||
parsed.data.refSql,
|
parsed.data.refSql,
|
||||||
@@ -1082,17 +1037,8 @@ adminProblemRoutes.post(
|
|||||||
"/sql-test-cases/generate",
|
"/sql-test-cases/generate",
|
||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const parsed = generateSqlTestCaseRequestSchema.safeParse(
|
const parsed = await parseBody(c, generateSqlTestCaseRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const sql = await completeChat(
|
const sql = await completeChat(
|
||||||
`你是一个 SQL 出题助手。用户会给你一道 SQL 题的标准答案(查询题的
|
`你是一个 SQL 出题助手。用户会给你一道 SQL 题的标准答案(查询题的
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import { Hono } from "hono"
|
|||||||
import { requireTeacher, type AppEnv } from "../../auth/middleware"
|
import { requireTeacher, type AppEnv } from "../../auth/middleware"
|
||||||
import type { AuthUser } from "../../auth/session"
|
import type { AuthUser } from "../../auth/session"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { recalculateBadge, resyncProgress } from "../../services/problemset"
|
import { recalculateBadge, resyncProgress } from "../../services/problemset"
|
||||||
import { asFilterValue, queryInteger, sampleUser } from "../helpers"
|
import { asFilterValue, queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
@@ -175,17 +175,8 @@ adminProblemSetRoutes.get("/problem-sets", requireTeacher, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminProblemSetRoutes.post("/problem-sets", requireTeacher, async (c) => {
|
adminProblemSetRoutes.post("/problem-sets", requireTeacher, async (c) => {
|
||||||
const parsed = createProblemSetRequestSchema.safeParse(
|
const parsed = await parseBody(c, createProblemSetRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(schema.problemset)
|
.insert(schema.problemset)
|
||||||
@@ -211,17 +202,8 @@ adminProblemSetRoutes.get("/problem-sets/:id", requireTeacher, async (c) => {
|
|||||||
adminProblemSetRoutes.put("/problem-sets/:id", requireTeacher, async (c) => {
|
adminProblemSetRoutes.put("/problem-sets/:id", requireTeacher, async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = updateProblemSetRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateProblemSetRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(schema.problemset)
|
.update(schema.problemset)
|
||||||
.set({
|
.set({
|
||||||
@@ -258,11 +240,12 @@ adminProblemSetRoutes.put(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = updateProblemSetStatusRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
updateProblemSetStatusRequestSchema,
|
||||||
|
"status 不合法",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "status 不合法")
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(schema.problemset)
|
.update(schema.problemset)
|
||||||
.set({
|
.set({
|
||||||
@@ -331,17 +314,8 @@ adminProblemSetRoutes.post(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = addProblemToSetRequestSchema.safeParse(
|
const parsed = await parseBody(c, addProblemToSetRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [problem] = await db
|
const [problem] = await db
|
||||||
.select({ id: schema.problem.id })
|
.select({ id: schema.problem.id })
|
||||||
.from(schema.problem)
|
.from(schema.problem)
|
||||||
@@ -394,10 +368,12 @@ adminProblemSetRoutes.put(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = updateProblemInSetRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
updateProblemInSetRequestSchema,
|
||||||
|
"参数错误",
|
||||||
)
|
)
|
||||||
if (!parsed.success) return failure(c, 400, "invalid-request", "参数错误")
|
if (!parsed.success) return parsed.response
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(schema.problemsetProblem)
|
.update(schema.problemsetProblem)
|
||||||
.set(parsed.data)
|
.set(parsed.data)
|
||||||
@@ -512,17 +488,8 @@ adminProblemSetRoutes.post(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = createProblemSetBadgeRequestSchema.safeParse(
|
const parsed = await parseBody(c, createProblemSetBadgeRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(schema.problemsetBadge)
|
.insert(schema.problemsetBadge)
|
||||||
.values({
|
.values({
|
||||||
@@ -542,17 +509,8 @@ adminProblemSetRoutes.put(
|
|||||||
async (c) => {
|
async (c) => {
|
||||||
const row = await loadOwned(c, c.get("user")!)
|
const row = await loadOwned(c, c.get("user")!)
|
||||||
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
if (!row) return failure(c, 404, "problem-set-not-found", "题单不存在")
|
||||||
const parsed = updateProblemSetBadgeRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateProblemSetBadgeRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
.update(schema.problemsetBadge)
|
.update(schema.problemsetBadge)
|
||||||
.set(parsed.data)
|
.set(parsed.data)
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
} from "../../auth/middleware"
|
} from "../../auth/middleware"
|
||||||
import type { AuthUser } from "../../auth/session"
|
import type { AuthUser } from "../../auth/session"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { JudgeStatus } from "../../judge/status"
|
import { JudgeStatus } from "../../judge/status"
|
||||||
import { completeChat } from "../../services/ai"
|
import { completeChat } from "../../services/ai"
|
||||||
import { localTime, localYear } from "../../time"
|
import { localTime, localYear } from "../../time"
|
||||||
@@ -82,11 +82,8 @@ adminTagRoutes.get("/problem-tags", requireProblemPermission, async (c) => {
|
|||||||
|
|
||||||
adminTagRoutes.put("/problem-tags/:id", requireProblemPermission, async (c) => {
|
adminTagRoutes.put("/problem-tags/:id", requireProblemPermission, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = renameTagRequestSchema.safeParse(
|
const parsed = await parseBody(c, renameTagRequestSchema, "标签名不能为空")
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success)
|
|
||||||
return failure(c, 400, "invalid-request", "标签名不能为空")
|
|
||||||
const name = parsed.data.name
|
const name = parsed.data.name
|
||||||
|
|
||||||
const [tag] = await db
|
const [tag] = await db
|
||||||
@@ -177,17 +174,8 @@ adminTagRoutes.post(
|
|||||||
"/problems/batch-tag",
|
"/problems/batch-tag",
|
||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const parsed = batchProblemTagRequestSchema.safeParse(
|
const parsed = await parseBody(c, batchProblemTagRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "参数错误",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const filters = [
|
const filters = [
|
||||||
inArray(schema.problem.id, parsed.data.problemIds),
|
inArray(schema.problem.id, parsed.data.problemIds),
|
||||||
@@ -462,11 +450,12 @@ adminTagRoutes.post(
|
|||||||
"/problems/flowchart",
|
"/problems/flowchart",
|
||||||
requireProblemPermission,
|
requireProblemPermission,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const parsed = generateFlowchartRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
generateFlowchartRequestSchema,
|
||||||
|
"python 代码不能为空",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "python 代码不能为空")
|
|
||||||
try {
|
try {
|
||||||
const flowchart = await completeChat(
|
const flowchart = await completeChat(
|
||||||
`你是一个可以将Python代码转换为mermaid的助手。
|
`你是一个可以将Python代码转换为mermaid的助手。
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
import { requireSuperAdmin, type AppEnv } from "../../auth/middleware"
|
||||||
import { db, schema } from "../../db"
|
import { db, schema } from "../../db"
|
||||||
import { failure, success } from "../../http"
|
import { failure, parseBody, success } from "../../http"
|
||||||
import { exerciseDataError } from "../../services/exercise"
|
import { exerciseDataError } from "../../services/exercise"
|
||||||
import { objectValue, queryInteger, sampleUser } from "../helpers"
|
import { objectValue, queryInteger, sampleUser } from "../helpers"
|
||||||
|
|
||||||
@@ -72,17 +72,8 @@ adminTutorialRoutes.get("/tutorials", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminTutorialRoutes.post("/tutorials", requireSuperAdmin, async (c) => {
|
adminTutorialRoutes.post("/tutorials", requireSuperAdmin, async (c) => {
|
||||||
const parsed = createTutorialRequestSchema.safeParse(
|
const parsed = await parseBody(c, createTutorialRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
const [created] = await db
|
const [created] = await db
|
||||||
.insert(schema.tutorial)
|
.insert(schema.tutorial)
|
||||||
@@ -108,17 +99,8 @@ adminTutorialRoutes.get("/tutorials/:id", requireSuperAdmin, async (c) => {
|
|||||||
|
|
||||||
adminTutorialRoutes.put("/tutorials/:id", requireSuperAdmin, async (c) => {
|
adminTutorialRoutes.put("/tutorials/:id", requireSuperAdmin, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = updateTutorialRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateTutorialRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(schema.tutorial)
|
.update(schema.tutorial)
|
||||||
.set({ ...parsed.data, updatedAt: new Date().toISOString() })
|
.set({ ...parsed.data, updatedAt: new Date().toISOString() })
|
||||||
@@ -135,11 +117,12 @@ adminTutorialRoutes.put(
|
|||||||
requireSuperAdmin,
|
requireSuperAdmin,
|
||||||
async (c) => {
|
async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = setTutorialVisibilityRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
setTutorialVisibilityRequestSchema,
|
||||||
|
"isPublic is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "isPublic is required")
|
|
||||||
// 只改可见性,不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉
|
// 只改可见性,不动 updatedAt —— 上下架不是内容修改,改了会打乱按更新时间排序的直觉
|
||||||
const updated = await db
|
const updated = await db
|
||||||
.update(schema.tutorial)
|
.update(schema.tutorial)
|
||||||
@@ -199,17 +182,8 @@ adminTutorialRoutes.get(
|
|||||||
)
|
)
|
||||||
|
|
||||||
adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => {
|
adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => {
|
||||||
const parsed = createExerciseRequestSchema.safeParse(
|
const parsed = await parseBody(c, createExerciseRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const [tutorial] = await db
|
const [tutorial] = await db
|
||||||
.select({ id: schema.tutorial.id })
|
.select({ id: schema.tutorial.id })
|
||||||
.from(schema.tutorial)
|
.from(schema.tutorial)
|
||||||
@@ -233,17 +207,8 @@ adminTutorialRoutes.post("/exercises", requireSuperAdmin, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
adminTutorialRoutes.put("/exercises/:id", requireSuperAdmin, async (c) => {
|
adminTutorialRoutes.put("/exercises/:id", requireSuperAdmin, async (c) => {
|
||||||
const parsed = updateExerciseRequestSchema.safeParse(
|
const parsed = await parseBody(c, updateExerciseRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
if (!parsed.success) return parsed.response
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
|
||||||
400,
|
|
||||||
"invalid-request",
|
|
||||||
parsed.error.issues[0]?.message ?? "Invalid payload",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
const dataError = exerciseDataError(parsed.data.type, parsed.data.data)
|
const dataError = exerciseDataError(parsed.data.type, parsed.data.data)
|
||||||
if (dataError) return failure(c, 400, "invalid-exercise", dataError)
|
if (dataError) return failure(c, 400, "invalid-exercise", dataError)
|
||||||
const [updated] = await db
|
const [updated] = await db
|
||||||
|
|||||||
+18
-26
@@ -36,7 +36,7 @@ import { getPreviousLogin, type AuthUser } from "../auth/session"
|
|||||||
import { config } from "../config"
|
import { config } from "../config"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { JudgeStatus, type JudgeStatusValue } from "../judge/status"
|
import { JudgeStatus, type JudgeStatusValue } from "../judge/status"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, readJson, success } from "../http"
|
||||||
import { completeChat, streamChat, streamWhole } from "../services/ai"
|
import { completeChat, streamChat, streamWhole } from "../services/ai"
|
||||||
import { generateFilteredHint } from "../services/hint-filter"
|
import { generateFilteredHint } from "../services/hint-filter"
|
||||||
import { decideHintLevel } from "../services/hint-level"
|
import { decideHintLevel } from "../services/hint-level"
|
||||||
@@ -882,16 +882,12 @@ aiRoutes.get("/ai/pinned", requireAuth, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
aiRoutes.post("/ai/analysis", requireAuth, async (c) => {
|
aiRoutes.post("/ai/analysis", requireAuth, async (c) => {
|
||||||
const parsed = aiAnalysisRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
|
||||||
)
|
|
||||||
if (!parsed.success)
|
|
||||||
return failure(
|
|
||||||
c,
|
c,
|
||||||
400,
|
aiAnalysisRequestSchema,
|
||||||
"invalid-request",
|
|
||||||
"start, end and duration are required",
|
"start, end and duration are required",
|
||||||
)
|
)
|
||||||
|
if (!parsed.success) return parsed.response
|
||||||
if (
|
if (
|
||||||
Number.isNaN(Date.parse(parsed.data.start)) ||
|
Number.isNaN(Date.parse(parsed.data.start)) ||
|
||||||
Number.isNaN(Date.parse(parsed.data.end))
|
Number.isNaN(Date.parse(parsed.data.end))
|
||||||
@@ -983,11 +979,12 @@ async function recordHint(
|
|||||||
}
|
}
|
||||||
|
|
||||||
aiRoutes.post("/ai/hint", requireAuth, async (c) => {
|
aiRoutes.post("/ai/hint", requireAuth, async (c) => {
|
||||||
const parsed = aiHintRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
aiHintRequestSchema,
|
||||||
|
"submissionId is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "submissionId is required")
|
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select({ submission: schema.submission, problem: schema.problem })
|
.select({ submission: schema.submission, problem: schema.problem })
|
||||||
.from(schema.submission)
|
.from(schema.submission)
|
||||||
@@ -1081,9 +1078,7 @@ aiRoutes.post("/ai/hint", requireAuth, async (c) => {
|
|||||||
|
|
||||||
aiRoutes.post("/ai/hint/:id/feedback", requireAuth, async (c) => {
|
aiRoutes.post("/ai/hint/:id/feedback", requireAuth, async (c) => {
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = aiHintFeedbackRequestSchema.safeParse(
|
const parsed = await readJson(c, aiHintFeedbackRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
|
||||||
)
|
|
||||||
if (!id || !parsed.success)
|
if (!id || !parsed.success)
|
||||||
return failure(c, 400, "invalid-request", "helpful is required")
|
return failure(c, 400, "invalid-request", "helpful is required")
|
||||||
// 只能评自己的提示:顺着 submission 核对是不是本人。别人的和不存在的一样回 404,
|
// 只能评自己的提示:顺着 submission 核对是不是本人。别人的和不存在的一样回 404,
|
||||||
@@ -1114,11 +1109,12 @@ aiRoutes.post("/ai/hint/:id/feedback", requireAuth, async (c) => {
|
|||||||
aiRoutes.post("/ai/class-analysis", requireAuth, async (c) => {
|
aiRoutes.post("/ai/class-analysis", requireAuth, async (c) => {
|
||||||
if (!isTeacherOrAbove(c.get("user")))
|
if (!isTeacherOrAbove(c.get("user")))
|
||||||
return failure(c, 403, "permission-denied", "Permission denied")
|
return failure(c, 403, "permission-denied", "Permission denied")
|
||||||
const parsed = classAnalysisRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
classAnalysisRequestSchema,
|
||||||
|
"Class data is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Class data is required")
|
|
||||||
const limited = await throttleAi(c)
|
const limited = await throttleAi(c)
|
||||||
if (limited) return limited
|
if (limited) return limited
|
||||||
return streamChat(
|
return streamChat(
|
||||||
@@ -1130,16 +1126,12 @@ aiRoutes.post("/ai/class-analysis", requireAuth, async (c) => {
|
|||||||
aiRoutes.post("/ai/class-pk-analysis", requireAuth, async (c) => {
|
aiRoutes.post("/ai/class-pk-analysis", requireAuth, async (c) => {
|
||||||
if (!isTeacherOrAbove(c.get("user")))
|
if (!isTeacherOrAbove(c.get("user")))
|
||||||
return failure(c, 403, "permission-denied", "Permission denied")
|
return failure(c, 403, "permission-denied", "Permission denied")
|
||||||
const parsed = classPkAnalysisRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
|
||||||
)
|
|
||||||
if (!parsed.success)
|
|
||||||
return failure(
|
|
||||||
c,
|
c,
|
||||||
400,
|
classPkAnalysisRequestSchema,
|
||||||
"invalid-request",
|
|
||||||
"At least two classes are required",
|
"At least two classes are required",
|
||||||
)
|
)
|
||||||
|
if (!parsed.success) return parsed.response
|
||||||
const limited = await throttleAi(c)
|
const limited = await throttleAi(c)
|
||||||
if (limited) return limited
|
if (limited) return limited
|
||||||
return streamChat(
|
return streamChat(
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { createSession, destroySession } from "../auth/session"
|
|||||||
import { publishSessionRevoked } from "../events"
|
import { publishSessionRevoked } from "../events"
|
||||||
import { hashPassword, verifyPassword } from "../auth/password"
|
import { hashPassword, verifyPassword } from "../auth/password"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { getUserProfileById } from "../services/profile"
|
import { getUserProfileById } from "../services/profile"
|
||||||
import {
|
import {
|
||||||
clientIp,
|
clientIp,
|
||||||
@@ -32,17 +32,12 @@ const LOGIN_PER_USERNAME: AttemptRule = { limit: 10, windowSeconds: 15 * 60 }
|
|||||||
const LOGIN_PER_IP: AttemptRule = { limit: 100, windowSeconds: 15 * 60 }
|
const LOGIN_PER_IP: AttemptRule = { limit: 100, windowSeconds: 15 * 60 }
|
||||||
|
|
||||||
authRoutes.post("/auth/login", async (c) => {
|
authRoutes.post("/auth/login", async (c) => {
|
||||||
const parsed = loginRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
|
||||||
)
|
|
||||||
if (!parsed.success) {
|
|
||||||
return failure(
|
|
||||||
c,
|
c,
|
||||||
400,
|
loginRequestSchema,
|
||||||
"invalid-request",
|
|
||||||
"Username and password are required",
|
"Username and password are required",
|
||||||
)
|
)
|
||||||
}
|
if (!parsed.success) return parsed.response
|
||||||
|
|
||||||
const usernameKey = `login:user:${parsed.data.username.toLowerCase()}`
|
const usernameKey = `login:user:${parsed.data.username.toLowerCase()}`
|
||||||
const ipKey = `login:ip:${clientIp(c)}`
|
const ipKey = `login:ip:${clientIp(c)}`
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireAuth, type AppEnv } from "../auth/middleware"
|
import { requireAuth, type AppEnv } from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { JudgeStatus } from "../judge/status"
|
||||||
import { queryInteger, rounded } from "./helpers"
|
import { queryInteger, rounded } from "./helpers"
|
||||||
|
|
||||||
@@ -165,11 +165,12 @@ classroomRoutes.get("/me/class-rank", requireAuth, async (c) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
classroomRoutes.post("/classes/comparison", async (c) => {
|
classroomRoutes.post("/classes/comparison", async (c) => {
|
||||||
const parsed = classComparisonRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
classComparisonRequestSchema,
|
||||||
|
"At least one class is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "At least one class is required")
|
|
||||||
const users = await loadClassUsers(parsed.data.classNames)
|
const users = await loadClassUsers(parsed.data.classNames)
|
||||||
const allAc = users.map((user) => user.acceptedNumber)
|
const allAc = users.map((user) => user.acceptedNumber)
|
||||||
const globalQ1 = quantile(allAc, 0.25)
|
const globalQ1 = quantile(allAc, 0.25)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import { Hono } from "hono"
|
|||||||
|
|
||||||
import { requireAuth, requireSuperAdmin, type AppEnv } from "../auth/middleware"
|
import { requireAuth, requireSuperAdmin, type AppEnv } from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { JudgeStatus } from "../judge/status"
|
||||||
import { objectValue, queryInteger, sampleUser } from "./helpers"
|
import { objectValue, queryInteger, sampleUser } from "./helpers"
|
||||||
|
|
||||||
@@ -187,11 +187,12 @@ contentRoutes.get("/messages", requireAuth, async (c) => {
|
|||||||
*/
|
*/
|
||||||
contentRoutes.post("/messages", requireSuperAdmin, async (c) => {
|
contentRoutes.post("/messages", requireSuperAdmin, async (c) => {
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const parsed = createMessageRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
createMessageRequestSchema,
|
||||||
|
"Invalid message payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid message payload")
|
|
||||||
if (parsed.data.recipientId === user.id)
|
if (parsed.data.recipientId === user.id)
|
||||||
return failure(
|
return failure(
|
||||||
c,
|
c,
|
||||||
@@ -263,11 +264,12 @@ contentRoutes.get("/problems/:id/reaction", requireAuth, async (c) => {
|
|||||||
|
|
||||||
contentRoutes.post("/problems/:id/reaction", requireAuth, async (c) => {
|
contentRoutes.post("/problems/:id/reaction", requireAuth, async (c) => {
|
||||||
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const problemId = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = setReactionRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
setReactionRequestSchema,
|
||||||
|
"Invalid reaction",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid reaction")
|
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const [[problem], [solved]] = await Promise.all([
|
const [[problem], [solved]] = await Promise.all([
|
||||||
db
|
db
|
||||||
@@ -447,11 +449,12 @@ contentRoutes.get("/learn/progress", requireAuth, async (c) => {
|
|||||||
contentRoutes.post("/tutorials/:id/progress", requireAuth, async (c) => {
|
contentRoutes.post("/tutorials/:id/progress", requireAuth, async (c) => {
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = tutorialProgressPingSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
tutorialProgressPingSchema,
|
||||||
|
"Invalid progress payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid progress payload")
|
|
||||||
const [tutorial] = await db
|
const [tutorial] = await db
|
||||||
.select({ id: schema.tutorial.id })
|
.select({ id: schema.tutorial.id })
|
||||||
.from(schema.tutorial)
|
.from(schema.tutorial)
|
||||||
@@ -501,11 +504,12 @@ contentRoutes.post("/tutorials/:id/progress", requireAuth, async (c) => {
|
|||||||
contentRoutes.post("/exercises/:id/attempts", requireAuth, async (c) => {
|
contentRoutes.post("/exercises/:id/attempts", requireAuth, async (c) => {
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
const id = queryInteger(c.req.param("id"), 0, { min: 1 })
|
||||||
const parsed = exerciseAttemptRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
exerciseAttemptRequestSchema,
|
||||||
|
"Invalid attempt payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid attempt payload")
|
|
||||||
// 练习跟着教程走:教程没公开,它底下的练习也不该能上报
|
// 练习跟着教程走:教程没公开,它底下的练习也不该能上报
|
||||||
const [exercise] = await db
|
const [exercise] = await db
|
||||||
.select({ id: schema.exercise.id })
|
.select({ id: schema.exercise.id })
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import { optionalAuth, requireAuth } from "../auth/middleware"
|
|||||||
import { setContestPassword } from "../auth/session"
|
import { setContestPassword } from "../auth/session"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { astRequirements } from "../judge/ast"
|
import { astRequirements } from "../judge/ast"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import {
|
import {
|
||||||
canAccessContest,
|
canAccessContest,
|
||||||
checkContestPassword,
|
checkContestPassword,
|
||||||
@@ -163,11 +163,12 @@ contestRoutes.post("/contests/:id/access", requireAuth, async (c) => {
|
|||||||
)
|
)
|
||||||
if (!contest || !contest.password)
|
if (!contest || !contest.password)
|
||||||
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
return failure(c, 404, "contest-not-found", "Contest does not exist")
|
||||||
const parsed = contestPasswordRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
contestPasswordRequestSchema,
|
||||||
|
"Password is required",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Password is required")
|
|
||||||
// 比赛密码往往就是几位数字,不限的话一个脚本几分钟就能扫完。
|
// 比赛密码往往就是几位数字,不限的话一个脚本几分钟就能扫完。
|
||||||
// 按「人 × 比赛」计失败次数:猜错的是自己,锁的也只是自己进这一场。
|
// 按「人 × 比赛」计失败次数:猜错的是自己,锁的也只是自己进这一场。
|
||||||
const attemptKey = `contest-password:${contest.id}:${c.get("user")!.id}`
|
const attemptKey = `contest-password:${contest.id}:${c.get("user")!.id}`
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ import { Hono } from "hono"
|
|||||||
import { requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
import { requireAuth, requireTeacher, type AppEnv } from "../auth/middleware"
|
||||||
import { config } from "../config"
|
import { config } from "../config"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, readJson, success } from "../http"
|
||||||
import { flowchartQueue } from "../queue"
|
import { flowchartQueue } from "../queue"
|
||||||
import { getBooleanOption } from "../services/options"
|
import { getBooleanOption } from "../services/options"
|
||||||
import { consumeToken } from "../services/throttling"
|
import { consumeToken } from "../services/throttling"
|
||||||
@@ -85,9 +85,7 @@ function flowchartData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
flowchartRoutes.post("/flowcharts", requireAuth, async (c) => {
|
flowchartRoutes.post("/flowcharts", requireAuth, async (c) => {
|
||||||
const parsed = createFlowchartRequestSchema.safeParse(
|
const parsed = await readJson(c, createFlowchartRequestSchema)
|
||||||
await c.req.json().catch(() => null),
|
|
||||||
)
|
|
||||||
if (
|
if (
|
||||||
!parsed.success ||
|
!parsed.success ||
|
||||||
JSON.stringify(parsed.data?.flowchartData ?? {}).length > 500 * 1024
|
JSON.stringify(parsed.data?.flowchartData ?? {}).length > 500 * 1024
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { z } from "zod"
|
|||||||
|
|
||||||
import { config } from "../config"
|
import { config } from "../config"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure } from "../http"
|
import { failure, readJson } from "../http"
|
||||||
|
|
||||||
const heartbeatSchema = z.object({
|
const heartbeatSchema = z.object({
|
||||||
hostname: z.string().min(1).max(128),
|
hostname: z.string().min(1).max(128),
|
||||||
@@ -38,7 +38,7 @@ async function heartbeat(c: Context) {
|
|||||||
return failure(c, 403, "invalid-judge-token", "Invalid token")
|
return failure(c, 403, "invalid-judge-token", "Invalid token")
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = heartbeatSchema.safeParse(await c.req.json().catch(() => null))
|
const parsed = await readJson(c, heartbeatSchema)
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
return failure(c, 400, "invalid-heartbeat", "Invalid heartbeat payload")
|
return failure(c, 400, "invalid-heartbeat", "Invalid heartbeat payload")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
type AppEnv,
|
type AppEnv,
|
||||||
} from "../auth/middleware"
|
} from "../auth/middleware"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { computeProgress } from "../services/problemset"
|
import { computeProgress } from "../services/problemset"
|
||||||
import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers"
|
import { asFilterValue, objectValue, queryInteger, sampleUser } from "./helpers"
|
||||||
|
|
||||||
@@ -342,11 +342,12 @@ async function recomputeProgress(
|
|||||||
}
|
}
|
||||||
|
|
||||||
problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
problemsetRoutes.post("/problem-set-progress", requireAuth, async (c) => {
|
||||||
const parsed = joinProblemSetRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
joinProblemSetRequestSchema,
|
||||||
|
"Invalid problem set",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid problem set")
|
|
||||||
const user = c.get("user")!
|
const user = c.get("user")!
|
||||||
const [problemSet] = await db
|
const [problemSet] = await db
|
||||||
.select({ id: schema.problemset.id })
|
.select({ id: schema.problemset.id })
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
} from "../auth/middleware"
|
} from "../auth/middleware"
|
||||||
import type { AuthUser } from "../auth/session"
|
import type { AuthUser } from "../auth/session"
|
||||||
import { db, schema } from "../db"
|
import { db, schema } from "../db"
|
||||||
import { failure, success } from "../http"
|
import { failure, parseBody, success } from "../http"
|
||||||
import { JudgeStatus } from "../judge/status"
|
import { JudgeStatus } from "../judge/status"
|
||||||
import { judgeQueue } from "../queue"
|
import { judgeQueue } from "../queue"
|
||||||
import {
|
import {
|
||||||
@@ -93,12 +93,12 @@ async function saveTrace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
submissionRoutes.post("/submissions", requireAuth, async (c) => {
|
submissionRoutes.post("/submissions", requireAuth, async (c) => {
|
||||||
const parsed = createSubmissionRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
createSubmissionRequestSchema,
|
||||||
|
"Invalid submission payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success) {
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid submission payload")
|
|
||||||
}
|
|
||||||
let contestId: number | null = null
|
let contestId: number | null = null
|
||||||
if (parsed.data.contestId) {
|
if (parsed.data.contestId) {
|
||||||
// 这里用不了 requireContestAccess 中间件:比赛 id 来自请求体,
|
// 这里用不了 requireContestAccess 中间件:比赛 id 来自请求体,
|
||||||
@@ -293,11 +293,12 @@ submissionRoutes.post(
|
|||||||
)
|
)
|
||||||
|
|
||||||
submissionRoutes.post("/code/format", requireAuth, async (c) => {
|
submissionRoutes.post("/code/format", requireAuth, async (c) => {
|
||||||
const parsed = formatCodeRequestSchema.safeParse(
|
const parsed = await parseBody(
|
||||||
await c.req.json().catch(() => null),
|
c,
|
||||||
|
formatCodeRequestSchema,
|
||||||
|
"Invalid format payload",
|
||||||
)
|
)
|
||||||
if (!parsed.success)
|
if (!parsed.success) return parsed.response
|
||||||
return failure(c, 400, "invalid-request", "Invalid format payload")
|
|
||||||
try {
|
try {
|
||||||
const code = await formatCode(parsed.data.code, parsed.data.language)
|
const code = await formatCode(parsed.data.code, parsed.data.language)
|
||||||
return success(c, { code } satisfies FormatCodeResponse)
|
return success(c, { code } satisfies FormatCodeResponse)
|
||||||
|
|||||||
Reference in new issue
Block a user