From 3a25e4e53fbb8afe936b4da527d3ddff8fc1322c Mon Sep 17 00:00:00 2001 From: undefined Date: Mon, 6 Oct 2025 08:38:20 +0800 Subject: [PATCH] core: allow extra user private & public fields (#1059) --- packages/hydrooj/locales/zh.yaml | 2 +- packages/hydrooj/src/handler/contest.ts | 21 +++----- packages/hydrooj/src/handler/domain.ts | 2 +- packages/hydrooj/src/handler/problem.ts | 2 +- packages/hydrooj/src/handler/training.ts | 2 +- packages/hydrooj/src/model/builtin.ts | 6 ++- packages/hydrooj/src/model/setting.ts | 15 ++++-- packages/hydrooj/src/model/user.ts | 49 ++++++++++++------- .../ui-default/templates/components/user.html | 2 +- .../ui-default/templates/training_detail.html | 4 +- .../ui-default/templates/user_detail.html | 2 +- 11 files changed, 61 insertions(+), 46 deletions(-) diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index af35c8b2b..5c50ad7f6 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -886,7 +886,7 @@ View Contest: 查看比赛 View contests: 查看比赛 View Details: 查看详情 View discussions: 查看讨论 -View domain user displayname: 显示域中用户的显示名 +View domain user private info: 显示域中用户的私有信息 View hidden contest submission status and scoreboard: 查看隐藏的比赛递交状态和成绩表 View hidden homework submission status and scoreboard: 查看隐藏的作业递交状态和成绩表 View hidden problems: 查看隐藏的题目 diff --git a/packages/hydrooj/src/handler/contest.ts b/packages/hydrooj/src/handler/contest.ts index 074bcc056..f082036cf 100644 --- a/packages/hydrooj/src/handler/contest.ts +++ b/packages/hydrooj/src/handler/contest.ts @@ -222,12 +222,7 @@ export class ContestPrintHandler extends ContestDetailBaseHandler { const tasks = await contest.getMultiPrintTask(domainId, tid, isContestAdmin ? {} : { owner: this.user._id }) .project({ _id: 1, title: 1, owner: 1, status: 1 }).sort({ _id: 1 }).toArray(); const uids = Array.from(new Set(tasks.map((i) => i.owner))); - const udict = await user.getListForRender( - domainId, uids, - this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) - ? ['displayName', 'school', 'studentId'] - : [], - ); + const udict = await user.getListForRender(domainId, uids, this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO)); this.response.body = { tasks, udict }; } @@ -516,7 +511,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler { files: sortFiles(this.tdoc.files || []), udict: await user.getListForRender( domainId, tcdocs.map((i) => i.owner), - this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : [], + this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO), ), tcdocs, urlForFile: (filename: string) => this.url('contest_file_download', { tid, filename }), @@ -628,7 +623,7 @@ export class ContestUserHandler extends ContestManagementBaseHandler { } const udict = await user.getListForRender( domainId, [this.tdoc.owner, ...tsdocs.map((i) => i.uid)], - this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : [], + this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO), ); this.response.body = { tdoc: this.tdoc, tsdocs, udict }; this.response.pjax = 'partials/contest_user.html'; @@ -669,7 +664,7 @@ export class ContestBalloonHandler extends ContestManagementBaseHandler { owner_udoc: await user.getById(domainId, this.tdoc.owner), pdict: await problem.getList(domainId, this.tdoc.pids, true, true, problem.PROJECTION_CONTEST_LIST), bdocs, - udict: await user.getListForRender(domainId, uids, this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : []), + udict: await user.getListForRender(domainId, uids, this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO)), }; this.response.pjax = 'partials/contest_balloon.html'; this.response.template = 'contest_balloon.html'; @@ -836,7 +831,7 @@ export async function apply(ctx: Context) { if (realtime && !this.user.own(tdoc)) { this.checkPerm(PERM.PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD); } - const config: ScoreboardConfig = { isExport: false, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) }; + const config: ScoreboardConfig = { isExport: false, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) }; if (!realtime && this.tdoc.lockAt && !this.tdoc.unlocked) { config.lockAt = this.tdoc.lockAt; } @@ -896,7 +891,7 @@ export async function apply(ctx: Context) { ].concat( tdoc.pids.map((i, idx) => `@p ${pid(idx)},${escape(pdict[i]?.title || 'Unknown Problem')},20,0`), teams.map((i, idx) => { - const showName = this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) && udict[i.uid].displayName + const showName = this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) && udict[i.uid].displayName ? udict[i.uid].displayName : udict[i.uid].uname; const teamName = `${i.rank ? '*' : ''}${escape(udict[i.uid].school || unknownSchool)}-${escape(showName)}`; return `@t ${idx + 1},0,1,"${teamName}"`; @@ -911,7 +906,7 @@ export async function apply(ctx: Context) { async display({ tdoc }) { await this.limitRate('scoreboard_download', 60, 3); const [, rows] = await contest.getScoreboard.call(this, tdoc.domainId, tdoc._id, { - isExport: true, lockAt: this.tdoc.lockAt, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME), + isExport: true, lockAt: this.tdoc.lockAt, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO), }); this.binary(await this.renderHTML('contest_scoreboard_download_html.html', { rows, tdoc }), `${this.tdoc.title}.html`); }, @@ -921,7 +916,7 @@ export async function apply(ctx: Context) { async display({ tdoc }) { await this.limitRate('scoreboard_download', 60, 3); const [, rows] = await contest.getScoreboard.call(this, tdoc.domainId, tdoc._id, { - isExport: true, lockAt: this.tdoc.lockAt, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME), + isExport: true, lockAt: this.tdoc.lockAt, showDisplayName: this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO), }); this.binary(toCSV(rows.map((r) => r.map((c) => c.value.toString())), { bom: true }), `${this.tdoc.title}.csv`); }, diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 0c693ccb1..e5e62ce57 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -143,7 +143,7 @@ class DomainUserHandler extends ManageHandler { user: 1, role: 1, join: 1, - ...(this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? { displayName: 1 } : {}), + ...(this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) ? { displayName: 1 } : {}), }, }, ]).toArray(), diff --git a/packages/hydrooj/src/handler/problem.ts b/packages/hydrooj/src/handler/problem.ts index b554e28de..eb4418c01 100644 --- a/packages/hydrooj/src/handler/problem.ts +++ b/packages/hydrooj/src/handler/problem.ts @@ -974,7 +974,7 @@ export class ProblemStatisticsHandler extends ProblemDetailHandler { 'record', ); const [udict, udoc] = await Promise.all([ - user.getListForRender(domainId, rsdocs.map((i) => i.uid), this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : []), + user.getListForRender(domainId, rsdocs.map((i) => i.uid), this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO)), user.getById(domainId, this.pdoc.owner), ]); this.response.template = 'problem_statistics.html'; diff --git a/packages/hydrooj/src/handler/training.ts b/packages/hydrooj/src/handler/training.ts index 6ebab8e15..cde9aff5e 100644 --- a/packages/hydrooj/src/handler/training.ts +++ b/packages/hydrooj/src/handler/training.ts @@ -111,7 +111,7 @@ class TrainingDetailHandler extends Handler { const canViewHidden = this.user.hasPerm(PERM.PERM_VIEW_PROBLEM_HIDDEN) || this.user._id; const [udoc, udict, pdict, psdict, selfPsdict] = await Promise.all([ user.getById(domainId, tdoc.owner), - user.getListForRender(domainId, enrollUsers, this.user.hasPerm(PERM.PERM_VIEW_DISPLAYNAME) ? ['displayName'] : []), + user.getListForRender(domainId, enrollUsers, this.user.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO)), problem.getList(domainId, pids, canViewHidden, true), problem.getListStatus(domainId, uid, pids), shouldCompare ? problem.getListStatus(domainId, this.user._id, pids) : {}, diff --git a/packages/hydrooj/src/model/builtin.ts b/packages/hydrooj/src/model/builtin.ts index f6028a515..f02c73761 100644 --- a/packages/hydrooj/src/model/builtin.ts +++ b/packages/hydrooj/src/model/builtin.ts @@ -12,7 +12,9 @@ export const PERM = { // Domain Settings PERM_VIEW: 1n << 0n, PERM_EDIT_DOMAIN: 1n << 1n, + /** @deprecated use PERM_VIEW_USER_PRIVATE_INFO instead */ PERM_VIEW_DISPLAYNAME: 1n << 67n, + PERM_VIEW_USER_PRIVATE_INFO: 1n << 67n, PERM_MOD_BADGE: 1n << 2n, // Problem @@ -105,7 +107,7 @@ export const Permission = (family: string, key: bigint, desc: string) => ({ fami export const PERMS = [ Permission('perm_general', PERM.PERM_VIEW, 'View this domain'), - Permission('perm_general', PERM.PERM_VIEW_DISPLAYNAME, 'View domain user displayname'), + Permission('perm_general', PERM.PERM_VIEW_USER_PRIVATE_INFO, 'View domain user private info'), Permission('perm_general', PERM.PERM_EDIT_DOMAIN, 'Edit domain settings'), Permission('perm_general', PERM.PERM_MOD_BADGE, 'Show MOD badge'), Permission('perm_problem', PERM.PERM_CREATE_PROBLEM, 'Create problems'), @@ -191,7 +193,7 @@ PERM.PERM_BASIC = PERM.PERM_VIEW | PERM.PERM_VIEW_RECORD; PERM.PERM_DEFAULT = PERM.PERM_VIEW - | PERM.PERM_VIEW_DISPLAYNAME + | PERM.PERM_VIEW_USER_PRIVATE_INFO | PERM.PERM_VIEW_PROBLEM | PERM.PERM_EDIT_PROBLEM_SELF | PERM.PERM_SUBMIT_PROBLEM diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index 730533653..91754ac01 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -31,6 +31,8 @@ export const FLAG_HIDDEN = 1; export const FLAG_DISABLED = 2; export const FLAG_SECRET = 4; export const FLAG_PRO = 8; +export const FLAG_PUBLIC = 16; +export const FLAG_PRIVATE = 32; export const PREFERENCE_SETTINGS: _Setting[] = []; export const ACCOUNT_SETTINGS: _Setting[] = []; @@ -75,6 +77,7 @@ declare global { interface Meta { // eslint-disable-line ts/no-unused-vars family?: string; secret?: boolean; + flag?: number; } } } @@ -93,6 +96,7 @@ function schemaToSettings(schema: Schema) { : s.meta?.role === 'markdown' ? 'markdown' : s.meta?.role === 'textarea' ? 'textarea' : 'text'; if (s.meta?.role === 'password') flag |= FLAG_SECRET; + if (s.meta?.flag) flag |= s.meta?.flag; const options = {}; for (const item of actualList || []) { if (item.type !== 'const') throw new Error(`List item must be a constant, got ${item.type}`); @@ -256,9 +260,9 @@ AccountSetting( Setting('setting_info', 'qq', null, 'text', 'QQ'), Setting('setting_info', 'gender', builtin.USER_GENDER_OTHER, builtin.USER_GENDER_RANGE, 'Gender'), Setting('setting_info', 'bio', null, 'markdown', 'Bio'), - Setting('setting_info', 'school', '', 'text', 'School'), - Setting('setting_info', 'studentId', '', 'text', 'Student ID'), - Setting('setting_info', 'phone', null, 'text', 'Phone', null, FLAG_DISABLED), + Setting('setting_info', 'school', '', 'text', 'School', '', FLAG_PRIVATE), + Setting('setting_info', 'studentId', '', 'text', 'Student ID', '', FLAG_PRIVATE), + Setting('setting_info', 'phone', null, 'text', 'Phone', null, FLAG_DISABLED | FLAG_PRIVATE), Setting('setting_customize', 'backgroundImage', '/components/profile/backgrounds/1.jpg', 'text', 'Profile Background Image', 'Choose the background image in your profile page.'), @@ -278,7 +282,8 @@ DomainSetting( ); DomainUserSetting(Schema.object({ - displayName: Schema.transform(String, (input) => saslPrep(input)).default('').description('Display Name').extra('family', 'setting_info'), + displayName: Schema.transform(String, (input) => saslPrep(input)).default('').description('Display Name') + .extra('family', 'setting_info').extra('flag', FLAG_PRIVATE), rpInfo: Schema.any().extra('family', 'setting_storage').disabled().hidden(), @@ -415,6 +420,8 @@ global.Hydro.model.setting = { FLAG_DISABLED, FLAG_SECRET, FLAG_PRO, + FLAG_PUBLIC, + FLAG_PRIVATE, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 52d34e9b7..eae2aa125 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -1,4 +1,4 @@ -import { escapeRegExp, pick, uniq } from 'lodash'; +import { escapeRegExp, omit, pick, uniq } from 'lodash'; import { LRUCache } from 'lru-cache'; import { Collection, Filter, ObjectId } from 'mongodb'; import { serializer } from '@hydrooj/framework'; @@ -59,6 +59,8 @@ export class User { _loginip: string; _tfa: string; _authenticators: Authenticator[]; + _privateFields: string[] = []; + _publicFields: string[] = []; mail: string; uname: string; @@ -101,14 +103,15 @@ export class User { this.tfa = !!udoc.tfa; this.authn = (udoc.authenticators || []).length > 0; if (dudoc.group) this.group = dudoc.group; - - for (const key in setting.SETTINGS_BY_KEY) { - this[key] = udoc[key] ?? (setting.SETTINGS_BY_KEY[key].value || system.get(`preference.${key}`)); - } - - for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) { - this[key] = dudoc[key] ?? (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value || system.get(`preference.${key}`)); - } + const load = (settings: Record>, source: any) => { + for (const key in settings) { + this[key] = source[key] ?? (settings[key].value || system.get(`preference.${key}`)); + if (settings[key].flag & setting.FLAG_PUBLIC) this._publicFields.push(key); + if (settings[key].flag & setting.FLAG_PRIVATE) this._privateFields.push(key); + } + }; + load(setting.SETTINGS_BY_KEY, udoc); + load(setting.DOMAIN_USER_SETTINGS_BY_KEY, dudoc); } async init() { @@ -159,13 +162,14 @@ export class User { return user; } + getFields(type: 'public' | 'private' = 'public') { + const fields = ['_id', 'uname', 'mail', 'perm', 'role', 'priv', 'regat', 'loginat', 'tfa', 'authn'].concat(this._publicFields); + return type === 'public' ? fields : fields.concat(this._privateFields); + } + serialize(h) { if (!this._isPrivate) { - const fields = ['_id', 'uname', 'mail', 'perm', 'role', 'priv', 'regat', 'loginat', 'tfa', 'authn']; - if (h?.user?.hasPerm(PERM.PERM_VIEW_DISPLAYNAME)) { - fields.push('displayName', 'school', 'studentId'); - } - return pick(this, fields); + return pick(this, this.getFields(h?.user?.hasPerm(PERM.PERM_VIEW_USER_PRIVATE_INFO) ? 'private' : 'public')); } return JSON.stringify(this, serializer(true, h)); } @@ -398,19 +402,26 @@ class UserModel { return projection ? coll.find(params).project(buildProjection(projection)) : coll.find(params); } + static async getListForRender(domainId: string, uids: number[], showPrivateInfo: boolean, extraFields?: string[]): Promise; static async getListForRender(domainId: string, uids: number[], extraFields?: string[]): Promise; - static async getListForRender(domainId: string, uids: number[], arg: string[] | boolean) { - const fields = ['_id', 'uname', 'mail', 'avatar', 'school', 'studentId'].concat(arg instanceof Array ? arg : []); - const showDisplayName = arg instanceof Array ? fields.includes('displayName') : arg; + static async getListForRender(domainId: string, uids: number[], arg: string[] | boolean, extraFields?: string[]) { + const _extraFields = Array.isArray(arg) ? arg : Array.isArray(extraFields) ? extraFields : []; + const showPrivateInfo = arg === true || _extraFields.includes('displayName'); + const fields = Array.from(new Set([ + ...(await UserModel.getById('system', 0)).getFields(showPrivateInfo ? 'private' : 'public'), + ..._extraFields, + ])); const [udocs, vudocs, dudocs] = await Promise.all([ UserModel.getMulti({ _id: { $in: uids } }, fields).toArray(), collV.find({ _id: { $in: uids } }).toArray(), - domain.getDomainUserMulti(domainId, uids).project({ uid: 1, ...(showDisplayName ? { displayName: 1 } : {}) }).toArray(), + domain.getDomainUserMulti(domainId, uids).project(buildProjection(fields.concat('uid'))).toArray(), ]); const udict = {}; for (const udoc of udocs) udict[udoc._id] = udoc; for (const udoc of vudocs) udict[udoc._id] = udoc; - if (showDisplayName) for (const dudoc of dudocs) udict[dudoc.uid].displayName = dudoc.displayName; + if (showPrivateInfo) { + for (const dudoc of dudocs) Object.assign(udict[dudoc.uid], omit(dudoc, ['_id', 'uid'])); + } for (const uid of uids) udict[uid] ||= { ...UserModel.defaultUser }; for (const key in udict) { udict[key].school ||= ''; diff --git a/packages/ui-default/templates/components/user.html b/packages/ui-default/templates/components/user.html index 61f9706ee..ed30c594c 100644 --- a/packages/ui-default/templates/components/user.html +++ b/packages/ui-default/templates/components/user.html @@ -5,7 +5,7 @@ {% endif %}