diff --git a/.eslintrc.js b/.eslintrc.js index 8176c19d9..22a72819d 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -9,6 +9,7 @@ module.exports = { globals: { Atomics: 'readonly', SharedArrayBuffer: 'readonly', + BigInt: true, }, parser: '@typescript-eslint/parser', plugins: ['@typescript-eslint'], diff --git a/TODO b/TODO index 4a925a4a1..155ed765b 100644 --- a/TODO +++ b/TODO @@ -1,5 +1,4 @@ @requireCsrfToken 为model.system添加缓存 bug修复 -使用BigInt管理权限 修复discussion前端权限节点显示异常 diff --git a/hydro/handler/domain.ts b/hydro/handler/domain.ts index 176fd3a20..1a4f34d93 100644 --- a/hydro/handler/domain.ts +++ b/hydro/handler/domain.ts @@ -114,6 +114,13 @@ class DomainUserHandler extends ManageHandler { class DomainPermissionHandler extends ManageHandler { async get({ domainId }) { const roles = await domain.getRoles(domainId); + const log2 = (val: bigint) => { + // @ts-ignore + for (let i = 0n; ; i++) { + // @ts-ignore + if ((val >> i) === 0n) return parseInt(i.toString(), 10) - 1; + } + }; const path = [ ['Hydro', 'homepage'], ['domain', null], @@ -121,7 +128,7 @@ class DomainPermissionHandler extends ManageHandler { ]; this.response.template = 'domain_permission.html'; this.response.body = { - roles, PERMS_BY_FAMILY, domain: this.domain, path, + roles, PERMS_BY_FAMILY, domain: this.domain, path, log2, }; } @@ -129,7 +136,11 @@ class DomainPermissionHandler extends ManageHandler { const roles = {}; for (const role in this.request.body) { if (this.request.body[role] instanceof Array) { - roles[role] = this.request.body[role].join(''); + const perms = this.request.body[role]; + // @ts-ignore + roles[role] = 0n; + // @ts-ignore + for (const r of perms) roles[role] += 1n << BigInt(r); } } await domain.setRoles(domainId, roles); diff --git a/hydro/handler/misc.ts b/hydro/handler/misc.ts index 2e5c41b37..3c409efcb 100644 --- a/hydro/handler/misc.ts +++ b/hydro/handler/misc.ts @@ -82,7 +82,7 @@ class StatusHandler extends Handler { class StatusUpdateHandler extends Handler { async post(args) { - this.checkPerm(PRIV.PRIV_JUDGE); + this.checkPriv(PRIV.PRIV_JUDGE); args.type = 'judger'; return coll.updateOne( { mid: args.mid, type: 'judger' }, diff --git a/hydro/handler/problem.ts b/hydro/handler/problem.ts index 07de89d5e..2ab89783d 100644 --- a/hydro/handler/problem.ts +++ b/hydro/handler/problem.ts @@ -20,7 +20,7 @@ import { NoProblemError, ProblemDataNotFoundError, BadRequestError, SolutionNotFoundError, } from '../error'; -import { Pdoc, Udoc, Rdoc } from '../interface'; +import { Pdoc, User, Rdoc } from '../interface'; const parseCategory = (value: string) => { if (!value) return []; @@ -165,7 +165,7 @@ class ProblemRandomHandler extends ProblemHandler { class ProblemDetailHandler extends ProblemHandler { pdoc: Pdoc; - udoc: Udoc; + udoc: User; @param('pid', Types.String, null, parsePid) async _prepare(domainId: string, pid: number | string) { @@ -457,12 +457,9 @@ class ProblemSolutionHandler extends ProblemDetailHandler { async postEditReply(domainId: string, psid: ObjectID, psrid: ObjectID, content: string) { const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); - if (!(psdoc.owner === this.user._id - && this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) { - if (!(psrdoc.owner === this.user._id - && this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) { - this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY); - } + if (!(psrdoc.owner === this.user._id + && this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) { + this.checkPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY); } await solution.editReply(domainId, psid, psrid, content); this.back(); @@ -473,12 +470,9 @@ class ProblemSolutionHandler extends ProblemDetailHandler { async postDeleteReply(domainId: string, psid: ObjectID, psrid: ObjectID) { const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid); if ((!psdoc) || psdoc.pid !== this.pdoc.docId) throw new SolutionNotFoundError(psid); - if (!(psdoc.owner === this.user._id - && this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION))) { - if (!(psrdoc.owner === this.user._id - && this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) { - this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY); - } + if (!(psrdoc.owner === this.user._id + && this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) { + this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY); } await solution.delReply(domainId, psid, psrid); this.back(); diff --git a/hydro/handler/user.ts b/hydro/handler/user.ts index dfd3d986e..c61c30004 100644 --- a/hydro/handler/user.ts +++ b/hydro/handler/user.ts @@ -172,9 +172,13 @@ class UserDetailHandler extends Handler { sdoc.updateIp = ''; sdoc._id = ''; } + const path = [ + ['Hydro', 'homepage'], + ['user_detail', 'user_detail', { uid }], + ]; this.response.template = 'user_detail.html'; this.response.body = { - isSelfProfile, udoc, sdoc, rdocs, pdict, + isSelfProfile, udoc, sdoc, rdocs, pdict, path, }; } } @@ -321,7 +325,7 @@ class OauthCallbackHandler extends Handler { r.uname.push(String.random(16)); for (const uname of r.uname) { // eslint-disable-next-line no-await-in-loop - const nudoc = await user.getByUname('system', uname, true); + const nudoc = await user.getByUname('system', uname); if (!nudoc) { username = uname; break; diff --git a/hydro/interface.ts b/hydro/interface.ts index 682b2f258..fd0ee80ec 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -15,6 +15,22 @@ export interface Setting { } export interface Udoc extends Dictionary { + _id: number, + mail: string, + mailLower: string, + uname: string, + unameLower: string, + salt: string, + hash: string, + hashType: string, + priv: number, + regat: Date, + loginat: Date, + regip: string, + loginip: string, +} + +export interface User extends Dictionary { udoc: () => any, dudoc: () => any, _id: number, @@ -26,16 +42,16 @@ export interface Udoc extends Dictionary { priv: number, regat: Date, loginat: Date, - perm: string, + perm: bigint, role: string, regip: () => string, loginip: () => string, - hasPerm: (perm: string) => boolean, + hasPerm: (perm: bigint) => boolean, hasPriv: (priv: number) => boolean, checkPassword: (password: string) => void, } -export type Udict = NumericDictionary; +export type Udict = NumericDictionary; export interface Pdoc { _id: ObjectID, diff --git a/hydro/lib/nav.ts b/hydro/lib/nav.ts index a82711247..62a82bd9a 100644 --- a/hydro/lib/nav.ts +++ b/hydro/lib/nav.ts @@ -4,7 +4,7 @@ import { PERM, PRIV } from '../model/builtin'; global.Hydro.ui.nav = []; const trueChecker = () => true; -const Checker = (perm: string, priv: number, checker: Function = trueChecker) => (handler) => ( +const Checker = (perm: bigint, priv: number, checker: Function = trueChecker) => (handler) => ( checker(handler) && (perm ? handler.user.hasPerm(perm) : true) && (priv ? handler.user.hasPriv(priv) : true) @@ -12,10 +12,10 @@ const Checker = (perm: string, priv: number, checker: Function = trueChecker) => const Item = ( name: string, args: any, prefix: string, - ...permPrivChecker: Array> + ...permPrivChecker: Array> ) => { let _priv: number; - let _perm: string; + let _perm: bigint; let checker: Function = trueChecker; for (const item of permPrivChecker) { if (item instanceof Object && !isNull(item)) { @@ -23,7 +23,7 @@ const Item = ( if (typeof item[0] === 'number') { // @ts-ignore _priv = item; - } else if (typeof item[0] === 'string') { + } else if (typeof item[0] === 'bigint') { // @ts-ignore _perm = item; } @@ -32,7 +32,7 @@ const Item = ( } } else if (typeof item === 'number') { _priv = item; - } else if (typeof item === 'string') { + } else if (typeof item === 'bigint') { _perm = item; } } diff --git a/hydro/lib/template.ts b/hydro/lib/template.ts index 4bbade300..6dbfbbee4 100644 --- a/hydro/lib/template.ts +++ b/hydro/lib/template.ts @@ -1,5 +1,6 @@ import fs from 'fs'; import path from 'path'; +import serialize from 'serialize-javascript'; import nunjucks from 'nunjucks'; import * as markdown from './markdown'; import { STATUS, PERM, PRIV } from '../model/builtin'; @@ -33,6 +34,7 @@ class Nunjucks extends nunjucks.Environment { constructor() { super(new Loader(), { autoescape: true, trimBlocks: true }); this.addFilter('json', (self) => JSON.stringify(self), false); + this.addFilter('serialize', (self, ignoreFunction = true) => serialize(self, { ignoreFunction })); this.addFilter('assign', (self, data) => Object.assign(self, data)); this.addFilter('markdown', (self, safe = true) => { if (safe) return markdown.safe.render(self); diff --git a/hydro/model/builtin.ts b/hydro/model/builtin.ts index 74bddbdb8..9b1475647 100644 --- a/hydro/model/builtin.ts +++ b/hydro/model/builtin.ts @@ -1,91 +1,95 @@ +// @ts-nocheck +/* Why nocheck? + * BitInt requires at least ES2020, but we can't use it as + * NodeJS doesn't support some of this new features. + * e.g.: object?.property + */ export const PERM = { - PERM_NONE: '', + PERM_NONE: 0n, // Domain Settings - PERM_VIEW: '0', - PERM_EDIT_DOMAIN: '1', - PERM_MOD_BADGE: '2', + PERM_VIEW: 1n << 0n, + PERM_EDIT_DOMAIN: 1n << 1n, + PERM_MOD_BADGE: 1n << 2n, // Problem and Record - PERM_CREATE_PROBLEM: '3', - PERM_EDIT_PROBLEM: '4', - PERM_EDIT_PROBLEM_SELF: '5', - PERM_VIEW_PROBLEM: '6', - PERM_VIEW_PROBLEM_HIDDEN: '7', - PERM_SUBMIT_PROBLEM: '8', - PERM_READ_PROBLEM_DATA: '9', - PERM_READ_PROBLEM_DATA_SELF: 'A', - PERM_READ_RECORD_CODE: 'B', - PERM_REJUDGE_PROBLEM: 'C', - PERM_REJUDGE: 'D', + PERM_CREATE_PROBLEM: 1n << 4n, + PERM_EDIT_PROBLEM: 1n << 5n, + PERM_EDIT_PROBLEM_SELF: 1n << 6n, + PERM_VIEW_PROBLEM: 1n << 7n, + PERM_VIEW_PROBLEM_HIDDEN: 1n << 8n, + PERM_SUBMIT_PROBLEM: 1n << 9n, + PERM_READ_PROBLEM_DATA: 1n << 10n, + PERM_READ_PROBLEM_DATA_SELF: 1n << 11n, + PERM_READ_RECORD_CODE: 1n << 12n, + PERM_REJUDGE_PROBLEM: 1n << 13n, + PERM_REJUDGE: 1n << 14n, // Problem Solution - PERM_VIEW_PROBLEM_SOLUTION: 'E', - PERM_CREATE_PROBLEM_SOLUTION: 'F', - PERM_VOTE_PROBLEM_SOLUTION: 'G', - PERM_EDIT_PROBLEM_SOLUTION: 'H', - PERM_EDIT_PROBLEM_SOLUTION_SELF: 'I', - PERM_DELETE_PROBLEM_SOLUTION: 'J', - PERM_DELETE_PROBLEM_SOLUTION_SELF: 'K', - PERM_REPLY_PROBLEM_SOLUTION: 'L', - PERM_EDIT_PROBLEM_SOLUTION_REPLY: 'M', - PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 'N', - PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'x', - PERM_DELETE_PROBLEM_SOLUTION_REPLY: 'O', - PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 'P', - PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF_SOLUTION: 'y', + PERM_VIEW_PROBLEM_SOLUTION: 1n << 15n, + PERM_CREATE_PROBLEM_SOLUTION: 1n << 16n, + PERM_VOTE_PROBLEM_SOLUTION: 1n << 17n, + PERM_EDIT_PROBLEM_SOLUTION: 1n << 18n, + PERM_EDIT_PROBLEM_SOLUTION_SELF: 1n << 19n, + PERM_DELETE_PROBLEM_SOLUTION: 1n << 20n, + PERM_DELETE_PROBLEM_SOLUTION_SELF: 1n << 21n, + PERM_REPLY_PROBLEM_SOLUTION: 1n << 22n, + PERM_EDIT_PROBLEM_SOLUTION_REPLY: 1n << 23n, + PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF: 1n << 24n, + PERM_DELETE_PROBLEM_SOLUTION_REPLY: 1n << 25n, + PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF: 1n << 26n, // Discussion - PERM_VIEW_DISCUSSION: 'Q', - PERM_CREATE_DISCUSSION: 'R', - PERM_HIGHLIGHT_DISCUSSION: 'S', - PERM_EDIT_DISCUSSION: 'T', - PERM_EDIT_DISCUSSION_SELF: 'U', - PERM_DELETE_DISCUSSION: 'V', - PERM_DELETE_DISCUSSION_SELF: 'W', - PERM_REPLY_DISCUSSION: 'X', - PERM_EDIT_DISCUSSION_REPLY: 'Y', - PERM_EDIT_DISCUSSION_REPLY_SELF: 'Z', - PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 'a', - PERM_DELETE_DISCUSSION_REPLY: 'b', - PERM_DELETE_DISCUSSION_REPLY_SELF: 'c', - PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 'd', + PERM_VIEW_DISCUSSION: 1n << 27n, + PERM_CREATE_DISCUSSION: 1n << 28n, + PERM_HIGHLIGHT_DISCUSSION: 1n << 29n, + PERM_EDIT_DISCUSSION: 1n << 30n, + PERM_EDIT_DISCUSSION_SELF: 1n << 31n, + PERM_DELETE_DISCUSSION: 1n << 32n, + PERM_DELETE_DISCUSSION_SELF: 1n << 33n, + PERM_REPLY_DISCUSSION: 1n << 34n, + PERM_EDIT_DISCUSSION_REPLY: 1n << 35n, + PERM_EDIT_DISCUSSION_REPLY_SELF: 1n << 36n, + PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 37n, + PERM_DELETE_DISCUSSION_REPLY: 1n << 38n, + PERM_DELETE_DISCUSSION_REPLY_SELF: 1n << 39n, + PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION: 1n << 40n, // Contest - PERM_VIEW_CONTEST: 'e', - PERM_VIEW_CONTEST_SCOREBOARD: 'f', - PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 'g', - PERM_CREATE_CONTEST: 'h', - PERM_ATTEND_CONTEST: 'i', - PERM_EDIT_CONTEST: 'j', - PERM_EDIT_CONTEST_SELF: 'k', + PERM_VIEW_CONTEST: 1n << 41n, + PERM_VIEW_CONTEST_SCOREBOARD: 1n << 42n, + PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD: 1n << 43n, + PERM_CREATE_CONTEST: 1n << 44n, + PERM_ATTEND_CONTEST: 1n << 45n, + PERM_EDIT_CONTEST: 1n << 50n, + PERM_EDIT_CONTEST_SELF: 1n << 51n, // Homework - PERM_VIEW_HOMEWORK: 'l', - PERM_VIEW_HOMEWORK_SCOREBOARD: 'm', - PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 'n', - PERM_CREATE_HOMEWORK: 'o', - PERM_ATTEND_HOMEWORK: 'p', - PERM_EDIT_HOMEWORK: 'q', - PERM_EDIT_HOMEWORK_SELF: 'r', + PERM_VIEW_HOMEWORK: 1n << 52n, + PERM_VIEW_HOMEWORK_SCOREBOARD: 1n << 53n, + PERM_VIEW_HOMEWORK_HIDDEN_SCOREBOARD: 1n << 54n, + PERM_CREATE_HOMEWORK: 1n << 55n, + PERM_ATTEND_HOMEWORK: 1n << 56n, + PERM_EDIT_HOMEWORK: 1n << 57n, + PERM_EDIT_HOMEWORK_SELF: 1n << 58n, // Training - PERM_VIEW_TRAINING: 's', - PERM_CREATE_TRAINING: 't', - PERM_EDIT_TRAINING: 'u', - PERM_EDIT_TRAINING_SELF: 'v', + PERM_VIEW_TRAINING: 1n << 46n, + PERM_CREATE_TRAINING: 1n << 47n, + PERM_EDIT_TRAINING: 1n << 48n, + PERM_EDIT_TRAINING_SELF: 1n << 49n, // Ranking - PERM_VIEW_RANKING: 'w', + PERM_VIEW_RANKING: 1n << 50n, // Placeholder - PERM_ALL: '', - PERM_BASIC: '', - PERM_DEFAULT: '', - PERM_ADMIN: '', + PERM_ALL: 0n, + PERM_BASIC: 0n, + PERM_DEFAULT: 0n, + PERM_ADMIN: 0n, }; -export const Permission = (family: string, key: string, desc: string) => ({ family, key, desc }); +export const Permission = (family: string, key: BigInt, desc: string) => ({ family, key, desc }); export const PERMS = [ Permission('perm_general', PERM.PERM_VIEW, 'View this domain'), @@ -149,7 +153,7 @@ export const PERMS = [ Permission('perm_ranking', PERM.PERM_VIEW_RANKING, 'View ranking'), ]; -let PERM_ALL = ''; +let PERM_ALL = 0n; for (const p in PERM) if (PERM[p]) PERM_ALL += PERM[p]; PERM.PERM_ALL = PERM_ALL; @@ -160,59 +164,59 @@ for (const p of PERMS) { } PERM.PERM_BASIC = PERM.PERM_VIEW - + PERM.PERM_VIEW_PROBLEM - + PERM.PERM_VIEW_PROBLEM_SOLUTION - + PERM.PERM_VIEW_DISCUSSION - + PERM.PERM_VIEW_CONTEST - + PERM.PERM_VIEW_CONTEST_SCOREBOARD - + PERM.PERM_VIEW_HOMEWORK - + PERM.PERM_VIEW_HOMEWORK_SCOREBOARD - + PERM.PERM_VIEW_TRAINING - + PERM.PERM_VIEW_RANKING; + | PERM.PERM_VIEW_PROBLEM + | PERM.PERM_VIEW_PROBLEM_SOLUTION + | PERM.PERM_VIEW_DISCUSSION + | PERM.PERM_VIEW_CONTEST + | PERM.PERM_VIEW_CONTEST_SCOREBOARD + | PERM.PERM_VIEW_HOMEWORK + | PERM.PERM_VIEW_HOMEWORK_SCOREBOARD + | PERM.PERM_VIEW_TRAINING + | PERM.PERM_VIEW_RANKING; PERM.PERM_DEFAULT = PERM.PERM_VIEW - + PERM.PERM_VIEW_PROBLEM - + PERM.PERM_EDIT_PROBLEM_SELF - + PERM.PERM_SUBMIT_PROBLEM - + PERM.PERM_READ_PROBLEM_DATA_SELF - + PERM.PERM_VIEW_PROBLEM_SOLUTION - + PERM.PERM_CREATE_PROBLEM_SOLUTION - + PERM.PERM_VOTE_PROBLEM_SOLUTION - + PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF - + PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF - + PERM.PERM_REPLY_PROBLEM_SOLUTION - + PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF - + PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF - + PERM.PERM_VIEW_DISCUSSION - + PERM.PERM_CREATE_DISCUSSION - + PERM.PERM_EDIT_DISCUSSION_SELF - + PERM.PERM_REPLY_DISCUSSION - + PERM.PERM_EDIT_DISCUSSION_REPLY_SELF + | PERM.PERM_VIEW_PROBLEM + | PERM.PERM_EDIT_PROBLEM_SELF + | PERM.PERM_SUBMIT_PROBLEM + | PERM.PERM_READ_PROBLEM_DATA_SELF + | PERM.PERM_VIEW_PROBLEM_SOLUTION + | PERM.PERM_CREATE_PROBLEM_SOLUTION + | PERM.PERM_VOTE_PROBLEM_SOLUTION + | PERM.PERM_EDIT_PROBLEM_SOLUTION_SELF + | PERM.PERM_DELETE_PROBLEM_SOLUTION_SELF + | PERM.PERM_REPLY_PROBLEM_SOLUTION + | PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF + | PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF + | PERM.PERM_VIEW_DISCUSSION + | PERM.PERM_CREATE_DISCUSSION + | PERM.PERM_EDIT_DISCUSSION_SELF + | PERM.PERM_REPLY_DISCUSSION + | PERM.PERM_EDIT_DISCUSSION_REPLY_SELF // PERM_EDIT_DISCUSSION_REPLY_SELF_DISCUSSION - + PERM.PERM_DELETE_DISCUSSION_REPLY_SELF - + PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION - + PERM.PERM_VIEW_CONTEST - + PERM.PERM_VIEW_CONTEST_SCOREBOARD - + PERM.PERM_ATTEND_CONTEST - + PERM.PERM_EDIT_CONTEST_SELF - + PERM.PERM_VIEW_HOMEWORK - + PERM.PERM_VIEW_HOMEWORK_SCOREBOARD - + PERM.PERM_ATTEND_HOMEWORK - + PERM.PERM_EDIT_HOMEWORK_SELF - + PERM.PERM_VIEW_TRAINING - + PERM.PERM_CREATE_TRAINING - + PERM.PERM_EDIT_TRAINING_SELF - + PERM.PERM_VIEW_RANKING - + PERM.PERM_SUBMIT_PROBLEM - + PERM.PERM_CREATE_PROBLEM_SOLUTION - + PERM.PERM_VOTE_PROBLEM_SOLUTION - + PERM.PERM_REPLY_PROBLEM_SOLUTION - + PERM.PERM_CREATE_DISCUSSION - + PERM.PERM_REPLY_DISCUSSION - + PERM.PERM_ATTEND_CONTEST - + PERM.PERM_CREATE_TRAINING - + PERM.PERM_ATTEND_HOMEWORK - + PERM.PERM_VIEW_RANKING; + | PERM.PERM_DELETE_DISCUSSION_REPLY_SELF + | PERM.PERM_DELETE_DISCUSSION_REPLY_SELF_DISCUSSION + | PERM.PERM_VIEW_CONTEST + | PERM.PERM_VIEW_CONTEST_SCOREBOARD + | PERM.PERM_ATTEND_CONTEST + | PERM.PERM_EDIT_CONTEST_SELF + | PERM.PERM_VIEW_HOMEWORK + | PERM.PERM_VIEW_HOMEWORK_SCOREBOARD + | PERM.PERM_ATTEND_HOMEWORK + | PERM.PERM_EDIT_HOMEWORK_SELF + | PERM.PERM_VIEW_TRAINING + | PERM.PERM_CREATE_TRAINING + | PERM.PERM_EDIT_TRAINING_SELF + | PERM.PERM_VIEW_RANKING + | PERM.PERM_SUBMIT_PROBLEM + | PERM.PERM_CREATE_PROBLEM_SOLUTION + | PERM.PERM_VOTE_PROBLEM_SOLUTION + | PERM.PERM_REPLY_PROBLEM_SOLUTION + | PERM.PERM_CREATE_DISCUSSION + | PERM.PERM_REPLY_DISCUSSION + | PERM.PERM_ATTEND_CONTEST + | PERM.PERM_CREATE_TRAINING + | PERM.PERM_ATTEND_HOMEWORK + | PERM.PERM_VIEW_RANKING; PERM.PERM_ADMIN = PERM.PERM_ALL; diff --git a/hydro/model/domain.ts b/hydro/model/domain.ts index 9b5c0bb98..927c492d5 100644 --- a/hydro/model/domain.ts +++ b/hydro/model/domain.ts @@ -8,7 +8,7 @@ const collUser = db.collection('domain.user'); export async function add(domainId: string, owner: number, name: string, bulletin: string) { await coll.insertOne({ - _id: domainId, owner, name, bulletin, roles: BUILTIN_ROLES, + _id: domainId, owner, name, bulletin, roles: {}, }); return domainId; } @@ -50,14 +50,14 @@ export function setUserRole(domainId: string, uid: number, role: string) { return collUser.updateOne({ uid, domainId }, { role }, { upsert: true }); } -export async function getRoles(arg: string | any) { - let ddoc; - if (typeof arg === 'string') ddoc = get(arg); +export async function getRoles(arg: string | DomainDoc) { + let ddoc: DomainDoc; + if (typeof arg === 'string') ddoc = await get(arg); else ddoc = arg; const roles = []; const r = []; for (const role in ddoc.roles) { - roles.push({ _id: role, perm: ddoc.roles[role] }); + roles.push({ _id: role, perm: BigInt(ddoc.roles[role]) }); r.push(role); } for (const role in BUILTIN_ROLES) { @@ -68,18 +68,18 @@ export async function getRoles(arg: string | any) { return roles; } -export async function setRoles(domainId: string, roles: any) { +export async function setRoles(domainId: string, roles: Dictionary) { const current = await get(domainId); for (const role in roles) { - current.roles[role] = roles[role]; + current.roles[role] = roles[role].toString(); } - return await coll.updateOne({ _id: domainId }, { $set: { roles: current } }); + return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } }); } -export async function addRole(domainId: string, name: string, permission: string) { +export async function addRole(domainId: string, name: string, permission: bigint) { const current = await get(domainId); - current.roles[name] = permission; - return await coll.updateOne({ _id: domainId }, { $set: { roles: current } }); + current.roles[name] = permission.toString(); + return await coll.updateOne({ _id: domainId }, { $set: { roles: current.roles } }); } export async function deleteRoles(domainId: string, roles: string[]) { @@ -98,7 +98,9 @@ export async function getDomainUser(domainId: string, udoc: Udoc) { if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin'; dudoc.role = dudoc.role || 'default'; const ddoc = await get(domainId); - dudoc.perm = ddoc.roles[dudoc.role] || BUILTIN_ROLES[dudoc.role]; + dudoc.perm = ddoc.roles[dudoc.role] + ? BigInt(ddoc.roles[dudoc.role]) + : BUILTIN_ROLES[dudoc.role]; return dudoc; } diff --git a/hydro/model/user.ts b/hydro/model/user.ts index b2b7d13dc..b95853cde 100644 --- a/hydro/model/user.ts +++ b/hydro/model/user.ts @@ -6,19 +6,21 @@ import { BUILTIN_USERS, PRIV } from './builtin'; import { UserNotFoundError, UserAlreadyExistError, LoginError } from '../error'; import pwhash from '../lib/hash.hydro'; import * as db from '../service/db'; -import { Udoc, Udict } from '../interface'; +import { User as _User, Udoc, Udict } from '../interface'; const coll = db.collection('user'); -export function setPassword(uid: number, password: string) { +export async function setPassword(uid: number, password: string): Promise { const salt = String.random(); - return coll.findOneAndUpdate( + const res = await coll.findOneAndUpdate( { _id: uid }, { $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } }, + { returnOriginal: false }, ); + return res.value; } -export class User implements Udoc { +export class User implements _User { udoc: () => any; dudoc: () => any; @@ -41,7 +43,7 @@ export class User implements Udoc { loginat: Date; - perm: string; + perm: bigint; role: string; @@ -49,6 +51,8 @@ export class User implements Udoc { loginip: () => string; + [key: string]: any; + constructor(udoc, dudoc) { this.udoc = () => udoc; this.dudoc = () => dudoc; @@ -81,8 +85,8 @@ export class User implements Udoc { } } - hasPerm(p: string) { - return this.perm.includes(p); + hasPerm(p: bigint) { + return (this.perm & p) === p; } hasPriv(p: number) { @@ -100,7 +104,7 @@ export class User implements Udoc { } } -export async function getById(domainId: string, _id: number): Promise { +export async function getById(domainId: string, _id: number): Promise { const udoc = _id === 0 || _id === 1 ? BUILTIN_USERS[_id] : await coll.findOne({ _id }); @@ -117,24 +121,19 @@ export async function getList(domainId: string, uids: number[]): Promise return r; } -export async function getByUname( - domainId: string, uname: string, ignoreMissing = false, -): Promise { +export async function getByUname(domainId: string, uname: string): Promise { const unameLower = uname.trim().toLowerCase(); const udoc = (unameLower === 'guest') ? BUILTIN_USERS[0] : unameLower === 'hydro' ? BUILTIN_USERS[1] : await coll.findOne({ unameLower }); - if (!udoc) { - if (ignoreMissing) return null; - throw new UserNotFoundError(uname); - } + if (!udoc) return null; const dudoc = await domain.getDomainUser(domainId, udoc); return new User(udoc, dudoc); } -export async function getByEmail(domainId: string, mail: string): Promise { +export async function getByEmail(domainId: string, mail: string): Promise { const mailLower = mail.trim().toLowerCase(); const udoc = (mailLower === 'guest@hydro.local') ? BUILTIN_USERS[0] diff --git a/hydro/service/server.ts b/hydro/service/server.ts index 84f43e2bb..58dfd2cf7 100644 --- a/hydro/service/server.ts +++ b/hydro/service/server.ts @@ -12,12 +12,14 @@ import Body from 'koa-body'; import Router from 'koa-router'; import cache from 'koa-static-cache'; import sockjs from 'sockjs'; +import serialize from 'serialize-javascript'; import { lrucache } from '../utils'; -import { Udoc, DomainDoc } from '../interface'; +import { User, DomainDoc } from '../interface'; import { UserNotFoundError, BlacklistedError, PermissionError, UserFacingError, ValidationError, PrivilegeError, - CsrfTokenError, InvalidOperationError, MethodNotAllowedError, NotFoundError, + CsrfTokenError, InvalidOperationError, MethodNotAllowedError, + NotFoundError, } from '../error'; import { render } from '../lib/template'; import * as misc from '../lib/misc'; @@ -222,7 +224,7 @@ export class Handler { csrfToken: string; - user: Udoc; + user: User; domain: DomainDoc; @@ -266,7 +268,11 @@ export class Handler { async renderHTML(name: string, context: any): Promise { if (enableLog) console.time(name); - const UserContext = { ...this.user, gravatar: misc.gravatar(this.user.gravatar || '', 128) }; + const UserContext = { + ...this.user, + gravatar: misc.gravatar(this.user.gravatar || '', 128), + perm: this.user.perm.toString(), + }; const res = await render(name, { handler: this, UserContext, @@ -291,7 +297,7 @@ export class Handler { return `${this.translate(str)} - Hydro`; } - checkPerm(...args) { + checkPerm(...args: Array) { for (const i in args) { if (args[i] instanceof Array) { let p = false; @@ -302,13 +308,14 @@ export class Handler { } } if (!p) throw new PermissionError([args[i]]); + // @ts-ignore } else if (!this.user.hasPerm(args[i])) { throw new PermissionError([[args[i]]]); } } } - checkPriv(...args) { + checkPriv(...args: Array) { for (const i in args) { if (args[i] instanceof Array) { let p = false; @@ -319,6 +326,7 @@ export class Handler { } } if (!p) throw new PrivilegeError([args[i]]); + // @ts-ignore } else if (!this.user.hasPriv(args[i])) { throw new PrivilegeError([[args[i]]]); } @@ -412,14 +420,16 @@ export class Handler { } async renderBody() { - if (!this.response.redirect && !this.request.json) { - if (this.response.body || this.response.template) { - if (this.request.query.noTemplate || this.request.json) return; - const templateName = this.request.query.template || this.response.template; - if (templateName) { - this.response.body = this.response.body || {}; - await this.render(templateName, this.response.body); - } + if (this.response.type) return; + if (this.request.json || this.response.redirect || !this.response.template) { + this.response.body = serialize(this.response.body); + this.response.type = 'application/json'; + } else if (this.response.body || this.response.template) { + if (this.request.query.noTemplate || this.request.json) return; + const templateName = this.request.query.template || this.response.template; + if (templateName) { + this.response.body = this.response.body || {}; + await this.render(templateName, this.response.body); } } } @@ -535,7 +545,12 @@ async function handle(ctx, HandlerClass, checker) { if (h.cleanup) await h.cleanup(args); if (h.finish) await h.finish(args); } catch (e) { - await h.onerror(e); + try { + await h.onerror(e); + } catch (err) { + h.response.code = 500; + h.response.body = `${err.message}\n${err.stack}`; + } } } @@ -549,12 +564,12 @@ const Checker = (permPrivChecker) => { checker = item; } if (typeof item[0] === 'number') { priv = item; - } else if (typeof item[0] === 'string') { + } else if (typeof item[0] === 'bigint') { perm = item; } } else if (typeof item === 'number') { priv = item; - } else if (typeof item === 'string') { + } else if (typeof item === 'bigint') { perm = item; } } @@ -620,7 +635,7 @@ export class ConnectionHandler { return `${this.translate(str)} - Hydro`; } - checkPerm(...args) { + checkPerm(...args: Array) { for (const i in args) { if (args[i] instanceof Array) { let p = false; @@ -637,7 +652,7 @@ export class ConnectionHandler { } } - checkPriv(...args) { + checkPriv(...args: Array) { for (const i in args) { if (args[i] instanceof Array) { let p = false; @@ -703,7 +718,7 @@ export class ConnectionHandler { export function Connection( name: string, prefix: string, RouteConnHandler: any, - ...permPrivChecker: Array + ...permPrivChecker: Array ) { const sock = sockjs.createServer({ prefix }); const checker = Checker(permPrivChecker); diff --git a/package.json b/package.json index 843369af0..db0e62984 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "nunjucks": "^3.2.1", "prismjs": "^1.20.0", "saslprep": "^1.0.3", + "serialize-javascript": "^4.0.0", "sockjs": "^0.3.20", "superagent": "^5.2.2", "superagent-proxy": "^2.0.0", @@ -88,4 +89,4 @@ ] }, "snyk": true -} \ No newline at end of file +} diff --git a/templates/domain_permission.html b/templates/domain_permission.html index 7816da8a6..8c1563041 100644 --- a/templates/domain_permission.html +++ b/templates/domain_permission.html @@ -36,7 +36,7 @@ {% for role in roles %} {% endfor %} diff --git a/templates/partials/path.html b/templates/partials/path.html index 66cadbc05..b64378ceb 100644 --- a/templates/partials/path.html +++ b/templates/partials/path.html @@ -8,7 +8,7 @@ / {% if loop.index < path.length %}{{ c[0] if c[3] else _(c[0]) }}{% endif %} {%- endfor %} -

{{ path[path.length - 1][0] if path[path.length - 1][2] else _(path[path.length - 1][0]) }}

+

{{ path[path.length - 1][0] if path[path.length - 1][3] else _(path[path.length - 1][0]) }}

{% include "partials/hamburger.html" %} diff --git a/yarn.lock b/yarn.lock index ad59d66fc..a85e5dd22 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5642,6 +5642,13 @@ serialize-javascript@^3.1.0: dependencies: randombytes "^2.1.0" +serialize-javascript@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-4.0.0.tgz#b525e1238489a5ecfc42afacc3fe99e666f4b1aa" + integrity sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw== + dependencies: + randombytes "^2.1.0" + set-immediate-shim@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61"