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
parent 9c04b00e3f
commit b4b61af6b0
6 changed files with 52 additions and 27 deletions

View File

@@ -35,7 +35,7 @@ import { failure, success } from "../http"
import { JudgeStatus } from "../judge/status"
import { getBooleanOption } from "../services/options"
import { getUserProfileById } from "../services/profile"
import { objectValue, queryInteger } from "./helpers"
import { objectValue, queryInteger, sampleUser } from "./helpers"
export const accountRoutes = new Hono<AppEnv>()
@@ -99,6 +99,10 @@ accountRoutes.post("/users", async (c) => {
})
accountRoutes.get("/profiles/:username", optionalAuth, async (c) => {
// 对齐旧后端 account/views/oj.py 的 UserProfileAPI.get 首行:
// `if not user.is_authenticated: return self.success()` —— 匿名一律返回空,
// 否则用户名可经 /rankings/users 公开枚举,进而无 cookie 批量收集全校学生的邮箱与最后登录时间。
if (!c.get("user")) return success(c, null)
const [target] = await db.select({ id: schema.user.id }).from(schema.user)
.where(and(sql`lower(${schema.user.username}) = lower(${c.req.param("username")})`, eq(schema.user.isDisabled, false))).limit(1)
if (!target) return failure(c, 404, "user-not-found", "User does not exist")
@@ -165,7 +169,7 @@ accountRoutes.get("/rankings/users", async (c) => {
.limit(top > 0 ? Math.min(top, 250) : limit).offset(top > 0 ? 0 : offset)
const results = rows.map(({ profile, user }) => rankProfileSchema.parse({
id: profile.id,
user: { id: user.id, username: user.username, realName: profile.realName },
user: sampleUser(user, profile.realName),
acceptedNumber: profile.acceptedNumber,
submissionNumber: profile.submissionNumber,
mood: profile.mood,