update
Deploy / deploy (push) Canceled after 0s

This commit is contained in:
xuyue committed 2026-09-27 19:36:28 +08:00
1 parent 23844ae301
commit d25b5dbe6d
268 files changed
+3684 -8861

No files matched your search

+47 -103
View File
@@ -47,12 +47,7 @@ import { JudgeStatus } from "../judge/status"
import { getBooleanOption } from "../services/options"
import { sniffImageExtension } from "../services/image"
import { getUserProfileById } from "../services/profile"
import {
clientIp,
countAttempt,
lockoutRemaining,
type AttemptRule,
} from "../services/throttling"
import { clientIp, countAttempt, lockoutRemaining, type AttemptRule } from "../services/throttling"
import { localTime, weekStart } from "../time"
import { isTeacherOrAbove, asRecord, queryInteger, sampleUser } from "./helpers"
@@ -65,19 +60,10 @@ export const accountRoutes = new Hono<AppEnv>()
const REGISTER_PER_IP: AttemptRule = { limit: 100, windowSeconds: 60 * 60 }
accountRoutes.post("/users", async (c) => {
const parsed = await parseBody(
c,
registerRequestSchema,
"Invalid registration payload",
)
const parsed = await parseBody(c, registerRequestSchema, "Invalid registration payload")
if (!parsed.success) return parsed.response
if (!(await getBooleanOption("allow_register", true))) {
return failure(
c,
403,
"registration-disabled",
"Register function has been disabled by admin",
)
return failure(c, 403, "registration-disabled", "Register function has been disabled by admin")
}
const registerKey = `register:ip:${clientIp(c)}`
@@ -159,47 +145,31 @@ accountRoutes.get("/profiles/:username", optionalAuth, async (c) => {
)
.limit(1)
if (!target) return failure(c, 404, "user-not-found", "User does not exist")
const profile = await getUserProfileById(
target.id,
c.get("user")?.id === target.id,
)
if (!profile)
return failure(c, 404, "profile-not-found", "User profile does not exist")
const profile = await getUserProfileById(target.id, c.get("user")?.id === target.id)
if (!profile) return failure(c, 404, "profile-not-found", "User profile does not exist")
return success(c, profile)
})
accountRoutes.put("/me/profile", requireAuth, async (c) => {
const parsed = await parseBody(
c,
updateProfileRequestSchema,
"Invalid profile payload",
)
const parsed = await parseBody(c, updateProfileRequestSchema, "Invalid profile payload")
if (!parsed.success) return parsed.response
const values = Object.fromEntries(
Object.entries(parsed.data).map(([key, value]) => [
key,
value === "" ? null : value,
]),
Object.entries(parsed.data).map(([key, value]) => [key, value === "" ? null : value]),
)
await db
.update(schema.userProfile)
.set(values)
.where(eq(schema.userProfile.userId, c.get("user")!.id))
const profile = await getUserProfileById(c.get("user")!.id, true)
if (!profile)
return failure(c, 404, "profile-not-found", "User profile does not exist")
if (!profile) return failure(c, 404, "profile-not-found", "User profile does not exist")
return success(c, profile)
})
accountRoutes.post("/me/avatar", requireAuth, async (c) => {
const body: Record<string, string | File> = await c.req
.parseBody()
.catch(() => ({}))
const body: Record<string, string | File> = await c.req.parseBody().catch(() => ({}))
const image = body.image
if (!(image instanceof File))
return failure(c, 400, "invalid-file", "Invalid file content")
if (image.size > 2 * 1024 * 1024)
return failure(c, 400, "file-too-large", "Picture is too large")
if (!(image instanceof File)) return failure(c, 400, "invalid-file", "Invalid file content")
if (image.size > 2 * 1024 * 1024) return failure(c, 400, "file-too-large", "Picture is too large")
const extension = await sniffImageExtension(image)
if (!extension) {
return failure(c, 400, "unsupported-file", "Unsupported file format")
@@ -223,14 +193,11 @@ accountRoutes.get("/users/:id/metrics", async (c) => {
.select({
first: min(schema.submission.createTime),
latest: max(schema.submission.createTime),
activeDays: countDistinct(
sql`date(${localTime(schema.submission.createTime)})`,
),
activeDays: countDistinct(sql`date(${localTime(schema.submission.createTime)})`),
})
.from(schema.submission)
.where(eq(schema.submission.userId, userId))
if (!row?.first || !row.latest)
return failure(c, 404, "no-submissions", "暂无提交")
if (!row?.first || !row.latest) return failure(c, 404, "no-submissions", "暂无提交")
return success(c, {
now: new Date().toISOString(),
first: row.first,
@@ -397,10 +364,7 @@ accountRoutes.get("/rankings/activity", async (c) => {
and(
isNull(schema.submission.contestId),
gte(schema.submission.createTime, start),
inArray(schema.submission.result, [
JudgeStatus.ACCEPTED,
JudgeStatus.AST_CHECK_FAILED,
]),
inArray(schema.submission.result, [JudgeStatus.ACCEPTED, JudgeStatus.AST_CHECK_FAILED]),
eq(schema.user.isDisabled, false),
ne(schema.user.adminType, "Super Admin"),
),
@@ -447,8 +411,7 @@ accountRoutes.get("/rankings/weekly", optionalAuth, async (c) => {
const user = c.get("user")
const scope = c.req.query("scope") === "class" ? "class" : "global"
const className = scope === "class" ? (user?.className ?? null) : null
if (scope === "class" && !className)
return failure(c, 400, "class-missing", "用户没有班级信息")
if (scope === "class" && !className) return failure(c, 400, "class-missing", "用户没有班级信息")
const start = weekStart()
@@ -503,9 +466,7 @@ accountRoutes.get("/rankings/weekly", optionalAuth, async (c) => {
.groupBy(schema.submission.userId),
])
const submissions = new Map(
submittedRows.map((row) => [row.userId, row.value]),
)
const submissions = new Map(submittedRows.map((row) => [row.userId, row.value]))
/**
* 排序键与全服榜同构:解决多的在前 → 同解决数时提交少的在前 → 再同按 id。
* 第三档同样不是凑数,周榜上「都是 1 题」的学生成片存在,没有稳定兜底键时
@@ -551,8 +512,7 @@ accountRoutes.get("/problems/:displayId/rank", requireAuth, async (c) => {
),
)
.limit(1)
if (!problem)
return failure(c, 404, "problem-not-found", "Problem does not exist")
if (!problem) return failure(c, 404, "problem-not-found", "Problem does not exist")
const accepted = and(
eq(schema.submission.problemId, problem.id),
inArray(schema.submission.result, [0, 10]),
@@ -570,12 +530,7 @@ accountRoutes.get("/problems/:displayId/rank", requireAuth, async (c) => {
db
.select({ id: schema.user.id })
.from(schema.user)
.where(
and(
eq(schema.user.className, className),
eq(schema.user.isDisabled, false),
),
),
.where(and(eq(schema.user.className, className), eq(schema.user.isDisabled, false))),
),
)
: accepted
@@ -617,44 +572,33 @@ accountRoutes.get("/problems/:displayId/rank", requireAuth, async (c) => {
* 题目一旦被隐藏或删除,display_ids 就比 ids 短 —— 轻则把编号张冠李戴写进库,
* 重则 `id_map[k]` KeyError。这里改成按 id 建 Map、查不到就不动。
*/
accountRoutes.post(
"/me/problem-display-ids/refresh",
requireAuth,
async (c) => {
const user = c.get("user")!
const [profile] = await db
.select({ value: schema.userProfile.acmProblemsStatus })
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, user.id))
.limit(1)
const status = asRecord(profile?.value)
const problems = asRecord(status.problems)
const ids = Object.keys(problems).map(Number).filter(Number.isInteger)
if (ids.length > 0) {
const rows = await db
.select({ id: schema.problem.id, displayId: schema.problem.displayId })
.from(schema.problem)
.where(
and(
inArray(schema.problem.id, ids),
eq(schema.problem.visible, true),
),
)
const displayIds = new Map(
rows.map((row) => [String(row.id), row.displayId]),
)
for (const [id, value] of Object.entries(problems)) {
const item = asRecord(value)
const displayId = displayIds.get(id)
if (displayId) item._id = displayId
problems[id] = item
}
status.problems = problems
await db
.update(schema.userProfile)
.set({ acmProblemsStatus: status })
.where(eq(schema.userProfile.userId, user.id))
accountRoutes.post("/me/problem-display-ids/refresh", requireAuth, async (c) => {
const user = c.get("user")!
const [profile] = await db
.select({ value: schema.userProfile.acmProblemsStatus })
.from(schema.userProfile)
.where(eq(schema.userProfile.userId, user.id))
.limit(1)
const status = asRecord(profile?.value)
const problems = asRecord(status.problems)
const ids = Object.keys(problems).map(Number).filter(Number.isInteger)
if (ids.length > 0) {
const rows = await db
.select({ id: schema.problem.id, displayId: schema.problem.displayId })
.from(schema.problem)
.where(and(inArray(schema.problem.id, ids), eq(schema.problem.visible, true)))
const displayIds = new Map(rows.map((row) => [String(row.id), row.displayId]))
for (const [id, value] of Object.entries(problems)) {
const item = asRecord(value)
const displayId = displayIds.get(id)
if (displayId) item._id = displayId
problems[id] = item
}
return success(c, null)
},
)
status.problems = problems
await db
.update(schema.userProfile)
.set({ acmProblemsStatus: status })
.where(eq(schema.userProfile.userId, user.id))
}
return success(c, null)
})