core: allow extra user private & public fields (#1059)

This commit is contained in:
undefined authored and GitHub committed 2025-10-06 01:38:20 +01:00
1 parent 8f2c5a17cd
commit 3a25e4e53f
11 files changed
+61 -46

No files matched your search

+4 -2
View File
@@ -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
+11 -4
View File
@@ -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<T> { // eslint-disable-line ts/no-unused-vars
family?: string;
secret?: boolean;
flag?: number;
}
}
}
@@ -93,6 +96,7 @@ function schemaToSettings(schema: Schema<any>) {
: 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,
+30 -19
View File
@@ -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<string, ReturnType<typeof setting.Setting>>, 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<Udoc>(buildProjection(projection)) : coll.find(params);
}
static async getListForRender(domainId: string, uids: number[], showPrivateInfo: boolean, extraFields?: string[]): Promise<BaseUserDict>;
static async getListForRender(domainId: string, uids: number[], extraFields?: string[]): Promise<BaseUserDict>;
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 ||= '';