mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core&ui: add manage user priv (#433)
This commit is contained in:
1 parent
b36adcdf13
commit
a5494b1345
13 files changed
+274
-11
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -23,25 +23,27 @@ export const collV: Collection<VUdoc> = db.collection('vuser');
|
||||
export const collGroup: Collection<GDoc> = db.collection('user.group');
|
||||
const cache = new LRU<string, User>({ 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,
|
||||
|
||||
@@ -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<DomainDoc>) => VoidReturn
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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} 通用账户
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@hydrooj/ui-default",
|
||||
"version": "4.41.4",
|
||||
"version": "4.41.5",
|
||||
"author": "undefined <i@undefined.moe>",
|
||||
"license": "AGPL-3.0",
|
||||
"main": "index.ts",
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
@@ -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') }}
|
||||
</ol>
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
{% extends "manage_base.html" %}
|
||||
{% block manage_content %}
|
||||
<div style="display: none" class="dialog__body--set-priv">
|
||||
<div>
|
||||
<div class="row"><div class="columns">
|
||||
<h1>{{ _('Set Privilege') }}</h1>
|
||||
</div></div>
|
||||
{% set _mark = Math.ceil(Object.keys(Priv).length/3) %}
|
||||
{% macro col(PRIVS) %}
|
||||
<div class="medium-4 columns">
|
||||
<table class="data-table priv_table">
|
||||
<colgroup>
|
||||
<col class="col--description">
|
||||
<col class="col--p">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--description">{{ _('Name') }}</th>
|
||||
<th class="col--p">{{ _('Privilege') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- for p in PRIVS -%}
|
||||
<tr>
|
||||
<td class="col--description">
|
||||
{{ _(p) }}
|
||||
</td>
|
||||
<td class="col--p">
|
||||
<label class="compact checkbox">
|
||||
<input class="priv" name="{{ p }}" data-checkbox-group="priv" data-checkbox-range value="{{ Priv[p] }}" type="checkbox">
|
||||
</label>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
<div class="row">
|
||||
{{ col(Object.keys(Priv).slice(0, _mark)) }}
|
||||
{{ col(Object.keys(Priv).slice(_mark, _mark*2)) }}
|
||||
{{ col(Object.keys(Priv).slice(_mark*2, _mark*3)) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section">
|
||||
<div class="section__header">
|
||||
<h1 class="section__title">{{ _('User Privilege') }}</h1>
|
||||
<div class="section__tools">
|
||||
<button class="primary rounded button" name="select_user"><span class="icon icon-search"></span>{{ _('Select User') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
{{ noscript_note.render() }}
|
||||
<div class="section__body no-padding">
|
||||
<table class="data-table">
|
||||
<colgroup>
|
||||
<col class="col--uid">
|
||||
<col class="col--user">
|
||||
<col class="col--priv_info">
|
||||
<col class="col--priv">
|
||||
<col class="col--actions">
|
||||
</colgroup>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="col--uid">{{ _('User ID') }}</th>
|
||||
<th class="col--user">{{ _('Username') }}</th>
|
||||
<td class="col--priv_info">{{ _('Privilege') }}</th>
|
||||
<th class="col--priv">{{ _('Value') }}</th>
|
||||
<th class="col--actions">{{ _('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% include "partials/manage_user_priv.html" %}
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,55 @@
|
||||
{% import "components/user.html" as user with context %}
|
||||
<tbody data-fragment-id="user_priv-tbody">
|
||||
<tr data-uid="{{ udoc._id }}">
|
||||
<td class="col--uid">
|
||||
#
|
||||
</td>
|
||||
<td class="col--user">
|
||||
Default Privilege
|
||||
</td>
|
||||
<td class="col--priv_info">
|
||||
{%- for name, priv in Priv -%}
|
||||
{%- if defaultPriv|bitand(priv) -%}
|
||||
{{ name }}<br>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
</td>
|
||||
<td class="col--priv" title="{{ defaultPriv }}">
|
||||
{{ defaultPriv }}
|
||||
</td>
|
||||
<td class="col--actions">
|
||||
<a name="set_priv" data-uid="default" data-priv="{{ defaultPriv }}">{{ _('Edit') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- for udoc in udocs -%}
|
||||
<tr data-uid="{{ udoc._id }}">
|
||||
<td class="col--uid">
|
||||
{{ udoc._id }}
|
||||
</td>
|
||||
<td class="col--user">
|
||||
{{ user.render_inline(udoc, badge=false) }}
|
||||
</td>
|
||||
<td class="col--priv_info">
|
||||
{%- 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) -%}
|
||||
<p class="priv_plus">+{{ name }}</p>
|
||||
{%- elif priv|bitand(defaultPriv) and not priv|bitand(udoc.priv) -%}
|
||||
<p class="priv_minus">-{{ name }}</p>
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{%- endif -%}
|
||||
</td>
|
||||
<td class="col--priv" title="{{ udoc.priv }}">
|
||||
{{ udoc.priv }}
|
||||
</td>
|
||||
<td class="col--actions">
|
||||
<a name="set_priv" data-uid="{{ udoc._id }}" data-priv="{{ udoc.priv }}">{{ _('Edit') }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{%- endfor -%}
|
||||
</tbody>
|
||||
Reference in new issue
Block a user