diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index fe2d5fa18..92e733723 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -12,8 +12,8 @@ const { PERM_VIEW_CONTEST, PERM_EDIT_CONTEST_SELF, }, PRIV: { - PRIV_READ_RECORD_CODE - } + PRIV_READ_RECORD_CODE, + }, } = require('../model/builtin'); const contest = require('../model/contest'); const document = require('../model/document'); diff --git a/hydro/handler/domain.js b/hydro/handler/domain.js index 82d142d59..88096445b 100644 --- a/hydro/handler/domain.js +++ b/hydro/handler/domain.js @@ -2,9 +2,9 @@ const user = require('../model/user'); const domain = require('../model/domain'); const system = require('../model/system'); const { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } = require('../model/setting'); +const { PERM: { PERM_EDIT_DOMAIN }, PERMS_BY_FAMILY } = require('../model/builtin'); const paginate = require('../lib/paginate'); const { Route, Handler } = require('../service/server'); -const { PERM_MANAGE } = require('../model/builtin').PERM; const { RoleAlreadyExistError, ValidationError, PermissionError } = require('../error'); class DomainRankHandler extends Handler { @@ -55,7 +55,7 @@ class DomainCreateHandler extends Handler { class ManageHandler extends Handler { async prepare({ domainId }) { - this.checkPerm(PERM_MANAGE); + this.checkPerm(PERM_EDIT_DOMAIN); this.domain = await domain.get(domainId); } } @@ -95,9 +95,8 @@ class DomainDashboardHandler extends ManageHandler { class DomainUserHandler extends ManageHandler { async get({ domainId }) { - const uids = []; const rudocs = {}; - const [udocs, roles] = await Promise.all([ + const [dudocs, roles] = await Promise.all([ user.getMultiInDomain(domainId, { $and: [ { role: { $ne: 'default' } }, @@ -106,17 +105,14 @@ class DomainUserHandler extends ManageHandler { }).toArray(), user.getRoles(domainId), ]); + const uids = dudocs.map((dudoc) => dudoc.uid); + const udict = await user.getList(domainId, uids); for (const role of roles) rudocs[role._id] = []; - for (const udoc of udocs) { - uids.push(udoc.uid); - const ud = user.getById(domainId, udoc.uid); + for (const dudoc of dudocs) { + const ud = udict[dudoc.uid]; rudocs[ud.role].push(ud); } - const tasks = []; - for (const key in rudocs) tasks.push(Promise.all(rudocs[key])); - await Promise.all(tasks); const rolesSelect = roles.map((role) => [role._id, role._id]); - const udict = await user.getList(domainId, uids); const path = [ ['Hydro', 'homepage'], ['domain', null], @@ -143,7 +139,9 @@ class DomainPermissionHandler extends ManageHandler { ['domain_permission', null], ]; this.response.template = 'domain_permission.html'; - this.response.body = { roles, domain: this.domain, path }; + this.response.body = { + roles, PERMS_BY_FAMILY, domain: this.domain, path, + }; } async post({ domainId }) { diff --git a/hydro/handler/misc.js b/hydro/handler/misc.js index 593fce709..ac11584a5 100644 --- a/hydro/handler/misc.js +++ b/hydro/handler/misc.js @@ -49,8 +49,9 @@ class StatusUpdateHandler extends Handler { class SwitchLanguageHandler extends Handler { async get({ lang }) { - if (this.user.hasPriv(PRIV_USER_PROFILE)) await user.setById(this.user._id, { viewLang: lang }); - else this.session.viewLang = lang; + if (this.user.hasPriv(PRIV_USER_PROFILE)) { + await user.setById(this.user._id, { viewLang: lang }); + } else this.session.viewLang = lang; this.back(); } } diff --git a/hydro/handler/system.js b/hydro/handler/system.js index 52cf5e7d4..26bdd5f96 100644 --- a/hydro/handler/system.js +++ b/hydro/handler/system.js @@ -2,13 +2,13 @@ const domain = require('../model/domain'); const system = require('../model/system'); const setting = require('../model/setting'); const { Route, Handler } = require('../service/server'); -const { PERM_MANAGE } = require('../model/builtin').PERM; +const { PRIV_EDIT_SYSTEM } = require('../model/builtin').PRIV; const hpm = require('../lib/hpm'); const loader = require('../loader'); class SystemHandler extends Handler { async prepare({ domainId }) { - this.checkPerm(PERM_MANAGE); + this.checkPriv(PRIV_EDIT_SYSTEM); this.domain = await domain.get(domainId); } } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 24971b5a2..28779dd9f 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -19,7 +19,7 @@ const { PERM_VIEW_PROBLEM_HIDDEN, }, PRIV: { - PRIV_USER_PROFILE, PRIV_REGISTER_USER, + PRIV_USER_PROFILE, PRIV_REGISTER_USER, PRIV_NONE, }, } = require('../model/builtin'); @@ -41,7 +41,7 @@ class UserLoginHandler extends Handler { if (!udoc) throw new LoginError(uname); if (udoc) udoc.checkPassword(password); await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip }); - if (udoc.ban) throw new BlacklistedError(uname); + if (udoc.priv === PRIV_NONE) throw new BlacklistedError(uname); this.session.uid = udoc._id; this.session.rememberme = rememberme; this.response.redirect = this.request.referer.endsWith('/login') ? '/' : this.request.referer; diff --git a/hydro/lib/nav.js b/hydro/lib/nav.js index e109130d4..339f24512 100644 --- a/hydro/lib/nav.js +++ b/hydro/lib/nav.js @@ -1,14 +1,19 @@ const { isNull } = require('lodash'); - -const permission = require('../model/builtin').PERM; +const { PRIV, PERM } = require('../model/builtin'); global.Hydro.ui.nav = []; const trueChecker = () => true; +const Checker = (perm, priv, checker = trueChecker) => (handler) => ( + checker(handler) + && (perm ? handler.user.hasPerm(perm) : true) + && (priv ? handler.user.hasPriv(priv) : true) +); + const Item = (name, args, prefix, ...permPrivChecker) => { let _priv; let _perm; - let checker; + let checker = trueChecker; for (const item of permPrivChecker) { if (typeof item === 'object' && !isNull(item)) { if (typeof item.call !== 'undefined') { @@ -24,27 +29,21 @@ const Item = (name, args, prefix, ...permPrivChecker) => { _perm = item; } } - if ((_perm || _priv) && checker) { - checker = ((_chk) => (handler) => _chk(handler) - && (_perm ? handler.user.hasPerm(_perm) : true) - && (_priv ? handler.user.hasPriv(_priv) : true))(checker); - } else if (_perm) { - checker = (handler) => handler.user.hasPerm(_perm); - } else if (!checker) checker = trueChecker; + checker = Checker(_perm, _priv, checker); global.Hydro.ui.nav.push({ name, args: args || {}, prefix, checker, }); }; Item('homepage', null, 'homepage'); -Item('problem_main', null, 'problem', permission.PERM_VIEW_PROBLEM); -Item('training_main', null, 'training', permission.PERM_VIEW_TRAINING); -Item('homework_main', null, 'homework', permission.PERM_VIEW_HOMEWORK); -Item('discussion_main', null, 'discussion', permission.PERM_VIEW_DISCUSSION); -Item('contest_main', null, 'contest', permission.PERM_VIEW_CONTEST); +Item('problem_main', null, 'problem', PERM.PERM_VIEW_PROBLEM); +Item('training_main', null, 'training', PERM.PERM_VIEW_TRAINING); +Item('homework_main', null, 'homework', PERM.PERM_VIEW_HOMEWORK); +Item('discussion_main', null, 'discussion', PERM.PERM_VIEW_DISCUSSION); +Item('contest_main', null, 'contest', PERM.PERM_VIEW_CONTEST); Item('record_main', null, 'record'); Item('ranking', null, 'ranking'); -Item('domain_dashboard', null, 'domain', permission.PERM_MANAGE); -Item('manage_dashboard', null, 'manage', null, (handler) => handler.user.priv === 1); +Item('domain_dashboard', null, 'domain', PERM.PERM_EDIT_DOMAIN); +Item('manage_dashboard', null, 'manage', PRIV.PRIV_SET_PRIV); global.Hydro.lib.nav = module.exports = Item; diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js index 1bf700455..8a4c2dca0 100644 --- a/hydro/model/builtin.js +++ b/hydro/model/builtin.js @@ -73,6 +73,9 @@ const PERM = { PERM_CREATE_TRAINING: 't', PERM_EDIT_TRAINING: 'u', PERM_EDIT_TRAINING_SELF: 'v', + + // Ranking + PERM_VIEW_RANKING: 'w', }; const Permission = (family, key, desc) => ({ family, key, desc }); @@ -206,7 +209,7 @@ PERM.PERM_ADMIN = PERM.PERM_ALL; const PRIV = { PRIV_NONE: 0, - PRIV_SET_PRIV: 1 << 0, + PRIV_EDIT_SYSTEM: 1 << 0, // renamed from PRIV_SET_PRIV PRIV_SET_PERM: 1 << 1, PRIV_USER_PROFILE: 1 << 2, PRIV_REGISTER_USER: 1 << 3, diff --git a/hydro/model/domain.js b/hydro/model/domain.js index f1fef8305..45702737a 100644 --- a/hydro/model/domain.js +++ b/hydro/model/domain.js @@ -15,12 +15,23 @@ const coll = db.collection('domain'); * @param {string} domainId * @param {number} owner */ -function add(domainId, owner, name) { - const tasks = [ - coll.insertOne({ - _id: domainId, owner, name, bulletin: '', - }), - ]; +function add(domainId, owner, name, isEnsure = false) { + const tasks = []; + if (isEnsure) { + tasks.push( + coll.updateOne( + { _id: domainId }, + { $set: { owner }, $setOnInsert: { name, bulletin: '' } }, + { upsert: true }, + ), + ); + } else { + tasks.push( + coll.insertOne({ + _id: domainId, owner, name, bulletin: '', + }), + ); + } for (const id in builtin.BUILTIN_ROLES) { tasks.push( document.add( diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 30bd5417a..6a5e2191f 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -1,10 +1,10 @@ +const { ObjectId } = require('bson'); const { STATUS_ACCEPTED } = require('./builtin').STATUS; const file = require('./file'); const document = require('./document'); const domain = require('./domain'); const { ProblemNotFoundError } = require('../error'); const readConfig = require('../lib/readConfig'); -const { ObjectId } = require('bson'); /** * @typedef {import('../interface').Pdoc} Pdoc diff --git a/hydro/model/setting.js b/hydro/model/setting.js index 5cafd0a63..63d71eca0 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.js @@ -12,18 +12,20 @@ const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]); const PREFERENCE_SETTINGS = []; const ACCOUNT_SETTINGS = []; const DOMAIN_SETTINGS = []; +const DOMAIN_USER_SETTINGS = []; const SYSTEM_SETTINGS = []; const SETTINGS = []; const SETTINGS_BY_KEY = {}; +const DOMAIN_USER_SETTINGS_BY_KEY = {}; const DOMAIN_SETTINGS_BY_KEY = {}; const SYSTEM_SETTINGS_BY_KEY = {}; const Setting = ( family, key, range = null, value = null, ui = 'text', name = '', - desc = '', + desc = '', hidden = false, ) => ({ - family, key, range, value, ui, name, desc, + family, key, range, value, ui, name, desc, hidden, }); const PreferenceSetting = (...settings) => { @@ -40,6 +42,12 @@ const AccountSetting = (...settings) => { SETTINGS_BY_KEY[setting.key] = setting; } }; +const DomainUserSetting = (...settings) => { + for (const setting of settings) { + DOMAIN_USER_SETTINGS.push(setting); + DOMAIN_USER_SETTINGS_BY_KEY[setting.key] = setting; + } +}; const DomainSetting = (...settings) => { for (const setting of settings) { DOMAIN_SETTINGS.push(setting); @@ -119,6 +127,7 @@ global.Hydro.model.setting = module.exports = { PreferenceSetting, AccountSetting, DomainSetting, + DomainUserSetting, SystemSetting, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, @@ -128,4 +137,6 @@ global.Hydro.model.setting = module.exports = { SYSTEM_SETTINGS_BY_KEY, DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY, + DOMAIN_USER_SETTINGS, + DOMAIN_USER_SETTINGS_BY_KEY, }; diff --git a/hydro/model/user.js b/hydro/model/user.js index f19bd1fdc..d80c5fdaa 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -1,4 +1,4 @@ -const builtin = require('./builtin'); +const { BUILTIN_ROLES, BUILTIN_USERS, PRIV } = require('./builtin'); const document = require('./document'); const system = require('./system'); const token = require('./token'); @@ -6,7 +6,6 @@ const setting = require('./setting'); const { UserNotFoundError, UserAlreadyExistError, LoginError } = require('../error'); const pwhash = require('../lib/hash.hydro'); const db = require('../service/db'); -const { PRIV } = require('./builtin'); const coll = db.collection('user'); @@ -63,13 +62,13 @@ async function getInDomain(domainId, udoc) { // eslint-disable-next-line no-bitwise if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin'; const p = await document.get(domainId, document.TYPE_DOMAIN_USER, dudoc.role || 'default'); - dudoc.perm = p ? p.content : builtin.BUILTIN_ROLES[dudoc.role || 'default'].perm; + dudoc.perm = p ? p.content : BUILTIN_ROLES[dudoc.role || 'default'].perm; return dudoc; } async function getById(domainId, _id, throwError = false) { const udoc = _id === 0 || _id === 1 - ? builtin.BUILTIN_USERS[_id] + ? BUILTIN_USERS[_id] : await coll.findOne({ _id }); if (!udoc) { if (throwError) throw new UserNotFoundError(_id); @@ -90,9 +89,9 @@ async function getList(domainId, uids) { async function getByUname(domainId, uname, ignoreMissing = false) { const unameLower = uname.trim().toLowerCase(); const udoc = (unameLower === 'guest') - ? builtin.BUILTIN_USERS[0] + ? BUILTIN_USERS[0] : unameLower === 'hydro' - ? builtin.BUILTIN_USERS[1] + ? BUILTIN_USERS[1] : await coll.findOne({ unameLower }); if (!udoc) { if (ignoreMissing) return null; @@ -105,9 +104,9 @@ async function getByUname(domainId, uname, ignoreMissing = false) { async function getByEmail(domainId, mail, ignoreMissing = false) { const mailLower = mail.trim().toLowerCase(); const udoc = (mailLower === 'guest@hydro.local') - ? builtin.BUILTIN_USERS[0] + ? BUILTIN_USERS[0] : mailLower === 'hydro@hydro.local' - ? builtin.BUILTIN_USERS[1] + ? BUILTIN_USERS[1] : await coll.findOne({ mailLower }); if (!udoc) { if (ignoreMissing) return null; @@ -259,15 +258,11 @@ function deleteRoles(domainId, roles) { function ban(uid) { return Promise.all([ - coll.updateOne({ _id: uid }, { $set: { ban: true } }), + setPriv(uid, PRIV.PRIV_NONE), token.delByUid(uid), ]); } -function setSuperAdmin(uid) { - return setById(uid, { priv: 1 }); -} - function ensureIndexes() { return Promise.all([ coll.createIndex('unameLower', { unique: true }), @@ -300,7 +295,6 @@ global.Hydro.model.user = module.exports = { getInDomain, addRole, deleteRoles, - setSuperAdmin, ban, ensureIndexes, }; diff --git a/hydro/script/deleteUser.js b/hydro/script/deleteUser.js index 0007683ad..8e3c8d8bd 100644 --- a/hydro/script/deleteUser.js +++ b/hydro/script/deleteUser.js @@ -4,7 +4,7 @@ const document = require('../model/document'); const setting = require('../model/setting'); const db = require('../service/db'); const paginate = require('../lib/paginate'); -const { STATUS } = require('../model/builtin'); +const { STATUS, PRIV } = require('../model/builtin'); const collDocument = db.collection('document'); const collStatus = db.collection('document.status'); @@ -78,7 +78,7 @@ async function run({ await user.setById(uid, { uname, unameLower: uname.toLowerCase(), - priv: 0, + priv: PRIV.PRIV_NONE, }, $unset); await user.setPassword(uid, String.random(32)); // TODO delete contest, homework, etc diff --git a/hydro/script/install.js b/hydro/script/install.js index 995d59d68..f9e8dfeac 100644 --- a/hydro/script/install.js +++ b/hydro/script/install.js @@ -1,5 +1,6 @@ const { defaults } = require('lodash'); const db = require('../service/db'); +const { PRIV_ALL } = require('../model/builtin').PRIV; const system = require('../model/system'); const domain = require('../model/domain'); const pwhash = require('../lib/hash.hydro'); @@ -43,27 +44,7 @@ async function run({ username, password } = {}) { })); } await Promise.all(tasks); - tasks = [ - domain.add('system', 0, 'Hydro').catch(() => domain.edit('system', { owner: 0, name: 'Hydro' })), - collUser.updateOne({ _id: 0 }, { - $set: defaults({ - _id: 0, - uname: 'Hydro', - unameLower: 'hydro', - mail: 'hydro@hydro', - mailLower: 'hydro@hydro', - }, udoc), - }, { upsert: true }), - collUser.updateOne({ _id: 1 }, { - $set: defaults({ - _id: 1, - mail: 'guest@hydro', - mailLower: 'guest@hydro', - uname: 'Guest', - unameLower: 'guest', - }, udoc), - }, { upsert: true }), - ]; + tasks = [domain.add('system', 0, 'Hydro', true)]; if (username && password) { const salt = String.random(); tasks.push( @@ -77,7 +58,7 @@ async function run({ username, password } = {}) { hash: pwhash(password, salt), salt, gravatar: 'root@hydro', - priv: 1, + priv: PRIV_ALL, }, udoc), }, { upsert: true }), ); diff --git a/hydro/script/setSuperadmin.js b/hydro/script/setSuperadmin.js index 39602e17e..fc591efd1 100644 --- a/hydro/script/setSuperadmin.js +++ b/hydro/script/setSuperadmin.js @@ -1,11 +1,12 @@ const user = require('../model/user'); +const { PRIV_ALL } = require('../model/builtin').PRIV; async function run({ uid, }) { uid = parseInt(uid, 10); if (Number.isNaN(uid)) throw new Error('uid'); - await user.setSuperAdmin(uid); + await user.setPriv(uid, PRIV_ALL); return uid; } diff --git a/templates/domain_permission.html b/templates/domain_permission.html index fbdc6b8c4..7816da8a6 100644 --- a/templates/domain_permission.html +++ b/templates/domain_permission.html @@ -21,7 +21,7 @@ {% endfor %} - {% for family, perms in perm.PERMS_BY_FAMILY %} + {% for family, perms in PERMS_BY_FAMILY %}