From a5494b1345e2cc316c3055140ed1b976e2ad7aa4 Mon Sep 17 00:00:00 2001 From: panda Date: Thu, 6 Oct 2022 21:22:24 +0800 Subject: [PATCH] core&ui: add manage user priv (#433) --- packages/hydrooj/package.json | 2 +- packages/hydrooj/src/handler/manage.ts | 46 ++++++++++- packages/hydrooj/src/model/setting.ts | 2 +- packages/hydrooj/src/model/user.ts | 11 ++- packages/hydrooj/src/service/bus.ts | 2 +- packages/hydrooj/src/service/decorators.ts | 5 +- packages/ui-default/locales/zh.yaml | 4 + packages/ui-default/package.json | 2 +- .../ui-default/pages/manage_user_priv.page.js | 61 +++++++++++++++ .../pages/manage_user_priv.page.styl | 18 +++++ .../ui-default/templates/manage_base.html | 1 + .../templates/manage_user_priv.html | 76 +++++++++++++++++++ .../templates/partials/manage_user_priv.html | 55 ++++++++++++++ 13 files changed, 274 insertions(+), 11 deletions(-) create mode 100644 packages/ui-default/pages/manage_user_priv.page.js create mode 100644 packages/ui-default/pages/manage_user_priv.page.styl create mode 100644 packages/ui-default/templates/manage_user_priv.html create mode 100644 packages/ui-default/templates/partials/manage_user_priv.html diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index 85a44f093..69865ff7f 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -1,6 +1,6 @@ { "name": "hydrooj", - "version": "4.0.6", + "version": "4.0.7", "bin": "bin/hydrooj.js", "main": "src/plugin-api", "module": "src/plugin-api", diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index 2273fe570..e5ce1bd0b 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -3,7 +3,9 @@ import { inspect } from 'util'; import * as yaml from 'js-yaml'; import Schema from 'schemastery'; import * as check from '../check'; -import { BadRequestError, ValidationError } from '../error'; +import { + BadRequestError, ForbiddenError, UserNotFoundError, ValidationError, +} from '../error'; import { isEmail, isPassword, isUname } from '../lib/validator'; import { Logger } from '../logger'; import { PRIV, STATUS } from '../model/builtin'; @@ -261,6 +263,47 @@ class SystemUserImportHandler extends SystemHandler { } } +const Priv = Object.fromEntries(Object.entries(PRIV).filter( + (i) => (!i[0].endsWith('SELF') && !['PRIV_DEFAULT', 'PRIV_NEVER', 'PRIV_NONE', 'PRIV_ALL'].includes(i[0])), +)); +const allPriv = Math.sum(Object.values(Priv)); + +class SystemUserPrivHandler extends SystemHandler { + @requireSudo + async get({ pjax }) { + const defaultPriv = system.get('default.priv'); + const udocs = await user.getMulti({ _id: { $gte: -1000, $ne: 1 }, priv: { $nin: [0, defaultPriv] } }).limit(1000).sort({ _id: 1 }).toArray(); + const banudocs = await user.getMulti({ _id: { $gte: -1000, $ne: 1 }, priv: 0 }).limit(1000).sort({ _id: 1 }).toArray(); + this.response.body = { + udocs: [...udocs, ...banudocs], + defaultPriv, + Priv, + }; + if (pjax) { + const html = await this.renderHTML('partials/manage_user_priv.html', this.response.body); + this.response.body = { fragments: [{ html }] }; + } else this.response.template = 'manage_user_priv.html'; + } + + @requireSudo + @param('uid', Types.Int, true) + @param('priv', Types.UnsignedInt) + async post(domainId: string, uid: number, priv: number) { + if (typeof uid === 'number') { + const udoc = await user.getById(domainId, uid); + if (!udoc) throw new UserNotFoundError(uid); + if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.'); + await user.setPriv(uid, priv); + } else { + const defaultPriv = system.get('default.priv'); + await user.coll.updateMany({ priv: defaultPriv }, { $set: { priv } }); + await system.set('default.priv', priv); + bus.broadcast('user/delcache', true); + } + this.back(); + } +} + export async function apply(ctx) { ctx.Route('manage', '/manage', SystemMainHandler); ctx.Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler); @@ -268,5 +311,6 @@ export async function apply(ctx) { ctx.Route('manage_setting', '/manage/setting', SystemSettingHandler); ctx.Route('manage_config', '/manage/config', SystemConfigHandler); ctx.Route('manage_user_import', '/manage/userimport', SystemUserImportHandler); + ctx.Route('manage_user_priv', '/manage/userpriv', SystemUserPrivHandler); ctx.Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index c936314fd..115c67490 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -217,7 +217,7 @@ SystemSetting( Setting('setting_limits', 'limit.submission_user', 15, 'number', 'limit.submission_user', 'Max submission count per user per minute'), Setting('setting_limits', 'limit.pretest', 60, 'number', 'limit.pretest', 'Max pretest count per minute'), Setting('setting_basic', 'avatar.gravatar_url', '//cn.gravatar.com/avatar/', 'text', 'avatar.gravatar_url', 'Gravatar URL Prefix'), - Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege', FLAG_PRO), + Setting('setting_basic', 'default.priv', builtin.PRIV.PRIV_DEFAULT, 'number', 'default.priv', 'Default Privilege', FLAG_HIDDEN), Setting('setting_basic', 'discussion.nodes', builtin.DEFAULT_NODES, 'yaml', 'discussion.nodes', 'Discussion Nodes'), Setting('setting_basic', 'problem.categories', builtin.CATEGORIES, 'yaml', 'problem.categories', 'Problem Categories'), Setting('setting_basic', 'pagination.problem', 100, 'number', 'pagination.problem', 'Problems per page'), diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index b65f642a4..64e4a57dc 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -23,25 +23,27 @@ export const collV: Collection = db.collection('vuser'); export const collGroup: Collection = db.collection('user.group'); const cache = new LRU({ max: 10000, ttl: 300 * 1000 }); -export function deleteUserCache(udoc: User | Udoc | string | undefined | null, receiver = false) { - if (!udoc) return; +export function deleteUserCache(udoc: User | Udoc | string | true | undefined | null, receiver = false) { + if (!udoc) return false; if (!receiver) { bus.broadcast( 'user/delcache', JSON.stringify(typeof udoc === 'string' ? udoc : pick(udoc, ['uname', 'mail', '_id'])), ); } + if (udoc === true) return cache.clear(); if (typeof udoc === 'string') { // is domainId for (const key of [...cache.keys()].filter((i) => i.endsWith(`/${udoc}`))) cache.delete(key); - return; + return true; } const id = [`id/${udoc._id.toString()}`, `name/${udoc.uname.toLowerCase()}`, `mail/${udoc.mail.toLowerCase()}`]; for (const key of [...cache.keys()].filter((k) => id.includes(`${k.split('/')[0]}/${k.split('/')[1]}`))) { cache.delete(key); } + return true; } -bus.on('user/delcache', (content) => deleteUserCache(JSON.parse(content), true)); +bus.on('user/delcache', (content) => deleteUserCache(typeof content === 'string' ? JSON.parse(content) : content, true)); export class User { _id: number; @@ -156,6 +158,7 @@ function handleMailLower(mail: string) { } class UserModel { + static coll = coll; static User = User; static defaultUser: Udoc = { _id: 0, diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index 8dcf12141..9194c7be4 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -50,7 +50,7 @@ export interface EventMap extends LifecycleEvents, HandlerEvents { 'user/message': (uid: number, mdoc: MessageDoc) => void 'user/get': (udoc: User) => void - 'user/delcache': (content: string) => void + 'user/delcache': (content: string | true) => void 'domain/create': (ddoc: DomainDoc) => VoidReturn 'domain/before-get': (query: FilterQuery) => VoidReturn diff --git a/packages/hydrooj/src/service/decorators.ts b/packages/hydrooj/src/service/decorators.ts index a0f9cee50..dd4674c49 100644 --- a/packages/hydrooj/src/service/decorators.ts +++ b/packages/hydrooj/src/service/decorators.ts @@ -185,7 +185,7 @@ function _descriptor(v: ParamOption) { : this.request.body; const value = src[item.name]; if (!item.isOptional || value) { - if (!value) throw new ValidationError(item.name); + if (value === undefined || value === null || value === '') throw new ValidationError(item.name); if (item.validate && !item.validate(value)) throw new ValidationError(item.name); if (item.convert) c.push(item.convert(value)); else c.push(value); @@ -216,7 +216,8 @@ export function requireSudo(target: any, funcName: string, obj: any) { const originalMethod = obj.value; obj.value = function sudo(this: Handler, ...args: any[]) { if (this.session.sudo && Date.now() - this.session.sudo < Time.hour) { - this.request.headers.referer = this.session.sudoArgs.referer; + if (this.session.sudoArgs?.referer) this.request.headers.referer = this.session.sudoArgs.referer; + this.session.sudoArgs = null; return originalMethod.call(this, ...args); } this.session.sudoArgs = { diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 25925d647..ce3d068de 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -475,6 +475,7 @@ manage_join_applications: 加域申请 manage_module: 管理模块 manage_script: 脚本管理 manage_setting: 系统设置 +manage_user_priv: 用户权限 manage: 管理 Manage: 管理 Manager: 管理 @@ -594,6 +595,7 @@ Pretest Data: 自测数据 Pretest Your Code: 使用自定义数据测试 Pretest: 自测 Pretest?: 自测? +Privilege: 权限 Privacy: 隐私 Problem {1} not found.: 题目 {1} 不存在。 Problem Data: 题目数据 @@ -727,6 +729,7 @@ Send Verification Email: 发送验证邮件 Send: 发送 Seperated with ',': 用 ',' 分隔 Service Status: 服务状态 +Set Privilege: 设置权限 Set Role: 设置角色 Set Roles for Selected User: 设置所选用户的角色 Set Roles for selected users: 为所选用户设置角色 @@ -881,6 +884,7 @@ User-defined role: 用户定义的角色 User: 用户 Username / UID: 用户名或 UID Username: 用户名 +User Privilege: 用户权限 Users with those roles will be removed from the domain.: 拥有这些角色的用户将从此域中移除。 Users: 用户 Using your {0} universal account: 使用您的 {0} 通用账户 diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 9601c43d5..c15fb8200 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.41.4", + "version": "4.41.5", "author": "undefined ", "license": "AGPL-3.0", "main": "index.ts", diff --git a/packages/ui-default/pages/manage_user_priv.page.js b/packages/ui-default/pages/manage_user_priv.page.js new file mode 100644 index 000000000..aa5f132fd --- /dev/null +++ b/packages/ui-default/pages/manage_user_priv.page.js @@ -0,0 +1,61 @@ +import $ from 'jquery'; +import { ActionDialog } from 'vj/components/dialog'; +import Notification from 'vj/components/notification'; +import selectUser from 'vj/components/selectUser'; +import { NamedPage } from 'vj/misc/Page'; +import i18n from 'vj/utils/i18n'; +import pjax from 'vj/utils/pjax'; +import request from 'vj/utils/request'; + +const page = new NamedPage('manage_user_priv', () => { + const setPrivDialog = new ActionDialog({ + $body: $('.dialog__body--set-priv > div'), + }); + setPrivDialog.clear = function (priv) { + this.$dom.find('input.priv[type=checkbox]:hidden').each((i, e) => { + e.checked = priv & e.value; + }); + return this; + }; + + async function changeUserPriv(uid, priv) { + try { + const res = await request.post('', uid !== 'default' ? { + uid, + priv, + } : { priv }); + if (res.url && res.url !== window.location.href) window.location.href = res.url; + else { + Notification.success(i18n('Priv has been updated to {0}.', priv)); + pjax.request({ push: false }); + } + } catch (error) { + Notification.error(error.message); + } + } + + async function handleOpenUserPrivDialog(ev) { + const uid = ev.target ? $(ev.target).data('uid') : ev._id; + const priv = ev.target ? $(ev.target).data('priv') : ev.priv; + await setPrivDialog.clear(priv); + const action = await setPrivDialog.open(); + if (action !== 'ok') return; + let userPriv = 0; + setPrivDialog.$dom.find('input.priv[type=checkbox]').each((i, e) => { + if (e.checked) userPriv |= e.value; + }); + changeUserPriv(uid, userPriv); + } + + async function handleClickSelectUser() { + const target = await selectUser(); + if (!target) return; + const user = await request.get(`/user/${target._id}`); + handleOpenUserPrivDialog(user.udoc); + } + + $('[name="select_user"]').on('click', () => handleClickSelectUser()); + $('[name="set_priv"]').on('click', (ev) => handleOpenUserPrivDialog(ev)); +}); + +export default page; diff --git a/packages/ui-default/pages/manage_user_priv.page.styl b/packages/ui-default/pages/manage_user_priv.page.styl new file mode 100644 index 000000000..0be9139e7 --- /dev/null +++ b/packages/ui-default/pages/manage_user_priv.page.styl @@ -0,0 +1,18 @@ +.page--manage_user_priv + .col--uid + width: 100px + + .col--priv + width: 100px + + .col--actions + width: 100px + + .priv_plus + color: green + + .priv_minus + color: red + + .col--p + width: 30px diff --git a/packages/ui-default/templates/manage_base.html b/packages/ui-default/templates/manage_base.html index a50fa48db..3a662f781 100644 --- a/packages/ui-default/templates/manage_base.html +++ b/packages/ui-default/templates/manage_base.html @@ -15,6 +15,7 @@ {{ sidemenu.render_item(null, 'manage_dashboard') }} {{ sidemenu.render_item(null, 'manage_script') }} {{ sidemenu.render_item(null, 'manage_user_import') }} + {{ sidemenu.render_item(null, 'manage_user_priv') }} {{ sidemenu.render_item(null, 'manage_setting') }} {{ sidemenu.render_item(null, 'manage_config') }} diff --git a/packages/ui-default/templates/manage_user_priv.html b/packages/ui-default/templates/manage_user_priv.html new file mode 100644 index 000000000..062cdadcd --- /dev/null +++ b/packages/ui-default/templates/manage_user_priv.html @@ -0,0 +1,76 @@ +{% extends "manage_base.html" %} +{% block manage_content %} + +
+
+

{{ _('User Privilege') }}

+
+ +
+
+ {{ noscript_note.render() }} +
+ + + + + + + + + + + + + + + + + {% include "partials/manage_user_priv.html" %} +
{{ _('User ID') }}{{ _('Username') }}{{ _('Privilege') }} + {{ _('Value') }}{{ _('Actions') }}
+
+
+{% endblock %} diff --git a/packages/ui-default/templates/partials/manage_user_priv.html b/packages/ui-default/templates/partials/manage_user_priv.html new file mode 100644 index 000000000..9c121ed41 --- /dev/null +++ b/packages/ui-default/templates/partials/manage_user_priv.html @@ -0,0 +1,55 @@ +{% import "components/user.html" as user with context %} + + + + # + + + Default Privilege + + + {%- for name, priv in Priv -%} + {%- if defaultPriv|bitand(priv) -%} + {{ name }}
+ {%- endif -%} + {%- endfor -%} + + + {{ defaultPriv }} + + + {{ _('Edit') }} + + + {%- for udoc in udocs -%} + + + {{ udoc._id }} + + + {{ user.render_inline(udoc, badge=false) }} + + + {%- if udoc.priv == 0 -%} + PRIV_NONE + {%- elif udoc.priv == -1 -%} + PRIV_ALL + {%- else -%} + {%- for name, priv in Priv -%} + {%- if priv|bitand(udoc.priv) and not priv|bitand(defaultPriv) -%} +

+{{ name }}

+ {%- elif priv|bitand(defaultPriv) and not priv|bitand(udoc.priv) -%} +

-{{ name }}

+ {%- endif -%} + {%- endfor -%} + {%- endif -%} + + + {{ udoc.priv }} + + + {{ _('Edit') }} + + + {%- endfor -%} + \ No newline at end of file