Hello airbnb

This commit is contained in:
masnn committed 2020-04-15 15:08:05 +08:00
1 parent f9468536de
commit 8c195661d7
78 files changed
+2032 -1471

No files matched your search

+29
View File
@@ -0,0 +1,29 @@
module.exports = {
env: {
commonjs: true,
node: true,
},
extends: [
'airbnb-base',
],
globals: {
Atomics: 'readonly',
SharedArrayBuffer: 'readonly',
},
parserOptions: {
ecmaVersion: 2018,
},
rules: {
"indent": ["warn", 4],
"no-plusplus": "off",
"no-underscore-dangle": "off",
"no-console": "off",
"no-extend-native": "off",
"no-restricted-syntax": "off",
"max-classes-per-file": "off",
"radix": "off",
"guard-for-in": "off",
"no-param-reassign": "off",
"global-require": "off"
},
};
-36
View File
@@ -1,36 +0,0 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"indent": [
"warn",
4
],
"linebreak-style": [
"warn",
"unix"
],
"quotes": [
"warn",
"single"
],
"semi": [
"warn",
"always"
],
"no-unused-vars": "warn",
"no-unreachable": "warn",
"require-atomic-updates": "off"
}
}
+15 -14
View File
@@ -1,28 +1,21 @@
process.stdin.setEncoding('utf8');
process.stdin.on('data', async input => {
process.stdin.on('data', async (input) => {
try {
let t = eval(input.toString().trim());
const t = eval(input.toString().trim()); // eslint-disable-line no-eval
if (t instanceof Promise) console.log(await t);
else console.log(t);
} catch (e) {
console.warn(e);
}
});
process.on('restart', async () => {
console.log('Signal detected, restarting...');
await global.Hydro.stop();
await global.Hydro.destory();
delete global.Hydro;
delete require.cache;
run();
});
const path = require('path');
const EventEmitter = require('events');
const i18n = require('./lib/i18n');
i18n(path.resolve(__dirname, '..', 'locales', 'zh_CN.yaml'), 'zh_CN');
i18n(path.resolve(__dirname, '..', 'locales', 'zh_TW.yaml'), 'zh_TW');
i18n(path.resolve(__dirname, '..', 'locales', 'en.yaml'), 'en');
const EventEmitter = require('events');
global.bus = new EventEmitter();
async function run() {
require('./utils');
@@ -34,7 +27,7 @@ async function run() {
});
});
require('./service/gridfs');
let server = require('./service/server');
const server = require('./service/server');
require('./handler/home');
require('./handler/problem');
require('./handler/record');
@@ -43,7 +36,15 @@ async function run() {
require('./handler/contest');
server.start();
}
run().catch(e => {
process.on('restart', async () => {
console.log('Signal detected, restarting...');
await global.Hydro.stop();
await global.Hydro.destory();
delete global.Hydro;
delete require.cache;
run();
});
run().catch((e) => {
console.error(e);
process.exit(1);
});
});
+29 -15
View File
@@ -2,7 +2,7 @@ class UserFacingError extends Error {
constructor(type) {
super(type);
this.code = 500;
//this.stack = '';
// this.stack = '';
this.params = [];
}
}
@@ -75,9 +75,9 @@ class VerifyPasswordError extends ForbiddenError {
}
}
class OpcountExceededError extends ForbiddenError {
constructor(op, period_secs, max_operations) {
constructor(op, periodSecs, maxOperations) {
super('OpcountExceededError');
this.params = [op, period_secs, max_operations];
this.params = [op, periodSecs, maxOperations];
}
}
@@ -155,15 +155,31 @@ class ProblemDataNotFoundError extends NotFoundError {
}
}
module.exports = {
BadRequestError, ForbiddenError, NotFoundError,
LoginError, UserAlreadyExistError, InvalidTokenError,
UserNotFoundError, VerifyPasswordError, ProblemDataNotFoundError,
OpcountExceededError, PermissionError, NoProblemError,
ValidationError, ProblemNotFoundError, TrainingNotFoundError,
ContestNotFoundError, RecordNotFoundError, SolutionNotFoundError,
AlreadyVotedError, ContestNotAttendedError, ContestNotLiveError,
ContestScoreboardHiddenError, ContestAlreadyAttendedError, UserFacingError,
SystemError
BadRequestError,
ForbiddenError,
NotFoundError,
LoginError,
UserAlreadyExistError,
InvalidTokenError,
UserNotFoundError,
VerifyPasswordError,
ProblemDataNotFoundError,
OpcountExceededError,
PermissionError,
NoProblemError,
ValidationError,
ProblemNotFoundError,
TrainingNotFoundError,
ContestNotFoundError,
RecordNotFoundError,
SolutionNotFoundError,
AlreadyVotedError,
ContestNotAttendedError,
ContestNotLiveError,
ContestScoreboardHiddenError,
ContestAlreadyAttendedError,
UserFacingError,
SystemError,
};
/*
@@ -201,7 +217,6 @@ class UserFacingError(Error):
return 'An error has occurred.'
class BlacklistedError(ForbiddenError):
@property
def message(self):
@@ -240,7 +255,6 @@ class InvalidOperationError(ForbiddenError):
pass
class InvalidTokenDigestError(ForbiddenError):
pass
@@ -374,4 +388,4 @@ class SendMailError(UserFacingError):
@property
def message(self):
return 'Failed to send mail to {0}.'
*/
*/
+114 -78
View File
@@ -1,20 +1,23 @@
const
{ ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError } = require('../error'),
{ PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission'),
{ constants } = require('../options'),
paginate = require('../lib/paginate'),
contest = require('../model/contest'),
problem = require('../model/problem'),
record = require('../model/record'),
user = require('../model/user'),
{ Route, Handler } = require('../service/server');
const {
ContestNotLiveError, ValidationError, ProblemNotFoundError,
ContestNotAttendedError,
} = require('../error');
const { PERM_CREATE_CONTEST, PERM_EDIT_CONTEST } = require('../permission');
const { constants } = require('../options');
const paginate = require('../lib/paginate');
const contest = require('../model/contest');
const problem = require('../model/problem');
const record = require('../model/record');
const user = require('../model/user');
const { Route, Handler } = require('../service/server');
const ContestHandler = contest.ContestHandlerMixin(Handler);
class ContestListHandler extends ContestHandler {
async get({ rule = 0, page = 1 }) {
this.response.template = 'contest_main.html';
let tdocs, qs, tpcount;
let tdocs;
let qs;
let tpcount;
if (!rule) {
tdocs = contest.getMulti().sort({ beginAt: -1 });
qs = '';
@@ -23,12 +26,13 @@ class ContestListHandler extends ContestHandler {
tdocs = contest.getMulti({ rule }).sort({ beginAt: -1 });
qs = 'rule={0}'.format(rule);
}
// eslint-disable-next-line prefer-const
[tdocs, tpcount] = await paginate(tdocs, page, constants.CONTEST_PER_PAGE);
let tids = [];
for (let tdoc of tdocs) tids.push(tdoc._id);
let tsdict = await contest.getListStatus(this.user._id, tids);
const tids = [];
for (const tdoc of tdocs) tids.push(tdoc._id);
const tsdict = await contest.getListStatus(this.user._id, tids);
this.response.body = {
page, tpcount, qs, rule, tdocs, tsdict
page, tpcount, qs, rule, tdocs, tsdict,
};
}
}
@@ -36,35 +40,35 @@ class ContestDetailHandler extends ContestHandler {
async _prepare({ tid }) {
this.tdoc = await contest.get(tid);
}
async get({ page = 1 }) {
this.response.template = 'contest_detail.html';
let [tsdoc, pdict] = await Promise.all([
const [tsdoc, pdict] = await Promise.all([
contest.getStatus(this.tdoc._id, this.user._id),
problem.getList(this.tdoc.pids)
problem.getList(this.tdoc.pids),
]);
let psdict = {}, rdict = {}, attended;
const psdict = {}; let rdict = {}; let
attended;
if (tsdoc) {
attended = tsdoc.attend == 1;
for (let pdetail of tsdoc.journal || [])
psdict[pdetail.pid] = pdetail;
attended = tsdoc.attend === 1;
for (const pdetail of tsdoc.journal || []) { psdict[pdetail.pid] = pdetail; }
if (this.canShowRecord(this.tdoc)) {
let q = [];
for (let i in psdict) q.push(psdict[i].rid);
const q = [];
for (const i in psdict) q.push(psdict[i].rid);
rdict = await record.getList(q);
} else
for (let i in psdict)
rdict[psdict[i].rid] = { _id: psdict[i].rid };
} else { for (const i in psdict) rdict[psdict[i].rid] = { _id: psdict[i].rid }; }
} else attended = false;
let udict = await user.getList([this.tdoc.owner]);
let path = [
const udict = await user.getList([this.tdoc.owner]);
const path = [
['contest_main', '/c'],
[this.tdoc.title, null, true]
[this.tdoc.title, null, true],
];
this.response.body = {
path, tdoc: this.tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page
path, tdoc: this.tdoc, tsdoc, attended, udict, pdict, psdict, rdict, page,
};
}
async post_attend() {
async postAttend() {
if (contest.is_done(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
await contest.attend(this.tdoc._id, this.user._id);
this.back();
@@ -72,108 +76,136 @@ class ContestDetailHandler extends ContestHandler {
}
class ContestScoreboardHandler extends ContestDetailHandler {
async get({ tid }) {
let [tdoc, rows, udict] = await this.getScoreboard(tid);
let path = [
const [tdoc, rows, udict] = await this.getScoreboard(tid);
const path = [
['contest_main', '/c'],
[tdoc.title, `/c/${tid}`, true],
['contest_scoreboard', null]
['contest_scoreboard', null],
];
this.response.template = 'contest_scoreboard.html';
this.response.body = { tdoc, rows, path, udict };
this.response.body = {
tdoc, rows, path, udict,
};
}
}
class ContestEditHandler extends ContestDetailHandler {
async prepare({ tid }) {
let tdoc = await contest.get(tid);
if (!tdoc.owner != this.user._id) this.checkPerm(PERM_EDIT_CONTEST);
const tdoc = await contest.get(tid);
if (!tdoc.owner !== this.user._id) this.checkPerm(PERM_EDIT_CONTEST);
}
async get() {
this.response.template = 'contest_edit.html';
let rules = {};
for (let i in contest.RULES)
rules[i] = contest.RULES[i].TEXT;
let duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
let path = [
const rules = {};
for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
const duration = (this.tdoc.endAt.getTime() - this.tdoc.beginAt.getTime()) / 3600 / 1000;
const path = [
['contest_main', '/c'],
[this.tdoc.title, `/c/${this.tdoc._id}`, true],
['contest_edit', null]
['contest_edit', null],
];
let dt = this.tdoc.beginAt;
const dt = this.tdoc.beginAt;
this.response.body = {
rules, tdoc: this.tdoc, duration, path, pids: this.tdoc.pids.join(','),
rules,
tdoc: this.tdoc,
duration,
path,
pids: this.tdoc.pids.join(','),
date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`,
time_text: `${dt.getHours()}:${dt.getMinutes()}`,
page_name: 'contest_edit'
page_name: 'contest_edit',
};
}
async post({ beginAtDate, beginAtTime, duration, title, content, rule, pids }) {
let beginAt, endAt;
async post({
beginAtDate, beginAtTime, duration, title, content, rule, pids,
}) {
let beginAt;
try {
beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`));
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
endAt = new Date(beginAt + duration * 3600 * 1000);
const endAt = new Date(beginAt + duration * 3600 * 1000);
if (beginAt >= endAt) throw new ValidationError('duration');
pids = await this.verifyProblems(pids);
await contest.edit(this.tdoc._id, title, content, rule, beginAt, endAt, pids);
if (this.tdoc.beginAt != beginAt || this.tdoc.endAt != endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule != rule)
if (this.tdoc.beginAt !== beginAt || this.tdoc.endAt !== endAt
|| Array.isDiff(this.tdoc.pids, pids) || this.tdoc.rule !== rule) {
await contest.recalcStatus(this.tdoc._id);
}
if (this.preferJson) this.response.body = { tid: this.tdoc._id };
else this.response.redirect = `/c/${this.tdoc._id}`;
}
}
class ContestProblemHandler extends ContestDetailHandler {
async prepare({ tid, pid }) {
[this.tdoc, this.pdoc] = await Promise.all([contest.get(tid), problem.get(pid, this.user._id)]);
[this.tdoc, this.pdoc] = await Promise.all([
contest.get(tid),
problem.get(pid, this.user._id),
]);
[this.tsdoc, this.udoc] = await Promise.all([
contest.getStatus(this.tdoc._id, this.user._id),
user.getById(this.tdoc.owner)
user.getById(this.tdoc.owner),
]);
this.attended = this.tsdoc && this.tsdoc.attend == 1;
this.attended = this.tsdoc && this.tsdoc.attend === 1;
this.response.template = 'problem_detail.html';
if (contest.is_done(this.tdoc)) {
if (!this.attended) throw new ContestNotAttendedError(this.tdoc._id);
if (!contest.is_ongoing(this.tdoc)) throw new ContestNotLiveError(this.tdoc._id);
}
console.log(this.tdoc.pids, this.pdoc._id);
if (!this.tdoc.pids.map(s => s.toString()).includes(this.pdoc._id.toString()))
if (!this.tdoc.pids.map((s) => s.toString()).includes(this.pdoc._id.toString())) {
throw new ProblemNotFoundError(pid, this.tdoc._id);
}
}
async get({ tid }) {
let path = [
const path = [
['contest_main', '/c'],
[this.tdoc.title, `/c/${tid}`, true],
[this.pdoc.title, null, true]
[this.pdoc.title, null, true],
];
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc,
udoc: this.udoc, attended: this.attended, path
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attended,
path,
};
}
}
class ContestProblemSubmitHandler extends ContestProblemHandler {
async get({ tid, pid }) {
let rdocs = [];
if (this.canShowRecord(this.tdoc))
if (this.canShowRecord(this.tdoc)) {
rdocs = await record.getUserInProblemMulti(this.user._id, this.pdoc._id, true)
.sort({ _id: -1 }).limit(10).toArray();
}
this.response.template = 'problem_submit.html';
let path = [
const path = [
['contest_main', '/c'],
[this.tdoc.title, `/c/${tid}`, true],
[this.pdoc.title, `/c/${tid}/p/${pid}`, true],
['contest_detail_problem_submit', null]
['contest_detail_problem_submit', null],
];
this.response.body = {
tdoc: this.tdoc, pdoc: this.pdoc, tsdoc: this.tsdoc,
udoc: this.udoc, attended: this.attend, path, rdocs
tdoc: this.tdoc,
pdoc: this.pdoc,
tsdoc: this.tsdoc,
udoc: this.udoc,
attended: this.attend,
path,
rdocs,
};
}
async post({ tid, lang, code }) {
await this.limitRate('add_record', 60, 100);
let rid = await record.add({ pid: this.pdoc._id, uid: this.user._id, lang, code, tid: this.tdoc._id, hidden: true });
const rid = await record.add({
pid: this.pdoc._id, uid: this.user._id, lang, code, tid: this.tdoc._id, hidden: true,
});
await contest.updateStatus(this.tdoc._id, this.user._id, rid, this.pdoc._id, false, 0);
if (!this.canShowRecord(this.tdoc)) {
this.response.body = { tid };
@@ -188,33 +220,37 @@ class ContestCreateHandler extends ContestHandler {
async prepare() {
this.checkPerm(PERM_CREATE_CONTEST);
}
async get() {
this.response.template = 'contest_edit.html';
let rules = {};
for (let i in contest.RULES)
rules[i] = contest.RULES[i].TEXT;
let now = new Date();
const rules = {};
for (const i in contest.RULES) { rules[i] = contest.RULES[i].TEXT; }
const now = new Date();
let ts = now.getTime();
ts = ts - ts % (15 * 60 * 1000) + 15 * 60 * 1000;
let dt = new Date(ts);
ts = ts - (ts % (15 * 60 * 1000)) + 15 * 60 * 1000;
const dt = new Date(ts);
this.response.body = {
rules, page_name: 'contest_create',
rules,
page_name: 'contest_create',
date_text: `${dt.getFullYear()}-${dt.getMonth() + 1}-${dt.getDate()}`,
time_text: `${dt.getHours()}:${dt.getMinutes()}`,
pids: '1000, 1001'
pids: '1000, 1001',
};
}
async post({ title, content, rule, beginAtDate, beginAtTime, duration, pids }) {
let beginAt, endAt;
async post({
title, content, rule, beginAtDate, beginAtTime, duration, pids,
}) {
let beginAt;
try {
beginAt = new Date(Date.parse(`${beginAtDate} ${beginAtTime.replace('-', ':')}`));
} catch (e) {
throw new ValidationError('beginAtDate', 'beginAtTime');
}
endAt = new Date(beginAt.getTime() + duration * 3600 * 1000);
const endAt = new Date(beginAt.getTime() + duration * 3600 * 1000);
if (beginAt >= endAt) throw new ValidationError('duration');
pids = await this.verifyProblems(pids);
let tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids);
const tid = await contest.add(title, content, this.user._id, rule, beginAt, endAt, pids);
this.response.body = { tid };
this.response.redirect = `/c/${tid}`;
}
+33 -25
View File
@@ -1,16 +1,17 @@
const
{ PERM_JUDGE } = require('../permission'),
record = require('../model/record'),
problem = require('../model/problem'),
bus = require('../service/bus'),
queue = require('../service/queue'),
{ Route, Handler, Connection, ConnectionHandler } = require('../service/server');
const { PERM_JUDGE } = require('../permission');
const record = require('../model/record');
const problem = require('../model/problem');
const bus = require('../service/bus');
const queue = require('../service/queue');
const {
Route, Handler, Connection, ConnectionHandler,
} = require('../service/server');
queue.assert('judge');
async function next(body) {
let rdoc = await record.get(body.rid);
let $set = {};
const $set = {};
if (body.case) {
rdoc.cases.push(body.case);
$set.cases = rdoc.cases;
@@ -32,7 +33,7 @@ async function next(body) {
}
async function end(body) {
let rdoc = await record.get(body.rid);
let $set = {};
const $set = {};
if (body.case) {
rdoc.cases.push(body.case);
$set.cases = rdoc.cases;
@@ -59,29 +60,33 @@ class JudgeHandler extends Handler {
async prepare() {
this.checkPerm(PERM_JUDGE);
}
async get({ check = false }) {
this.response.body = {};
if (check) return;
let rid = await queue.get('judge', false);
const rid = await queue.get('judge', false);
if (rid) {
let rdoc = await record.get(rid);
let pdoc = await problem.getById(rdoc.pid);
let task = {
const rdoc = await record.get(rid);
const pdoc = await problem.getById(rdoc.pid);
const task = {
event: 'judge',
rid, type: 0,
rid,
type: 0,
pid: rdoc.pid,
data: pdoc.data,
lang: rdoc.lang,
code: rdoc.code
code: rdoc.code,
};
this.response.body = { task };
}
}
async post_next() {
async postNext() {
await next(this.request.body);
this.response.body = {};
}
async post_end() {
async postEnd() {
await end(this.request.body);
this.response.body = {};
}
@@ -90,26 +95,29 @@ class JudgeConnectionHandler extends ConnectionHandler {
async prepare() {
this.checkPerm(PERM_JUDGE);
}
async message(msg) {
if (msg.key == 'next') await next(msg);
else if (msg.key == 'end') {
if (msg.key === 'next') await next(msg);
else if (msg.key === 'end') {
await end({ judger: this.user._id, ...msg });
this.processing = null;
let rid = await queue.get('judge');
let rdoc = await record.get(rid);
let pdoc = await problem.getById(rdoc.pid);
let task = {
const rid = await queue.get('judge');
const rdoc = await record.get(rid);
const pdoc = await problem.getById(rdoc.pid);
const task = {
event: 'judge',
rid, type: 0,
rid,
type: 0,
pid: rdoc.pid,
data: pdoc.data,
lang: rdoc.lang,
code: rdoc.code
code: rdoc.code,
};
this.send({ task });
this.processing = task.rid;
}
}
async cleanup() {
if (this.processing) {
await record.reset(this.processing);
+121 -89
View File
@@ -1,23 +1,26 @@
const
fs = require('fs'),
paginate = require('../lib/paginate'),
validator = require('../lib/validator'),
problem = require('../model/problem'),
record = require('../model/record'),
user = require('../model/user'),
solution = require('../model/solution'),
system = require('../model/system'),
{ Route, Handler } = require('../service/server'),
gridfs = require('../service/gridfs'),
queue = require('../service/queue'),
{ NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError } = require('../error'),
{ constants } = require('../options'),
{ PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM,
PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM,
PERM_JUDGE, PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION,
PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY,
PERM_REPLY_PROBLEM_SOLUTION } = require('../permission');
const fs = require('fs');
const paginate = require('../lib/paginate');
const validator = require('../lib/validator');
const problem = require('../model/problem');
const record = require('../model/record');
const user = require('../model/user');
const solution = require('../model/solution');
const system = require('../model/system');
const { Route, Handler } = require('../service/server');
const gridfs = require('../service/gridfs');
const queue = require('../service/queue');
const {
NoProblemError, ProblemDataNotFoundError, BadRequestError,
SolutionNotFoundError,
} = require('../error');
const { constants } = require('../options');
const {
PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, PERM_SUBMIT_PROBLEM,
PERM_CREATE_PROBLEM, PERM_READ_PROBLEM_DATA, PERM_EDIT_PROBLEM,
PERM_JUDGE, PERM_VIEW_PROBLEM_SOLUTION, PERM_CREATE_PROBLEM_SOLUTION,
PERM_EDIT_PROBLEM_SOLUTION, PERM_DELETE_PROBLEM_SOLUTION, PERM_EDIT_PROBLEM_SOLUTION_REPLY,
PERM_REPLY_PROBLEM_SOLUTION,
} = require('../permission');
queue.assert('judge');
@@ -25,27 +28,30 @@ class ProblemHandler extends Handler {
async _prepare() {
this.checkPerm(PERM_VIEW_PROBLEM);
}
async get({ page = 1, category = null }) {
this.response.template = 'problem_main.html';
let q = {};
const q = {};
if (category) q.category = category;
if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
let pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE);
const pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE);
this.response.body = {
path: [
['Hydro', '/'],
['problem_main', null]
['problem_main', null],
],
page, pdocs, category: ''
page,
pdocs,
category: '',
};
}
}
class ProblemRandomHandler extends ProblemHandler {
async get() {
let q = {};
const q = {};
if (!this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false;
let pid = await problem.random(q);
const pid = await problem.random(q);
if (!pid) throw new NoProblemError();
this.response.body = { pid };
}
@@ -57,19 +63,20 @@ class ProblemDetailHandler extends ProblemHandler {
this.uid = this.user._id;
this.pid = pid;
if (pid) this.pdoc = await problem.get(pid, this.uid);
if (this.pdoc.hidden && this.pdoc.owner != this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
if (this.pdoc.hidden && this.pdoc.owner !== this.uid) this.checkPerm(PERM_VIEW_PROBLEM_HIDDEN);
if (this.pdoc) this.udoc = await user.getById(this.pdoc.owner);
this.response.body = {
pdoc: this.pdoc,
udoc: this.udoc,
title: (this.pdoc || {}).title || ''
title: (this.pdoc || {}).title || '',
};
}
async get() {
this.response.body.path = [
['Hydro', '/'],
['problem_main', '/p'],
[this.pdoc.title, null, true]
[this.pdoc.title, null, true],
];
}
}
@@ -78,26 +85,31 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
async prepare() {
this.checkPerm(PERM_SUBMIT_PROBLEM);
}
async get() {
this.response.template = 'problem_submit.html';
let rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id).sort({ _id: -1 }).limit(10).toArray();
const rdocs = await record.getUserInProblemMulti(this.uid, this.pdoc._id)
.sort({ _id: -1 })
.limit(10)
.toArray();
this.response.body = {
path: [
['Hydro', '/'],
['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pid}`, true],
['problem_submit', null]
['problem_submit', null],
],
pdoc: this.pdoc,
udoc: this.udoc,
rdocs,
title: this.pdoc.title
title: this.pdoc.title,
};
}
async post() {
let { lang, code } = this.request.body;
let rid = await record.add({
uid: this.uid, lang, code, pid: this.pdoc._id
const { lang, code } = this.request.body;
const rid = await record.add({
uid: this.uid, lang, code, pid: this.pdoc._id,
});
await queue.push('judge', rid);
this.user.nSubmit++;
@@ -108,7 +120,7 @@ class ProblemSubmitHandler extends ProblemDetailHandler {
class ProblemManageHandler extends ProblemDetailHandler {
async prepare() {
if (this.pdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM);
if (this.pdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM);
}
}
@@ -119,9 +131,10 @@ class ProblemSettingsHandler extends ProblemManageHandler {
['Hydro', '/'],
['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pid}`, true],
['problem_settings', null]
['problem_settings', null],
];
}
async post() {
// TODO(masnn)
this.back();
@@ -135,43 +148,45 @@ class ProblemEditHandler extends ProblemManageHandler {
['Hydro', '/'],
['problem_main', '/p'],
[this.pdoc.title, `/p/${this.request.params.pid}`, true],
['problem_edit', null]
['problem_edit', null],
];
this.response.body.page_name = 'problem_edit';
}
async post({ title, content }) {
let pid = validator.checkPid(this.request.body.pid);
let pdoc = await problem.get(this.params.pid);
const pid = validator.checkPid(this.request.body.pid);
const pdoc = await problem.get(this.params.pid);
await problem.edit(pdoc._id, { title, content, pid });
this.response.redirect = `/p/${pid}`;
}
}
class ProblemDataUploadHandler extends ProblemManageHandler {
async prepare(){
async prepare() {
this.response.template = 'problem_upload.html';
}
async get() {
if (this.pdoc.data && typeof this.pdoc.data == 'object') {
let files = await gridfs.find({ _id: this.pdoc.data }).toArray();
if (this.pdoc.data && typeof this.pdoc.data === 'object') {
const files = await gridfs.find({ _id: this.pdoc.data }).toArray();
this.md5 = files[0].md5;
}
this.response.body.md5 = this.md5;
}
async post() {
if (!this.request.files.file) throw new BadRequestError();
const r = fs.createReadStream(this.request.files.file.path);
let f = gridfs.openUploadStream('data.zip');
const f = gridfs.openUploadStream('data.zip');
await new Promise((resolve, reject) => {
r.pipe(f);
f.once('finish', resolve);
f.once('error', reject);
});
if (this.pdoc.data && typeof this.pdoc.data == 'object')
gridfs.delete(this.pdoc.data);
if (this.pdoc.data && typeof this.pdoc.data === 'object') { gridfs.delete(this.pdoc.data); }
this.pdoc = await problem.edit(this.pdoc._id, { data: f.id });
if (this.pdoc.data && typeof this.pdoc.data == 'object') {
let files = await gridfs.find({ _id: this.pdoc.data }).toArray();
if (this.pdoc.data && typeof this.pdoc.data === 'object') {
const files = await gridfs.find({ _id: this.pdoc.data }).toArray();
this.md5 = files[0].md5;
}
this.response.body.md5 = this.md5;
@@ -180,9 +195,9 @@ class ProblemDataUploadHandler extends ProblemManageHandler {
class ProblemDataDownloadHandler extends ProblemDetailHandler {
async get({ pid }) {
if (this.uid != this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]);
if (this.uid !== this.pdoc.owner) this.checkPerm([PERM_READ_PROBLEM_DATA, PERM_JUDGE]);
if (!this.pdoc.data) throw new ProblemDataNotFoundError(pid);
else if (typeof this.pdoc.data == 'string') this.ctx.setRedirect = this.pdoc.data.split('from:')[1];
else if (typeof this.pdoc.data === 'string') [, this.ctx.setRedirect] = this.pdoc.data.split('from:');
this.response.attachment(`${this.pdoc.title}.zip`);
this.response.body = gridfs.openDownloadStream(this.pdoc.data);
}
@@ -192,70 +207,81 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
async get({ page = 1 }) {
this.response.template = 'problem_solution.html';
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let [psdocs, pcount, pscount] = await paginate(solution.getMulti(this.pdoc._id), page, constants.SOLUTION_PER_PAGE);
let uids = [this.pdoc.owner], docids = [];
for (let psdoc of psdocs) {
const [psdocs, pcount, pscount] = await paginate(
solution.getMulti(this.pdoc._id),
page, constants.SOLUTION_PER_PAGE,
);
const uids = [this.pdoc.owner]; const
docids = [];
for (const psdoc of psdocs) {
docids.push(psdoc._id);
uids.push(psdoc.owner);
if (psdoc.reply.length)
for (let psrdoc of psdoc.reply)
uids.push(psrdoc.owner);
if (psdoc.reply.length) { for (const psrdoc of psdoc.reply) uids.push(psrdoc.owner); }
}
let udict = await user.getList(uids);
let pssdict = solution.getListStatus(docids, this.uid);
let path = [
const udict = await user.getList(uids);
const pssdict = solution.getListStatus(docids, this.uid);
const path = [
['problem_main', '/p'],
[this.pdoc.title, `/p/${this.pdoc.pid}`, true],
['problem_solution', null]
['problem_solution', null],
];
this.response.body = {
path, psdocs, page, pcount, pscount, udict, pssdict
path, psdocs, page, pcount, pscount, udict, pssdict,
};
}
async post({ psid }) {
if (psid) this.psdoc = await solution.get(psid);
}
async post_submit({ content }) {
async postSubmit({ content }) {
this.checkPerm(PERM_CREATE_PROBLEM_SOLUTION);
await solution.add(this.pdoc._id, this.uid, content);
this.back();
}
async post_edit_solution({ content }) {
if (this.psdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION);
async postEditSolution({ content }) {
if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION);
this.psdoc = await solution.edit(this.psdoc._id, content);
this.ctx.body.psdoc = this.psdoc;
this.back();
}
async post_delete_solution() {
if (this.psdoc.owner != this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION);
async postDeleteSolution() {
if (this.psdoc.owner !== this.uid) this.checkPerm(PERM_DELETE_PROBLEM_SOLUTION);
await solution.del(this.psdoc._id);
this.back();
}
async post_reply({ psid, content }) {
async postReply({ psid, content }) {
this.checkPerm(PERM_REPLY_PROBLEM_SOLUTION);
let psdoc = await solution.get(psid);
const psdoc = await solution.get(psid);
await solution.reply(psdoc._id, this.uid, content);
}
async post_edit_reply({ content, psid, psrid }) {
let [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
async postEditReply({ content, psid, psrid }) {
const [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
await solution.editReply(psid, psrid, content);
}
async post_delete_reply({ psid, psrid }) {
let [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner != this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
async postDeleteReply({ psid, psrid }) {
const [psdoc, psrdoc] = await solution.getReply(psid, psrid);
if ((!psdoc) || psdoc.pid !== this.pdoc._id) throw new SolutionNotFoundError(psid);
if (psrdoc.owner !== this.uid) this.checkPerm(PERM_EDIT_PROBLEM_SOLUTION_REPLY);
await solution.delReply(psid, psrid);
this.back();
}
async post_upvote() {
let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1);
async postUpvote() {
const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, 1);
this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote };
if (!this.preferJson) this.back();
}
async post_downvote() {
let [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1);
async postDownvote() {
const [psdoc, pssdoc] = await solution.vote(this.psdoc._id, this.uid, -1);
this.response.body = { vote: psdoc.vote, user_vote: pssdoc.vote };
if (!this.preferJson) this.back();
}
@@ -264,7 +290,7 @@ class ProblemSolutionHandler extends ProblemDetailHandler {
class ProblemSolutionRawHandler extends ProblemDetailHandler {
async get({ psid }) {
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let psdoc = await solution.get(psid);
const psdoc = await solution.get(psid);
this.response.type = 'text/markdown';
this.response.body = psdoc.content;
}
@@ -273,7 +299,7 @@ class ProblemSolutionRawHandler extends ProblemDetailHandler {
class ProblemSolutionReplyRawHandler extends ProblemDetailHandler {
async get({ psid }) {
this.checkPerm(PERM_VIEW_PROBLEM_SOLUTION);
let [psdoc, psrdoc] = await solution.getReply(psid);
const [psdoc, psrdoc] = await solution.getReply(psid);
if ((!psdoc) || psdoc.pid != this.pdoc._id) throw new SolutionNotFoundError(psid);
this.response.type = 'text/markdown';
this.response.body = psrdoc.content;
@@ -288,14 +314,20 @@ class ProblemCreateHandler extends Handler {
path: [
['Hydro', '/'],
['problem_main', '/p'],
['problem_create', null]
], page_name: 'problem_create'
['problem_create', null],
],
page_name: 'problem_create',
};
}
async post({ title, pid, content, hidden }) {
async post({
title, pid, content, hidden,
}) {
validator.checkPid(pid);
pid = pid || await system.incPidCounter();
await problem.add({ title, content, owner: this.user._id, pid, hidden });
await problem.add({
title, content, owner: this.user._id, pid, hidden,
});
this.response.body = { pid };
this.response.redirect = `/p/${pid}/settings`;
}
@@ -322,7 +354,7 @@ class ProblemPretestHandler(base.Handler):
@base.requirePerm(builtin.PERM_SUBMIT_PROBLEM)
@base.route_argument
@base.post_argument
@base.require_csrf_token
@base.require_csrfToken
@base.sanitize
@base.limitRate('add_record', 60, 100)
async def post(self, *, pid: document.convert_doc_id, lang: str, code: str,
@@ -379,7 +411,7 @@ class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connec
@base.requirePriv(builtin.PRIV_USER_PROFILE)
@base.route_argument
@base.post_argument
@base.require_csrf_token
@base.require_csrfToken
@base.sanitize
async def post(self, *, pid: document.convert_doc_id, title: str, content: str):
pdoc = await problem.get(self.domainId, pid)
@@ -408,4 +440,4 @@ class ProblemStatisticsHandler(base.Handler):
self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc,
page_title=pdoc['title'], path_components=path_components)
*/
*/
+57 -34
View File
@@ -1,53 +1,71 @@
const
{ constants } = require('../options'),
{ PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD,
PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'),
problem = require('../model/problem'),
record = require('../model/record'),
contest = require('../model/contest'),
user = require('../model/user'),
bus = require('../service/bus'),
queue = require('../service/queue'),
{ Route, Handler, Connection, ConnectionHandler } = require('../service/server');
const { constants } = require('../options');
const {
PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD,
PERM_REJUDGE, PERM_VIEW_PROBLEM_HIDDEN,
} = require('../permission');
const problem = require('../model/problem');
const record = require('../model/record');
const contest = require('../model/contest');
const user = require('../model/user');
const bus = require('../service/bus');
const queue = require('../service/queue');
const {
Route, Handler, Connection, ConnectionHandler,
} = require('../service/server');
class RecordListHandler extends Handler {
async get({ page = 1 }) {
this.response.template = 'record_main.html';
let q = {};
let rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE);
let pdict = {}, udict = {};
for (let rdoc of rdocs) {
udict[rdoc.uid] = await user.getById(rdoc.uid);
pdict[rdoc.pid] = await problem.get(rdoc.pid, this.user._id);
const q = {};
const rdocs = await record.getMany(q, { _id: -1 }, page, constants.RECORD_PER_PAGE);
const pdict = {};
const udict = {};
const ulist = [];
const plist = [];
for (const rdoc of rdocs) {
ulist[rdoc.uid] = user.getById(rdoc.uid);
plist[rdoc.pid] = problem.get(rdoc.pid, this.user._id);
}
await Promise.all(ulist);
await Promise.all(plist);
for (const udoc of ulist) {
udict[udoc._id] = udoc;
}
for (const pdoc of plist) {
pdict[pdoc._id] = pdoc;
}
this.response.body = {
path: [
['Hydro', '/'],
['record_main', null]
['record_main', null],
],
page, rdocs, pdict, udict
page,
rdocs,
pdict,
udict,
};
}
}
class RecordDetailHandler extends Handler {
async get({ rid }) {
this.response.template = 'record_detail.html';
let rdoc = await record.get(rid);
const rdoc = await record.get(rid);
if (rdoc.hidden) this.checkPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
if (rdoc.uid != this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null;
if (rdoc.uid !== this.user.uid && !this.user.hasPerm(PERM_READ_RECORD_CODE)) rdoc.code = null;
this.response.body = {
path: [
['Hydro', '/'],
['record_detail', null]
['record_detail', null],
],
rdoc, show_status: true
rdoc,
show_status: true,
};
}
}
class RecordRejudgeHandler extends Handler {
async post({ rid }) {
this.checkPerm(PERM_REJUDGE);
let rdoc = await record.get(rid);
const rdoc = await record.get(rid);
if (rdoc) {
await record.reset(rid);
await queue.push('judge', rid);
@@ -59,18 +77,21 @@ class RecordConnectionHandler extends ConnectionHandler {
async prepare() {
bus.subscribe(['record_change'], this.onRecordChange);
}
async message(msg) {
for (let rid of msg.rids) {
let rdoc = await record.get(rid);
await this.onRecordChange({ value: rdoc });
for (const rid of msg.rids) {
const rdoc = await record.get(rid); // eslint-disable-line no-await-in-loop
await this.onRecordChange({ value: rdoc }); // eslint-disable-line no-await-in-loop
}
}
async cleanup() {
bus.unsubscribe(['record_change'], this.onRecordChange);
}
async onRecordChange(data) {
let rdoc = data.value;
if (rdoc.tid && rdoc.tid.toString() != this.tid) return;
const rdoc = data.value;
if (rdoc.tid && rdoc.tid.toString() !== this.tid) return;
let [udoc, pdoc] = await Promise.all([user.getById(rdoc.uid), problem.getById(rdoc.pid)]);
if (pdoc.hidden && !this.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) pdoc = null;
this.send({ html: await this.renderHTML('record_main_tr.html', { rdoc, udoc, pdoc }) });
@@ -78,9 +99,9 @@ class RecordConnectionHandler extends ConnectionHandler {
}
class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(ConnectionHandler) {
async prepare({ rid }) {
let rdoc = await record.get(rid);
const rdoc = await record.get(rid);
if (rdoc.tid) {
let tdoc = await contest.get(rdoc.tid);
const tdoc = await contest.get(rdoc.tid);
if (!this.canShowRecord(tdoc)) {
this.close();
return;
@@ -90,14 +111,16 @@ class RecordDetailConnectionHandler extends contest.ContestHandlerMixin(Connecti
bus.subscribe(['record_change'], this.onRecordChange);
this.onRecordChange({ value: rdoc });
}
async onRecordChange(data) {
let rdoc = data.value;
if (rdoc._id.toString() != this.rid) return;
const rdoc = data.value;
if (rdoc._id.toString() !== this.rid) return;
this.send({
status_html: await this.renderHTML('record_detail_status.html', { rdoc }),
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc })
summary_html: await this.renderHTML('record_detail_summary.html', { rdoc }),
});
}
async cleanup() {
bus.unsubscribe(['record_change'], this.onRecordChange);
}
-26
View File
@@ -1,26 +0,0 @@
const
opcount = require('../model/opcount'),
{ PermissionError } = require('../error');
module.exports = {
requirePerm(perm) {
return async (ctx, next) => {
for (let i in arguments) {
if (arguments[i] instanceof Array) {
let p = false;
for (let j in arguments)
if (ctx.state.user.hasPerm(arguments[i][j])) {
p = true;
break;
}
if (!p) throw new PermissionError([arguments[i]]);
} else {
if (ctx.state.user.hasPerm(arguments[i])) continue;
else throw new PermissionError([arguments[i]]);
}
}
await next();
};
},
};
+61 -39
View File
@@ -1,22 +1,24 @@
const
{ Route, Handler } = require('../service/server.js'),
user = require('../model/user'),
token = require('../model/token'),
system = require('../model/system'),
{ sendMail } = require('../lib/mail'),
misc = require('../lib/misc'),
validator = require('../lib/validator'),
options = require('../options'),
{ PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission'),
{ UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, LoginError, SystemError } = require('../error');
const { Route, Handler } = require('../service/server.js');
const user = require('../model/user');
const token = require('../model/token');
const system = require('../model/system');
const { sendMail } = require('../lib/mail');
const misc = require('../lib/misc');
const validator = require('../lib/validator');
const options = require('../options');
const { PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission');
const {
UserAlreadyExistError, InvalidTokenError, VerifyPasswordError,
UserNotFoundError, LoginError, SystemError,
} = require('../error');
class UserLoginHandler extends Handler {
async get() {
this.response.template = 'user_login.html';
}
async post({ uname, password, rememberme = false }) {
let udoc = await user.getByUname(uname);
const udoc = await user.getByUname(uname);
if (!udoc) throw new LoginError(uname);
if (udoc) udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
@@ -25,7 +27,7 @@ class UserLoginHandler extends Handler {
this.session.uid = udoc._id;
this.session.rememberme = rememberme;
this.response.body = {};
let referer = this.request.headers.referer || '/';
const referer = this.request.headers.referer || '/';
this.response.redirect = referer.endsWith('/login') ? '/' : referer;
}
}
@@ -34,9 +36,11 @@ class UserLogoutHandler extends Handler {
super(ctx);
this.checkPerm(PERM_LOGGEDIN);
}
async get() {
this.response.template = 'user_logout.html';
}
async post() {
this.session = { uid: 1 };
this.response.body = {};
@@ -47,16 +51,22 @@ class UserRegisterHandler extends Handler {
super(ctx);
this.checkPerm(PERM_REGISTER_USER);
}
async get() {
this.response.template = 'user_register.html';
}
async post({ mail }) {
validator.checkEmail(mail);
if (await user.getByEmail(mail, true)) throw new UserAlreadyExistError(mail);
this.limitRate('send_mail', 3600, 30);
let t = await token.add(token.TYPE_REGISTRATION, options.registration_token_expire_seconds, { mail });
const t = await token.add(
token.TYPE_REGISTRATION,
options.registration_token_expire_seconds,
{ mail },
);
if (options.smtp.user) {
let m = await this.renderHTML('user_register_mail', { url: `/register/${t}` });
const m = await this.renderHTML('user_register_mail', { url: `/register/${t}` });
await sendMail(mail, 'Sign Up', 'user_register_mail', m);
this.response.body = {};
this.response.template = 'user_register_mail_sent.html';
@@ -70,18 +80,24 @@ class UserRegisterWithCodeHandler extends Handler {
super(ctx);
this.checkPerm(PERM_REGISTER_USER);
}
async get({ code }) {
this.response.template = 'user_register_with_code.html';
let { mail } = await token.get(code, token.TYPE_REGISTRATION);
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
this.response.body = { mail };
}
async post({ code, password, verify_password, uname }) {
let { mail } = await token.get(code, token.TYPE_REGISTRATION);
async post({
code, password, verifyPassword, uname,
}) {
const { mail } = await token.get(code, token.TYPE_REGISTRATION);
if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (password != verify_password) throw new VerifyPasswordError();
let uid = await system.incUserCounter();
await user.create({ uid, uname, password, mail, regip: this.request.ip });
if (password !== verifyPassword) throw new VerifyPasswordError();
const uid = await system.incUserCounter();
await user.create({
uid, uname, password, mail, regip: this.request.ip,
});
await token.delete(code, token.TYPE_REGISTRATION);
this.session.uid = uid;
this.response.body = {};
@@ -93,19 +109,21 @@ class UserLostPassHandler extends Handler {
if (!options.smtp.user) throw new SystemError('Cannot send mail');
super(ctx);
}
async get() {
this.response.template = 'user_lostpass.html';
}
async post({ mail }) {
validator.checkEmail(mail);
let udoc = await user.getByEmail(mail);
const udoc = await user.getByEmail(mail);
if (!udoc) throw new UserNotFoundError(mail);
let tid = await token.add(
const tid = await token.add(
token.TYPE_LOSTPASS,
options.lostpass_token_expire_seconds,
{ uid: udoc._id }
{ uid: udoc._id },
);
let m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname });
const m = await this.renderHTML('user_lostpass_mail', { url: `/lostpass/${tid}`, uname: udoc.uname });
await sendMail(mail, 'Lost Password', 'user_lostpass_mail', m);
this.response.body = {};
this.response.template = 'user_lostpass_mail_sent.html';
@@ -113,15 +131,16 @@ class UserLostPassHandler extends Handler {
}
class UserLostPassWithCodeHandler extends Handler {
async get({ code }) {
let tdoc = await token.get(code, token.TYPE_LOSTPASS);
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
let udoc = await user.getById(tdoc.uid);
const udoc = await user.getById(tdoc.uid);
this.response.body = { uname: udoc.uname };
}
async post({ code, password, verify_password }) {
let tdoc = await token.get(code, token.TYPE_LOSTPASS);
async post({ code, password, verifyPassword }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
if (password != verify_password) throw new VerifyPasswordError();
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.setPassword(tdoc.uid, password);
await token.delete(code, token.TYPE_LOSTPASS);
this.response.redirect = '/';
@@ -132,28 +151,31 @@ class UserDetailHandler extends Handler {
super(ctx);
this.response.template = 'user_detail.html';
}
async get({ uid }) {
let isSelfProfile = this.ctx.state.user._id == uid;
let udoc = await user.getById(uid);
const isSelfProfile = this.ctx.state.user._id === uid;
const udoc = await user.getById(uid);
if (!udoc) throw new UserNotFoundError(uid);
let sdoc = await token.getMostRecentSessionByUid(uid);
const sdoc = await token.getMostRecentSessionByUid(uid);
this.ctx.body = { isSelfProfile, udoc, sdoc };
}
}
class UserSearchHandler extends Handler {
async get({ q, exact_match = false }) {
async get({ q, exactMatch = false }) {
let udocs;
if (exact_match) udocs = [];
if (exactMatch) udocs = [];
else udocs = await user.getPrefixList(q, 20);
try {
let udoc = await user.getById(parseInt(q));
const udoc = await user.getById(parseInt(q));
if (udoc) udocs.insert(0, udoc);
} catch (e) {
/* Ignore */
}
for (let i in udocs)
if (udocs[i].gravatar)
for (const i in udocs) {
if (udocs[i].gravatar) {
udocs[i].gravatar_url = misc.gravatar_url[udocs[i].gravatar];
}
}
this.ctx.body = { udocs };
}
}
+27 -28
View File
@@ -1,32 +1,31 @@
require('./utils');
const
Mongo = require('mongodb'),
{ defaults } = require('lodash'),
builtin = require('./model/builtin'),
pwhash = require('./lib/pwhash'),
options = require('./options'),
{ udoc } = require('./interface');
const Mongo = require('mongodb');
const { defaults } = require('lodash');
const builtin = require('./model/builtin');
const pwhash = require('./lib/pwhash');
const options = require('./options');
const { udoc } = require('./interface');
async function run() {
let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
let db = Database.db(options.db.name);
let coll_user = db.collection('user');
let coll_role = db.collection('role');
let coll_blacklist = db.collection('blacklist');
let coll_token = db.collection('token');
const Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
const db = Database.db(options.db.name);
const collUser = db.collection('user');
const collRole = db.collection('role');
const collBlacklist = db.collection('blacklist');
const collToken = db.collection('token');
async function createUser() {
let salt = pwhash.salt();
await coll_user.insertMany([
const salt = pwhash.salt();
await collUser.insertMany([
defaults({
_id: 0,
uname: 'Hydro',
unameLower: 'hydro',
mail: 'hydro@hydro',
mailLower: 'hydro@hydro',
role: 'guest'
role: 'guest',
}, udoc),
defaults({
_id: 1,
@@ -34,7 +33,7 @@ async function run() {
mailLower: 'guest@hydro',
uname: 'Guest',
unameLower: 'guest',
role: 'guest'
role: 'guest',
}, udoc),
defaults({
_id: -1,
@@ -45,22 +44,22 @@ async function run() {
hash: pwhash.hash('rootroot', salt),
salt,
gravatar: 'root@hydro',
role: 'admin'
}, udoc)
role: 'admin',
}, udoc),
]);
}
await coll_user.createIndex('unameLower', { unique: true });
await coll_user.createIndex('mailLower', { sparse: true });
await coll_role.insertMany(builtin.BUILTIN_ROLES);
await coll_blacklist.createIndex('expireAt', { expireAfterSeconds: 0 });
await coll_token.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true });
await coll_token.createIndex('expireAt', { expireAfterSeconds: 0 });
await collUser.createIndex('unameLower', { unique: true });
await collUser.createIndex('mailLower', { sparse: true });
await collRole.insertMany(builtin.BUILTIN_ROLES);
await collBlacklist.createIndex('expireAt', { expireAfterSeconds: 0 });
await collToken.createIndex([{ uid: 1 }, { tokenType: 1 }, { updateAt: -1 }], { sparse: true });
await collToken.createIndex('expireAt', { expireAfterSeconds: 0 });
await createUser();
console.log('Installed');
process.exit(0);
}
run().catch(e => {
run().catch((e) => {
console.error(e);
process.exit(1);
});
});
+2 -2
View File
@@ -16,7 +16,7 @@ exports.udoc = {
regip: '0.0.0.0',
gravatar: '',
loginat: new Date(),
loginip: '0.0.0.0'
loginip: '0.0.0.0',
};
/*
export interface Pdoc {
@@ -68,4 +68,4 @@ export interface Tdoc {
content: string
pids: ObjectID[]
}
*/
*/
+24 -21
View File
@@ -1,37 +1,40 @@
const
fs = require('fs'),
yaml = require('js-yaml');
const fs = require('fs');
const yaml = require('js-yaml');
let locales = {};
const locales = {};
String.prototype.format = function (args) {
var result = this;
if (arguments.length > 0) {
if (arguments.length == 1 && typeof (args) == 'object') {
for (var key in args)
if (args[key] != undefined) {
let reg = new RegExp('(\\{' + key + '\\})', 'g');
String.prototype.format = function formatStr(...args) {
let result = this;
if (args.length > 0) {
if (args.length === 1 && typeof (args[0]) === 'object') {
for (const key in args) {
if (args[key] !== undefined) {
const reg = new RegExp(`(\\{${key}\\})`, 'g');
result = result.replace(reg, args[key]);
}
} else for (var i = 0; i < arguments.length; i++)
if (arguments[i] != undefined) {
let reg = new RegExp('(\\{)' + i + '(\\})', 'g');
result = result.replace(reg, arguments[i]);
}
} else {
for (let i = 0; i < args.length; i++) {
if (args[i] !== undefined) {
const reg = new RegExp(`(\\{)${i}(\\})`, 'g');
result = result.replace(reg, args[i]);
}
}
}
}
return result;
};
String.prototype.rawformat = function (object) {
let res = this.split('{@}');
String.prototype.rawformat = function rawFormat(object) {
const res = this.split('{@}');
return [res[0], object, res[1]];
};
String.prototype.translate = function (language = 'zh_CN') {
String.prototype.translate = function translate(language = 'zh_CN') {
if (locales[language]) return locales[language][this] || this;
else return this;
return this;
};
module.exports = function load(file, language) {
if (!locales[language]) locales[language] = {};
let content = fs.readFileSync(file).toString();
const content = fs.readFileSync(file).toString();
Object.assign(locales[language], yaml.safeLoad(content));
};
};
+13 -11
View File
@@ -1,7 +1,6 @@
const
nodemailer = require('nodemailer'),
options = require('../options'),
{ SendMailError } = require('../error');
const nodemailer = require('nodemailer');
const options = require('../options');
const { SendMailError } = require('../error');
let transporter = null;
try {
@@ -11,8 +10,8 @@ try {
secure: options.VJ_SMTP_SECURE,
auth: {
user: options.VJ_SMTP_USER,
pass: options.VJ_SMTP_PASSWORD
}
pass: options.VJ_SMTP_PASSWORD,
},
});
} catch (e) {
console.error(e);
@@ -20,8 +19,8 @@ try {
module.exports = {
/**
* @param {string} to
* @param {string} subject
* @param {string} to
* @param {string} subject
* @param {string} text
* @param {string} html
*/
@@ -31,7 +30,10 @@ module.exports = {
t = await new Promise((resolve, reject) => {
transporter.sendMail({
from: options.VJ_SMTP_FROM,
to, subject, text, html
to,
subject,
text,
html,
}, (err, info) => {
if (err) reject(err);
else resolve(info);
@@ -41,5 +43,5 @@ module.exports = {
throw new SendMailError(to);
}
return t;
}
};
},
};
+7 -7
View File
@@ -1,19 +1,19 @@
const
hljs = require('highlight.js'),
MarkdownIt = require('markdown-it'),
katex = require('markdown-it-katex');
const hljs = require('highlight.js');
const MarkdownIt = require('markdown-it');
const katex = require('markdown-it-katex');
class Markdown extends MarkdownIt {
constructor() {
super({
linkify: true,
highlight: function (str, lang) {
if (lang && hljs.getLanguage(lang))
highlight(str, lang) {
if (lang && hljs.getLanguage(lang)) {
try {
return hljs.highlight(lang, str).value;
} catch (__) { } // eslint-disable-line no-empty
}
return '';
}
},
});
this.linkify.tlds('.py', false);
this.use(katex);
+27 -31
View File
@@ -1,8 +1,7 @@
'use strict';
/* eslint-disable no-bitwise */
function safeAdd(x, y) {
var lsw = (x & 0xffff) + (y & 0xffff);
var msw = (x >> 16) + (y >> 16) + (lsw >> 16);
const lsw = (x & 0xffff) + (y & 0xffff);
const msw = (x >> 16) + (y >> 16) + (lsw >> 16);
return (msw << 16) | (lsw & 0xffff);
}
const bitRotateLeft = (num, cnt) => (num << cnt) | (num >>> (32 - cnt));
@@ -14,15 +13,15 @@ const md5ii = (a, b, c, d, x, s, t) => md5cmn(c ^ (b | ~d), a, b, x, s, t);
function binlMD5(x, len) {
x[len >> 5] |= 0x80 << len % 32;
x[(((len + 64) >>> 9) << 4) + 14] = len;
var i;
var olda;
var oldb;
var oldc;
var oldd;
var a = 1732584193;
var b = -271733879;
var c = -1732584194;
var d = 271733878;
let i;
let olda;
let oldb;
let oldc;
let oldd;
let a = 1732584193;
let b = -271733879;
let c = -1732584194;
let d = 271733878;
for (i = 0; i < x.length; i += 16) {
olda = a;
oldb = b;
@@ -100,34 +99,31 @@ function binlMD5(x, len) {
return [a, b, c, d];
}
function binl2rstr(input) {
var i;
var output = '';
var length32 = input.length * 32;
for (i = 0; i < length32; i += 8)
output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff);
let i;
let output = '';
const length32 = input.length * 32;
for (i = 0; i < length32; i += 8) { output += String.fromCharCode((input[i >> 5] >>> i % 32) & 0xff); }
return output;
}
function rstr2binl(input) {
var i;
var output = [];
let i;
const output = [];
output[(input.length >> 2) - 1] = undefined;
for (i = 0; i < output.length; i += 1)
output[i] = 0;
var length8 = input.length * 8;
for (i = 0; i < length8; i += 8)
output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
for (i = 0; i < output.length; i += 1) { output[i] = 0; }
const length8 = input.length * 8;
for (i = 0; i < length8; i += 8) output[i >> 5] |= (input.charCodeAt(i / 8) & 0xff) << i % 32;
return output;
}
const rstrMD5 = s => binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
const rstrMD5 = (s) => binl2rstr(binlMD5(rstr2binl(s), s.length * 8));
function rstr2hex(input) {
var hexTab = '0123456789abcdef';
var output = '';
var x;
var i;
const hexTab = '0123456789abcdef';
let output = '';
let x;
let i;
for (i = 0; i < input.length; i += 1) {
x = input.charCodeAt(i);
output += hexTab.charAt((x >>> 4) & 0x0f) + hexTab.charAt(x & 0x0f);
}
return output;
}
module.exports = s => rstr2hex(rstrMD5(unescape(encodeURIComponent(s))));
module.exports = (s) => rstr2hex(rstrMD5(unescape(encodeURIComponent(s))));
+23 -20
View File
@@ -1,41 +1,44 @@
const md5 = require('./md5');
exports.gravatar_url = function (email, size) {
exports.gravatar_url = function gravatarUrl(email, size) {
return `//gravatar.loli.net/avatar/${md5((email || '').toString().trim().toLowerCase())}?d=mm&s=${size}`;
};
exports.datetime_span = function (dt, relative = true, format = '%Y-%m-%d %H:%M:%S') {
exports.datetime_span = function datetimeSpan(dt, relative = true, format = '%Y-%m-%d %H:%M:%S') {
if (dt.generationTime) dt = new Date(dt.generationTime * 1000);
else if (typeof dt == 'number' || typeof dt == 'string') dt = new Date(dt);
else if (typeof dt === 'number' || typeof dt === 'string') dt = new Date(dt);
return '<span class="time{0}" data-timestamp="{1}">{2}</span>'.format(
relative ? ' relative' : '',
dt.getTime() / 1000,
dt.format(format)
dt.format(format),
);
};
exports.paginate = function* (page, num_pages) {
let radius = 2, first, last;
exports.paginate = function* paginate(page, numPages) {
const radius = 2; let first; let
last;
if (page > 1) {
yield ['first', 1];
yield ['previous', page - 1];
}
if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, num_pages)];
else if (page >= num_pages - radius) [first, last] = [Math.max(1, num_pages - radius * 2), num_pages];
else[first, last] = [page - radius, page + radius];
if (page <= radius) [first, last] = [1, Math.min(1 + radius * 2, numPages)];
else if (page >= numPages - radius) {
[first, last] = [Math.max(1, numPages - radius * 2), numPages];
} else [first, last] = [page - radius, page + radius];
if (first > 1) yield ['ellipsis', 0];
for (let page0 = first; page0 < last + 1; page0++) {
if (page0 != page) yield ['page', page0];
if (page0 !== page) yield ['page', page0];
else yield ['current', page];
}
if (last < num_pages) yield ['ellipsis', 0];
if (page < num_pages) yield ['next', page + 1];
yield ['last', num_pages];
if (last < numPages) yield ['ellipsis', 0];
if (page < numPages) yield ['next', page + 1];
yield ['last', numPages];
};
exports.format_size = function (size, base = 1) {
exports.format_size = function formatSize(size, base = 1) {
size *= base;
let unit = 1024;
let unit_names = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
for (let unit_name of unit_names) {
if (size < unit) return '{0} {1}'.format(Math.round(size), unit_name);
const unit = 1024;
const unitNames = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
for (const unitName of unitNames) {
if (size < unit) return '{0} {1}'.format(Math.round(size), unitName);
size /= unit;
}
return '{0} {1}'.format(Math.round(size * unit), unit_names[unit_names.length - 1]);
};
return '{0} {1}'.format(Math.round(size * unit), unitNames[unitNames.length - 1]);
};
+6 -5
View File
@@ -1,10 +1,11 @@
const { ValidationError } = require('../error');
module.exports = async function paginate(cursor, page, page_size) {
module.exports = async function paginate(cursor, page, pageSize) {
if (page <= 0) throw new ValidationError('page');
let [count, page_docs] = await Promise.all([
const [count, pageDocs] = await Promise.all([
cursor.count(),
cursor.skip((page - 1) * page_size).limit(page_size).toArray()
cursor.skip((page - 1) * pageSize).limit(pageSize).toArray(),
]);
let num_pages = Math.floor((count + page_size - 1) / page_size);
return [page_docs, num_pages, count];
const numPages = Math.floor((count + pageSize - 1) / pageSize);
return [pageDocs, numPages, count];
};
+9 -8
View File
@@ -1,26 +1,27 @@
const crypto = require('crypto');
module.exports = {
_b64encode: str => new Buffer(str).toString('base64'),
_b64decode: str => new Buffer(str, 'base64').toString(),
_md5: str => crypto.createHash('md5').update(str).digest('hex'),
_sha1: str => crypto.createHash('sha1').update(str).digest('hex'),
_b64encode: (str) => Buffer.from(str).toString('base64'),
_b64decode: (str) => Buffer.from(str, 'base64').toString(),
_md5: (str) => crypto.createHash('md5').update(str).digest('hex'),
_sha1: (str) => crypto.createHash('sha1').update(str).digest('hex'),
/**
* @param {string} password
* @param {string} salt
* @param {string} hash
*/
check(password, salt, hash) {
return hash == this.hash(password, salt);
return hash === this.hash(password, salt);
},
/**
* @param {string} password
* @param {string} salt
*/
hash(password, salt) {
let res = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256');
const res = crypto.pbkdf2Sync(password, salt, 100000, 64, 'sha256');
return res.toString('Hex');
},
salt() {
return String.random();
}
};
},
};
+7 -7
View File
@@ -1,11 +1,11 @@
module.exports = function* ranked(diter, equ = (a, b) => a == b) {
let last_doc = null;
let r = 0, count = 0;
for (let doc of diter) {
module.exports = function* ranked(diter, equ = (a, b) => a === b) {
let last = null;
let r = 0;
let count = 0;
for (const doc of diter) {
count++;
if (count == 1 || !equ(last_doc, doc))
r = count;
last_doc = doc;
if (count === 1 || !equ(last, doc)) r = count;
last = doc;
yield [r, doc];
}
};
+17 -24
View File
@@ -1,34 +1,27 @@
const
path = require('path'),
nunjucks = require('nunjucks'),
markdown = require('./markdown'),
options = require('../options'),
perm = require('../permission'),
builtin = require('../model/builtin'),
model = require('../model'),
misc = require('../lib/misc');
const path = require('path');
const nunjucks = require('nunjucks');
const markdown = require('./markdown');
const options = require('../options');
const perm = require('../permission');
const builtin = require('../model/builtin');
const model = require('../model');
const misc = require('./misc');
class Nunjucks extends nunjucks.Environment {
constructor() {
super(
new nunjucks.FileSystemLoader(path.resolve(options.template.path)),
{ autoescape: true, trimBlocks: true }
{ autoescape: true, trimBlocks: true },
);
this.addFilter('json', function (self) {
return JSON.stringify(self);
}, false);
this.addFilter('assign', function (self, data) {
return Object.assign(self, data);
});
this.addFilter('markdown', function (self) {
return markdown.render(self);
});
this.addFilter('json', (self) => JSON.stringify(self), false);
this.addFilter('assign', (self, data) => Object.assign(self, data));
this.addFilter('markdown', (self) => markdown.render(self));
this.addFilter('gravatar_url', misc.gravatar_url);
this.addFilter('format_size', misc.format_size);
this.addGlobal('typeof', o => typeof o);
this.addGlobal('typeof', (o) => typeof o);
this.addGlobal('console', console);
this.addGlobal('static_url', str => `/${str}`);
this.addGlobal('reverse_url', str => str);
this.addGlobal('static_url', (str) => `/${str}`);
this.addGlobal('reverse_url', (str) => str);
this.addGlobal('datetime_span', misc.datetime_span);
this.addGlobal('paginate', misc.paginate);
this.addGlobal('perm', perm);
@@ -36,6 +29,6 @@ class Nunjucks extends nunjucks.Environment {
this.addGlobal('model', model);
}
}
let env = new Nunjucks();
const env = new Nunjucks();
module.exports = env;
module.exports = env;
+39 -32
View File
@@ -1,37 +1,44 @@
const
{ ValidationError } = require('../error'),
RE_UID = /^-?\d+$/i,
RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i,
RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i,
RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
const { ValidationError } = require('../error');
const
isTitle = s => s && s.length < 64,
checkTitle = s => { if (!isTitle(s)) throw new ValidationError('title'); else return s; },
isUid = s => RE_UID.test(s),
checkUid = s => { if (!isUid(s)) throw new ValidationError('uid'); else return s; },
isUname = s => RE_UNAME.test(s),
checkUname = s => { if (!isUname(s)) throw new ValidationError('uname'); else return s; },
isPassword = s => s.length >= 5,
checkPassword = s => { if (!isPassword(s)) throw new ValidationError('password'); else return s; },
isEmail = s => RE_MAIL.test(s),
checkEmail = s => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; },
isContent = s => s && s.length < 65536,
checkContent = s => { if (!isContent(s)) throw new ValidationError('content'); else return s; },
isName = s => s && s.length < 256,
checkName = s => { if (!isName(s)) throw new ValidationError('name'); else return s; },
isPid = s => RE_PID.test(s.toString()),
checkPid = s => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
const RE_UID = /^-?\d+$/i;
const RE_PID = /^([a-zA-Z]+[a-zA-Z0-9]*)|$/i;
const RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i;
const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
const isTitle = (s) => s && s.length < 64;
const checkTitle = (s) => { if (!isTitle(s)) throw new ValidationError('title'); else return s; };
const isUid = (s) => RE_UID.test(s);
const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; };
const isUname = (s) => RE_UNAME.test(s);
const checkUname = (s) => { if (!isUname(s)) throw new ValidationError('uname'); else return s; };
const isPassword = (s) => s.length >= 5;
const checkPassword = (s) => { if (!isPassword(s)) throw new ValidationError('password'); else return s; };
const isEmail = (s) => RE_MAIL.test(s);
const checkEmail = (s) => { if (!isEmail(s)) throw new ValidationError('mail'); else return s; };
const isContent = (s) => s && s.length < 65536;
const checkContent = (s) => { if (!isContent(s)) throw new ValidationError('content'); else return s; };
const isName = (s) => s && s.length < 256;
const checkName = (s) => { if (!isName(s)) throw new ValidationError('name'); else return s; };
const isPid = (s) => RE_PID.test(s.toString());
const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
module.exports = {
isTitle, checkTitle,
isUid, checkUid,
isUname, checkUname,
isPassword, checkPassword,
isEmail, checkEmail,
isContent, checkContent,
isName, checkName,
isPid, checkPid
isTitle,
checkTitle,
isUid,
checkUid,
isUname,
checkUname,
isPassword,
checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isName,
checkName,
isPid,
checkPid,
};
/*
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
@@ -104,4 +111,4 @@ def check_domain_invitation_code(s):
if not is_domain_invitation_code(s):
raise error.ValidationError('invitation_code')
*/
*/
+4 -4
View File
@@ -1,9 +1,9 @@
const
db = require('../service/db.js'),
coll = db.collection('blacklist');
const db = require('../service/db.js');
const coll = db.collection('blacklist');
async function add(ip) {
let expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
const expireAt = new Date(new Date().getTime() + 365 * 24 * 60 * 60 * 1000);
return coll.findOneAndUpdate({ _id: ip }, { $set: { expireAt } }, { upsert: true });
}
function get(ip) {
+49 -40
View File
@@ -1,20 +1,21 @@
const perm = require('../permission');
const BUILTIN_ROLES = [
{ _id: 'guest', perm: perm.PERM_BASIC },
{ _id: 'default', perm: perm.PERM_DEFAULT },
{ _id: 'admin', perm: perm.PERM_ADMIN }
{ _id: 'admin', perm: perm.PERM_ADMIN },
];
const CATEGORIES = {
'动态规划': [
动态规划: [
'LCS',
'LIS',
'背包',
'单调性DP',
'环形DP',
'树形DP',
'状态压缩DP'
'状态压缩DP',
],
'搜索': [
搜索: [
'枚举',
'搜索与剪枝',
'启发式搜索',
@@ -22,9 +23,9 @@ const CATEGORIES = {
'双向搜索',
'折半搜索',
'记忆化搜索',
'模拟退火'
'模拟退火',
],
'计算几何': [
计算几何: [
'半平面交',
'凸包',
'几何图形的交与并',
@@ -37,19 +38,19 @@ const CATEGORIES = {
'平面图的对偶图',
'三角剖分',
'梯形剖分',
'几何知识'
'几何知识',
],
'贪心': [],
'树结构': [
贪心: [],
树结构: [
'最近公共祖先',
'生成树',
'DFS序列',
'树上倍增',
'树的分治',
'树链剖分',
'Link-Cut-Tree'
'Link-Cut-Tree',
],
'图结构': [
图结构: [
'平面图',
'二分图',
'二分图匹配',
@@ -60,9 +61,9 @@ const CATEGORIES = {
'强连通分量',
'割点割边',
'欧拉回路',
'2-SAT'
'2-SAT',
],
'数论': [
数论: [
'素数判定',
'欧几里得算法',
'不定方程',
@@ -73,10 +74,10 @@ const CATEGORIES = {
'大整数质因数分解',
'勾股方程',
'积性函数',
'Fibonacci数列'
'Fibonacci数列',
],
'模拟': [],
'数据结构': [
模拟: [],
数据结构: [
'栈',
'队列',
'链表',
@@ -93,41 +94,41 @@ const CATEGORIES = {
'k-d树',
'块状链表',
'Hashing',
'函数式编程'
'函数式编程',
],
'博弈论': [],
'字符串': [
博弈论: [],
字符串: [
'KMP',
'后缀数据结构',
'Trie树',
'AC自动机',
'Manacher',
'表达式处理',
'最小表示法'
'最小表示法',
],
'组合数学': [
组合数学: [
'生成函数',
'容斥原理',
'康托展开',
'Catalan数列',
'Stirling数',
'差分',
'Polya定理'
'Polya定理',
],
'线性代数': [
线性代数: [
'矩阵乘法',
'高斯消元',
'线性规划'
'线性规划',
],
'高精度': [
'FFT'
高精度: [
'FFT',
],
'递推': [],
'概率论': [
'随机化'
递推: [],
概率论: [
'随机化',
],
'NPC': [],
'其他': [
NPC: [],
其他: [
'二分查找',
'三分查找',
'双指针扫描',
@@ -137,15 +138,15 @@ const CATEGORIES = {
'快速幂',
'数学',
'排序',
'构造'
]
'构造',
],
};
const FOOTER_EXTRA_HTMLS = [];
const VIEW_LANGS = [
{ code: 'zh-CN', name: 'zh-CN' }
{ code: 'zh-CN', name: 'zh-CN' },
];
const LANGS = {
cc: 'cpp'
cc: 'cpp',
};
const LANG_TEXTS = {
c: 'C',
@@ -192,7 +193,7 @@ const STATUS = {
STATUS_JUDGING: 20,
STATUS_COMPILING: 21,
STATUS_FETCHED: 22,
STATUS_IGNORED: 30
STATUS_IGNORED: 30,
};
const STATUS_TEXTS = {
[STATUS.STATUS_WAITING]: 'Waiting',
@@ -226,9 +227,17 @@ const STATUS_CODES = {
20: 'progress',
21: 'progress',
22: 'progress',
30: 'ignored'
30: 'ignored',
};
module.exports = {
BUILTIN_ROLES, CATEGORIES, VIEW_LANGS, FOOTER_EXTRA_HTMLS, LANGS,
LANG_TEXTS, LANG_HIGHLIGHT_ID, STATUS, STATUS_TEXTS, STATUS_CODES
};
BUILTIN_ROLES,
CATEGORIES,
VIEW_LANGS,
FOOTER_EXTRA_HTMLS,
LANGS,
LANG_TEXTS,
LANG_HIGHLIGHT_ID,
STATUS,
STATUS_TEXTS,
STATUS_CODES,
};
+84 -64
View File
@@ -1,18 +1,20 @@
const
user = require('./user'),
problem = require('./problem'),
{ ValidationError, ContestNotFoundError, ContestAlreadyAttendedError,
ContestNotAttendedError, ProblemNotFoundError, ContestScoreboardHiddenError } = require('../error'),
{ PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'),
validator = require('../lib/validator'),
db = require('../service/db'),
coll = db.collection('contest'),
coll_status = db.collection('contest.status');
const user = require('./user');
const problem = require('./problem');
const {
ValidationError, ContestNotFoundError, ContestAlreadyAttendedError,
ContestNotAttendedError, ProblemNotFoundError, ContestScoreboardHiddenError,
} = require('../error');
const { PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission');
const validator = require('../lib/validator');
const db = require('../service/db');
const coll = db.collection('contest');
const collStatus = db.collection('contest.status');
const RULES = {
homework: require('../module/contest/homework'),
oi: require('../module/contest/oi'),
acm: require('../module/contest/acm')
acm: require('../module/contest/acm'),
};
/**
@@ -21,13 +23,13 @@ const RULES = {
*/
/**
* @param {string} title
* @param {string} content
* @param {number} owner
* @param {string} rule
* @param {Date} beginAt
* @param {Date} endAt
* @param {ObjectID[]} pids
* @param {string} title
* @param {string} content
* @param {number} owner
* @param {string} rule
* @param {Date} beginAt
* @param {Date} endAt
* @param {ObjectID[]} pids
* @param {object} data
* @returns {ObjectID} tid
*/
@@ -37,112 +39,120 @@ async function add(title, content, owner, rule,
validator.checkContent(content);
if (!this.RULES[rule]) throw new ValidationError('rule');
if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt');
Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0 });
Object.assign(data, {
content, owner, title, rule, beginAt, endAt, pids, attend: 0,
});
this.RULES[rule].check(data);
let res = await coll.insertOne(data);
const res = await coll.insertOne(data);
return res.insertedId;
}
/**
* @param {ObjectID} tid
* @param {object} $set
* @param {ObjectID} tid
* @param {object} $set
* @returns {Tdoc} tdoc after modification
*/
async function edit(tid, $set) {
if ($set.title) validator.checkTitle($set.title);
if ($set.content) validator.checkIntro($set.content);
if ($set.rule)
if (!this.RULES[$set.rule]) throw new ValidationError('rule');
if ($set.beginAt && $set.endAt)
if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt');
let tdoc = await coll.findOne({ tid });
if ($set.rule) { if (!this.RULES[$set.rule]) throw new ValidationError('rule'); }
if ($set.beginAt && $set.endAt) if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt');
const tdoc = await coll.findOne({ tid });
if (!tdoc) throw new ContestNotFoundError(tid);
this.RULES[$set.rule || tdoc.rule].check(Object.assign(tdoc, $set));
await coll.findOneAndUpdate({ tid }, { $set });
return tdoc;
}
/**
* @param {ObjectID} tid
* @param {ObjectID} tid
* @returns {Tdoc}
*/
async function get(tid) {
let tdoc = await coll.findOne({ _id: tid });
const tdoc = await coll.findOne({ _id: tid });
if (!tdoc) throw new ContestNotFoundError(tid);
return tdoc;
}
async function updateStatus(tid, uid, rid, pid, accept, score) {
let tdoc = await get(tid);
let tsdoc = await coll_status.findOneAndUpdate({ tid: tdoc._id, uid }, {
$push: { journal: { rid, pid, accept, score } },
$inc: { rev: 1 }
const tdoc = await get(tid);
const tsdoc = await collStatus.findOneAndUpdate({ tid: tdoc._id, uid }, {
$push: {
journal: {
rid, pid, accept, score,
},
},
$inc: { rev: 1 },
}, { upsert: true });
if (!tsdoc.value.attend) throw new ContestNotAttendedError(tid, uid);
}
async function getListStatus(uid, tids) {
let r = {};
for (let tid of tids) r[tid] = await this.getStatus(tid, uid);
const r = {};
for (const tid of tids) r[tid] = await this.getStatus(tid, uid); // eslint-disable-line no-await-in-loop
return r;
}
async function attend(tid, uid) {
try {
await coll_status.insertOne({ tid, uid, attend: 1 });
await collStatus.insertOne({ tid, uid, attend: 1 });
} catch (e) {
throw new ContestAlreadyAttendedError(tid, uid);
}
await coll.findOneAndUpdate({ _id: tid }, { $inc: { attend: 1 } });
}
function getMultiStatus(query) {
return coll_status.find(query);
return collStatus.find(query);
}
function is_new(tdoc, days = 1) {
let now = new Date().getTime();
let readyAt = tdoc.beginAt.getTime();
const now = new Date().getTime();
const readyAt = tdoc.beginAt.getTime();
return (now < readyAt - days * 24 * 3600 * 1000);
}
function is_upcoming(tdoc, days = 1) {
let now = new Date().getTime();
let readyAt = tdoc.beginAt.getTime();
const now = new Date().getTime();
const readyAt = tdoc.beginAt.getTime();
return (now > readyAt - days * 24 * 3600 * 1000 && now < tdoc.beginAt);
}
function is_not_started(tdoc) {
return (new Date()) < tdoc.beginAt;
}
function is_ongoing(tdoc) {
let now = new Date();
const now = new Date();
return (tdoc.beginAt <= now && now < tdoc.endAt);
}
function is_done(tdoc) {
return tdoc.endAt <= new Date();
}
const ContestHandlerMixin = c => class extends c {
const ContestHandlerMixin = (c) => class extends c {
canViewHiddenScoreboard() {
return this.user.hasPerm(PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD);
}
canShowRecord(tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showRecord(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true;
return false;
}
canShowScoreboard(tdoc, allowPermOverride = true) {
if (RULES[tdoc.rule].showScoreboard(tdoc, new Date())) return true;
if (allowPermOverride && this.canViewHiddenScoreboard(tdoc)) return true;
return false;
}
async getScoreboard(tid, isExport = false) {
let tdoc = await get(tid);
const tdoc = await get(tid);
if (!this.canShowScoreboard(tdoc)) throw new ContestScoreboardHiddenError(tid);
let tsdocs = await getMultiStatus(tid).sort(RULES[tdoc.rule].statusSort).toArray();
let uids = [];
for (let tsdoc of tsdocs) uids.push(tsdoc.uid);
let [udict, pdict] = await Promise.all([user.getList(uids), problem.getList(tdoc['pids'])]);
let ranked_tsdocs = RULES[tdoc.rule].rank(tsdocs);
let rows = RULES[tdoc.rule].scoreboard(isExport, str => str ? str.toString().translate(this.user.language) : '', tdoc, ranked_tsdocs, udict, pdict);
const tsdocs = await getMultiStatus(tid).sort(RULES[tdoc.rule].statusSort).toArray();
const uids = [];
for (const tsdoc of tsdocs) uids.push(tsdoc.uid);
const [udict, pdict] = await Promise.all([user.getList(uids), problem.getList(tdoc.pids)]);
const rankedTsdocs = RULES[tdoc.rule].rank(tsdocs);
const rows = RULES[tdoc.rule].scoreboard(isExport, (str) => (str ? str.toString().translate(this.user.language) : ''), tdoc, rankedTsdocs, udict, pdict);
return [tdoc, rows, udict];
}
async verifyProblems(pids) {
let r = [];
for (let pid of pids) {
let res = await problem.get(pid);
async verifyProblems(pids) { // eslint-disable-line class-methods-use-this
const r = [];
for (const pid of pids) {
const res = await problem.get(pid); // eslint-disable-line no-await-in-loop
if (res) r.push(res._id);
else throw new ProblemNotFoundError(pid);
}
@@ -151,26 +161,37 @@ const ContestHandlerMixin = c => class extends c {
};
module.exports = {
RULES, ContestHandlerMixin, add, getListStatus, attend, edit, get, updateStatus,
count: query => coll.find(query).count(),
getMulti: query => coll.find(query),
RULES,
ContestHandlerMixin,
add,
getListStatus,
attend,
edit,
get,
updateStatus,
count: (query) => coll.find(query).count(),
getMulti: (query) => coll.find(query),
getStatus: (tid, uid) => coll_status.findOne({ tid, uid }),
setStatus: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }),
is_new, is_upcoming, is_not_started, is_ongoing, is_done,
status_text: tdoc =>
is_new,
is_upcoming,
is_not_started,
is_ongoing,
is_done,
status_text: (tdoc) => (
is_new(tdoc)
? 'New'
: is_upcoming(tdoc)
? 'Ready (☆▽☆)'
: is_ongoing(tdoc)
? 'Live...'
: 'Done',
get_status: tdoc =>
: 'Done'),
get_status: (tdoc) => (
is_not_started(tdoc)
? 'not_started'
: is_ongoing(tdoc)
? 'ongoing'
: 'finished',
: 'finished'),
};
/*
@@ -336,7 +357,6 @@ def _format_pids(pids_list):
return ','.join([str(pid) for pid in pids_list])
class ContestStatusMixin(object):
@property
@functools.lru_cache()
@@ -447,4 +467,4 @@ class ContestCommonOperationMixin(object):
class ContestMixin(ContestStatusMixin, ContestVisibilityMixin, ContestCommonOperationMixin):
pass
*/
*/
+1 -1
View File
@@ -9,5 +9,5 @@ module.exports = {
system: require('./system'),
token: require('./token'),
training: require('./training'),
user: require('./user')
user: require('./user'),
};
+18 -25
View File
@@ -1,35 +1,28 @@
const
{ OpcountExceededError } = require('../error'),
db = require('../service/db.js'),
coll = db.collection('opcount');
const { OpcountExceededError } = require('../error');
const db = require('../service/db.js');
const coll = db.collection('opcount');
module.exports = {
/**
* @param {string} op
* @param {string} ident
* @param {number} period_secs
* @param {number} max_operations
* @param {string} op
* @param {string} ident
* @param {number} period_secs
* @param {number} max_operations
*/
async inc(op, ident, period_secs, max_operations) {
let cur_time = new Date().getTime();
let begin_at = new Date(cur_time - cur_time % (period_secs * 1000));
let expire_at = new Date(begin_at.getTime() + period_secs * 1000);
async inc(op, ident, periodSecs, maxOperations) {
const curTime = new Date().getTime();
const beginAt = new Date(curTime - (curTime % (periodSecs * 1000)));
const expireAt = new Date(beginAt.getTime() + periodSecs * 1000);
try {
await coll.findOneAndUpdate({
ident, begin_at, expire_at,
op: { $not: { $gte: max_operations } }
ident,
beginAt,
expireAt,
op: { $not: { $gte: maxOperations } },
}, { $inc: { op: 1 } }, { upsert: true });
} catch (e) {
throw new OpcountExceededError(op, period_secs, max_operations);
throw new OpcountExceededError(op, periodSecs, maxOperations);
}
}
},
};
/*
@argmethod.wrap
async def ensure_indexes():
coll = db.coll('opcount')
await coll.create_index([('ident', 1),
('begin_at', 1),
('expire_at', 1)], unique=True)
await coll.create_index('expire_at', expireAfterSeconds=0)
*/
+34 -37
View File
@@ -1,10 +1,10 @@
const
{ ObjectID } = require('bson'),
{ ProblemNotFoundError } = require('../error'),
validator = require('../lib/validator'),
db = require('../service/db.js'),
coll = db.collection('problem'),
coll_status = db.collection('problem.status');
const { ObjectID } = require('bson');
const { ProblemNotFoundError } = require('../error');
const validator = require('../lib/validator');
const db = require('../service/db.js');
const coll = db.collection('problem');
const collStatus = db.collection('problem.status');
/**
* @typedef {import('../interface').Pdoc} Pdoc
@@ -13,14 +13,14 @@ const
*/
/**
* @param {string} title
* @param {string} content
* @param {number} owner
* @param {number} pid
* @param {import('bson').ObjectID} data
* @param {string[]} category
* @param {string[]} tag
* @param {boolean} hidden
* @param {string} title
* @param {string} content
* @param {number} owner
* @param {number} pid
* @param {import('bson').ObjectID} data
* @param {string[]} category
* @param {string[]} tag
* @param {boolean} hidden
*/
async function add({
title,
@@ -30,7 +30,7 @@ async function add({
data = null,
category = [],
tag = [],
hidden = false
hidden = false,
}) {
validator.checkTitle(title);
validator.checkContent(content);
@@ -44,7 +44,7 @@ async function add({
tag,
hidden,
nSubmit: 0,
nAccept: 0
nAccept: 0,
});
return pid;
}
@@ -55,13 +55,13 @@ async function add({
*/
async function get(pid, uid = null) {
let query = {};
if (pid.generationTime || pid.length == 24) query = { _id: new ObjectID(pid) };
if (pid.generationTime || pid.length === 24) query = { _id: new ObjectID(pid) };
else query = { pid: parseInt(pid) || pid };
let pdoc = await coll.findOne(query);
const pdoc = await coll.findOne(query);
if (!pdoc) throw new ProblemNotFoundError(pid);
if (uid) {
query.uid = uid;
pdoc.psdoc = await coll_status.findOne(query);
pdoc.psdoc = await collStatus.findOne(query);
}
return pdoc;
}
@@ -71,7 +71,7 @@ async function get(pid, uid = null) {
*/
async function getById(_id) {
_id = new ObjectID(_id);
let pdoc = await coll.findOne({ _id });
const pdoc = await coll.findOne({ _id });
if (!pdoc) throw new ProblemNotFoundError(_id);
return pdoc;
}
@@ -81,7 +81,8 @@ async function getById(_id) {
* @returns {Pdoc[]}
*/
function getMany(query, sort, page, limit) {
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray();
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
}
/**
* @param {object} query
@@ -99,28 +100,24 @@ async function edit(_id, $set) {
if ($set.title) validator.checkTitle($set.title);
if ($set.content) validator.checkContent($set.content);
await coll.findOneAndUpdate({ _id }, { $set });
let pdoc = await getById(_id);
const pdoc = await getById(_id);
if (!pdoc) throw new ProblemNotFoundError(_id);
return pdoc;
}
/**
* @param {object} query
* @returns {number}
*/
async function count(query) {
return await coll.find(query).count();
function count(query) {
return coll.find(query).count();
}
async function random(query) {
let pdocs = coll.find(query);
let pcount = await pdocs.count();
const pdocs = coll.find(query);
const pcount = await pdocs.count();
if (pcount) {
let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0];
const pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0];
return pdoc.pid;
} else return null;
} return null;
}
async function getList(pids) {
let r = {};
for (let pid of pids) r[pid] = await get(pid);
const r = {};
for (const pid of pids) r[pid] = await get(pid); // eslint-disable-line no-await-in-loop
return r;
}
@@ -133,5 +130,5 @@ module.exports = {
random,
getById,
getMulti,
getList
};
getList,
};
+27 -26
View File
@@ -1,13 +1,13 @@
const
_ = require('lodash'),
{ ObjectID } = require('bson'),
{ RecordNotFoundError } = require('../error'),
{ STATUS_WAITING } = require('../model/builtin').STATUS,
db = require('../service/db.js'),
coll = db.collection('record');
const _ = require('lodash');
const { ObjectID } = require('bson');
const { RecordNotFoundError } = require('../error');
const { STATUS_WAITING } = require('./builtin').STATUS;
const db = require('../service/db.js');
const coll = db.collection('record');
/**
* @param {import('../interface').Record} data
* @param {import('../interface').Record} data
*/
async function add(data) {
_.defaults(data, {
@@ -20,33 +20,34 @@ async function add(data) {
compilerTexts: [],
testCases: [],
judger: null,
judgeAt: null
judgeAt: null,
});
let res = await coll.insertOne(data);
const res = await coll.insertOne(data);
return res.insertedId;
}
/**
* @param {string} rid
* @param {string} rid
* @returns {import('../interface').Record}
*/
async function get(rid) {
let _id = new ObjectID(rid);
let rdoc = await coll.findOne({ _id });
const _id = new ObjectID(rid);
const rdoc = await coll.findOne({ _id });
if (!rdoc) throw new RecordNotFoundError(rid);
return rdoc;
}
async function getMany(query, sort, page, limit) {
return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray();
function getMany(query, sort, page, limit) {
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
}
async function update(rid, $set) {
let _id = new ObjectID(rid);
const _id = new ObjectID(rid);
await coll.findOneAndUpdate({ _id }, { $set });
let rdoc = await coll.findOne({ _id });
const rdoc = await coll.findOne({ _id });
if (!rdoc) throw new RecordNotFoundError(rid);
return rdoc;
}
async function reset(rid) {
return await update(rid, {
function reset(rid) {
return update(rid, {
score: 0,
status: STATUS_WAITING,
time: 0,
@@ -56,15 +57,15 @@ async function reset(rid) {
compilerTexts: [],
judgeAt: null,
judger: null,
rejudged: true
rejudged: true,
});
}
async function count(query) {
return await coll.find(query).count();
function count(query) {
return coll.find(query).count();
}
async function getList(rids) {
let r = {};
for (let rid of rids) r[rid] = await get(rid);
const r = {};
for (const rid of rids) r[rid] = await get(rid); // eslint-disable-line no-await-in-loop
return r;
}
function getUserInProblemMulti(uid, pid) {
@@ -79,5 +80,5 @@ module.exports = {
count,
reset,
getList,
getUserInProblemMulti
};
getUserInProblemMulti,
};
+46 -45
View File
@@ -1,93 +1,94 @@
const
{ ObjectID } = require('bson'),
{ SolutionNotFoundError, AlreadyVotedError } = require('../error'),
validator = require('../lib/validator'),
db = require('../service/db.js'),
coll = db.collection('solution'),
coll_status = db.collection('solution.status');
const { ObjectID } = require('bson');
const { SolutionNotFoundError } = require('../error');
const validator = require('../lib/validator');
const db = require('../service/db.js');
/**
const coll = db.collection('solution');
const collStatus = db.collection('solution.status');
/**
* @param {string} pid
* @param {number} owner
* @param {string} content
* @param {number} owner
* @param {string} content
*/
async function add(pid, owner, content) {
validator.checkContent(content);
pid = new ObjectID(pid);
let res = await coll.insertOne({ content, owner, pid, reply: [], vote: 0 });
const res = await coll.insertOne({
content, owner, pid, reply: [], vote: 0,
});
return res.insertedId;
}
async function get(psid) {
psid = new ObjectID(psid);
let psdoc = await coll.findOne({ _id: psid });
const psdoc = await coll.findOne({ _id: psid });
if (!psdoc) throw new SolutionNotFoundError();
return psdoc;
}
async function getMany(query, sort, page, limit) {
return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray();
function getMany(query, sort, page, limit) {
return coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit)
.toArray();
}
async function edit(_id, content) {
validator.checkContent(content);
await coll.findOneAndUpdate({ _id }, { $set: { content } });
let psdoc = await get(_id);
const psdoc = await get(_id);
if (!psdoc) throw new SolutionNotFoundError(_id);
return psdoc;
}
async function del(psid) {
psid = new ObjectID(psid);
return await coll.deleteOne({ _id: psid });
function del(psid) {
return coll.deleteOne({ _id: psid });
}
async function count(query) {
return await coll.find(query).count();
function count(query) {
return coll.find(query).count();
}
function getMulti(pid) {
return coll.find({ pid }).sort({ vote: -1 });
}
async function reply(psid, owner, content) {
psid = new ObjectID(psid);
function reply(psid, owner, content) {
validator.checkContent(content);
return await coll.findOneAndUpdate({ _id: psid }, { $push: { reply: { content, owner, _id: new ObjectID() } } });
return coll.findOneAndUpdate(
{ _id: psid },
{ $push: { reply: { content, owner, _id: new ObjectID() } } },
);
}
async function getReply(psid, psrid) {
psid = new ObjectID(psid);
psrid = new ObjectID(psrid);
let psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
if (!psdoc) return [null, null];
for (let psrdoc of psdoc)
if (psrdoc._id == psrid)
return [psdoc, psrdoc];
for (const psrdoc of psdoc) if (psrdoc._id === psrid) return [psdoc, psrdoc];
return [psdoc, null];
}
async function editReply(psid, psrid, content) {
validator.checkContent(content);
psid = new ObjectID(psid);
psrid = new ObjectID(psrid);
let psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
let reply = psdoc.reply;
for (let i in reply)
if (reply[i]._id == psrid) {
const psdoc = await coll.findOne({ _id: psid, reply: { $elemMatch: { _id: psrid } } });
const { reply } = psdoc;
for (const i in reply) {
if (reply[i]._id === psrid) {
reply[i].content = content;
break;
}
}
return await coll.findOneAndUpdate({ _id: psdoc._id }, { $set: { reply } });
}
async function delReply(psid, psrid) {
return await coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } });
function delReply(psid, psrid) {
return coll.findOneAndUpdate({ _id: psid }, { $pull: { reply: { _id: psrid } } });
}
async function vote(psid, uid, value) {
let pssdoc = await coll_status.findOne({ psid, uid });
if (pssdoc) await coll_status.deleteOne({ psid, uid });
await coll_status.insertOne({ psid, uid, vote: value });
let pssdoc = await collStatus.findOne({ psid, uid });
if (pssdoc) await collStatus.deleteOne({ psid, uid });
await collStatus.insertOne({ psid, uid, vote: value });
if (pssdoc) value += -pssdoc.vote;
await coll.findOneAndUpdate({ _id: psid }, { $inc: { vote: value } });
pssdoc = await coll_status.findOne({ psid, uid });
let psdoc = await coll.findOne({ _id: psid });
pssdoc = await collStatus.findOne({ psid, uid });
const psdoc = await coll.findOne({ _id: psid });
return [psdoc, pssdoc];
}
async function getListStatus(list, uid) {
let result = {};
let res = await coll_status.find({ uid, psid: { $in: list } }).toArray();
for (let i of res) result[i.psid] = i;
const result = {};
const res = await collStatus.find({ uid, psid: { $in: list } }).toArray();
for (const i of res) result[i.psid] = i;
return result;
}
module.exports = {
@@ -103,5 +104,5 @@ module.exports = {
editReply,
delReply,
vote,
getListStatus
};
getListStatus,
};
+15 -82
View File
@@ -1,42 +1,43 @@
const
db = require('../service/db.js'),
coll = db.collection('system');
const db = require('../service/db.js');
const coll = db.collection('system');
async function get(_id) {
let doc = await coll.findOne({ _id });
const doc = await coll.findOne({ _id });
if (doc) return doc.value;
return null;
}
async function update(_id, update, config) {
await coll.findOneAndUpdate({ _id }, update, config);
return this.get(_id);
return get(_id);
}
/**
* Increments the user counter.
* @returns {number} Integer value after increment.
*/
async function incUserCounter() {
return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true });
function incUserCounter() {
return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
}
/**
* Increments the problem ID counter.
* @returns {number} Integer value before increment.
*/
async function incPidCounter() {
return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true });
function incPidCounter() {
return update('userCounter', { $inc: { value: 1 } }, { upsert: true });
}
async function acquireLock(name) {
let value = Math.floor(Math.random() * 0xFFFFFFFF);
const value = Math.floor(Math.random() * 0xFFFFFFFF);
try {
await coll.updateOne({ _id: 'lock_' + name, value: 0 }, { $set: { value } }, { upsert: true });
await coll.updateOne({ _id: `lock_${name}`, value: 0 }, { $set: { value } }, { upsert: true });
} catch (e) { return null; }
return value;
}
async function releaseLock(name, value) {
let result = await coll.updateOne({ _id: 'lock_' + name, value }, { $set: { value: 0 } });
const result = await coll.updateOne({ _id: `lock_${name}`, value }, { $set: { value: 0 } });
return !!result.matchedCount;
}
async function releaseLockAnyway(name) {
await coll.updateOne({ _id: 'lock_' + name }, { $set: { value: 0 } });
await coll.updateOne({ _id: `lock_${name}` }, { $set: { value: 0 } });
return true;
}
module.exports = {
@@ -46,73 +47,5 @@ module.exports = {
incUserCounter,
acquireLock,
releaseLock,
releaseLockAnyway
releaseLockAnyway,
};
/*
EXPECTED_DB_VERSION = 1
async def acquire_upgrade_lock():
lock = await acquire_lock('upgrade')
if not lock:
raise error.UpgradeLockAcquireError()
return lock
async def release_upgrade_lock(lock: int):
success = await release_lock('upgrade', lock)
if not success:
raise error.UpgradeLockReleaseError()
return True
@argmethod.wrap
async def release_upgrade_lock_anyway():
return await release_lock_anyway('upgrade')
@argmethod.wrap
async def get_db_version():
coll = db.coll('system')
doc = await coll.find_one({'_id': 'db_version'})
if doc is None:
return 0
else:
return doc['value']
async def set_db_version(version: int):
coll = db.coll('system')
result = await coll.update_one(filter={'_id': 'db_version'},
update={'$set': {'value': version}},
upsert=True)
return result.modified_count
async def ensure_db_version(allowed_version=None):
if allowed_version is None:
allowed_version = EXPECTED_DB_VERSION
current_version = await get_db_version()
if current_version != allowed_version:
raise error.DatabaseVersionMismatchError(current_version, allowed_version)
@argmethod.wrap
async def setup():
"""
Set up for fresh install
"""
coll = db.coll('system')
fdoc = await coll.find_one({'_id': 'user_counter'})
if fdoc:
# skip if not fresh install
return
await set_db_version(EXPECTED_DB_VERSION)
if __name__ == '__main__':
argmethod.invoke_by_args()
*/
+20 -17
View File
@@ -1,6 +1,6 @@
const
db = require('../service/db.js'),
coll = db.collection('token');
const db = require('../service/db.js');
const coll = db.collection('token');
module.exports = {
TYPE_SESSION: 0,
@@ -14,15 +14,16 @@ module.exports = {
* @returns {Array} token ID, token data
*/
async add(tokenType, expireSeconds, data) {
let now = new Date();
let str = String.random(32);
let res = await coll.insertOne(Object.assign({}, data, {
const now = new Date();
const str = String.random(32);
const res = await coll.insertOne({
...data,
_id: str,
tokenType,
createAt: now,
updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000)
}));
expireAt: new Date(now.getTime() + expireSeconds * 1000),
});
return [str, res.ops];
},
@@ -45,12 +46,14 @@ module.exports = {
* @returns {object} The token document, or null.
*/
async update(tokenId, tokenType, expireSeconds, data) {
let now = new Date();
let res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, {
$set: Object.assign({
const now = new Date();
const res = await coll.findOneAndUpdate({ _id: tokenId, tokenType }, {
$set: {
updateAt: now,
expireAt: new Date(now.getTime() + expireSeconds * 1000)
}, data, { tokenType })
expireAt: new Date(now.getTime() + expireSeconds * 1000),
...data,
tokenType,
},
});
return res.value;
},
@@ -62,10 +65,10 @@ module.exports = {
* @returns {boolean} true if deleted, or false.
*/
async delete(tokenId, tokenType) {
let result = await coll.deleteOne({ _id: tokenId, tokenType });
const result = await coll.deleteOne({ _id: tokenId, tokenType });
return !!result.deletedCount;
},
async getMostRecentSessionByUid(uid) {
return await coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } });
}
getMostRecentSessionByUid(uid) {
return coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } });
},
};
+25 -27
View File
@@ -1,26 +1,27 @@
const
validator = require('../lib/validator'),
{ ValidationError, TrainingNotFoundError } = require('../error'),
db = require('../service/db.js'),
coll = db.collection('traning'),
coll_status = db.collection('training.status');
const validator = require('../lib/validator');
const { ValidationError, TrainingNotFoundError } = require('../error');
const db = require('../service/db.js');
const coll = db.collection('traning');
const collStatus = db.collection('training.status');
module.exports = {
SETTING_DIFFICULTY_ALGORITHM: 0,
SETTING_DIFFICULTY_ADMIN: 1,
SETTING_DIFFICULTY_AVERAGE: 2,
async add(title, content, owner_uid, dag = [], desc = '') {
validator.check_title(title);
validator.check_intro(content);
validator.check_description(desc);
for (let node of dag)
for (let nid of node['require_nids'])
if (nid >= node._id)
throw new ValidationError('dag');
async add(title, content, owner, dag = [], desc = '') {
validator.checkTitle(title);
validator.checkIntro(content);
validator.checkDescription(desc);
for (const node of dag) { for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
return await coll.insertOne({
content, owner_uid,
dag, title, desc, enroll: 0
content,
owner,
dag,
title,
desc,
enroll: 0,
});
},
count: (query) => coll.find(query).count(),
@@ -28,24 +29,21 @@ module.exports = {
if ($set.title) validator.check_title($set.title);
if ($set.content) validator.check_intro($set.content);
if ($set.desc) validator.check_description($set.desc);
if ($set.dag)
for (let node of $set.dag)
for (let nid of node.require_nids)
if (nid >= node._id) throw new ValidationError('dag');
if ($set.dag) { for (const node of $set.dag) for (const nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); }
await coll.findOneAndUpdate({ tid }, { $set });
let tdoc = await coll.findOne({ tid });
const tdoc = await coll.findOne({ tid });
if (!tdoc) throw new TrainingNotFoundError(tid);
return tdoc;
},
async get(tid) {
let tdoc = await coll.findOne({ tid });
const tdoc = await coll.findOne({ tid });
if (!tdoc) throw new TrainingNotFoundError(tid);
return tdoc;
},
get_multi: (query) => coll.find( query),
get_multi_status: (query) => coll_status.find(query),
get_status: (tid, uid) => coll_status.findOne({ tid, uid }),
set_status: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set })
get_multi: (query) => coll.find(query),
get_multi_status: (query) => collStatus.find(query),
get_status: (tid, uid) => collStatus.findOne({ tid, uid }),
set_status: (tid, uid, $set) => collStatus.findOneAndUpdate({ tid, uid }, { $set }),
};
@@ -66,4 +64,4 @@ async def get_dict(domainId, tids, *, fields=None):
fields=fields):
result[tdoc['doc_id']] = tdoc
return result
*/
*/
+51 -42
View File
@@ -1,11 +1,11 @@
const
system = require('./system'),
{ UserNotFoundError, UserAlreadyExistError } = require('../error'),
pwhash = require('../lib/pwhash'),
validator = require('../lib/validator'),
db = require('../service/db'),
coll = db.collection('user'),
coll_role = db.collection('role');
const system = require('./system');
const { UserNotFoundError, UserAlreadyExistError } = require('../error');
const pwhash = require('../lib/pwhash');
const validator = require('../lib/validator');
const db = require('../service/db');
const coll = db.collection('user');
const collRole = db.collection('role');
class USER {
constructor(user) {
@@ -24,75 +24,78 @@ class USER {
this.nAccept = user.nAccept || 0;
this.nSubmit = user.nSubmit || 0;
}
hasPerm(perm) {
return this.perm == '-' || (this.perm || '').includes(perm);
return this.perm === '-' || (this.perm || '').includes(perm);
}
checkPassword(password) {
return pwhash.check(password, this.salt, this.hash);
}
}
async function getById(_id) {
let udoc = await coll.findOne({ _id });
const udoc = await coll.findOne({ _id });
if (!udoc) throw new UserNotFoundError(_id);
let role = await coll_role.findOne({ _id: udoc.role || 'default' });
const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm;
return new USER(udoc);
}
async function getList(uids) {
let r = {};
for (let uid of uids)
r[uid] = await getById(uid);
const r = {};
for (const uid of uids) r[uid] = await getById(uid); // eslint-disable-line no-await-in-loop
return r;
}
async function getByUname(uname) {
let unameLower = uname.trim().toLowerCase();
let udoc = await coll.findOne({ unameLower });
const unameLower = uname.trim().toLowerCase();
const udoc = await coll.findOne({ unameLower });
if (!udoc) throw new UserNotFoundError(uname);
let role = await coll_role.findOne({ _id: udoc.role || 'default' });
const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm;
return new USER(udoc);
}
async function getByEmail(mail, ignoreMissing = false) {
let mailLower = mail.trim().toLowerCase();
let udoc = await coll.findOne({ mailLower });
const mailLower = mail.trim().toLowerCase();
const udoc = await coll.findOne({ mailLower });
if (!udoc) {
if (ignoreMissing) return null;
else throw new UserNotFoundError(mail);
throw new UserNotFoundError(mail);
}
let role = await coll_role.findOne({ _id: udoc.role || 'default' });
const role = await collRole.findOne({ _id: udoc.role || 'default' });
udoc.perm = role.perm;
return new USER(udoc);
}
async function setPassword(uid, password) {
function setPassword(uid, password) {
validator.checkPassword(password);
let salt = pwhash.salt();
return await coll.findOneAndUpdate({ _id: uid }, {
$set: { salt, hash: pwhash.hash(password, salt) }
const salt = pwhash.salt();
return coll.findOneAndUpdate({ _id: uid }, {
$set: { salt, hash: pwhash.hash(password, salt) },
});
}
async function setEmail(uid, mail) {
validator.checkEmail(mail);
return await setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
}
function setById(uid, args) {
coll.findOneAndUpdate({ _id: uid }, { $set: args });
}
function setEmail(uid, mail) {
validator.checkEmail(mail);
return setById(uid, { mail, mailLower: mail.trim().toLowerCase() });
}
async function changePassword(uid, currentPassword, newPassword) {
validator.checkPassword(newPassword);
let udoc = await getById(uid);
const udoc = await getById(uid);
udoc.checkPassword(currentPassword);
let salt = pwhash.salt();
const salt = pwhash.salt();
return await coll.findOneAndUpdate({
_id: udoc._id
_id: udoc._id,
}, {
$set: { salt, hash: pwhash.hash(newPassword, salt) }
$set: { salt, hash: pwhash.hash(newPassword, salt) },
});
}
async function create({ uid, mail, uname, password, regip = '127.0.0.1', role = 'default' }) {
async function create({
uid, mail, uname, password, regip = '127.0.0.1', role = 'default',
}) {
validator.checkUname(uname);
validator.checkPassword(password);
validator.checkEmail(mail);
let salt = pwhash.salt();
const salt = pwhash.salt();
if (!uid) uid = system.incUserCounter();
try {
await coll.insertOne({
@@ -108,10 +111,10 @@ async function create({ uid, mail, uname, password, regip = '127.0.0.1', role =
loginat: new Date(),
loginip: regip,
role,
gravatar: mail
gravatar: mail,
});
} catch (e) {
throw new UserAlreadyExistError([uid, uname, email]);
throw new UserAlreadyExistError([uid, uname, mail]);
}
}
@@ -120,8 +123,14 @@ function getMany(params) {
}
module.exports = {
changePassword, create, getByEmail,
getById, getByUname, getMany,
setById, setEmail,
setPassword, getList
};
changePassword,
create,
getByEmail,
getById,
getByUname,
getMany,
setById,
setEmail,
setPassword,
getList,
};
+37 -38
View File
@@ -4,77 +4,76 @@ module.exports = {
showScoreboard: () => true,
showRecord: (tdoc, now) => now > tdoc.endAt,
stat: (tdoc, journal) => {
let naccept = {}, effective = {}, detail = [], accept = 0, time = 0;
for (let j in journal) {
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept))
effective[j.pid] = j;
if (!j.accept)
naccept[j.pid]++;
const naccept = {}; const effective = {}; const detail = []; let accept = 0; let time = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) { effective[j.pid] = j; }
if (!j.accept) { naccept[j.pid]++; }
}
function _time(jdoc) {
let real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
let penalty = 20 * 60 * naccept[jdoc.pid];
const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
const penalty = 20 * 60 * naccept[jdoc.pid];
return real + penalty;
}
for (let j of effective)
detail.push(Object.assign({}, j, {
naccept: naccept[j.pid], time: _time(j)
}));
for (let d of detail) {
for (const j of effective) { detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); }
for (const d of detail) {
accept += d.accept;
if (d.accept) time += d.time;
}
return { accept, time, detail };
},
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
let columns = [
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'display_name', value: _('Display Name') },
{ type: 'solved_problems', value: _('Solved Problems') }
{ type: 'solved_problems', value: _('Solved Problems') },
];
if (is_export) {
columns.push({ 'type': 'total_time', 'value': _('Total Time (Seconds)') });
columns.push({ 'type': 'total_time_str', 'value': _('Total Time') });
columns.push({ type: 'total_time', value: _('Total Time (Seconds)') });
columns.push({ type: 'total_time_str', value: _('Total Time') });
}
for (let i in tdoc.pids)
for (const i in tdoc.pids) {
if (is_export) {
columns.push({
type: 'problem_flag',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title)
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
});
columns.push({
type: 'problem_time',
value: '#{0} {1}'.format(i + 1, _('Time (Seconds)'))
value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')),
});
columns.push({
type: 'problem_time_str',
value: '#{0} {1}'.format(i + 1, _('Time'))
value: '#{0} {1}'.format(i + 1, _('Time')),
});
} else
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]]
value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]],
});
let rows = [columns];
for (let [rank, tsdoc] of ranked_tsdocs) {
let tsddict = {};
if (tdoc.detail)
for (let item of tsdoc.detail)
tsddict[item.pid] = item;
let row = [];
}
}
const rows = [columns];
for (const [rank, tsdoc] of ranked_tsdocs) {
const tsddict = {};
if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
const row = [];
row.push(
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
{ type: 'string', value: tsdoc.accept || 0 });
if (is_export)
{ type: 'string', value: tsdoc.accept || 0 },
);
if (is_export) {
row.push(
{ type: 'string', value: tsdoc.time || 0.0 },
{ type: 'string', value: tsdoc.time || 0.0 }
{ type: 'string', value: tsdoc.time || 0.0 },
);
for (let pid of tdoc.pids) {
let rdoc, col_accepted, col_time, col_time_str;
}
for (const pid of tdoc.pids) {
let rdoc; let col_accepted; let col_time; let
col_time_str;
if ((tsddict[pid] || {}).accept) {
rdoc = tsddict[pid].rid;
col_accepted = _('Accepted');
@@ -94,12 +93,12 @@ module.exports = {
row.push({
type: 'record',
value: '{0}\n{1}'.format(col_accepted, col_time_str),
raw: rdoc
raw: rdoc,
});
}
rows.push(row);
}
}
return rows;
}
};
},
};
+60 -53
View File
@@ -1,4 +1,5 @@
const { ValidationError } = require('../../error');
module.exports = {
TEXT: 'Assignment',
check(data) {
@@ -7,88 +8,94 @@ module.exports = {
if (data.penalty_since > data.end_at) throw new ValidationError('penalty_since', 'end_at');
},
stat(tdoc, journal) {
let effective = {}, detail = [], score = 0, penalty_score = 0, time = 0;
for (let j in journal)
if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept))
effective[j.pid] = j;
const _time = jdoc => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
const effective = {};
const detail = [];
let score = 0;
let penalty_score = 0;
let time = 0;
for (const j in journal) { if (tdoc.pids.includes(j.pid) && !(effective.includes(j.pid) && effective[j.pid].accept)) effective[j.pid] = j; }
const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000);
function _penalty_score(jdoc) {
let exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000);
const exceed_seconds = jdoc.rid.generationTime - Math.floor(tdoc.penalty_since / 1000);
if (exceed_seconds < 0) return jdoc.score;
else return 0.5 * jdoc.score;
return 0.5 * jdoc.score;
}
for (let j in effective) {
detail.push(Object.assign({}, effective[j], {
penalty_score: _penalty_score(effective[j]), time: _time(effective[j])
}));
for (const j in effective) {
detail.push({ ...effective[j], penalty_score: _penalty_score(effective[j]), time: _time(effective[j]) });
}
for (let d of detail) {
for (const d of detail) {
score += d.score;
penalty_score += d.penalty_score;
time += d.time;
}
return { score, penalty_score, time, detail };
return {
score, penalty_score, time, detail,
};
},
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, dudict, pdict) {
let columns = [
{ 'type': 'rank', 'value': _('Rank') },
{ 'type': 'user', 'value': _('User') },
{ 'type': 'display_name', 'value': _('Display Name') },
{ 'type': 'total_score', 'value': _('Score') }
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'display_name', value: _('Display Name') },
{ type: 'total_score', value: _('Score') },
];
if (is_export)
if (is_export) {
columns.push(
{ type: 'total_original_score', value: _('Original Score') },
{ type: 'total_time', value: _('Total Time (Seconds)') }
{ type: 'total_time', value: _('Total Time (Seconds)') },
);
}
columns.push({ type: 'total_time_str', value: _('Total Time') });
for (let i in tdoc.pids)
if (is_export)
for (const i in tdoc.pids) {
if (is_export) {
columns.push(
{ type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) },
{ type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) },
{ type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) },
{ type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) }
{ type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) },
);
else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), 'raw': pdict[tdoc.pids[i]] });
let rows = [columns];
for (let [rank, tsdoc] in ranked_tsdocs) {
let tsddict = {};
if (tsdoc.detail)
for (let item of tsdoc.detail)
tsddict[item.pid] = item;
let row = [
} else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] });
}
const rows = [columns];
for (const [rank, tsdoc] in ranked_tsdocs) {
const tsddict = {};
if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; }
const row = [
{ type: 'string', value: rank },
{ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] },
{ type: 'display_name', value: (dudict[tsdoc.uid] || {}).display_name || '' },
{ type: 'string', value: tsdoc.penalty_score || 0 }
{ type: 'string', value: tsdoc.penalty_score || 0 },
];
if (is_export)
if (is_export) {
row.push(
{ type: 'string', value: tsdoc.score || 0 },
{ type: 'string', value: tsdoc.time || 0.0 }
{ type: 'string', value: tsdoc.time || 0.0 },
);
}
row.push({ type: 'string', value: tsdoc.time || 0 });
for (let pid of tdoc.pids) {
let rdoc = (tsddict[pid] || {}).rid || null;
let col_score = (tsddict[pid] || {}).penalty_score || '-';
let col_original_score = (tsddict[pid] || {}).score || '-';
let col_time = (tsddict[pid] || {}).time || '-';
let col_time_str = col_time != '-' ? col_time : '-';
if (is_export) row.push(
{ type: 'string', value: col_score },
{ type: 'string', value: col_original_score },
{ type: 'string', value: col_time },
{ type: 'string', value: col_time_str },
);
else row.push({
type: 'record',
value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str),
raw: rdoc
});
for (const pid of tdoc.pids) {
const rdoc = (tsddict[pid] || {}).rid || null;
const col_score = (tsddict[pid] || {}).penalty_score || '-';
const col_original_score = (tsddict[pid] || {}).score || '-';
const col_time = (tsddict[pid] || {}).time || '-';
const col_time_str = col_time != '-' ? col_time : '-';
if (is_export) {
row.push(
{ type: 'string', value: col_score },
{ type: 'string', value: col_original_score },
{ type: 'string', value: col_time },
{ type: 'string', value: col_time_str },
);
} else {
row.push({
type: 'record',
value: '{0} / {1}\n{2}'.format(col_score, col_original_score, col_time_str),
raw: rdoc,
});
}
}
rows.push(row);
}
return rows;
}
};
},
};
+24 -21
View File
@@ -4,12 +4,14 @@ module.exports = {
TEXT: 'OI',
check: () => { },
stat: (tdoc, journal) => {
let detail = {}, score = 0;
for (let j in journal)
const detail = {};
let score = 0;
for (const j in journal) {
if (tdoc.pids.includes(j.pid)) {
detail[j.pid] = j;
score += j.score;
}
}
return { score, detail };
},
showScoreboard(tdoc, now) {
@@ -18,43 +20,44 @@ module.exports = {
showRecord(tdoc, now) {
return now > tdoc.endAt;
},
scoreboard(is_export, _, tdoc, ranked_tsdocs, udict, pdict) {
let columns = [
scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) {
const columns = [
{ type: 'rank', value: _('Rank') },
{ type: 'user', value: _('User') },
{ type: 'total_score', value: _('Total Score') }
{ type: 'total_score', value: _('Total Score') },
];
for (let i in tdoc.pids)
if (is_export)
for (const i in tdoc.pids) {
if (isExport) {
columns.push({
type: 'problem_score',
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title)
value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title),
});
else
} else {
columns.push({
type: 'problem_detail',
value: '#{0}'.format(i + 1),
raw: pdict[tdoc.pids[i]]
raw: pdict[tdoc.pids[i]],
});
let rows = [columns];
for (let [rank, tsdoc] of ranked_tsdocs) {
let tsddict = {};
if (tsdoc.journal)
for (let item of tsdoc.journal)
tsddict[item.pid] = item;
let row = [];
}
}
const rows = [columns];
for (const [rank, tsdoc] of rankedTsdocs) {
const tsddict = {};
if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; }
const row = [];
row.push({ type: 'string', value: rank });
row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] });
row.push({ type: 'string', value: tsdoc.score || 0 });
for (let pid of tdoc.pids)
for (const pid of tdoc.pids) {
row.push({
type: 'record',
value: (tsddict[pid] || {}).score || '-',
raw: (tsddict[pid] || {}).rid || null
raw: (tsddict[pid] || {}).rid || null,
});
}
rows.push(row);
}
return rows;
},
rank: tdocs => ranked(tdocs, (a, b) => a.score == b.score)
};
rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score),
};
+13 -14
View File
@@ -1,9 +1,8 @@
const
fs = require('fs'),
os = require('os'),
path = require('path'),
yaml = require('js-yaml'),
{ defaultsDeep } = require('lodash');
const fs = require('fs');
const os = require('os');
const path = require('path');
const yaml = require('js-yaml');
const { defaultsDeep } = require('lodash');
let options = {
db: {
@@ -11,16 +10,16 @@ let options = {
host: '127.0.0.1',
port: 27017,
username: '',
password: ''
password: '',
},
template: {
path: path.join(__dirname, '..', 'templates')
path: path.join(__dirname, '..', 'templates'),
},
listen: {
host: '127.0.0.1',
port: 8888,
domain: '',
https: false
https: false,
},
smtp: {
from: '',
@@ -28,7 +27,7 @@ let options = {
port: 465,
user: '',
pass: '',
secure: false
secure: false,
},
session: {
keys: ['Hydro'],
@@ -36,21 +35,21 @@ let options = {
domain: '127.0.0.1',
saved_expire_seconds: 3600 * 24,
unsaved_expire_seconds: 600,
registration_token_expire_seconds: 600
registration_token_expire_seconds: 600,
},
constants: {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
SOLUTION_PER_PAGE: 20,
CONTEST_PER_PAGE: 20
}
CONTEST_PER_PAGE: 20,
},
};
try {
let f = path.resolve(process.cwd(), 'config.yaml');
if (!fs.existsSync(f)) f = path.resolve(os.homedir(), '.config', 'hydro', 'config.yaml');
if (!fs.existsSync(f)) f = path.resolve('/config/config.yaml');
let t = yaml.safeLoad(fs.readFileSync(f));
const t = yaml.safeLoad(fs.readFileSync(f));
options = defaultsDeep(t, options);
} catch (e) {
console.error('Cannot load config');
+25 -27
View File
@@ -1,4 +1,4 @@
let permissions = {
const permissions = {
PERM_NONE: '0',
PERM_ALL: '-',
PERM_LOGGEDIN: '~',
@@ -41,32 +41,30 @@ let permissions = {
PERM_VIEW_TRAINING: 'a',
PERM_CREATE_TRAINING: 'b',
PERM_EDIT_TRAINING: 'c',
PERM_JUDGE: 'd'
PERM_JUDGE: 'd',
};
permissions.PERM_BASIC =
permissions.PERM_VIEW +
permissions.PERM_VIEW_PROBLEM +
permissions.PERM_VIEW_PROBLEM_SOLUTION +
permissions.PERM_VIEW_DISCUSSION +
permissions.PERM_VIEW_CONTEST +
permissions.PERM_VIEW_CONTEST_SCOREBOARD +
permissions.PERM_REGISTER_USER;
permissions.PERM_DEFAULT =
permissions.PERM_VIEW +
permissions.PERM_VIEW_PROBLEM +
permissions.PERM_SUBMIT_PROBLEM +
permissions.PERM_VIEW_PROBLEM_SOLUTION +
permissions.PERM_CREATE_PROBLEM_SOLUTION +
permissions.PERM_VOTE_PROBLEM_SOLUTION +
permissions.PERM_REPLY_PROBLEM_SOLUTION +
permissions.PERM_VIEW_DISCUSSION +
permissions.PERM_CREATE_DISCUSSION +
permissions.PERM_REPLY_DISCUSSION +
permissions.PERM_VIEW_CONTEST +
permissions.PERM_VIEW_CONTEST_SCOREBOARD +
permissions.PERM_ATTEND_CONTEST +
permissions.PERM_VIEW_TRAINING +
permissions.PERM_CREATE_TRAINING +
permissions.PERM_LOGGEDIN;
permissions.PERM_BASIC = permissions.PERM_VIEW
+ permissions.PERM_VIEW_PROBLEM
+ permissions.PERM_VIEW_PROBLEM_SOLUTION
+ permissions.PERM_VIEW_DISCUSSION
+ permissions.PERM_VIEW_CONTEST
+ permissions.PERM_VIEW_CONTEST_SCOREBOARD
+ permissions.PERM_REGISTER_USER;
permissions.PERM_DEFAULT = permissions.PERM_VIEW
+ permissions.PERM_VIEW_PROBLEM
+ permissions.PERM_SUBMIT_PROBLEM
+ permissions.PERM_VIEW_PROBLEM_SOLUTION
+ permissions.PERM_CREATE_PROBLEM_SOLUTION
+ permissions.PERM_VOTE_PROBLEM_SOLUTION
+ permissions.PERM_REPLY_PROBLEM_SOLUTION
+ permissions.PERM_VIEW_DISCUSSION
+ permissions.PERM_CREATE_DISCUSSION
+ permissions.PERM_REPLY_DISCUSSION
+ permissions.PERM_VIEW_CONTEST
+ permissions.PERM_VIEW_CONTEST_SCOREBOARD
+ permissions.PERM_ATTEND_CONTEST
+ permissions.PERM_VIEW_TRAINING
+ permissions.PERM_CREATE_TRAINING
+ permissions.PERM_LOGGEDIN;
permissions.PERM_ADMIN = permissions.PERM_ALL;
module.exports = permissions;
+5 -6
View File
@@ -1,15 +1,14 @@
const EventEmitter = require('events').EventEmitter;
const { EventEmitter } = require('events');
const bus = new EventEmitter();
module.exports = {
subscribe(events, handler) {
for (let event of events)
bus.on(event, handler);
for (const event of events) bus.on(event, handler);
},
unsubscribe(events, handler) {
for (let event of events)
bus.off(event, handler);
for (const event of events) bus.off(event, handler);
},
publish(event, data) {
bus.emit(event, { value: data, event });
}
},
};
+4 -3
View File
@@ -1,19 +1,20 @@
const Mongo = require('mongodb');
const options = require('../options');
let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
/**
* @type {import('mongodb').Db}
*/
let db = null;
Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
.then(Database => {
.then((Database) => {
db = Database.db(options.db.name);
db.Db = Database;
global.bus.emit('connected');
});
module.exports = {
collection: c => db.collection(c)
collection: (c) => db.collection(c),
};
+4 -4
View File
@@ -1,5 +1,5 @@
const
{ GridFSBucket } = require('mongodb'),
db = require('./db');
const { GridFSBucket } = require('mongodb');
const db = require('./db');
if (!db.s) db.s = {};
module.exports = new GridFSBucket(db);
module.exports = new GridFSBucket(db);
+12 -13
View File
@@ -1,6 +1,6 @@
let q = {};
const q = {};
module.exports = {
async connect() { },
async connect() { }, // eslint-disable-line no-empty-function
async assert(queue) {
q[queue] = q[queue] || [];
},
@@ -9,20 +9,19 @@ module.exports = {
},
async get(queue, wait = true) {
if (wait) {
while (!q[queue].length)
await new Promise(resolve => {
while (!q[queue].length) {
await new Promise((resolve) => { // eslint-disable-line no-await-in-loop
setTimeout(resolve, 100);
});
let data = q[queue][0];
}
const data = q[queue][0];
q[queue].splice(0, 1);
return data;
} else {
if (!q[queue].length) return null;
else {
let data = q[queue][0];
q[queue].splice(0, 1);
return data;
}
}
}
if (!q[queue].length) return null;
const data = q[queue][0];
q[queue].splice(0, 1);
return data;
},
};
+141 -101
View File
@@ -1,41 +1,42 @@
const
path = require('path'),
{ ObjectID } = require('bson'),
{ MongoError } = require('mongodb'),
Koa = require('koa'),
morgan = require('koa-morgan'),
Body = require('koa-body'),
Router = require('koa-router'),
cache = require('koa-static-cache'),
sockjs = require('sockjs'),
validator = require('../lib/validator'),
template = require('../lib/template'),
user = require('../model/user'),
blacklist = require('../model/blacklist'),
token = require('../model/token'),
opcount = require('../model/opcount'),
{ UserNotFoundError, BlacklistedError, PermissionError,
NotFoundError, UserFacingError } = require('../error');
const path = require('path');
const { ObjectID } = require('bson');
const Koa = require('koa');
const morgan = require('koa-morgan');
const Body = require('koa-body');
const Router = require('koa-router');
const cache = require('koa-static-cache');
const sockjs = require('sockjs');
const http = require('http');
const https = require('https');
const options = require('../options');
let http = options.listen.https ? require('https') : require('http');
let app = new Koa();
let server = http.createServer(app.callback());
const validator = require('../lib/validator');
const template = require('../lib/template');
const user = require('../model/user');
const blacklist = require('../model/blacklist');
const token = require('../model/token');
const opcount = require('../model/opcount');
const {
UserNotFoundError, BlacklistedError, PermissionError,
NotFoundError, UserFacingError,
} = require('../error');
const app = new Koa();
const server = (options.listen.https ? https : http).createServer(app.callback());
app.keys = options.session.keys;
app.use(cache(path.join(process.cwd(), '.uibuild'), {
maxAge: 365 * 24 * 60 * 60
maxAge: 365 * 24 * 60 * 60,
}));
app.use(Body({
multipart: true,
formidable: {
maxFileSize: 256 * 1024 * 1024
}
maxFileSize: 256 * 1024 * 1024,
},
}));
let router = new Router();
const router = new Router();
class Handler {
/**
* @param {import('koa').Context} ctx
* @param {import('koa').Context} ctx
*/
constructor(ctx) {
this.ctx = ctx;
@@ -46,7 +47,7 @@ class Handler {
body: ctx.request.body,
files: ctx.request.files,
query: ctx.query,
path: ctx.path
path: ctx.path,
};
this.response = {
body: '',
@@ -54,23 +55,24 @@ class Handler {
status: null,
template: null,
redirect: null,
attachment: name => ctx.attachment(name)
attachment: (name) => ctx.attachment(name),
};
this.UIContext = {
cdn_prefix: '/',
url_prefix: '/'
url_prefix: '/',
};
this._handler = {};
this.session = {};
}
renderHTML(name, context) {
console.time(name);
this.hasPerm = perm => this.user.hasPerm(perm);
this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, {
handler: this,
_: str => str ? str.toString().translate(this.user.language) : '',
user: this.user
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}), (error, res) => {
console.timeEnd(name);
if (error) reject(error);
@@ -78,35 +80,41 @@ class Handler {
});
});
}
async render(name, context) {
this.response.body = await this.renderHTML(name, context);
this.response.type = 'text/html';
}
render_title(str) {
renderTitle(str) { // eslint-disable-line class-methods-use-this
return str;
}
checkPerm(perm) {
for (let i in arguments) {
if (arguments[i] instanceof Array) {
checkPerm(...args) {
for (const i in args) {
if (args[i] instanceof Array) {
let p = false;
for (let j in arguments)
if (this.user.hasPerm(arguments[i][j])) {
for (const j in args) {
if (this.user.hasPerm(args[i][j])) {
p = true;
break;
}
if (!p) throw new PermissionError([arguments[i]]);
} else {
if (this.user.hasPerm(arguments[i])) continue;
else throw new PermissionError([[arguments[i]]]);
}
if (!p) throw new PermissionError([args[i]]);
} else if (!this.user.hasPerm(args[i])) {
throw new PermissionError([[args[i]]]);
}
}
}
async limitRate(op, period_secs, max_operations) {
await opcount.inc(op, this.request.ip, period_secs, max_operations);
async limitRate(op, periodSecs, maxOperations) {
await opcount.inc(op, this.request.ip, periodSecs, maxOperations);
}
back() {
this.response.redirect = this.request.headers.referer || '/';
}
async ___prepare() {
this.now = new Date();
this._handler.sid = this.request.cookies.get('sid');
@@ -114,19 +122,25 @@ class Handler {
this._handler.tokenType = token.TYPE_SESSION;
if (this._handler.save) this._handler.expireSeconds = options.session.saved_expire_seconds;
else this._handler.expireSeconds = options.session.unsaved_expire_seconds;
this.session = this._handler.sid ?
await token.update(this._handler.sid, this._handler.tokenType, this._handler.expireSeconds, {
update_ip: this.request.ip,
update_ua: this.request.headers['user-agent'] || ''
}) : { uid: 1 };
this.session = this._handler.sid
? await token.update(
this._handler.sid,
this._handler.tokenType,
this._handler.expireSeconds,
{
update_ip: this.request.ip,
update_ua: this.request.headers['user-agent'] || '',
},
) : { uid: 1 };
if (!this.session) this.session = { uid: 1 };
let bdoc = await blacklist.get(this.request.ip);
const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip);
this.user = await user.getById(this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid);
this.csrf_token = (await token.add(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path }))[0];
this.preferJson = (this.request.headers['accept'] || '').includes('application/json');
[this.csrfToken] = await token.add(token.TYPE_CSRF_TOKEN, 600, { path: this.request.path });
this.preferJson = (this.request.headers.accept || '').includes('application/json');
}
async ___cleanup() {
try {
await this.renderBody();
@@ -137,20 +151,24 @@ class Handler {
await this.putResponse();
await this.saveCookie();
}
async renderBody() {
if (!(this.response.body || this.response.template)) {
this.response.body = { error: new NotFoundError() };
this.response.template = 'error.html';
}
if (!this.preferJson)
if (!this.preferJson) {
if (this.response.body || this.response.template) {
if (this.request.query.noTemplate || this.preferJson) return;
if (this.request.query.template || this.response.template) {
const templateName = this.request.query.template || this.response.template;
if (templateName) {
this.response.body = this.response.body || {};
await this.render(this.request.query.template || this.response.template, this.response.body);
await this.render(templateName, this.response.body);
}
}
}
}
async putResponse() {
if (this.response.redirect && !this.preferJson) {
this.ctx.response.type = 'application/octet-stream';
@@ -164,27 +182,40 @@ class Handler {
if (this.response.type) this.ctx.response.type = this.response.type;
}
}
async saveCookie() {
if (this.session.sid)
await token.update(this.session.sid, this._handler.tokenType, this._handler.expireSeconds, {
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || ''
});
else
[this.session.sid] = await token.add(this._handler.tokenType, this._handler.expireSeconds, {
createIp: this.request.ip,
createUa: this.request.headers['user-agent'] || '',
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
...this.session
});
let cookie = { secure: options.session.secure };
if (this.session.sid) {
await token.update(
this.session.sid,
this._handler.tokenType,
this._handler.expireSeconds,
{
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
},
);
} else {
[this.session.sid] = await token.add(
this._handler.tokenType,
this._handler.expireSeconds,
{
createIp: this.request.ip,
createUa: this.request.headers['user-agent'] || '',
updateIp: this.request.ip,
updateUa: this.request.headers['user-agent'] || '',
...this.session,
},
);
}
const cookie = { secure: options.session.secure };
if (this._handler.save) {
cookie.expires = this.session.expireAt, cookie.maxAge = this._handler.expireSeconds;
cookie.expires = this.session.expireAt;
cookie.maxAge = this._handler.expireSeconds;
this.request.cookies.set('save', 'true', cookie);
}
this.ctx.cookies.set('sid', this.session.sid, cookie);
}
async onerror(error) {
console.error(error.message, error.params);
console.error(error.stack);
@@ -193,12 +224,12 @@ class Handler {
await this.___cleanup().catch(() => { });
}
}
function Route(route, handler) {
function Route(route, RouteHandler) {
router.all(route, async (ctx) => {
let h = new handler(ctx);
const h = new RouteHandler(ctx);
try {
let method = ctx.method.toLowerCase();
let args = Object.assign({}, ctx.params, ctx.query, ctx.request.body);
const method = ctx.method.toLowerCase();
const args = { ...ctx.params, ...ctx.query, ...ctx.request.body };
if (args.content) validator.checkContent(args.content);
if (args.title) validator.checkContent(args.title);
@@ -207,7 +238,7 @@ function Route(route, handler) {
if (args.rid) args.rid = new ObjectID(args.rid);
if (args.tid) args.tid = new ObjectID(args.tid);
if (args.duration) args.duration = parseFloat(args.duration);
if (args.pids) args.pids = args.pids.split(',').map(i => i.trim());
if (args.pids) args.pids = args.pids.split(',').map((i) => i.trim());
if (h.___prepare) await h.___prepare(args);
if (h.__prepare) await h.__prepare(args);
@@ -219,9 +250,10 @@ function Route(route, handler) {
if (h[`_${method}`]) await h[`_${method}`](args);
if (h[method]) await h[method](args);
if (method == 'post' && ctx.request.body.operation) {
if (h[`${method}_${ctx.request.body.operation}`])
if (method === 'post' && ctx.request.body.operation) {
if (h[`${method}_${ctx.request.body.operation}`]) {
await h[`${method}_${ctx.request.body.operation}`](args);
}
}
if (h.cleanup) await h.cleanup(args);
@@ -236,34 +268,35 @@ function Route(route, handler) {
class ConnectionHandler {
/**
* @param {import('sockjs').Connection} conn
* @param {import('sockjs').Connection} conn
*/
constructor(conn) {
let that = this;
const that = this;
this.conn = conn;
this.request = {
cookies: {
get(name) {
return that.request.cookies[name];
},
set() { }
set() { },
},
params: {},
headers: conn.headers
headers: conn.headers,
};
this._handler = {};
let p = (conn.url.split('?')[1] || '').split('&');
for (let i in p) p[i] = p[i].split('=');
for (let i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
const p = (conn.url.split('?')[1] || '').split('&');
for (const i in p) p[i] = p[i].split('=');
for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]);
}
renderHTML(name, context) {
console.time(name);
this.hasPerm = perm => this.user.hasPerm(perm);
this.hasPerm = (perm) => this.user.hasPerm(perm);
return new Promise((resolve, reject) => {
template.render(name, Object.assign(context, {
handler: this,
_: str => str ? str.toString().translate(this.user.language) : '',
user: this.user
_: (str) => (str ? str.toString().translate(this.user.language) : ''),
user: this.user,
}), (error, res) => {
console.timeEnd(name);
if (error) reject(error);
@@ -271,38 +304,43 @@ class ConnectionHandler {
});
});
}
send(data) {
this.conn.write(JSON.stringify(data));
}
close(code, reason) {
this.conn.close(code, reason);
}
async ___prepare() {
await new Promise((resolve, reject) => {
this.conn.once('data', msg => {
for (let i of msg.split(';')) {
i = i.trim().split('=');
this.request.cookies[i[0]] = i[1];
this.conn.once('data', (msg) => {
for (const i of msg.split(';')) {
const [k, v] = i.trim().split('=');
this.request.cookies[k] = v;
}
resolve();
});
setTimeout(reject, 5000);
});
this._handler.sid = this.request.cookies.get('sid');
this.session = this._handler.sid ? await token.get(this._handler.sid, token.TYPE_SESSION) : { uid: 1 };
this.session = this._handler.sid
? await token.get(this._handler.sid, token.TYPE_SESSION)
: { uid: 1 };
if (!this.session) this.session = { uid: 1 };
let bdoc = await blacklist.get(this.request.ip);
const bdoc = await blacklist.get(this.request.ip);
if (bdoc) throw new BlacklistedError(this.request.ip);
this.user = await user.getById(this.session.uid);
if (!this.user) throw new UserNotFoundError(this.session.uid);
}
}
function Connection(prefix, handler) {
function Connection(prefix, RouteConnHandler) {
const sock = sockjs.createServer({ prefix });
sock.on('connection', async conn => {
let h = new handler(conn);
sock.on('connection', async (conn) => {
const h = new RouteConnHandler(conn);
try {
let args = Object.assign({}, h.request.params);
const args = { ...h.request.params };
if (args.uid) args.uid = parseInt(validator.checkUid(args.uid));
if (args.page) args.page = parseInt(args.page);
@@ -313,9 +351,11 @@ function Connection(prefix, handler) {
if (h.__prepare) await h.__prepare(args);
if (h._prepare) await h._prepare(args);
if (h.prepare) await h.prepare(args);
if (h.message) conn.on('data', data => {
h.message(JSON.parse(data));
});
if (h.message) {
conn.on('data', (data) => {
h.message(JSON.parse(data));
});
}
conn.on('close', async () => {
if (h.cleanup) await h.cleanup(args);
if (h._cleanup) await h._cleanup(args);
+9 -8
View File
@@ -1,19 +1,20 @@
const
Mongo = require('mongodb'),
options = require('./options');
const Mongo = require('mongodb');
const options = require('./options');
async function run() {
let mongourl = 'mongodb://';
if (options.db.username) mongourl += options.db.username + ':' + options.db.password + '@';
if (options.db.username) mongourl += `${options.db.username}:${options.db.password}@`;
mongourl += `${options.db.host}:${options.db.port}/${options.db.name}`;
let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true });
let db = Database.db(options.db.name);
const Database = await Mongo.MongoClient.connect(mongourl, {
useNewUrlParser: true, useUnifiedTopology: true,
});
const db = Database.db(options.db.name);
await db.dropDatabase();
console.log('Dropped');
process.exit(0);
}
run().catch(e => {
run().catch((e) => {
console.error(e);
process.exit(1);
});
});
+6 -8
View File
@@ -5,13 +5,12 @@ const map = [
'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
'Y', 'Z', '1', '2', '3', '4', '5', '6', '7', '8',
'9', '0'
'9', '0',
];
String.random = function (digit) {
String.random = function random(digit) {
let str = '';
for (let i = 1; i <= digit; i++)
str += map[Math.floor(Math.random() * 62)];
for (let i = 1; i <= digit; i++) str += map[Math.floor(Math.random() * 62)];
return str;
};
@@ -20,15 +19,14 @@ String.random = function (digit) {
* @param {Array} b
*/
Array.isDiff = function isDiff(a, b) {
if (a.length != b.length) return true;
if (a.length !== b.length) return true;
a.sort();
b.sort();
for (let i in a)
if (a[i] != b[i]) return true;
for (const i in a) { if (a[i] !== b[i]) return true; }
return false;
};
Date.prototype.format = function (fmt = '%Y-%m-%d %H:%M:%S') {
Date.prototype.format = function formatDate(fmt = '%Y-%m-%d %H:%M:%S') {
return fmt
.replace('%Y', this.getFullYear())
.replace('%m', this.getMonth() + 1)
+5 -3
View File
@@ -13,7 +13,7 @@
"koa": "^2.11.0",
"koa-body": "^4.1.1",
"koa-morgan": "^1.0.1",
"koa-router": "^7.4.0",
"koa-router": "^8.0.8",
"koa-static-cache": "^5.1.2",
"lodash": "^4.17.15",
"markdown-it": "^10.0.0",
@@ -24,7 +24,9 @@
"sockjs": "^0.3.20"
},
"devDependencies": {
"eslint": "^6.6.0"
"eslint": "^6.8.0",
"eslint-config-airbnb-base": "^14.1.0",
"eslint-plugin-import": "^2.20.2"
},
"scripts": {
"build": "webpack --config webpack.config.js",
@@ -44,4 +46,4 @@
"bin": {
"judger": "hydro/development.js"
}
}
}
@@ -88,7 +88,7 @@
<input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)">
</div>
<input type="hidden" name="operation" value="{{ comment_post_op }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
</form>
</div>
<div class="nojs--hide">
+3 -3
View File
@@ -54,7 +54,7 @@
<input type="button" class="rounded button nojs--hide" name="dczcomments__box__cancel" value="{{ _('Cancel') }} (Esc)">
</div>
<input type="hidden" name="operation" value="{{ comment_post_op }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
</form>
</div>
<div class="nojs--hide">
@@ -91,13 +91,13 @@
<form method="post">
<input type="hidden" name="operation" value="upvote">
<input type="hidden" name="psid" value="{{ doc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="vote-button upvote{% if pssdict[doc._id].vote == 1 %} active{% endif %}" data-tooltip="{{ _('Upvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Upvote') }}"><span class="icon icon-vote--up"></span></button>
</form>
<form method="post">
<input type="hidden" name="operation" value="downvote">
<input type="hidden" name="psid" value="{{ doc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="vote-button downvote{% if pssdict[doc._id].vote == -1 %} active{% endif %}" data-tooltip="{{ _('Downvote') }}" data-tooltip-pos="bottom center" aria-label="{{ _('Downvote') }}"><span class="icon icon-vote--down"></span></button>
</form>
</div>
+1 -1
View File
@@ -64,7 +64,7 @@
markdown:true
}) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'contest_edit' %}
<button type="submit" class="rounded primary button">
{{ _('Update') }}
+1 -1
View File
@@ -10,7 +10,7 @@
<form method="post">
{% include "partials/discussion_edit_form.html" %}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
{{ _('Create') }} (Ctrl+Enter)
</button>
+1 -1
View File
@@ -40,7 +40,7 @@
{% if handler.hasPerm(perm.PERM_LOGGEDIN) %}
<li><form class="form--inline" method="post">
<input type="hidden" name="operation" value="{% if not dsdoc['star'] %}star{% else %}unstar{% endif %}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="star{% if dsdoc['star'] %} activated{% endif %}" type="submit">
<span class="starred--hide"><span class="icon icon-star--outline"></span> {{ _('Star Topic') }}</span>
<span class="starred--show"><span class="icon icon-star"></span> {{ _('Unstar Topic') }}</span>
+1 -1
View File
@@ -7,7 +7,7 @@
<form method="post">
{% include "partials/discussion_edit_form.html" %}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button name="operation" value="update" type="submit" class="rounded primary button">
{{ _('Update') }} (Ctrl+Enter)
</button>
+1 -1
View File
@@ -13,7 +13,7 @@
</div>
<div class="section__body typo">
<form method="post">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if join_settings['method'] == vj4.constant.domain.JOIN_METHOD_CODE %}
{{ form.form_text(columns=None, label='Invitation Code', name='code', required=true, help_text='You need to enter the invitation code to join the domain.', value=code, autofocus=True) }}
{% endif %}
+1 -1
View File
@@ -6,7 +6,7 @@
<div class="section__tools">
<form method="post">
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
<span class="icon icon-add"></span> {{ _('Initialize') }}
</button>
+1 -1
View File
@@ -7,7 +7,7 @@
{% include "partials/domain_edit_form.html" %}
{% endwith %}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
{{ _('Update') }}
</button>
@@ -50,7 +50,7 @@
name='invitation_code',
help_text='The invitation code to enter to successfully join the domain. You can only use letters and numbers in the code and it should not be longer than 64 characters.',
value=join_settings['code']|default('')) }}
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="rounded primary button" role="submit">
{{ _('Update Settings') }}
</button>
+1 -1
View File
@@ -47,7 +47,7 @@
</table>
</div>
<div class="section__body">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="rounded primary button" role="submit">
{{ _('Update Permission') }}
</button>
+1 -1
View File
@@ -10,7 +10,7 @@
<p>{{ _('Image Markdown') }}: <pre>![{{ _('Description') }}](vijos:/{{ reverse_url('fs_get', domain_id=vj4.model.builtin.DOMAIN_ID_SYSTEM, secret=fdoc['metadata']['secret']) }})</pre></p>
{% endif %}
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<p>
{{ _('New file') }}:
<input type="text" name="desc" placeholder="{{ _('Description') }}">
+1 -1
View File
@@ -47,7 +47,7 @@
</td>
<td class="col--action"><form method="post">
<input type="hidden" name="domain_id" value="{{ ddoc['_id'] }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="typo-a">{{ _('Continue') }}</button>
</form></td>
</tr>
+1 -1
View File
@@ -21,7 +21,7 @@
{% include "partials/domain_edit_form.html" %}
{% endwith %}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
{{ _('Create') }}
</button>
+4 -4
View File
@@ -13,7 +13,7 @@
{{ form.form_text(type='text', label='Current Email', columns=10, name='current_email', value=handler.user['mail'], disabled=true) }}
{{ form.form_text(type='text', label='New Email', columns=10, name='mail', required=true) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="operation" value="change_mail">
<input type="submit" class="rounded primary button" value="{{ _('Change Email') }}">
</div></div>
@@ -32,7 +32,7 @@
{{ form.form_text(type='password', label='New Password', columns=10, name='new_password', required=true) }}
{{ form.form_text(type='password', label='Repeat Password', columns=10, name='verify_password', required=true) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="operation" value="change_password">
<input type="submit" class="rounded primary button" value="{{ _('Change Password') }}">
</div></div>
@@ -72,7 +72,7 @@
<input type="hidden" name="operation" value="delete_token">
<input type="hidden" name="token_type" value="{{ session['token_type'] }}">
<input type="hidden" name="token_digest" value="{{ session['token_digest'] }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" value="{{ _('Logout This Session') }}" class="rounded button">
</form>
</div>
@@ -85,7 +85,7 @@
<div class="section__body">
<form method="post">
<input type="hidden" name="operation" value="delete_all_tokens">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" value="{{ _('Logout All Sessions') }}" class="primary rounded button">
</form>
<div class="typo">
+1 -1
View File
@@ -61,7 +61,7 @@
</div>
{% endfor %}
<div class="section__body">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="rounded primary button" value="{{ _('Save All Changes') }}">
</div>
</form>
+1 -1
View File
@@ -23,7 +23,7 @@
{{ form.form_text(columns=None, label='Problems', name='pids', value=pids) }}
{{ form.form_textarea(columns=None, label='Content', name='content', value=tdoc['content']|default(''), markdown=True) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'homework_edit' %}
<button type="submit" class="rounded primary button">
{{ _('Update') }}
+1 -1
View File
@@ -23,7 +23,7 @@
<meta name="theme-color" content="#56758f">
<link rel="stylesheet" media="all" href="{{ static_url('vj4.css') }}">
<title>{{ handler.render_title(page_title) }}</title>
<title>{{ handler.renderTitle(page_title) }}</title>
<script>
var _htmlNode = document.documentElement;
_htmlNode.className = _htmlNode.className.replace(' nojs', ' hasjs');
+1 -1
View File
@@ -27,7 +27,7 @@
{% if handler.hasPerm(perm.PERM_ATTEND_CONTEST) and handler.hasPerm(perm.PERM_LOGGEDIN) %}
<form action="/c/{{ tdoc._id }}" method="POST">
<input type="hidden" name="operation" value="attend">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Attend Contest') }}
</button>
+1 -1
View File
@@ -27,7 +27,7 @@
{% if handler.hasPerm(perm.PERM_ATTEND_HOMEWORK) and handler.hasPerm(perm.PERM_LOGGEDIN) %}
<form action="{{ reverse_url('homework_detail', tid=tdoc['doc_id']) }}" method="POST">
<input type="hidden" name="operation" value="attend">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Claim Homework') }}
</button>
+1 -1
View File
@@ -37,7 +37,7 @@
</label>
</div></div>
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'problem_edit' %}
<button type="submit" class="rounded primary button">
{{ _('Update') }}
+1 -1
View File
@@ -37,7 +37,7 @@
</label>
</div></div>
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
{{ _('Copy') }}
</button>
+1 -1
View File
@@ -42,7 +42,7 @@
value:pdoc['tag']|join(', ')
}) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="rounded primary button">
{{ _('Update') }}
</button>
+1 -1
View File
@@ -52,7 +52,7 @@
}) }}
{{ form.form_textarea({label:'Code', columns:12, name:'code', extra_class:'monospace', autofocus:true}) }}
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="submit" class="rounded primary button" value="{{ _('Submit') }}">
</div></div>
</form>
+1 -1
View File
@@ -8,7 +8,7 @@
<p>{{ _('MD5: {0}').format(md5 or _('(None)')) }}</p>
{% endif %}
<p>{{ _('New dataset') }}:
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="file" name="file">
<input type="submit" value="{{ _('Upload') }}" class="rounded primary button">
</p>
+2 -2
View File
@@ -33,7 +33,7 @@
<ol class="menu">
<li class="menu__item">
<form method="post" action="/r/{{ rdoc._id }}/rejudge">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="menu__link">
<span class="icon icon-refresh"></span> {{ _('Rejudge') }}
</button>
@@ -42,7 +42,7 @@
<li class="menu__item">
<form method="post" action="/judge/end">
<input type="hidden" name="rid" value="{{ rdoc._id }}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<input type="hidden" name="score" value="0">
<input type="hidden" name="message" value="{{ _('score canceled') }}">
<button type="submit" class="menu__link">
+1 -1
View File
@@ -5,7 +5,7 @@
<td class="col--problem col--problem-name">
{% if handler.hasPerm(perm.PERM_REJUDGE) %}
<form class="form--inline" method="post" action="/r/{{ rdoc._id }}/rejudge">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button type="submit" class="link text-maroon lighter">
<span class="icon icon-refresh"></span>
{{ _('Rejudge') }}
+2 -2
View File
@@ -115,7 +115,7 @@
<form class="form--inline" action="{{ reverse_url('problem_main') }}" method="post">
<input type="hidden" name="pid" value="{{ pdoc['_id'] }}">
<input type="hidden" name="operation" value="{% if not psdoc['star'] %}star{% else %}unstar{% endif %}">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="star{% if psdoc['star'] %} activated{% endif %}" type="submit">
<span class="starred--hide"><span class="icon icon-star--outline" data-tooltip="{{ _('Star') }}"></span></span>
<span class="starred--show"><span class="icon icon-star" data-tooltip="{{ _('Unstar') }}"></span></span>
@@ -168,7 +168,7 @@
<li class="menu__item">
<form action="" method="POST">
<input type="hidden" name="operation" value="enroll">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<button class="menu__link" type="submit">
<span class="icon icon-add"></span> {{ _('Enroll Training') }}
</button>
+1 -1
View File
@@ -33,7 +33,7 @@
</label>
</div></div>
<div class="row"><div class="columns">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
{% if page_name == 'training_edit' %}
<button type="submit" class="rounded primary button">
{{ _('Update') }}
+1 -1
View File
@@ -4,7 +4,7 @@
<div class="immersive--content immersive--center">
<h1>{{ _('Logout') }}</h1>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ handler.csrf_token }}">
<input type="hidden" name="csrfToken" value="{{ handler.csrfToken }}">
<div class="text-center">
<input type="submit" value="{{ _('Logout') }}" class="inverse rounded primary button">
</div>
+616 -173
View File
File diff suppressed because it is too large. Load diff