feat(阶段4): 后台地基 + 公告管理

地基:
- auth/middleware.ts 加四个角色守卫 requireAdmin / requireTeacher /
  requireSuperAdmin / requireProblemPermission,对应旧 account/decorators.py 的
  四个装饰器。未登录 401 login-required、角色不够 403 permission-denied,
  与前端 api2 拦截器按 code 分流的两支对上。
- routes/admin/ 目录 + 总入口挂在 /api/admin。角色守卫由各子路由自己挂,
  不在总入口兜一层 —— 否则「这个接口要什么角色」从注册行看不出来,
  正是阶段 3 Minor M2 踩过的坑。
- packages/contract/src/admin.ts 独立放后台契约。同一张表两侧下发的字段集不同
  (后台要 visible,oj 侧连键都不该出现),混在一起迟早有人在 oj 侧复用后台那个。
- utils/legacy.ts:把 toLegacy / legacyResponse 从 oj/api.ts 抽出来共用。
  admin 侧组件同样读 snake_case,走同一层适配,组件不动。

公告管理(旧 /api/admin/announcement 一个路径四个动词)拆成:
  GET/POST   admin/announcements
  GET/PUT/DELETE admin/announcements/:id

一处有意不对齐旧后端:删除不存在的公告,旧后端 filter().delete() 静默成功,
这里返回 404 —— 后台是人手点删除,静默成功会让人以为删掉了,刷新后它还在。

实测:匿名 401 / 学生 403 / 超管 200;增删改查、列表不含 content、
空标题 400、不存在 404 全部符合预期。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 07:06:22 -06:00
parent 359b91ab4d
commit 9a6c3ba225
9 changed files with 255 additions and 26 deletions

View File

@@ -0,0 +1,38 @@
import { z } from "zod"
import { paginatedSchema, sampleUserSchema } from "./common"
/**
* 后台侧的契约。与 oj 侧分开放:同一张表在两侧下发的字段集通常不同
* (后台要 `visible` 这类管理字段oj 侧连键都不该出现),
* 混在一个 schema 里迟早会有人为了省事在 oj 侧复用后台那个。
*/
export const adminAnnouncementSchema = z.object({
id: z.number().int(),
title: z.string(),
tag: z.string(),
content: z.string(),
visible: z.boolean(),
top: z.boolean(),
createdBy: sampleUserSchema,
createTime: z.string(),
lastUpdateTime: z.string(),
})
export const adminAnnouncementListSchema = paginatedSchema(
// 列表不带 content公告正文是 8MB 上限的富文本,列表页只显示标题
adminAnnouncementSchema.omit({ content: true }),
)
export const createAnnouncementRequestSchema = z.object({
title: z.string().trim().min(1).max(64),
tag: z.string().max(64),
content: z.string().max(1024 * 1024 * 8),
visible: z.boolean(),
top: z.boolean(),
})
export const updateAnnouncementRequestSchema = createAnnouncementRequestSchema
export type AdminAnnouncement = z.infer<typeof adminAnnouncementSchema>

View File

@@ -1,3 +1,4 @@
export * from "./admin"
export * from "./auth"
export * from "./account"
export * from "./achievement"