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:
@@ -22,3 +22,46 @@ export const requireAuth: MiddlewareHandler<AppEnv> = async (c, next) => {
|
||||
c.set("user", user)
|
||||
await next()
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台接口的角色守卫,对应旧后端 `account/decorators.py` 的四个装饰器。
|
||||
*
|
||||
* 未登录一律 401 `login-required`、登录但角色不够一律 403 `permission-denied`,
|
||||
* 与旧 `BasePermissionDecorator._permission_error` 的两分支一致 —— 前端 `utils/api2.ts`
|
||||
* 的拦截器就是按这两个 code 分别弹登录框和弹提示的。
|
||||
*
|
||||
* 「账号已禁用」这一支不需要单独处理:`getSessionUser` 对禁用用户直接返回 null,
|
||||
* 于是落到 401,比旧后端先认证再报 403 更早拦一步。
|
||||
*/
|
||||
function requireRole(
|
||||
allowed: (user: AuthUser) => boolean,
|
||||
): MiddlewareHandler<AppEnv> {
|
||||
return async (c, next) => {
|
||||
const user = await getSessionUser(c)
|
||||
if (!user) return failure(c, 401, "login-required", "请先登录")
|
||||
if (!allowed(user)) return failure(c, 403, "permission-denied", "权限不足")
|
||||
c.set("user", user)
|
||||
await next()
|
||||
}
|
||||
}
|
||||
|
||||
const ADMIN_ROLES = ["Student Admin", "Teacher Admin", "Super Admin"]
|
||||
const TEACHER_ROLES = ["Teacher Admin", "Super Admin"]
|
||||
|
||||
/** 旧 `@admin_role_required` */
|
||||
export const requireAdmin = requireRole((user) => ADMIN_ROLES.includes(user.adminType))
|
||||
|
||||
/** 旧 `@teacher_admin_required` */
|
||||
export const requireTeacher = requireRole((user) => TEACHER_ROLES.includes(user.adminType))
|
||||
|
||||
/** 旧 `@super_admin_required` */
|
||||
export const requireSuperAdmin = requireRole((user) => user.adminType === "Super Admin")
|
||||
|
||||
/**
|
||||
* 旧 `@problem_permission_required`:先要是管理员,再要 problem_permission 不为 None。
|
||||
* 注意它只管「能不能进这个接口」,「能改哪些题」(Own vs All)由各 handler 自己按
|
||||
* created_by 过滤 —— 旧后端也是这么分工的,别把两件事混在一起。
|
||||
*/
|
||||
export const requireProblemPermission = requireRole(
|
||||
(user) => ADMIN_ROLES.includes(user.adminType) && user.problemPermission !== "None",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user