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:
@@ -23,7 +23,7 @@ import {
|
||||
findVisibleContest,
|
||||
isContestAdmin,
|
||||
} from "../services/contest"
|
||||
import { objectValue, publicTemplates, queryInteger, stringArray } from "./helpers"
|
||||
import { objectValue, publicTemplates, queryInteger, sampleUser, stringArray } from "./helpers"
|
||||
|
||||
export const contestRoutes = new Hono<AppEnv>()
|
||||
|
||||
@@ -31,7 +31,7 @@ async function creator(id: number) {
|
||||
const [row] = await db.select({ id: schema.user.id, username: schema.user.username, realName: schema.userProfile.realName })
|
||||
.from(schema.user).leftJoin(schema.userProfile, eq(schema.userProfile.userId, schema.user.id))
|
||||
.where(eq(schema.user.id, id)).limit(1)
|
||||
return row ?? { id, username: "", realName: null }
|
||||
return sampleUser(row ?? { id, username: "" }, row?.realName)
|
||||
}
|
||||
|
||||
async function serializeContest(contest: typeof schema.contest.$inferSelect, includeNow = false) {
|
||||
@@ -128,7 +128,7 @@ contestRoutes.get("/contests/:id/problems", optionalAuth, async (c) => {
|
||||
submissionNumber: allowed ? problem.submissionNumber : 0,
|
||||
acceptedNumber: allowed ? problem.acceptedNumber : 0,
|
||||
difficulty: allowed ? problem.difficulty : "",
|
||||
createdBy: { id: user.id, username: user.username, realName },
|
||||
createdBy: sampleUser(user, realName),
|
||||
tags: tags.get(problem.id) ?? [],
|
||||
contestId: contest.id,
|
||||
allowFlowchart: problem.allowFlowchart,
|
||||
@@ -174,7 +174,7 @@ contestRoutes.get("/contests/:id/problems/:displayId", optionalAuth, async (c) =
|
||||
shareSubmission: row.problem.shareSubmission,
|
||||
contestId: contest.id,
|
||||
tags: tags.get(row.problem.id) ?? [],
|
||||
createdBy: { id: row.user.id, username: row.user.username, realName: row.realName },
|
||||
createdBy: sampleUser(row.user, row.realName),
|
||||
myStatus: null,
|
||||
myFailedCount: 0,
|
||||
allowFlowchart: row.problem.allowFlowchart,
|
||||
@@ -206,7 +206,9 @@ contestRoutes.get("/contests/:id/rank", optionalAuth, async (c) => {
|
||||
return success(c, contestRankSchema.parse({
|
||||
results: rows.map(({ rank, user, realName }) => contestRankItemSchema.parse({
|
||||
id: rank.id,
|
||||
user: { id: user.id, username: user.username, realName: admin ? realName : null },
|
||||
// 唯一显式打开真名的地方,对齐旧后端 contest/serializers.py:84
|
||||
// `UsernameSerializer(obj.user, need_real_name=self.is_contest_admin)`
|
||||
user: sampleUser(user, realName, { includeRealName: admin }),
|
||||
submissionNumber: rank.submissionNumber,
|
||||
acceptedNumber: rank.acceptedNumber,
|
||||
totalTime: rank.totalTime,
|
||||
|
||||
Reference in New Issue
Block a user