fix(阶段3): 匿名不可读用户档案,真名改为默认不下发

F1:GET /profiles/:username 只挂了 optionalAuth、handler 内无登录判断,
匿名可读 email、adminType、className、lastLogin。用户名又能经 /rankings/users
公开枚举,等于可以无 cookie 批量收集全校学生的邮箱与最后登录时间。
handler 开头补上未登录即返回空,对齐旧后端 account/views/oj.py 的
UserProfileAPI.get 首行 `if not user.is_authenticated: return self.success()`。

F2:旧后端把「是否下发真名」做成 UsernameSerializer(need_real_name=False)
的默认关闭开关,全仓 11 处调用只有比赛榜单一处显式打开;新后端没搬这一层,
真名随用户对象无条件下发,13 个下发点里 8 个匿名可达。

这里补回同一层:helpers.ts 新增 sampleUser(),realName 默认不下发,
需要的地方显式传 { includeRealName: true }。12 个下发点改为走这个函数,
只有比赛榜单一处打开(对齐 contest/serializers.py:84 的 is_contest_admin)。
没有逐处删字段 —— 那样下次新增端点还会重犯。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 01:59:05 -06:00
co-authored by Claude Opus 5
parent 9c04b00e3f
commit b4b61af6b0
6 changed files with 52 additions and 27 deletions
+27 -4
View File
@@ -1,5 +1,29 @@
import { sampleUserSchema, type SampleUser } from "@oj2/contract"
import type { AuthUser } from "../auth/session"
/**
* 用户对象的序列化层,对齐旧后端 `utils/api/_serializers.py` 的 `UsernameSerializer`。
*
* 旧后端把「是否下发真名」做成 `need_real_name` 开关,**默认关闭**,全仓 11 处调用里只有
* 比赛榜单一处显式打开。这里保持同一约定:`realName` 默认不下发,需要的地方显式传
* `{ includeRealName: true }`。
*
* 所有下发用户对象的地方都必须走这个函数,不要再手写 `{ id, username, realName }` ——
* 手写的话下次新增端点必然重犯「学生真名无条件下发」。
*/
export function sampleUser(
source: { id: number; username: string },
realName: string | null | undefined,
options: { includeRealName?: boolean } = {},
): SampleUser {
return sampleUserSchema.parse({
id: source.id,
username: source.username,
realName: options.includeRealName === true ? (realName ?? null) : null,
})
}
export function objectValue(value: unknown): Record<string, unknown> {
return value && typeof value === "object" && !Array.isArray(value)
? (value as Record<string, unknown>)
@@ -24,10 +48,9 @@ export function queryInteger(
return parsed
}
export function isRegularUser(user: AuthUser | null | undefined) {
return user?.adminType === "Regular User"
}
// 注意:不要再加 isRegularUser(user) 这类「是普通用户才受限」的判断 ——
// 匿名用户 user 为 null 时它返回 false,守卫会整体短路,匿名的权限反而大于登录学生。
// 需要「非管理员即受限」时一律用 !isAdminRole(user)。
export function isAdminRole(user: AuthUser | null | undefined) {
return Boolean(user && user.adminType !== "Regular User")
}