feat: validator decorator

This commit is contained in:
undefined committed 2020-07-08 20:37:06 +08:00
1 parent ba03d7a7bf
commit 4e85b696c0
23 files changed
+313 -180

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@ declare global {
}
declare module 'cluster' {
var isFirstWorker: boolean;
let isFirstWorker: boolean;
}
export { };
-1
View File
@@ -1,4 +1,3 @@
import fs from 'fs';
import {
VerifyPasswordError, UserAlreadyExistError, InvalidTokenError,
NotFoundError,
+1 -1
View File
@@ -347,7 +347,7 @@ class HomeworkCodeHandler extends HomeworkHandler {
}
}
async function apply() {
export async function apply() {
Route('homework_main', '/homework', HomeworkMainHandler, PERM.PERM_VIEW_HOMEWORK);
Route('homework_create', '/homework/create', HomeworkCreateHandler, PERM.PERM_CREATE_HOMEWORK);
Route('homework_detail', '/homework/:tid', HomeworkDetailHandler, PERM.PERM_VIEW_HOMEWORK);
+2 -2
View File
@@ -1,10 +1,10 @@
import fs from 'fs';
import path from 'path';
import os from 'os';
import { ValidationError } from '../error'
import { ValidationError } from '../error';
import * as problem from '../model/problem';
import { PERM } from '../model/builtin';
import { Route, Handler } from '../service/server'
import { Route, Handler } from '../service/server';
class ProblemImportHandler extends Handler {
async prepare() {
+1 -1
View File
@@ -6,7 +6,7 @@ import * as builtin from '../model/builtin';
import * as contest from '../model/contest';
import * as user from '../model/user';
import * as task from '../model/task';
import * as bus from '../service/bus';
import * as bus from '../service/bus';
import {
Route, Handler, Connection, ConnectionHandler,
} from '../service/server';
+4 -1
View File
@@ -8,6 +8,7 @@ import * as record from '../model/record';
import {
Route, Connection, Handler, ConnectionHandler,
} from '../service/server';
import { validate } from '../lib/validator';
import * as hpm from '../lib/hpm';
function set(key, value) {
@@ -103,7 +104,9 @@ class SystemScriptHandler extends SystemHandler {
}
async post({ domainId, id, args = '{}' }) {
if (!global.Hydro.script[id]) throw new ValidationError('id');
args = JSON.parse(args);
validate(global.Hydro.script[id].validate, args);
const rid = await record.add(domainId, {
pid: id,
uid: this.user._id,
@@ -161,7 +164,7 @@ class SystemSettingHandler extends SystemHandler {
}
}
async post(args) {
async post(args: any) {
const tasks = [];
for (const key in args) {
if (typeof args[key] === 'object') {
+2 -2
View File
@@ -1,14 +1,14 @@
import assert from 'assert';
import { ObjectID } from 'mongodb';
import { ValidationError, ProblemNotFoundError } from '../error';
import paginate from '../lib/paginate';
import { PERM, PRIV } from '../model/builtin';
import * as problem from '../model/problem';
import * as builtin from '../model/builtin';
import * as builtin from '../model/builtin';
import * as training from '../model/training';
import * as user from '../model/user';
import * as system from '../model/system';
import { Route, Handler } from '../service/server';
import { ObjectID } from 'mongodb';
async function _parseDagJson(domainId, dag) {
const parsed = [];
+93 -22
View File
@@ -1,3 +1,4 @@
/* eslint-disable no-empty */
import { ValidationError } from '../error';
const RE_UID = /^-?\d+$/i;
@@ -6,6 +7,75 @@ const RE_UNAME = /[^\s\u3000](.{,254}[^\s\u3000])?$/i;
const RE_ROLE = /^[_0-9A-Za-z]{1,256}$/i;
const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
// eslint-disable-next-line consistent-return
function _validate(scheme: any, arg: any, base: string) {
if (!(scheme instanceof Object)) return _validate({ $type: scheme }, arg, base);
if (scheme.$validate) scheme.$validate(arg);
if (scheme.$group) _validate(scheme.$group, arg, base);
if (scheme.$or) {
if (scheme.$or instanceof Array) {
for (const s of scheme.$or) {
let success = false;
try {
_validate(s, arg, base);
success = true;
} catch { }
// TODO beautify output
if (!success) throw new ValidationError(scheme.$or);
}
} else {
for (const skey in scheme.$or) {
let success = false;
try {
_validate(scheme.$or[skey], arg[skey], `${base}.${skey}`);
success = true;
} catch { }
// TODO beautify output
if (!success) throw new ValidationError(base, scheme.$or);
}
}
}
if (scheme.$and) for (const s of scheme.$and) _validate(s, arg, base);
if (scheme.$type) {
if (typeof scheme.$type === 'string') {
if (!typeof arg === scheme.$type) {
throw new ValidationError();
}
} else if (!(arg instanceof scheme.$type)) {
throw new ValidationError();
}
}
if (scheme.$eq && scheme.$eq !== arg) throw new ValidationError(base);
if (scheme.$not && scheme.$not === arg) throw new ValidationError(base);
if (scheme.$in && !(scheme.$in.includes(arg))) throw new ValidationError(base);
if (scheme.$nin && scheme.$nin.includes(arg)) throw new ValidationError(base);
for (const key in scheme) {
if (key[0] !== '$') {
if (arg instanceof Object) {
_validate(scheme[key], arg[key], `${base}.${key}`);
} else {
throw new ValidationError();
}
}
}
}
function descriptor(scheme: any) {
return function desc(target: any, name: string, descriptor: any) {
const originalMethod = descriptor.value;
descriptor.value = function val(...args: any[]) {
_validate(scheme, args[0], '');
return originalMethod.call(this, ...args);
};
return descriptor;
};
}
export function validate(scheme: any, arg: any) {
if (arguments.length === 1) return descriptor(scheme);
return _validate(scheme, arg, '');
}
export const isTitle = (s) => s && s.length < 64;
export const checkTitle = (s) => { if (!isTitle(s)) throw new ValidationError('title'); else return s; };
export const isUid = (s) => RE_UID.test(s);
@@ -30,28 +100,29 @@ export const isDescription = () => true;
export const checkDescription = (s) => { if (!isDescription()) throw new ValidationError('description'); else return s; };
global.Hydro.lib.validator = {
isTitle,
checkTitle,
isUid,
checkUid,
isUname,
checkUname,
isRole,
checkRole,
isPassword,
checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isName,
checkName,
isPid,
checkPid,
isIntro,
checkIntro,
isDescription,
checkDescription,
validate: _validate,
isTitle,
checkTitle,
isUid,
checkUid,
isUname,
checkUname,
isRole,
checkRole,
isPassword,
checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isName,
checkName,
isPid,
checkPid,
isIntro,
checkIntro,
isDescription,
checkDescription,
};
/*
ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?')
+1 -1
View File
@@ -42,7 +42,7 @@ export function getMany(query: any, sort: any, page: number, limit: number) {
.toArray();
}
export function getMulti(query: any) {
export function getMulti(query: any = {}) {
return coll.find(query);
}
+6
View File
@@ -44,6 +44,10 @@ export class User {
role: string;
regip: () => string;
loginip: () => string;
constructor(udoc, dudoc) {
this.udoc = () => udoc;
this.dudoc = () => dudoc;
@@ -55,7 +59,9 @@ export class User {
this.hashType = udoc.hashType || 'hydro';
this.priv = udoc.priv;
this.regat = udoc.regat;
this.regip = () => udoc.regip;
this.loginat = udoc.loginat;
this.loginip = () => udoc.loginip;
this.perm = dudoc.perm;
this.role = dudoc.role || 'default';
-66
View File
@@ -1,66 +0,0 @@
const description = 'Add blacklist by ip, uid';
// This code format is just for fun. DO NOT DO THIS IN YOUR CODE !
/* eslint-disable */
const { ObjectID } = require('mongodb');
const document = require('../model/document');
const discussion = require('../model/discussion');
const user = require('../model/user');
const blacklist = require('../model/blacklist');
const db = require('../service/db');
async function _address(ip, bset, uset, dset, dryrun, report) {
if (bset.has(ip))
return
bset.add(ip)
report({ message: `ip ${ip}` })
const users = await db.collection('user').find({ loginip: ip }).toArray()
const tasks = []
for (const udoc of users) {
tasks.push(_user(udoc._id, bset, uset, dset, dryrun, report)) }
await Promise.all(tasks)
if (!dryrun)
await blacklist.add(ip) }
async function _discussion(domainId, did, bset, uset, dset, dryrun, report) {
if (dset.has(did))
return
dset.add(did)
const ddoc = await discussion.get(domainId, did)
if (!ddoc)
return
report({ message: `discussion ${ddoc['title']}` })
await _user(ddoc['owner'], bset, uset, dset, dryrun, report)
if (ddoc.ip)
await _address(ddoc['ip'], bset, uset, dset, dryrun, report)
if (!dryrun)
await discussion.delete(domainId, ddoc['docId']) }
async function _user(uid, bset, uset, dset, dryrun, report) {
if (uset.has(uid))
return
uset.add(uid)
const udoc = await user.getById(uid)
if (!udoc)
return
report({ message: `user ${udoc['_id']} ${udoc['uname']}` })
await _address(udoc['loginip'], bset, uset, dset, dryrun, report)
const ddocs = await db.collection('document')
.find({ docType: document.TYPE_DISCUSSION }).sort({ domainId: 1, docId: 1 }).toArray()
const tasks = []
for (const ddoc of ddocs)
tasks.push(_discussion(ddoc.domainId, ddoc.docId, bset, uset, dset, dryrun, report))
await Promise.all(tasks)
if (!dryrun)
await user.ban(uid) }
async function run({address = null, discuss = null, user = null, dryrun = true}, report) {
if (address)
await _address(address, new Set(), new Set(), new Set(), dryrun, report)
if (discuss)
await _discussion(discuss.domainId, new ObjectID(discuss.did),
new Set(), new Set(), new Set(), dryrun, report)
if (user)
await _user(user, new Set(), new Set(), new Set(), dryrun, report) }
global.Hydro.script.blacklist = module.exports = { run, description };
+86
View File
@@ -0,0 +1,86 @@
/* eslint-disable no-use-before-define */
import { ObjectID } from 'mongodb';
import * as document from '../model/document';
import * as discussion from '../model/discussion';
import * as user from '../model/user';
import * as blacklist from '../model/blacklist';
import * as db from '../service/db';
export const description = 'Add blacklist by ip, uid';
async function _address(
ip: string,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (bset.has(ip)) return;
bset.add(ip);
report({ message: `ip ${ip}` });
const users = await db.collection('user').find({ loginip: ip }).toArray();
const tasks = [];
for (const udoc of users) {
tasks.push(_user(udoc._id, bset, uset, dset, dryrun, report));
}
await Promise.all(tasks);
if (!dryrun) await blacklist.add(ip);
}
async function _discussion(
domainId: string, did: ObjectID,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (dset.has(did)) return;
dset.add(did);
const ddoc = await discussion.get(domainId, did);
if (!ddoc) return;
report({ message: `discussion ${ddoc.title}` });
await _user(ddoc.owner, bset, uset, dset, dryrun, report);
if (ddoc.ip) await _address(ddoc.ip, bset, uset, dset, dryrun, report);
if (!dryrun) await discussion.del(domainId, ddoc.docId);
}
async function _user(
uid: number,
bset: Set<string>, uset: Set<number>, dset: Set<ObjectID>,
dryrun: boolean, report: Function,
) {
if (uset.has(uid)) return;
uset.add(uid);
const udoc = await user.getById('system', uid);
if (!udoc) return;
report({ message: `user ${udoc._id} ${udoc.uname}` });
await _address(udoc.loginip(), bset, uset, dset, dryrun, report);
const ddocs = await db.collection('document').find({ docType: document.TYPE_DISCUSSION })
.sort({ domainId: 1, docId: 1 }).toArray();
const tasks = [];
for (const ddoc of ddocs) {
tasks.push(_discussion(ddoc.domainId, ddoc.docId, bset, uset, dset, dryrun, report));
}
await Promise.all(tasks);
if (!dryrun) await user.ban(uid);
}
export async function run({
address = null, discuss = null, user = null, dryrun = true,
}, report) {
if (address) await _address(address, new Set(), new Set(), new Set(), dryrun, report);
if (discuss) {
await _discussion(
discuss.domainId, new ObjectID(discuss.did),
new Set(), new Set(), new Set(), dryrun, report,
);
}
if (user) await _user(user, new Set(), new Set(), new Set(), dryrun, report);
}
export const validate = {
$or: {
address: 'string',
discuss: ObjectID,
user: 'number',
},
dryrun: 'boolean',
};
global.Hydro.script.blacklist = { run, description, validate };
@@ -1,12 +1,12 @@
const description = 'Delete a user';
/* eslint-disable no-await-in-loop */
const user = require('../model/user');
const document = require('../model/document');
const setting = require('../model/setting');
const db = require('../service/db');
const paginate = require('../lib/paginate');
const { STATUS, PRIV } = require('../model/builtin');
import * as user from '../model/user';
import * as document from '../model/document';
import * as setting from '../model/setting';
import * as db from '../service/db';
import paginate from '../lib/paginate';
import { STATUS, PRIV } from '../model/builtin';
export const description = 'Delete a user';
const collDocument = db.collection('document');
const collStatus = db.collection('document.status');
@@ -19,7 +19,7 @@ for (const s of setting.PREFERENCE_SETTINGS) {
$unset[s.key] = '';
}
async function deleteRecord(uid) {
async function deleteRecord(uid: number) {
const [, pcount] = await paginate(
collRecord.find(),
1,
@@ -45,7 +45,7 @@ async function deleteRecord(uid) {
await collRecord.deleteMany({ uid });
}
async function deleteDiscussion(uid) {
async function deleteDiscussion(uid: number) {
const [, pcount] = await paginate(
collDocument.find({ docType: document.TYPE_DISCUSSION, owner: uid }),
1,
@@ -70,7 +70,7 @@ async function deleteDiscussion(uid) {
// TODO delete replies
}
async function run({
export async function run({
uid,
}) {
await deleteRecord(uid);
@@ -87,4 +87,8 @@ async function run({
return uid;
}
global.Hydro.script.deleteUser = module.exports = { run, description };
export const validate = {
uid: 'number',
};
global.Hydro.script.deleteUser = { run, description, validate };
@@ -1,12 +1,12 @@
const description = 'Install';
import { PRIV } from '../model/builtin';
import * as system from '../model/system';
import * as user from '../model/user';
import * as domain from '../model/domain';
import pwhash from '../lib/hash.hydro';
const { PRIV_ALL } = require('../model/builtin').PRIV;
const system = require('../model/system');
const user = require('../model/user');
const domain = require('../model/domain');
const pwhash = require('../lib/hash.hydro');
export const description = 'Install';
async function run({ username, password } = {}) {
export async function run({ username = '', password = '' } = {}) {
const def = {
PROBLEM_PER_PAGE: 100,
RECORD_PER_PAGE: 100,
@@ -52,7 +52,7 @@ async function run({ username, password } = {}) {
uname: username,
password,
regip: '127.0.0.1',
priv: PRIV_ALL,
priv: PRIV.PRIV_ALL,
});
} else {
const salt = String.random();
@@ -60,11 +60,18 @@ async function run({ username, password } = {}) {
uname: username,
unameLower: username.trim().toLowerCase(),
salt,
hash: pwhash.default(password, salt),
hash: pwhash(password, salt),
hashType: 'hydro',
});
}
}
}
global.Hydro.script.install = module.exports = { run, description };
export const validate = {
$or: [
{ username: 'string', password: 'string' },
{ username: 'undefined', password: 'undefined' },
],
};
global.Hydro.script.install = { run, description, validate };
@@ -1,10 +1,10 @@
const description = 'Recalcuates nSubmit and nAccept in problem status.';
import * as db from '../service/db';
import { STATUS } from '../model/builtin';
import * as document from '../model/document';
const db = require('../service/db');
const { STATUS_ACCEPTED } = require('../model/builtin').STATUS;
const document = require('../model/document');
export const description = 'Recalcuates nSubmit and nAccept in problem status.';
async function run() {
export async function run() {
const pipeline = [
{
$match: { hidden: false, type: { $ne: 'run' } },
@@ -15,7 +15,7 @@ async function run() {
nSubmit: { $sum: 1 },
nAccept: {
$sum: {
$cond: [{ $eq: ['$status', STATUS_ACCEPTED] }, 1, 0],
$cond: [{ $eq: ['$status', STATUS.STATUS_ACCEPTED] }, 1, 0],
},
},
},
@@ -29,8 +29,8 @@ async function run() {
},
];
const bulk = db.collection('document').initializeUnorderedBulkOp();
db.collection('record').aggregate(pipeline).forEach(
(adoc) => bulk.find({
db.collection('record').aggregate(pipeline).each(
(err, adoc: any) => bulk.find({
domainId: adoc._id.domainId,
docType: document.TYPE_PROBLEM,
docId: adoc._id.pid,
@@ -44,4 +44,6 @@ async function run() {
await bulk.execute();
}
global.Hydro.script.problemStat = module.exports = { run, description };
export const validate = {};
global.Hydro.script.problemStat = { run, description, validate };
@@ -1,11 +1,11 @@
const description = 'Calculate the rating of a contest';
import { ObjectID } from 'mongodb';
import * as contest from '../model/contest';
import * as user from '../model/user';
import rating from '../lib/rating';
const { ObjectID } = require('mongodb');
const contest = require('../model/contest');
const user = require('../model/user');
const rating = require('../lib/rating');
export const description = 'Calculate the rating of a contest';
async function run({ domainId, contestId }) {
export async function run({ domainId, contestId }) {
contestId = new ObjectID(contestId);
const tdoc = await contest.get(domainId, contestId);
const tsdocs = await contest.getMultiStatus(domainId, { docId: contestId })
@@ -20,4 +20,9 @@ async function run({ domainId, contestId }) {
return rated;
}
global.Hydro.script.rating = module.exports = { run, description };
export const validate = {
domainId: { $type: 'string' },
contestId: { $type: ObjectID },
};
global.Hydro.script.rating = { run, description, validate };
@@ -1,13 +1,13 @@
const description = 'Calculate rating of a domain, or all domains';
/* eslint-disable no-await-in-loop */
const domain = require('../model/domain');
const contest = require('../model/contest');
const user = require('../model/user');
const rating = require('../lib/rating');
const { STATUS } = require('../model/builtin');
import * as domain from '../model/domain';
import * as contest from '../model/contest';
import * as user from '../model/user';
import rating from '../lib/rating';
import { STATUS } from '../model/builtin';
async function run({ domainId, isSub = false }, report) {
export const description = 'Calculate rating of a domain, or all domains';
export async function run({ domainId, isSub = false }, report) {
if (!domainId) {
const domains = await domain.getMulti().toArray();
await report({ message: `Found ${domains.length} domains` });
@@ -66,4 +66,11 @@ async function run({ domainId, isSub = false }, report) {
return true;
}
global.Hydro.script.recalcRating = module.exports = { run, description };
export const validate = {
$or: [
{ domainId: 'string' },
{ domainId: 'undefined' },
],
};
global.Hydro.script.recalcRating = { run, description, validate };
-18
View File
@@ -1,18 +0,0 @@
const description = 'Create a new user';
const user = require('../model/user');
const system = require('../model/system');
async function run({
uname, password, mail, uid,
}) {
if (!uid) uid = await system.inc('user');
else uid = parseInt(uid, 10);
if (Number.isNaN(uid)) throw new Error('uid');
await user.create({
uid, uname, password, mail, regip: '127.0.0.1',
});
return uid;
}
global.Hydro.script.register = module.exports = { run, description };
+24
View File
@@ -0,0 +1,24 @@
import * as user from '../model/user';
import * as system from '../model/system';
export const description = 'Create a new user';
export async function run({
uname, password, mail, uid,
}) {
if (!uid) uid = await system.inc('user');
else uid = parseInt(uid, 10);
if (Number.isNaN(uid)) throw new Error('uid');
await user.create({
uid, uname, password, mail, regip: '127.0.0.1',
});
return uid;
}
export const validate = {
uname: { $type: 'string' },
password: { $type: 'string' },
mail: { $type: 'string' },
};
global.Hydro.script.register = { run, description, validate };
-15
View File
@@ -1,15 +0,0 @@
const description = 'Set a user as superadmin.';
const user = require('../model/user');
const { PRIV_ALL } = require('../model/builtin').PRIV;
async function run({
uid,
}) {
uid = parseInt(uid, 10);
if (Number.isNaN(uid)) throw new Error('uid');
await user.setPriv(uid, PRIV_ALL);
return uid;
}
global.Hydro.script.setSuperadmin = module.exports = { run, description };
+17
View File
@@ -0,0 +1,17 @@
import * as user from '../model/user';
import { PRIV } from '../model/builtin';
export const description = 'Set a user as superadmin.';
export async function run({ uid }) {
uid = parseInt(uid, 10);
if (Number.isNaN(uid)) throw new Error('uid');
await user.setPriv(uid, PRIV.PRIV_ALL);
return uid;
}
export const validate = {
uid: { $type: 'number' },
};
global.Hydro.script.setSuperadmin = { run, description, validate };
+1
View File
@@ -6,6 +6,7 @@ let mongourl = 'mongodb://';
if (options.username) mongourl += `${options.username}:${options.password}@`;
mongourl += `${options.host}:${options.port}/${options.name}`;
// eslint-disable-next-line import/no-mutable-exports
export let db: mongodb.Db = null;
mongodb.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true })
+2 -2
View File
@@ -73,7 +73,7 @@
"build:all": "cd ui && yarn && yarn build:production && cd .. && yarn && yarn build && yarn pack:production",
"start": "ts-node hydro/loader.ts",
"pkg": "pkg .",
"lint": "eslint build hydro module tool --fix",
"lint": "eslint build hydro module tool --ext ts,js --fix",
"snyk-protect": "snyk protect",
"prepare": "yarn run snyk-protect"
},
@@ -86,4 +86,4 @@
]
},
"snyk": true
}
}