diff --git a/README.md b/README.md
index 3e4edb88f..361b6d53f 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
Hydro是一个高效的信息学在线测评系统。
特点: 易于部署,轻量,功能强大。
-[中文文档](wiki/deploy.md)
+[中文文档](wiki/admin/deploy.md)
Hydro 的界面基于 Vijos 二次开发。
diff --git a/hydro/error.js b/hydro/error.js
index f4762f0af..a2cffc692 100644
--- a/hydro/error.js
+++ b/hydro/error.js
@@ -1,254 +1,92 @@
-class UserFacingError extends Error {
- constructor(type) {
- super(type);
- this.code = 500;
- // this.stack = '';
- this.params = [];
- }
-}
+/* eslint-disable func-names */
+const { isClass } = require('./utils');
-class SystemError extends Error {
- constructor(type) {
- super(type);
- this.code = 500;
- // this.stack = '';
- this.params = [];
- }
-}
-
-class BadRequestError extends UserFacingError {
- constructor(type) {
- super(type);
- this.code = 400;
- }
-}
-
-class ForbiddenError extends UserFacingError {
- constructor(type) {
- super(type);
- this.code = 403;
- }
-}
-
-class NotFoundError extends UserFacingError {
- constructor(type) {
- super(type || 'NotFoundError');
- this.code = 404;
- }
-}
-
-class RemoteOnlineJudgeError extends UserFacingError {
- constructor(message) {
- super('RemoteOnlineJudgeError');
- this.params = [message];
- }
-}
-
-class AlreadyVotedError extends BadRequestError {
- constructor(psid, uid) {
- super('You\'ve already voted.');
- this.params = [psid, uid];
- }
-}
-
-class LoginError extends ForbiddenError {
- constructor(uname) {
- super('LoginError');
- this.params = [uname];
- }
-}
-
-class UserAlreadyExistError extends ForbiddenError {
- constructor(uname) {
- super('UserAlreadyExistError');
- this.params = [uname];
- }
-}
-
-class InvalidTokenError extends ForbiddenError {
- constructor(token) {
- super('InvalidTokenError');
- this.params = [token];
- }
-}
-
-class BlacklistedError extends ForbiddenError {
- constructor(ip) {
- super('Address {0} is blacklisted.');
- this.params = [ip];
- }
-}
-
-class UserNotFoundError extends NotFoundError {
- constructor(user) {
- super('UserNotFoundError');
- this.params = [user];
- }
-}
-
-class NoProblemError extends NotFoundError {
- constructor() {
- super('NoProblemError');
- }
-}
-
-class VerifyPasswordError extends ForbiddenError {
- constructor() {
- super('VerifyPasswordError');
- }
-}
-
-class OpcountExceededError extends ForbiddenError {
- constructor(op, periodSecs, maxOperations) {
- super('OpcountExceededError');
- this.params = [op, periodSecs, maxOperations];
- }
-}
-
-class PermissionError extends ForbiddenError {
- constructor(perm) {
- super('PermissionError');
- this.params = [perm];
- }
-}
-
-class PrivilegeError extends ForbiddenError {
- constructor(priv) {
- if (priv === global.Hydro.model.builtin.PRIV.PRIV_USER_PROFILE) {
- super("You're not logged in.");
- } else {
- super("You don't have the required privilege.");
+const Err = (name, ...classMessageCode) => {
+ let Class;
+ let msg;
+ let code;
+ for (const item of classMessageCode) {
+ if (typeof item === 'number') {
+ code = item;
+ } else if (typeof item === 'string') {
+ msg = function () { return item; };
+ } else if (isClass(item)) {
+ Class = item;
+ } else if (typeof item === 'function') {
+ msg = item;
}
- this.params = [priv];
+ }
+ const HydroError = class extends Class { };
+ HydroError.prototype.name = name;
+ if (msg) HydroError.prototype.msg = msg;
+ if (code) HydroError.prototype.code = code;
+ return HydroError;
+};
+
+class HydroError extends Error {
+ constructor(...params) {
+ super();
+ this.params = params;
}
}
-class ValidationError extends ForbiddenError {
- constructor(field0, field1) {
- super('ValidationError');
- if (!field1) this.params = [field0];
- else this.params = [field0, field1];
- }
-}
+const UserFacingError = Err('UserFacingError', HydroError, 'UserFacingError', 400);
+const SystemError = Err('SystemError', HydroError, 'SystemError', 500);
-class ContestNotAttendedError extends ForbiddenError {
- constructor(tid) {
- super('You haven\'t attended this contest yet.');
- this.params = [tid];
- }
-}
+const BadRequestError = Err('BadRequestError', UserFacingError, 400);
+const ForbiddenError = Err('ForbiddenError', UserFacingError, 403);
+const NotFoundError = Err('NotFoundError', UserFacingError, 404);
+const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingError, 500);
-class ContestAlreadyAttendedError extends ForbiddenError {
- constructor(tid, uid) {
- super('You\'ve already attended this contest.');
- this.params = [tid, uid];
+const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted.");
+const LoginError = Err('LoginError', ForbiddenError, 'Invalid password for user {0}.');
+const UserAlreadyExistError = Err('UserAlreadyExistError', ForbiddenError, 'User {0} already exists.');
+const InvalidTokenError = Err('InvalidTokenError', ForbiddenError);
+const BlacklistedError = Err('BlacklistedError', ForbiddenError, 'Address or user {0} is blacklisted.');
+const VerifyPasswordError = Err('VerifyPasswordError', ForbiddenError, "Passwords don't match.");
+const OpcountExceededError = Err('OpcountExceededError', ForbiddenError, 'Too frequent operations of {0} (limit: {2} operations in {1} seconds).');
+const PermissionError = Err('PermissionError', ForbiddenError, "You don't have the required permission ({0}) in this domain.");
+const PrivilegeError = Err('PrivilegeError', ForbiddenError, function () {
+ if (this.params.includes(global.Hydro.model.builtin.PRIV.PRIV_USER_PROFILE)) {
+ return "You're not logged in.";
}
-}
+ return "You don't have the required privilege.";
+});
+const ValidationError = Err('ValidationError', ForbiddenError, function () {
+ if (this.params.length === 1) return 'Field {0} validation failed.';
+ return 'Field {0} or {1} validation failed.';
+});
+const ContestNotAttendedError = Err('ContestNotAttendedError', ForbiddenError, "You haven't attended this contest yet.");
+const ContestAlreadyAttendedError = Err('ContestAlreadyAttendedError', ForbiddenError, "You've already attended this contest.");
+const ContestNotLiveError = Err('ContestNotLiveError', ForbiddenError, 'This contest is not live.');
+const ContestScoreboardHiddenError = Err('ContestScoreboardHiddenError', ForbiddenError, 'Contest scoreboard is not visible.');
+const TrainingAlreadyEnrollError = Err('TrainingAlreadyEnrollError', ForbiddenError, "You've already enrolled this training.");
+const RoleAlreadyExistError = Err('RoleAlreadyExistError', ForbiddenError, 'This role already exists.');
+const CsrfTokenError = Err('CsrfTokenError', ForbiddenError);
-class ContestNotLiveError extends ForbiddenError {
- constructor(tid) {
- super('This contest is not live.');
- this.params = [tid];
- }
-}
+const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
+const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
+const RecordNotFoundError = Err('RecordNotFoundError', NotFoundError, 'Record {0} not found.');
+const ProblemDataNotFoundError = Err('ProblemDataNotFoundError', NotFoundError, 'Data of problem {1} not found.');
+const MessageNotFoundError = Err('MessageNotFoundError', NotFoundError, 'Message {0} not found.');
+const DocumentNotFoundError = Err('DocumentNotFoundError', NotFoundError, 'Document {2} not found.');
-class ContestScoreboardHiddenError extends ForbiddenError {
- constructor(tid) {
- super('Contest scoreboard is not visible.');
- this.params = [tid];
- }
-}
-
-class TrainingAlreadyEnrollError extends ForbiddenError {
- constructor(tid, uid) {
- super("You've already enrolled this training.");
- this.params = [tid, uid];
- }
-}
-
-class RoleAlreadyExistError extends ForbiddenError {
- constructor(role) {
- super('This role already exists.');
- this.params = [role];
- }
-}
-
-class ProblemNotFoundError extends NotFoundError {
- constructor(domainId, pid) {
- super('ProblemNotFoundError');
- this.params = [domainId, pid];
- }
-}
-
-class RecordNotFoundError extends NotFoundError {
- constructor(rid) {
- super('RecordNotFoundError');
- this.params = [rid];
- }
-}
-
-class SolutionNotFoundError extends NotFoundError {
- constructor(psid) {
- super('SolutionNotFoundError');
- this.params = [psid];
- }
-}
-
-class TrainingNotFoundError extends NotFoundError {
- constructor(tid) {
- super('TrainingNotFoundError');
- this.params = [tid];
- }
-}
-
-class ContestNotFoundError extends NotFoundError {
- constructor(cid) {
- super('ContestNotFoundError');
- this.params = [cid];
- }
-}
-
-class ProblemDataNotFoundError extends NotFoundError {
- constructor(pid) {
- super('Data of problem {0} not found.');
- this.params = [pid];
- }
-}
-
-class DiscussionNodeNotFoundError extends NotFoundError {
- constructor(type, docId) {
- super('Discussion node {0}/{1} not found.');
- this.params = [type, docId];
- }
-}
-
-class DocumentNotFoundError extends NotFoundError {
- constructor(docId) {
- super('Document {0} not found.');
- this.params = [docId];
- }
-}
-
-class DiscussionNotFoundError extends NotFoundError {
- constructor(did) {
- super('Discussion {0} not found.');
- this.params = [did];
- }
-}
-
-class MessageNotFoundError extends NotFoundError {
- constructor(mid) {
- super('Message {0} not found.');
- this.params = [mid];
- }
-}
+const ProblemNotFoundError = Err('ProblemNotFountError', DocumentNotFoundError, 'Problem {1} not found.');
+const SolutionNotFoundError = Err('SolutionNotFoundError', DocumentNotFoundError);
+const TrainingNotFoundError = Err('TrainingNotFoundError', DocumentNotFoundError);
+const ContestNotFoundError = Err('ContestNotFoundError', DocumentNotFoundError);
+const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNotFoundError, 'Discussion {0} not found.');
+const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.');
global.Hydro.error = module.exports = {
+ Err,
+ HydroError,
BadRequestError,
BlacklistedError,
ForbiddenError,
NotFoundError,
LoginError,
+ CsrfTokenError,
UserAlreadyExistError,
InvalidTokenError,
UserNotFoundError,
@@ -291,9 +129,6 @@ class FileTypeNotAllowedError(ValidationError):
def message(self):
return 'This type of files are not allowed to be uploaded.'
-class CsrfTokenError(ForbiddenError):
- pass
-
class InvalidOperationError(ForbiddenError):
pass
diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js
index 8811cad2c..6178a34bd 100644
--- a/hydro/handler/manage.js
+++ b/hydro/handler/manage.js
@@ -1,16 +1,15 @@
const setting = require('../model/setting');
const system = require('../model/system');
-const { STATUS } = require('../model/builtin');
+const { STATUS, PRIV } = require('../model/builtin');
const record = require('../model/record');
const judge = require('./judge');
const { Route, Handler } = require('../service/server');
const hpm = require('../lib/hpm');
const loader = require('../loader');
-const { PermissionError } = require('../error');
class SystemHandler extends Handler {
async prepare() {
- if (!this.user.priv) throw new PermissionError('???');
+ this.checkPriv(PRIV.PRIV_EDIT_SYSTEM);
this.response.body = {
path: [
['Hydro', 'homepage'],
diff --git a/hydro/lib/nav.js b/hydro/lib/nav.js
index 339f24512..b20a34d73 100644
--- a/hydro/lib/nav.js
+++ b/hydro/lib/nav.js
@@ -42,8 +42,8 @@ Item('homework_main', null, 'homework', PERM.PERM_VIEW_HOMEWORK);
Item('discussion_main', null, 'discussion', PERM.PERM_VIEW_DISCUSSION);
Item('contest_main', null, 'contest', PERM.PERM_VIEW_CONTEST);
Item('record_main', null, 'record');
-Item('ranking', null, 'ranking');
+Item('ranking', null, 'ranking', PERM.PERM_VIEW_RANKING);
Item('domain_dashboard', null, 'domain', PERM.PERM_EDIT_DOMAIN);
-Item('manage_dashboard', null, 'manage', PRIV.PRIV_SET_PRIV);
+Item('manage_dashboard', null, 'manage', PRIV.PRIV_EDIT_SYSTEM);
global.Hydro.lib.nav = module.exports = Item;
diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js
index 8a4c2dca0..ba9a8f1ae 100644
--- a/hydro/model/builtin.js
+++ b/hydro/model/builtin.js
@@ -160,7 +160,8 @@ PERM.PERM_BASIC = PERM.PERM_VIEW
+ PERM.PERM_VIEW_CONTEST_SCOREBOARD
+ PERM.PERM_VIEW_HOMEWORK
+ PERM.PERM_VIEW_HOMEWORK_SCOREBOARD
- + PERM.PERM_VIEW_TRAINING;
+ + PERM.PERM_VIEW_TRAINING
+ + PERM.PERM_VIEW_RANKING;
PERM.PERM_DEFAULT = PERM.PERM_VIEW
+ PERM.PERM_VIEW_PROBLEM
@@ -203,7 +204,8 @@ PERM.PERM_DEFAULT = PERM.PERM_VIEW
+ PERM.PERM_REPLY_DISCUSSION
+ PERM.PERM_ATTEND_CONTEST
+ PERM.PERM_CREATE_TRAINING
- + PERM.PERM_ATTEND_HOMEWORK;
+ + PERM.PERM_ATTEND_HOMEWORK
+ + PERM.PERM_VIEW_RANKING;
PERM.PERM_ADMIN = PERM.PERM_ALL;
diff --git a/hydro/model/token.js b/hydro/model/token.js
index e26de3027..2d79d1495 100644
--- a/hydro/model/token.js
+++ b/hydro/model/token.js
@@ -53,14 +53,17 @@ async function get(tokenId, tokenType) {
*/
async function update(tokenId, tokenType, expireSeconds, data) {
const now = new Date();
- const res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, {
- $set: {
- ...data,
- updateAt: now,
- expireAt: new Date(now.getTime() + expireSeconds * 1000),
- tokenType,
+ const res = await coll.updateOne(
+ { _id: tokenId, tokenType },
+ {
+ $set: {
+ ...data,
+ updateAt: now,
+ expireAt: new Date(now.getTime() + expireSeconds * 1000),
+ tokenType,
+ },
},
- });
+ );
return res.value;
}
diff --git a/hydro/service/server.js b/hydro/service/server.js
index a1fef27a8..24bf37be6 100644
--- a/hydro/service/server.js
+++ b/hydro/service/server.js
@@ -22,6 +22,7 @@ const opcount = require('../model/opcount');
const {
UserNotFoundError, BlacklistedError, PermissionError,
UserFacingError, ValidationError, PrivilegeError,
+ CsrfTokenError,
} = require('../error');
let enableLog = true;
@@ -282,6 +283,11 @@ class Handler {
this.preferJson = (this.request.headers.accept || '').includes('application/json');
}
+ async checkCsrfToken(csrfToken) {
+ const sdoc = await token.get(csrfToken, token.TYPE_CSRF_TOKEN);
+ if (!sdoc || sdoc.uid !== this.user._id) throw new CsrfTokenError(csrfToken);
+ }
+
async ___cleanup() {
try {
await this.renderBody();
@@ -361,12 +367,12 @@ class Handler {
}
async onerror(error) {
- console.error(error.message, error.params);
+ console.error(error.msg(), error.params);
console.error(error.stack);
this.response.status = error instanceof UserFacingError ? error.code : 500;
this.response.template = error instanceof UserFacingError ? 'error.html' : 'bsod.html';
this.response.body = {
- error: { message: error.message, params: error.params, stack: error.stack },
+ error: { message: error.msg(), params: error.params, stack: error.stack },
};
await this.___cleanup().catch(() => { });
}
@@ -384,6 +390,7 @@ async function handle(ctx, HandlerClass, checker) {
if (h.___prepare) await h.___prepare(args);
if (checker) checker.call(h);
+ if (method === 'post') await h.checkCsrfToken(args.csrfToken);
let checking = '';
try {
@@ -421,10 +428,16 @@ async function handle(ctx, HandlerClass, checker) {
}
}
+const Checker = (perm, priv, checker) => function __() {
+ checker();
+ if (perm) this.checkPerm(perm);
+ if (priv) this.checkPriv(priv);
+};
+
function Route(name, route, RouteHandler, ...permPrivChecker) {
let _priv;
let _perm;
- let checker;
+ let checker = () => { };
for (const item of permPrivChecker) {
if (typeof item === 'object') {
if (typeof item.call !== 'undefined') {
@@ -440,17 +453,7 @@ function Route(name, route, RouteHandler, ...permPrivChecker) {
_perm = item;
}
}
- if ((_perm || _priv) && checker) {
- checker = ((_chk) => function __() {
- _chk();
- if (_perm) this.checkPerm(_perm);
- if (_priv) this.checkPriv(_priv);
- })(checker);
- } else if (_perm) {
- checker = function __() {
- this.checkPerm(_perm);
- };
- }
+ checker = Checker(_perm, _priv, checker);
router.all(name, route, (ctx) => handle(ctx, RouteHandler, checker));
router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker));
}
diff --git a/hydro/utils.js b/hydro/utils.js
index 2d0aac066..ccaad2614 100644
--- a/hydro/utils.js
+++ b/hydro/utils.js
@@ -130,3 +130,27 @@ function parseMemoryMB(str) {
}
exports.parseMemoryMB = parseMemoryMB;
+
+/**
+ * Checks if an object could be an instantiable class.
+ * @param {any} obj
+ * @param {boolean} strict
+ * @returns {boolean}
+ */
+function isClass(obj, strict = false) {
+ if (typeof obj !== 'function') return false;
+ const str = obj.toString();
+ if (obj.prototype === undefined) return false;
+ if (obj.prototype.constructor !== obj) return false;
+ if (str.slice(0, 5) === 'class') return true;
+ if (Object.getOwnPropertyNames(obj.prototype).length >= 2) return true;
+ if (/^function\s+\(|^function\s+anonymous\(/.test(str)) return false;
+ if (strict && /^function\s+[A-Z]/.test(str)) return true;
+ if (/\b\(this\b|\bthis[.[]\b/.test(str)) {
+ if (!strict || /classCallCheck\(this/.test(str)) return true;
+ return /^function\sdefault_\d+\s*\(/.test(str);
+ }
+ return false;
+}
+
+exports.isClass = isClass;
diff --git a/templates/bsod.html b/templates/bsod.html
index 3d4654118..fe2c3bd5a 100644
--- a/templates/bsod.html
+++ b/templates/bsod.html
@@ -25,7 +25,8 @@
return 0;
}
/*
-{{error.stack}}
+{{ error.message }}
+{{ error.stack }}
*/