From 24a81783cc18b52d42318245af50d6f60a1acfd7 Mon Sep 17 00:00:00 2001 From: Chris Ye Date: Sun, 15 Oct 2023 15:25:50 +0800 Subject: [PATCH] Add system all users --- install/helm-single/readme.md | 2 +- .../templates/backend.deployment.yaml | 2 +- package.json | 1 + packages/hydrooj/locales/en.yaml | 1 + packages/hydrooj/locales/zh.yaml | 15 +- packages/hydrooj/src/handler/domain.ts | 52 +++++- packages/hydrooj/src/lib/ui.ts | 1 + packages/hydrooj/src/model/user.ts | 9 + packages/ui-default/locales/zh.yaml | 15 +- .../ui-default/pages/system_all_users.page.js | 155 ++++++++++++++++++ .../ui-default/templates/domain_user.html | 2 +- .../templates/system_all_users.html | 122 ++++++++++++++ 12 files changed, 359 insertions(+), 18 deletions(-) create mode 100644 packages/ui-default/pages/system_all_users.page.js create mode 100644 packages/ui-default/templates/system_all_users.html diff --git a/install/helm-single/readme.md b/install/helm-single/readme.md index f7c3fd2..a9640bf 100644 --- a/install/helm-single/readme.md +++ b/install/helm-single/readme.md @@ -13,7 +13,7 @@ hydrooj cli user setSuperAdmin 2 Helm Chart示例中尚未完全适配多节点以及HA需求。主要体现在 - Mongo的单节点部署 -- 为了理解和调试便利,后端容器`/Users/zhiye/data/file`和`/root/.hydro`,Mongo容器`/data/db`,评测机容器`/root/.config/hydro`使用了HostPath。 +- 为了理解和调试便利,后端容器`/data/file`和`/root/.hydro`,Mongo容器`/data/db`,评测机容器`/root/.config/hydro`使用了HostPath。 由于Judge需要以特权容器运行(cgroup所需),建议将Backend和Judge调度到不同的节点上。 diff --git a/install/helm-single/templates/backend.deployment.yaml b/install/helm-single/templates/backend.deployment.yaml index 7947e63..5cd88d9 100644 --- a/install/helm-single/templates/backend.deployment.yaml +++ b/install/helm-single/templates/backend.deployment.yaml @@ -23,7 +23,7 @@ spec: image: {{.Values.Backend.Image}} imagePullPolicy: IfNotPresent volumeMounts: - - mountPath: /Users/zhiye/data/file + - mountPath: /data/file name: file-volume - mountPath: /root/.hydro name: backend-volume diff --git a/package.json b/package.json index 7d09d7a..f3e58ac 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lint:ui": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --fix", "lint:ui:ci": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx .", "server": "node --trace-warnings --async-stack-traces --trace-deprecation packages/hydrooj/bin/hydrooj --debug --template --port=2333 --watch", + "server-local": "cross-env DEFAULT_STORE_PATH=/Users/zhiye/data/file/hydro node --trace-warnings --async-stack-traces --trace-deprecation packages/hydrooj/bin/hydrooj --debug --template --port=2333 --watch", "start": "node packages/hydrooj/bin/hydrooj --port=8888", "postinstall": "node build/prepare.js", "gen-patch": "node -r @hydrooj/utils/lib/register build/gen-patch.ts" diff --git a/packages/hydrooj/locales/en.yaml b/packages/hydrooj/locales/en.yaml index af974dc..ba92a2b 100644 --- a/packages/hydrooj/locales/en.yaml +++ b/packages/hydrooj/locales/en.yaml @@ -26,6 +26,7 @@ domain_main: Main domain_permission: System Permission domain_role: System Role domain_user: System User +system_all_users: All Users fs_upload: File Upload home_account: Account Settings home_domain_account: Profile @ Domain diff --git a/packages/hydrooj/locales/zh.yaml b/packages/hydrooj/locales/zh.yaml index cc499dd..da0cd49 100644 --- a/packages/hydrooj/locales/zh.yaml +++ b/packages/hydrooj/locales/zh.yaml @@ -260,17 +260,18 @@ Domain {0} is bulit-in and cannot be modified.: 网站 {0} 为内置,不可修 Domain {0} not found.: 网站 {0} 不存在。 Domain ID cannot be changed once the domain is created.: 在创建后不能更改 ID。 Domain ID: 网站 ID -domain_dashboard: 管理网站 +domain_dashboard: 管理站点 domain_discussion: 讨论节点 -domain_edit: 编辑网站资料 +domain_edit: 编辑站点资料 domain_file: 我的文件 domain_group: 小组管理 -domain_join_applications: 添加子网站 -domain_join: 加入网站 +domain_join_applications: 添加子站点 +domain_join: 加入站点 domain_main: 首页 domain_permission: 权限管理 domain_role: 角色管理 -domain_user: 子管理员管理 +domain_user: 系统用户管理 +system_all_users: 全站用户管理 domain: 网站 Domain: 网站 Don't have an account?: 还没有账户? @@ -357,7 +358,7 @@ Hint: 提示 home_account: 账户设置 home_domain_account: 当前网站的设置 home_domain_create: 创建网站 -home_domain: 我的网站 +home_domain: 我的站点 home_files: 我的文件 home_messages: 站内消息 home_preference: 偏好设置 @@ -478,7 +479,7 @@ monacoTheme: 编辑器主题 Month: 月 Monthly Popular: 月度最受欢迎 Most Upvoted Solutions: 最被赞同的题解 -My Domains: 我的网站 +My Domains: 我的站点 My Files: 我的文件 My Profile: 我的资料 My Recent Submissions: 我的最近递交记录 diff --git a/packages/hydrooj/src/handler/domain.ts b/packages/hydrooj/src/handler/domain.ts index 48d4e25..9ae82f7 100644 --- a/packages/hydrooj/src/handler/domain.ts +++ b/packages/hydrooj/src/handler/domain.ts @@ -59,7 +59,7 @@ class DomainRankHandler extends Handler { @query('page', Types.PositiveInt, true) async get(domainId: string, page = 1) { const [dudocs, upcount, ucount] = await paginate( - domain.getMultiUserInDomain(domainId, { uid: { $gt: 1 }, rp: { $gt: 0 } }).sort({ rp: -1 }), + domain.getMultiUserInDomain(domainId, { uid: { $gt: 1 } }).sort({ rp: -1 }), page, 100, ); @@ -185,6 +185,55 @@ class DomainUserHandler extends ManageHandler { } } +class SystemAllUserHandler extends ManageHandler { + @requireSudo + async get({ domainId }) { + if (domainId !== 'system') { + throw new ForbiddenError( + 'Only system domain can view all users', + ); + } + + const [roles] = await Promise.all([ + domain.getRoles(domainId), + ]); + const allUsers = (await user.fetchAllUsers()).filter((u) => u._id > 1); + // Set gender string for all users + allUsers.forEach((u) => { + u.genderStr = u.gender === 0 ? "男" : u.gender === 1 ? "女" : "未填写"; + }); + + this.response.template = 'system_all_users.html'; + this.response.body = { + roles, allUsers, domain: this.domain, + }; + } + + @requireSudo + @post('uid', Types.Int) + @post('role', Types.Role) + async postSetUser(domainId: string, uid: number, role: string) { + if (uid === this.domain.owner) throw new ForbiddenError(); + await Promise.all([ + domain.setUserRole(domainId, uid, role), + oplog.log(this, 'domain.setRole', { uid, role }), + ]); + this.back(); + } + + @requireSudo + @param('uid', Types.NumericArray) + @param('role', Types.Role) + async postSetUsers(domainId: string, uid: number[], role: string) { + if (uid.includes(this.domain.owner)) throw new ForbiddenError(); + await Promise.all([ + domain.setUserRole(domainId, uid, role), + oplog.log(this, 'domain.setRole', { uid, role }), + ]); + this.back(); + } +} + class DomainPermissionHandler extends ManageHandler { @requireSudo async get({ domainId }) { @@ -367,6 +416,7 @@ export async function apply(ctx: Context) { ctx.Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler); ctx.Route('domain_edit', '/domain/edit', DomainEditHandler); ctx.Route('domain_user', '/domain/user', DomainUserHandler); + ctx.Route('system_all_users', '/domain/allusers', SystemAllUserHandler); ctx.Route('domain_permission', '/domain/permission', DomainPermissionHandler); ctx.Route('domain_role', '/domain/role', DomainRoleHandler); ctx.Route('domain_group', '/domain/group', DomainUserGroupHandler); diff --git a/packages/hydrooj/src/lib/ui.ts b/packages/hydrooj/src/lib/ui.ts index b4686b6..d6c39d7 100644 --- a/packages/hydrooj/src/lib/ui.ts +++ b/packages/hydrooj/src/lib/ui.ts @@ -78,6 +78,7 @@ inject('DomainManage', 'domain_edit', { family: 'Properties', icon: 'info' }); inject('DomainManage', 'domain_join_applications', { family: 'Properties', icon: 'info' }); inject('DomainManage', 'domain_role', { family: 'Access Control', icon: 'user' }); inject('DomainManage', 'domain_user', { family: 'Access Control', icon: 'user' }); +inject('DomainManage', 'system_all_users', { family: 'Access Control', icon: 'user' }); inject('DomainManage', 'domain_permission', { family: 'Access Control', icon: 'user' }); inject('DomainManage', 'domain_group', { family: 'Access Control', icon: 'user' }); diff --git a/packages/hydrooj/src/model/user.ts b/packages/hydrooj/src/model/user.ts index 4ffcf32..92eb44c 100644 --- a/packages/hydrooj/src/model/user.ts +++ b/packages/hydrooj/src/model/user.ts @@ -237,6 +237,15 @@ class UserModel { return r; } + static async fetchAllUsers(): Promise { + const udocs = await coll.find({}).toArray(); + const r: User[] = []; + for (const udoc of udocs) { + r.push(new User(udoc, {})); + } + return r; + } + @ArgMethod static async getByUname(domainId: string, uname: string): Promise { const unameLower = uname.trim().toLowerCase(); diff --git a/packages/ui-default/locales/zh.yaml b/packages/ui-default/locales/zh.yaml index 60d150d..48d35b7 100644 --- a/packages/ui-default/locales/zh.yaml +++ b/packages/ui-default/locales/zh.yaml @@ -298,12 +298,13 @@ Domain ID: 网站 ID Domain Settings: 网站设置 domain_dashboard: 管理网站 domain_discussion: 讨论节点 -domain_edit: 编辑网站资料 -domain_join: 加入网站 +domain_edit: 编辑站点资料 +domain_join: 加入站点 domain_main: 首页 -domain_permission: 管理权限 -domain_role: 管理角色 -domain_user: 管理用户 +domain_permission: 权限管理 +domain_role: 角色管理 +domain_user: 系统用户管理 +system_all_users: 全站用户管理 domain: 网站 Domain: 网站 Don't have an account?: 还没有账户? @@ -413,7 +414,7 @@ Hitokoto: 一言 home_account: 账户设置 home_domain_account: 当前网站的设置 home_domain_create: 创建网站 -home_domain: 我的网站 +home_domain: 我的站点 home_messages: 站内消息 home_preference: 偏好设置 home_security: 安全设置 @@ -537,7 +538,7 @@ More: 更多 Most Upvoted Solutions: 最被赞同的题解 Move to subtask: 移动到子任务 Multi Platform Authenticator: 跨平台认证器 -My Domains: 我的网站 +My Domains: 我的站点 My Files: 我的文件 My Profile: 我的资料 My Recent Submissions: 我的最近递交记录 diff --git a/packages/ui-default/pages/system_all_users.page.js b/packages/ui-default/pages/system_all_users.page.js new file mode 100644 index 0000000..55ac0da --- /dev/null +++ b/packages/ui-default/pages/system_all_users.page.js @@ -0,0 +1,155 @@ +import $ from 'jquery'; +import _ from 'lodash'; +import UserSelectAutoComplete from 'vj/components/autocomplete/UserSelectAutoComplete'; +import { ActionDialog, ConfirmDialog } from 'vj/components/dialog'; +import Notification from 'vj/components/notification'; +import { NamedPage } from 'vj/misc/Page'; +import { + delay, i18n, request, tpl, +} from 'vj/utils'; + +const page = new NamedPage('system_all_users', () => { + const addUserSelector = UserSelectAutoComplete.getOrConstruct($('.dialog__body--add-user [name="user"]')); + const addUserDialog = new ActionDialog({ + $body: $('.dialog__body--add-user > div'), + onDispatch(action) { + const $role = addUserDialog.$dom.find('[name="role"]'); + if (action === 'ok') { + if (addUserSelector.value() === null) { + addUserSelector.focus(); + return false; + } + if ($role.val() === '') { + $role.focus(); + return false; + } + } + return true; + }, + }); + addUserDialog.clear = function () { + addUserSelector.clear(); + this.$dom.find('[name="role"]').val(''); + return this; + }; + + const setRolesDialog = new ActionDialog({ + $body: $('.dialog__body--set-role > div'), + onDispatch(action) { + const $role = setRolesDialog.$dom.find('[name="role"]'); + if (action === 'ok' && $role.val() === '') { + $role.focus(); + return false; + } + return true; + }, + }); + setRolesDialog.clear = function () { + this.$dom.find('[name="role"]').val(''); + return this; + }; + + async function handleClickAddUser() { + const action = await addUserDialog.clear().open(); + if (action !== 'ok') { + return; + } + const user = addUserSelector.value(); + const role = addUserDialog.$dom.find('[name="role"]').val(); + try { + await request.post('', { + operation: 'set_user', + uid: user._id, + role, + }); + window.location.reload(); + } catch (error) { + Notification.error(error.message); + } + } + + function ensureAndGetSelectedUsers() { + const users = _.map( + $('.domain-users tbody [type="checkbox"]:checked'), + (ch) => $(ch).closest('tr').attr('data-uid'), + ); + if (users.length === 0) { + Notification.error(i18n('Please select at least one user to perform this operation.')); + return null; + } + return users; + } + + async function handleClickRemoveSelected() { + const selectedUsers = ensureAndGetSelectedUsers(); + if (selectedUsers === null) { + return; + } + const action = await new ConfirmDialog({ + $body: tpl` +
+

