fix(老师权限): 老师在自己比赛里点题目「可见」没反应、看不到学生的智能分析入口等一批权限错位

- 比赛题的可见开关按题目作者判归属,从题库加进比赛的题作者是别人,老师点了 404 且无提示;
  改成和编辑、删除一样按比赛创建者判(canEdit)
- 学生主页「智能分析」和「查看指定用户」只给超管,后端本来就允许老师;改成老师可见
- 排名页班级详情的「AI 分析」学生也看得到,点了权限不足;只给老师显示
- 班级 PK 页解构 store 丢了响应性,资料晚到时老师看不到「AI 分析」
- 学生管理员能给任何人的流程图重新评分且不限流;别人的提交只有老师能重评,免限流也只给老师
- 超管没退登就过期的机房电脑上,下一个登录的老师继承演示模式、看不到后台和看板且关不掉;
  演示模式只对超管生效
- 老师的出题权限能设成「无权限」,设了比赛题目页全是权限不足;后端兜成「仅自己」,
  编辑框里老师不再给这个选项

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-08 14:00:04 +08:00
1 parent bde387e8bc
commit 30f34f0e36
10 files changed
+45 -15

No files matched your search

+4
View File
@@ -57,6 +57,9 @@ function classNameOf(
* 旧 UserAdminAPI.put 按 admin_type 归一 problem_permission:
* 超管恒为 All、普通用户恒为 None、两种管理员取传入值或兜底 Own。
* 不这么做的话,把一个超管降级成普通用户后,他还留着 All 的题目权限。
*
* 老师比旧后端多一条:不能是 None。比赛归老师管,而比赛题的接口挂的是
* requireProblemPermission —— 老师设成 None 就是进得了比赛、点题目全是「权限不足」。
*/
function normalizePermission(
adminType: AdminType,
@@ -64,6 +67,7 @@ function normalizePermission(
): ProblemPermission {
if (adminType === "Super Admin") return "All"
if (adminType === "Regular User") return "None"
if (adminType === "Teacher Admin" && requested === "None") return "Own"
return requested || "Own"
}
+4 -1
View File
@@ -52,7 +52,10 @@ function canManageAll(user: AuthUser) {
* 对齐旧后端:`ensure_created_by(problem.contest, user)` vs `ensure_created_by(problem, user)`。
* 一道比赛题的 created_by 可能是克隆时的操作人,跟谁有权改它没关系。
*/
async function canEdit(user: AuthUser, problem: ProblemRow) {
export async function canEdit(
user: AuthUser,
problem: Pick<ProblemRow, "contestId" | "createdById">,
) {
if (user.adminType === "Super Admin") return true
if (problem.contestId === null) {
return canManageAll(user) || problem.createdById === user.id
+5 -2
View File
@@ -35,7 +35,7 @@ import { JudgeStatus } from "../../judge/status"
import { completeChat } from "../../services/ai"
import { localTime, localYear } from "../../time"
import { queryInteger, rounded } from "../helpers"
import { findTagsByName, normalizeTagNames } from "./problem"
import { canEdit, findTagsByName, normalizeTagNames } from "./problem"
export const adminTagRoutes = new Hono<AppEnv>()
@@ -252,6 +252,7 @@ adminTagRoutes.put("/problems/:id/visibility", requireProblemPermission, async (
id: schema.problem.id,
visible: schema.problem.visible,
createdById: schema.problem.createdById,
contestId: schema.problem.contestId,
})
.from(schema.problem)
.where(eq(schema.problem.id, id))
@@ -260,7 +261,9 @@ adminTagRoutes.put("/problems/:id/visibility", requireProblemPermission, async (
// AttributeError(500)。这里正常返回 404。
if (!problem) return failure(c, 404, "problem-not-found", "题目不存在")
const user = c.get("user")!
if (!canManageAllProblems(user) && problem.createdById !== user.id) {
// 和编辑、删除同一个口径(canEdit):比赛题看比赛的创建者。原来只看题目的创建者,
// 而从题库加进比赛的题保留原作者,老师在自己比赛里点「可见」一律 404
if (!(await canEdit(user, problem))) {
return failure(c, 404, "problem-not-found", "题目不存在")
}
await db
+6 -1
View File
@@ -30,6 +30,7 @@ import { unknownDisplayIds } from "./submission-statistics"
import {
classCondition,
isAdminRole,
isTeacherOrAbove,
matchedUsers,
asRecord,
parseDisplayIds,
@@ -663,11 +664,15 @@ flowchartRoutes.post("/flowcharts/:id/retry", requireAuth, async (c) => {
.limit(1)
if (!row || !canView(user, row.flowchart, row.problem, await joinTimesFor(user, [row.flowchart])))
return failure(c, 404, "flowchart-not-found", "Submission does not exist")
// 看得见不等于能重评:canView 放进来的学生管理员、出题人只能看。重评会清掉原来的
// 评分再抽一次,别人的提交只有老师能动
if (row.flowchart.userId !== user.id && !isTeacherOrAbove(user))
return failure(c, 403, "permission-denied", "权限不足")
if (![2, 3].includes(row.flowchart.status))
return failure(c, 409, "retry-not-allowed", "Submission is not in a state that allows retry")
// canView 允许本人重试自己的提交,不限流的话学生可以反复点着刷 AI 调用。
// 教师放行:重新判题是他们的日常操作,成批点几十行是正常用法
if (!isAdminRole(user)) {
if (!isTeacherOrAbove(user)) {
const throttle = await consumeToken("user", flowchartThrottleKey(user.id))
if (!throttle.allowed) {
return failure(