refactor(后端): 权限判断收到一处,路由文件之间不再互相 import

- services/permissions.ts:「超管或创建者」原来在比赛、题单、比赛题目(4 处)、
  services/contest.ts 各抄一遍,「题目全部权限」在题目和标签两个路由各写一份
- services/submission-filters.ts:提交列表的用户名 / 班级 / 题号筛选,原来
  submission.ts、flowchart.ts 从 submission-statistics.ts 这个路由文件里拿
- services/class-activity.ts:课堂做题记录的口径和「正在上课的班」,原来后台
  conf.ts 从 classroom.ts 路由里拿
- services/problem-tags.ts:标签名去重、按名查标签,原来 tag.ts 从 admin/problem.ts 拿

纯搬家。三种身份(超管 / 老师 / 学生)打了 26 个受影响的接口,改前改后状态码和响应体
逐字节一致,包括「不是自己的比赛 / 题单 / 题目报不存在」那几条。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-16 14:53:24 +08:00
1 parent 5787efe023
commit 465dbfd42f
14 files changed
+388 -361

No files matched your search

+4 -12
View File
@@ -9,22 +9,14 @@ import { and, count, countDistinct, desc, eq, ilike, inArray } from "drizzle-orm
import { Hono } from "hono"
import { requireTeacher, type AppEnv } from "../../auth/middleware"
import type { AuthUser } from "../../auth/session"
import { db, schema } from "../../db"
import { failure, parseBody, success } from "../../http"
import { contestStatus } from "../../services/contest"
import { queryInteger, sampleUser } from "../helpers"
import { ownsOrSuper } from "../../services/permissions"
export const adminContestRoutes = new Hono<AppEnv>()
/**
* 「只能管自己建的」。对齐旧 `ensure_created_by`:超管放行,其余人只能碰自己创建的。
* 越权时报的是**不存在**而不是无权限 —— 不泄露「有这么个东西但你看不到」。
*/
function ownedBy(user: AuthUser, contest: { createdById: number }) {
return user.adminType === "Super Admin" || contest.createdById === user.id
}
async function serialize(row: {
contest: typeof schema.contest.$inferSelect
user: typeof schema.user.$inferSelect
@@ -132,7 +124,7 @@ adminContestRoutes.get("/contests", requireTeacher, async (c) => {
adminContestRoutes.get("/contests/:id", requireTeacher, async (c) => {
const [row] = await selectContest(queryInteger(c.req.param("id"), 0, { min: 1 }))
if (!row || !ownedBy(c.get("user")!, row.contest)) {
if (!row || !ownsOrSuper(c.get("user")!, row.contest)) {
return failure(c, 404, "contest-not-found", "Contest does not exist")
}
return success(c, await serialize(row))
@@ -170,7 +162,7 @@ adminContestRoutes.put("/contests/:id", requireTeacher, async (c) => {
const parsed = await parseBody(c, updateContestRequestSchema)
if (!parsed.success) return parsed.response
const [existing] = await selectContest(id)
if (!existing || !ownedBy(c.get("user")!, existing.contest)) {
if (!existing || !ownsOrSuper(c.get("user")!, existing.contest)) {
return failure(c, 404, "contest-not-found", "Contest does not exist")
}
const error = validatePayload(parsed.data)
@@ -307,7 +299,7 @@ adminContestRoutes.put("/contests/:id/acm-helper", requireTeacher, async (c) =>
.from(schema.contest)
.where(eq(schema.contest.id, contestId))
.limit(1)
if (!contest || !ownedBy(c.get("user")!, contest)) {
if (!contest || !ownsOrSuper(c.get("user")!, contest)) {
return failure(c, 404, "contest-not-found", "Contest does not exist")
}
// rank 必须属于这场比赛。旧后端只按 rank_id 取,不校验归属 ——