${i18n('Confirm removing the selected users?')}

+

${i18n('Their account will not be deleted and they will be with the default role.')}

+
`, + }).open(); + if (action !== 'yes') return; + try { + await request.post('', { + operation: 'set_users', + uid: selectedUsers, + role: 'default', + }); + Notification.success(i18n('Selected users have been removed from the domain.')); + await delay(2000); + window.location.reload(); + } catch (error) { + Notification.error(error.message); + } + } + + async function handleClickSetSelected() { + const selectedUsers = ensureAndGetSelectedUsers(); + if (selectedUsers === null) { + return; + } + const action = await setRolesDialog.clear().open(); + if (action !== 'ok') { + return; + } + const role = setRolesDialog.$dom.find('[name="role"]').val(); + try { + await request.post('', { + operation: 'set_users', + uid: selectedUsers, + role, + }); + Notification.success(i18n('Role has been updated to {0} for selected users.', role)); + await delay(2000); + window.location.reload(); + } catch (error) { + Notification.error(error.message); + } + } + + async function handleChangeUserRole(ev) { + const row = $(ev.currentTarget).closest('tr'); + const role = $(ev.currentTarget).val(); + try { + await request.post('', { + operation: 'set_user', + uid: row.attr('data-uid'), + role, + }); + Notification.success(i18n('Role has been updated to {0}.', role)); + } catch (error) { + Notification.error(error.message); + } + } + + $('[name="add_user"]').click(() => handleClickAddUser()); + $('[name="remove_selected"]').click(() => handleClickRemoveSelected()); + $('[name="set_roles"]').click(() => handleClickSetSelected()); + $('.domain-users [name="role"]').change((ev) => handleChangeUserRole(ev)); +}); + +export default page; diff --git a/packages/ui-default/templates/domain_user.html b/packages/ui-default/templates/domain_user.html index 0e52404..c8b72cd 100644 --- a/packages/ui-default/templates/domain_user.html +++ b/packages/ui-default/templates/domain_user.html @@ -48,7 +48,7 @@

{{ _('Set Role') }}

-

{{ _('Kathy: 用户').format(domain.name) }}

+

{{ _('Kathy: 系统用户管理').format(domain.name) }}

diff --git a/packages/ui-default/templates/system_all_users.html b/packages/ui-default/templates/system_all_users.html new file mode 100644 index 0000000..71ed361 --- /dev/null +++ b/packages/ui-default/templates/system_all_users.html @@ -0,0 +1,122 @@ +{% extends "domain_base.html" %} +{% block domain_content %} +{# {% set _rolesSelect = roles.filter(eval("(i) => i._id !== 'guest'")).map(eval('(role) => [role._id, role._id]')) %} +{% set _rolesSelectWithoutDefault = _rolesSelect.filter(eval("(i) => i[0] !== 'default'")) %} #} + + +
+
+

{{ _('Kathy: 全站用户').format(domain.name) }}

+
+
+
+ {{ noscript_note.render() }} +
+ + + + + + + + + + {# #} + + + + + + + + + + + + + {%- for usr in allUsers -%} + {% set is_disabled=(usr._id == handler.user._id) %} + + + + + + + + + + + + + {%- endfor -%} + +
+ + {{ _('User ID') }}{{ _('Username') }}邮箱RP 分数AC提交排行性别学校
+ {{ usr._id }} + + {{ user.render_inline(usr, badge=true) }} + + {{ usr.mail }} + + {{ usr.rp|default(0)|round(0) }} + + {{ usr.nAccept|default(0) }} + + {{ usr.nSubmit|default(0) }} + + {{ usr.rank|default('-') }} + + {{ usr.genderStr }} + + {{ usr.school }} +
+
+
+
+
+{% endblock %}