You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Hydro/packages/hydrooj/src/handler/domain.ts

319 lines
12 KiB
TypeScript

import moment from 'moment-timezone';
import { Dictionary } from 'lodash';
import {
RoleAlreadyExistError, ValidationError, DomainJoinForbiddenError,
DomainJoinAlreadyMemberError, InvalidJoinInvitationCodeError,
} from '../error';
import { log2 } from '../utils';
import type { DomainDoc } from '../interface';
import user from '../model/user';
import domain from '../model/domain';
import * as discussion from '../model/discussion';
import * as system from '../model/system';
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
import {
DEFAULT_NODES, PERM, PERMS_BY_FAMILY, PRIV,
} from '../model/builtin';
import avatar from '../lib/avatar';
import paginate from '../lib/paginate';
import {
Route, Handler, Types, param, query, post,
} from '../service/server';
class DomainRankHandler extends Handler {
@query('page', Types.PositiveInt, true)
async get(domainId: string, page = 1) {
const [dudocs, upcount, ucount] = await paginate(
domain.getMultiUserInDomain(domainId).sort({ rp: -1 }),
page,
100,
);
let udocs = [];
for (const dudoc of dudocs) {
udocs.push(user.getById(domainId, dudoc.uid));
}
udocs = await Promise.all(udocs);
const path = [
['Hydro', 'homepage'],
['ranking', null],
];
this.response.template = 'ranking.html';
this.response.body = {
udocs, upcount, ucount, page, path,
};
}
}
class ManageHandler extends Handler {
domain: DomainDoc;
async prepare({ domainId }) {
this.checkPerm(PERM.PERM_EDIT_DOMAIN);
this.domain = await domain.get(domainId);
}
}
class DomainEditHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_edit', null],
];
this.response.template = 'domain_edit.html';
this.response.body = { current: this.domain, settings: DOMAIN_SETTINGS, path };
}
async post(args) {
const $set = {};
for (const key in args) {
if (DOMAIN_SETTINGS_BY_KEY[key]) $set[key] = args[key];
}
await domain.edit(args.domainId, $set);
this.response.redirect = this.url('domain_dashboard');
}
}
class DomainDashboardHandler extends ManageHandler {
async get() {
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_dashboard', null],
];
this.response.template = 'domain_dashboard.html';
this.response.body = { domain: this.domain, path };
}
async postInitDiscussionNode({ domainId }) {
for (const category of Object.keys(DEFAULT_NODES)) {
for (const item of DEFAULT_NODES[category]) {
// eslint-disable-next-line no-await-in-loop
const curr = await discussion.getNode(domainId, item.name);
// eslint-disable-next-line no-await-in-loop
if (!curr) await discussion.addNode(domainId, item.name, category, item.pic ? { pic: item.pic } : undefined);
}
}
this.back();
}
}
class DomainUserHandler extends ManageHandler {
async get({ domainId }) {
const rudocs = {};
const [dudocs, roles] = await Promise.all([
domain.getMultiUserInDomain(domainId, {
$and: [
{ role: { $nin: ['default', 'guest'] } },
{ role: { $ne: null } },
],
}).toArray(),
domain.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 dudoc of dudocs) {
const ud = udict[dudoc.uid];
rudocs[ud.role || 'default'].push(ud);
}
const rolesSelect = roles.map((role) => [role._id, role._id]);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_user', null],
];
this.response.template = 'domain_user.html';
this.response.body = {
roles, rolesSelect, rudocs, udict, path, domain: this.domain,
};
}
@post('uid', Types.Int)
@post('role', Types.Name)
async postSetUser(domainId: string, uid: number, role: string) {
await domain.setUserRole(domainId, uid, role);
this.back();
}
@param('uid', Types.NumericArray)
@param('role', Types.Name)
async postSetUsers(domainId: string, uid: number[], role: string) {
await domain.setUserRole(domainId, uid, role);
this.back();
}
}
class DomainPermissionHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_permission', null],
];
this.response.template = 'domain_permission.html';
this.response.body = {
roles, PERMS_BY_FAMILY, domain: this.domain, path, log2,
};
}
async post({ domainId }) {
const roles = {};
delete this.request.body.csrfToken;
for (const role in this.request.body) {
const perms = this.request.body[role] instanceof Array
? this.request.body[role]
: [this.request.body[role]];
// @ts-expect-error
roles[role] = 0n;
// @ts-expect-error
for (const r of perms) roles[role] |= 1n << BigInt(r);
}
await domain.setRoles(domainId, roles);
this.back();
}
}
class DomainRoleHandler extends ManageHandler {
async get({ domainId }) {
const roles = await domain.getRoles(domainId, true);
const path = [
['Hydro', 'homepage'],
['domain', null],
['domain_role', null],
];
this.response.template = 'domain_role.html';
this.response.body = { roles, domain: this.domain, path };
}
@param('role', Types.Name)
async postAdd(domainId: string, role: string) {
const roles = await domain.getRoles(this.domain);
const rdict: Dictionary<any> = {};
for (const r of roles) rdict[r._id] = r.perm;
if (rdict[role]) throw new RoleAlreadyExistError(role);
await domain.addRole(domainId, role, rdict.default);
this.back();
}
@param('roles', Types.Array)
async postDelete(domainId: string, roles: string[]) {
for (const role of roles) {
if (['root', 'default', 'guest'].includes(role)) {
throw new ValidationError('role');
}
}
await domain.deleteRoles(domainId, roles);
this.back();
}
}
class DomainJoinApplicationsHandler extends ManageHandler {
async get() {
const r = await domain.getRoles(this.domain);
const roles = r.map((role) => role._id).sort();
this.response.body.rolesWithText = roles.map((role) => [role, role]);
this.response.body.joinSettings = domain.getJoinSettings(this.domain, roles);
this.response.body.expirations = { ...domain.JOIN_EXPIRATION_RANGE };
if (!this.response.body.joinSettings) {
delete this.response.body.expirations[domain.JOIN_EXPIRATION_KEEP_CURRENT];
}
this.response.body.url_prefix = system.get('server.url');
this.response.body.path = [
['Hydro', 'homepage'],
['domain', null],
['domain_join_applications', null],
];
this.response.template = 'domain_join_applications.html';
}
@post('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE]))
@post('role', Types.Name, true)
@post('expire', Types.Int, true)
@post('invitationCode', Types.Content, true)
async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') {
const r = await domain.getRoles(this.domain);
const roles = r.map((rl) => rl._id);
const current = domain.getJoinSettings(this.domain, roles);
let joinSettings;
if (method === domain.JOIN_METHOD_NONE) joinSettings = null;
else {
if (!roles.includes(role)) throw new ValidationError('role');
if (!current && expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) throw new ValidationError('expire');
joinSettings = { method, role };
if (expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) joinSettings.expire = current.expire;
else if (expire === domain.JOIN_EXPIRATION_UNLIMITED) joinSettings.expire = null;
else if (!domain.JOIN_EXPIRATION_RANGE[expire]) throw new ValidationError('expire');
else joinSettings.expire = moment().add(expire, 'hours').toDate();
if (method === domain.JOIN_METHOD_CODE) joinSettings.code = invitationCode;
}
await domain.edit(domainId, { _join: joinSettings });
this.back();
}
}
class DomainJoinHandler extends Handler {
joinSettings: any;
constructor(ctx) {
super(ctx);
this.noCheckPermView = true;
}
async prepare() {
const r = await domain.getRoles(this.domain);
const roles = r.map((role) => role._id);
this.joinSettings = domain.getJoinSettings(this.domain, roles);
if (!this.joinSettings) throw new DomainJoinForbiddenError(this.domain._id);
if (this.user.role !== 'default') throw new DomainJoinAlreadyMemberError(this.domain._id, this.user._id);
}
@param('code', Types.Content, true)
async get(domainId: string, code: string) {
this.response.template = 'domain_join.html';
this.response.body.joinSettings = this.joinSettings;
this.response.body.code = code;
this.response.body.path = [
['Hydro', 'homepage'],
['domain_join', 'domain_join', { domainId, code }],
];
}
@param('code', Types.Content, true)
async post(domainId: string, code: string) {
if (this.joinSettings.method === domain.JOIN_METHOD_CODE) {
if (this.joinSettings.code !== code) {
throw new InvalidJoinInvitationCodeError(this.domain._id);
}
}
await domain.setUserRole(this.domain._id, this.user._id, this.joinSettings.role);
this.response.redirect = this.url('domain_main', { query: { notification: 'Successfully joined domain.' } });
}
}
class DomainSearchHandler extends Handler {
@param('q', Types.Content)
async get(domainId: string, q: string) {
const ddocs = await domain.getPrefixSearch(q, 20);
for (let i = 0; i < ddocs.length; i++) {
ddocs[i].avatarUrl = ddocs[i].avatar ? avatar(ddocs[i].avatar, 64) : '/img/team_avatar.png';
}
this.response.body = ddocs;
}
}
export async function apply() {
Route('ranking', '/ranking', DomainRankHandler, PERM.PERM_VIEW_RANKING);
Route('domain_dashboard', '/domain/dashboard', DomainDashboardHandler);
Route('domain_edit', '/domain/edit', DomainEditHandler);
Route('domain_user', '/domain/user', DomainUserHandler);
Route('domain_permission', '/domain/permission', DomainPermissionHandler);
Route('domain_role', '/domain/role', DomainRoleHandler);
Route('domain_join_applications', '/domain/join_applications', DomainJoinApplicationsHandler);
Route('domain_join', '/domain/join', DomainJoinHandler, PRIV.PRIV_USER_PROFILE);
Route('domain_search', '/domain/search', DomainSearchHandler, PRIV.PRIV_USER_PROFILE);
}
global.Hydro.handler.domain = apply;