mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
加域申请管理
This commit is contained in:
1 parent
16108993cf
commit
c3ff5856e4
16 files changed
+165
-33
No files matched your search
+1
-1
@@ -10,7 +10,7 @@
|
||||
"lint": "eslint packages --ext ts --fix",
|
||||
"pub": "node build/publish",
|
||||
"jest": "node build/jest",
|
||||
"start": "node_modules/.bin/hydrooj"
|
||||
"start": "node --async-stack-traces --trace-deprecation --enable-source-maps node_modules/.bin/hydrooj"
|
||||
},
|
||||
"version": "1.0.0",
|
||||
"license": "AGPL-3.0-only",
|
||||
|
||||
@@ -3,5 +3,4 @@
|
||||
SETTINGS_PRIVACY.allowRegisteredUsers
|
||||
model.setting.ui.name
|
||||
pdoc.secretConfig?
|
||||
message.markAsRead
|
||||
domain_join_application
|
||||
message.markAsRead
|
||||
@@ -237,6 +237,7 @@ Domain ID: 域 ID
|
||||
domain_dashboard: 管理域
|
||||
domain_discussion: 讨论节点
|
||||
domain_edit: 编辑域资料
|
||||
domain_join_applications: 加域申请
|
||||
domain_join: 加入域
|
||||
domain_main: 首页
|
||||
domain_permission: 管理权限
|
||||
@@ -408,7 +409,6 @@ Lucky: 手气不错
|
||||
manage_dashboard: 控制面板
|
||||
manage_join_applications: 加域申请
|
||||
manage_module: 管理模块
|
||||
manage_script: 脚本管理
|
||||
manage_setting: 系统设置
|
||||
manage: 管理
|
||||
Manage: 管理
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "hydrooj",
|
||||
"version": "2.12.6",
|
||||
"version": "2.12.7",
|
||||
"bin": "bin/hydrooj.js",
|
||||
"main": "dist/loader.js",
|
||||
"typings": "dist/loader.d.ts",
|
||||
|
||||
@@ -13,11 +13,15 @@ const lockfile = path.resolve(tmpdir, 'lock.json');
|
||||
|
||||
export async function load(call: Entry) {
|
||||
if (fs.existsSync(lockfile) && !argv.ignorelock) {
|
||||
const file = require(lockfile);
|
||||
const plist = await pslist();
|
||||
if (file.pid && plist.map((i) => i.pid).includes(file.pid)) {
|
||||
console.error(`Lockfile exists, pid=${file.pid}`);
|
||||
process.exit(1);
|
||||
try {
|
||||
const file = require(lockfile);
|
||||
const plist = await pslist();
|
||||
if (file.pid && plist.map((i) => i.pid).includes(file.pid)) {
|
||||
console.error(`Lockfile exists, pid=${file.pid}`);
|
||||
process.exit(1);
|
||||
}
|
||||
} catch {
|
||||
// Invalid lockfile. ignore.
|
||||
}
|
||||
}
|
||||
const context = {
|
||||
|
||||
@@ -84,6 +84,9 @@ export const HomeworkNotLiveError = Err('HomeworkNotLiveError', ForbiddenError,
|
||||
export const HomeworkNotAttendedError = Err('HomeworkNotAttendedError', ForbiddenError, "You haven't claimed this homework yet.");
|
||||
export const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.');
|
||||
export const CsrfTokenError = Err('CsrfTokenError', ForbiddenError, 'CsrfTokenError');
|
||||
export const DomainJoinForbiddenError = Err('DomainJoinForbiddenError', ForbiddenError, 'You are not allowed to join the domain. The link is either invalid or expired.');
|
||||
export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError', ForbiddenError, 'Failed to join the domain. You are already a member.');
|
||||
export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.');
|
||||
|
||||
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
|
||||
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
|
||||
@@ -132,6 +135,9 @@ global.Hydro.error = {
|
||||
ContestNotLiveError,
|
||||
ContestScoreboardHiddenError,
|
||||
ContestAlreadyAttendedError,
|
||||
DomainJoinAlreadyMemberError,
|
||||
DomainJoinForbiddenError,
|
||||
InvalidJoinInvitationCodeError,
|
||||
UserFacingError,
|
||||
SystemError,
|
||||
TrainingAlreadyEnrollError,
|
||||
|
||||
@@ -24,7 +24,7 @@ const ContestHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
class ContestListHandler extends ContestHandler {
|
||||
@param('rule', Types.String, true)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, rule = '', page = 1) {
|
||||
this.response.template = 'contest_main.html';
|
||||
let tdocs;
|
||||
@@ -55,7 +55,7 @@ class ContestListHandler extends ContestHandler {
|
||||
|
||||
class ContestDetailHandler extends ContestHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const tdoc = await contest.get(domainId, tid);
|
||||
this.response.template = 'contest_detail.html';
|
||||
|
||||
@@ -71,7 +71,7 @@ class DiscussionHandler extends Handler {
|
||||
}
|
||||
|
||||
class DiscussionMainHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [ddocs, dpcount] = await paginate(
|
||||
discussion.getMulti(domainId),
|
||||
@@ -99,7 +99,7 @@ class DiscussionMainHandler extends Handler {
|
||||
class DiscussionNodeHandler extends DiscussionHandler {
|
||||
@param('type', Types.String)
|
||||
@param('name', Types.String)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, type: string, _name: string, page = 1) {
|
||||
let name: ObjectID | string | number;
|
||||
if (ObjectID.isValid(_name)) name = new ObjectID(_name);
|
||||
@@ -177,7 +177,7 @@ class DiscussionCreateHandler extends DiscussionHandler {
|
||||
|
||||
class DiscussionDetailHandler extends DiscussionHandler {
|
||||
@param('did', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, did: ObjectID, page = 1) {
|
||||
const dsdoc = this.user.hasPriv(PRIV.PRIV_USER_PROFILE)
|
||||
? await discussion.getStatus(domainId, did, this.user._id)
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import { RoleAlreadyExistError, ValidationError } from '../error';
|
||||
import moment from 'moment-timezone';
|
||||
import {
|
||||
RoleAlreadyExistError, ValidationError, DomainJoinForbiddenError,
|
||||
DomainJoinAlreadyMemberError, InvalidJoinInvitationCodeError,
|
||||
} from '../error';
|
||||
import * as user from '../model/user';
|
||||
import * as domain from '../model/domain';
|
||||
import { DOMAIN_SETTINGS, DOMAIN_SETTINGS_BY_KEY } from '../model/setting';
|
||||
@@ -10,7 +14,7 @@ import {
|
||||
} from '../service/server';
|
||||
|
||||
class DomainRankHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [dudocs, upcount, ucount] = await paginate(
|
||||
domain.getMultiInDomain(domainId).sort({ rp: -1 }),
|
||||
@@ -184,6 +188,75 @@ class DomainRoleHandler extends ManageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
class DomainJoinApplicationsHandler extends ManageHandler {
|
||||
async get() {
|
||||
const r = await domain.getRoles(this.domain);
|
||||
const roles = r.map((role) => role._id).sort();
|
||||
const rolesWithText = roles.map((role) => [role, role]);
|
||||
const joinSettings = domain.getJoinSettings(this.domain, roles);
|
||||
const expirations = { ...domain.JOIN_EXPIRATION_RANGE };
|
||||
if (!joinSettings) delete expirations[domain.JOIN_EXPIRATION_KEEP_CURRENT];
|
||||
this.response.template = 'domain_join_applications.html';
|
||||
this.response.body = { rolesWithText, joinSettings, expirations };
|
||||
}
|
||||
|
||||
@param('method', Types.UnsignedInt)
|
||||
@param('role', Types.String, true)
|
||||
@param('expire', Types.UnsignedInt, true)
|
||||
@param('invitationCode', Types.String, 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);
|
||||
if (!domain.JOIN_METHOD_RANGE[method]) throw new ValidationError('method');
|
||||
let joinSettings;
|
||||
if (method === domain.JOIN_METHOD_NONE) joinSettings = null;
|
||||
else {
|
||||
if (!roles.includes(role)) throw new ValidationError('role');
|
||||
if (!domain.JOIN_EXPIRATION_RANGE[expire]) throw new ValidationError('expire');
|
||||
if (!current && expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) throw new ValidationError('expire');
|
||||
joinSettings = { method, role };
|
||||
if (method === domain.JOIN_METHOD_CODE) joinSettings.code = invitationCode;
|
||||
if (expire === domain.JOIN_EXPIRATION_KEEP_CURRENT) {
|
||||
joinSettings.expire = current.expire;
|
||||
} else if (expire === domain.JOIN_EXPIRATION_UNLIMITED) joinSettings.expire = null;
|
||||
else joinSettings.expire = moment().add(expire, 'hours').toDate();
|
||||
}
|
||||
await domain.edit(this.domain._id, { join: joinSettings });
|
||||
this.back();
|
||||
}
|
||||
}
|
||||
|
||||
class DomainJoinHandler extends Handler {
|
||||
joinSettings: any;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// @base.require_priv(builtin.PRIV_USER_PROFILE)
|
||||
@param('code', Types.String, true)
|
||||
async get(domainId: string, code: string) {
|
||||
this.response.template = 'domain_join.html';
|
||||
this.response.body = { joinSettings: this.joinSettings, code };
|
||||
}
|
||||
|
||||
@param('code', Types.String, 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');
|
||||
}
|
||||
}
|
||||
|
||||
class DomainSearchHandler extends Handler {
|
||||
@param('q', Types.String)
|
||||
async get(domainId: string, q: string) {
|
||||
@@ -202,6 +275,8 @@ export async function apply() {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ class HomeworkMainHandler extends HomeworkHandler {
|
||||
|
||||
class HomeworkDetailHandler extends HomeworkHandler {
|
||||
@param('tid', Types.ObjectID)
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, tid: ObjectID, page = 1) {
|
||||
const tdoc = await contest.get(domainId, tid, document.TYPE_HOMEWORK);
|
||||
const [tsdoc, pdict] = await Promise.all([
|
||||
|
||||
@@ -57,7 +57,7 @@ class ProblemHandler extends Handler {
|
||||
}
|
||||
|
||||
class ProblemMainHandler extends ProblemHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
@param('q', Types.String, true)
|
||||
async get(domainId: string, page = 1, q = '') {
|
||||
this.response.template = 'problem_main.html';
|
||||
@@ -103,7 +103,7 @@ class ProblemMainHandler extends ProblemHandler {
|
||||
}
|
||||
|
||||
class ProblemCategoryHandler extends ProblemHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
@param('category', Types.String, null, parseCategory)
|
||||
async get(domainId: string, page = 1, category: string[]) {
|
||||
this.response.template = 'problem_main.html';
|
||||
@@ -469,7 +469,7 @@ class ProblemDataDownloadHandler extends ProblemDetailHandler {
|
||||
}
|
||||
|
||||
class ProblemSolutionHandler extends ProblemDetailHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
this.response.template = 'problem_solution.html';
|
||||
this.checkPerm(PERM.PERM_VIEW_PROBLEM_SOLUTION);
|
||||
|
||||
@@ -15,7 +15,7 @@ import { Rdoc } from '../interface';
|
||||
const RecordHandler = contest.ContestHandlerMixin(Handler);
|
||||
|
||||
class RecordListHandler extends RecordHandler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
@param('pid', Types.String, true)
|
||||
@param('tid', Types.ObjectID, true)
|
||||
@param('uidOrName', Types.String, true)
|
||||
|
||||
@@ -54,7 +54,7 @@ async function _parseDagJson(domainId, dag) {
|
||||
}
|
||||
|
||||
class TrainingMainHandler extends Handler {
|
||||
@param('page', Types.UnsignedInt, true)
|
||||
@param('page', Types.PositiveInt, true)
|
||||
async get(domainId: string, page = 1) {
|
||||
const [tdocs, tpcount] = await paginate(
|
||||
training.getMulti(domainId),
|
||||
|
||||
@@ -106,6 +106,13 @@ async function executeCommand(input: string) {
|
||||
}
|
||||
}
|
||||
|
||||
async function reload(count = 1) {
|
||||
console.log('Reloading');
|
||||
await stopWorker();
|
||||
console.log('Worker stopped');
|
||||
await startWorker(count);
|
||||
}
|
||||
|
||||
async function messageHandler(worker: cluster.Worker, msg: any) {
|
||||
if (!msg) msg = worker;
|
||||
if (msg.event) {
|
||||
@@ -120,10 +127,7 @@ async function messageHandler(worker: cluster.Worker, msg: any) {
|
||||
} else if (msg.event === 'stat') {
|
||||
global.Hydro.stat.reqCount += msg.count;
|
||||
} else if (msg.event === 'restart') {
|
||||
console.log('Restarting');
|
||||
await stopWorker();
|
||||
console.log('Worker stopped');
|
||||
await startWorker(msg.count);
|
||||
await reload(msg.count);
|
||||
} else if (msg.event === 'run') {
|
||||
await executeCommand(msg.command);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,34 @@
|
||||
import { Dictionary } from 'lodash';
|
||||
import { Collection } from 'mongodb';
|
||||
import { BUILTIN_ROLES, PRIV } from './builtin';
|
||||
import { DomainDoc } from '../interface';
|
||||
import * as db from '../service/db';
|
||||
|
||||
const coll = db.collection('domain');
|
||||
const coll: Collection<DomainDoc> = db.collection('domain');
|
||||
const collUser = db.collection('domain.user');
|
||||
|
||||
export const JOIN_METHOD_NONE = 0;
|
||||
export const JOIN_METHOD_ALL = 1;
|
||||
export const JOIN_METHOD_CODE = 2;
|
||||
export const JOIN_METHOD_RANGE = {
|
||||
[JOIN_METHOD_NONE]: 'No user is allowed to join this domain',
|
||||
[JOIN_METHOD_ALL]: 'Any user is allowed to join this domain',
|
||||
[JOIN_METHOD_CODE]: 'Any user is allowed to join this domain with an invitation code',
|
||||
};
|
||||
|
||||
export const JOIN_EXPIRATION_KEEP_CURRENT = 0;
|
||||
export const JOIN_EXPIRATION_UNLIMITED = -1;
|
||||
|
||||
export const JOIN_EXPIRATION_RANGE = {
|
||||
[JOIN_EXPIRATION_KEEP_CURRENT]: 'Keep current expiration',
|
||||
3: 'In 3 hours',
|
||||
24: 'In 1 day',
|
||||
[24 * 3]: 'In 3 days',
|
||||
[24 * 7]: 'In 1 week',
|
||||
[24 * 30]: 'In 1 month',
|
||||
[JOIN_EXPIRATION_UNLIMITED]: 'Never expire',
|
||||
};
|
||||
|
||||
export async function add(domainId: string, owner: number, name: string, bulletin: string) {
|
||||
const ddoc: DomainDoc = {
|
||||
_id: domainId,
|
||||
@@ -32,9 +55,11 @@ export function edit(domainId: string, $set: any) {
|
||||
return coll.updateOne({ _id: domainId }, { $set });
|
||||
}
|
||||
|
||||
export async function inc(domainId: string, field: string, n: number): Promise<number | null> {
|
||||
export async function inc(domainId: string, field: any, n: number): Promise<number | null> {
|
||||
const res = await coll.findOneAndUpdate(
|
||||
{ _id: domainId },
|
||||
// FIXME
|
||||
// @ts-ignore
|
||||
{ $inc: { [field]: n } },
|
||||
{ returnOriginal: false },
|
||||
);
|
||||
@@ -52,6 +77,8 @@ export function setUserRole(domainId: string, uid: number, role: string) {
|
||||
return collUser.updateOne({ uid, domainId }, { role }, { upsert: true });
|
||||
}
|
||||
|
||||
export async function getRoles(domainId: string): Promise<any[]>
|
||||
export async function getRoles(domain: DomainDoc): Promise<any[]>
|
||||
export async function getRoles(arg: string | DomainDoc) {
|
||||
let ddoc: DomainDoc;
|
||||
if (typeof arg === 'string') ddoc = await get(arg);
|
||||
@@ -144,6 +171,15 @@ export async function getDictUserByDomainId(uid: number) {
|
||||
return dudict;
|
||||
}
|
||||
|
||||
export function getJoinSettings(ddoc: DomainDoc, roles: string[]) {
|
||||
if (!ddoc.join) return null;
|
||||
const joinSettings = ddoc.join;
|
||||
if (joinSettings.method === JOIN_METHOD_NONE) return null;
|
||||
if (!roles.includes(joinSettings.role)) return null;
|
||||
if (joinSettings.expire && joinSettings.expire < new Date()) return null;
|
||||
return joinSettings;
|
||||
}
|
||||
|
||||
export async function getPrefixSearch(prefix: string, limit = 50) {
|
||||
const $regex = new RegExp(prefix, 'mi');
|
||||
const ddocs = await coll.find({
|
||||
@@ -153,6 +189,14 @@ export async function getPrefixSearch(prefix: string, limit = 50) {
|
||||
}
|
||||
|
||||
global.Hydro.model.domain = {
|
||||
JOIN_METHOD_NONE,
|
||||
JOIN_METHOD_ALL,
|
||||
JOIN_METHOD_CODE,
|
||||
JOIN_METHOD_RANGE,
|
||||
JOIN_EXPIRATION_KEEP_CURRENT,
|
||||
JOIN_EXPIRATION_UNLIMITED,
|
||||
JOIN_EXPIRATION_RANGE,
|
||||
|
||||
getRoles,
|
||||
add,
|
||||
inc,
|
||||
@@ -170,5 +214,6 @@ global.Hydro.model.domain = {
|
||||
incUserInDomain,
|
||||
getMultiInDomain,
|
||||
getDictUserByDomainId,
|
||||
getJoinSettings,
|
||||
getPrefixSearch,
|
||||
};
|
||||
@@ -47,15 +47,15 @@ interface ParamOption {
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-shadow
|
||||
export enum Types { String, Int, UnsignedInt, Float, ObjectID, Boolean, Date, Time }
|
||||
export enum Types { String, Int, UnsignedInt, PositiveInt, Float, ObjectID, Boolean, Date, Time }
|
||||
|
||||
const Tools: Array<[Converter, Validator, boolean?]> = [
|
||||
[(v) => v.toString(), null],
|
||||
[(v) => parseInt(v, 10), (v) => isSafeInteger(parseInt(v, 10))],
|
||||
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) >= 0],
|
||||
[(v) => parseInt(v, 10), (v) => parseInt(v, 10) > 0],
|
||||
[(v) => parseFloat(v), (v) => {
|
||||
const t = parseFloat(v);
|
||||
console.log(t);
|
||||
return t && !Number.isNaN(t) && Number.isFinite(t);
|
||||
}],
|
||||
[(v) => new ObjectID(v), ObjectID.isValid],
|
||||
@@ -117,8 +117,7 @@ export function param(name: string, ...args: any): MethodDecorator {
|
||||
if (args[cursor] !== null) v.validate = args[cursor];
|
||||
isValidate = false;
|
||||
} else {
|
||||
const I = args[cursor];
|
||||
v.convert = I;
|
||||
v.convert = args[cursor];
|
||||
}
|
||||
cursor++;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user