core: drop legacy validator (#497)

This commit is contained in:
undefined authored and GitHub committed 2023-01-28 00:03:31 +08:00
1 parent d8f5193a24
commit 1f585968a4
19 files changed
+130 -166

No files matched your search

+3 -3
View File
@@ -534,7 +534,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
}
@param('tid', Types.ObjectID)
@post('filename', Types.Name, true)
@post('filename', Types.Filename, true)
async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
throw new FileLimitExceededError('count');
@@ -557,7 +557,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
}
@param('tid', Types.ObjectID)
@post('files', Types.ArrayOf(Types.Name))
@post('files', Types.ArrayOf(Types.Filename))
async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) {
await Promise.all([
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
@@ -569,7 +569,7 @@ export class ContestManagementHandler extends ContestManagementBaseHandler {
export class ContestFileDownloadHandler extends ContestDetailBaseHandler {
@param('tid', Types.ObjectID)
@param('filename', Types.Name)
@param('filename', Types.Filename)
@param('noDisposition', Types.Boolean)
async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) {
this.response.addHeader('Cache-Control', 'public');
+3 -3
View File
@@ -29,7 +29,7 @@ class DiscussionHandler extends Handler {
drrdoc?: DiscussionTailReplyDoc;
vnode?: any;
@param('type', Types.Name, true)
@param('type', Types.Range(Object.keys(typeMapper)), true)
@param('name', Types.Name, true)
@param('did', Types.ObjectID, true)
@param('drid', Types.ObjectID, true)
@@ -94,7 +94,7 @@ class DiscussionMainHandler extends Handler {
}
class DiscussionNodeHandler extends DiscussionHandler {
@param('type', Types.Name)
@param('type', Types.Range(Object.keys(typeMapper)))
@param('name', Types.Name)
@param('page', Types.PositiveInt, true)
async get(domainId: string, type: string, _name: string, page = 1) {
@@ -140,7 +140,7 @@ class DiscussionCreateHandler extends DiscussionHandler {
this.response.body = { path, vnode: this.vnode };
}
@param('type', Types.Name)
@param('type', Types.Range(Object.keys(typeMapper)))
@param('title', Types.Title)
@param('content', Types.Content)
@param('highlight', Types.Boolean)
+5 -5
View File
@@ -159,7 +159,7 @@ class DomainUserHandler extends ManageHandler {
@requireSudo
@post('uid', Types.Int)
@post('role', Types.Name)
@post('role', Types.Role)
async postSetUser(domainId: string, uid: number, role: string) {
await Promise.all([
domain.setUserRole(domainId, uid, role),
@@ -170,7 +170,7 @@ class DomainUserHandler extends ManageHandler {
@requireSudo
@param('uid', Types.NumericArray)
@param('role', Types.Name)
@param('role', Types.Role)
async postSetUsers(domainId: string, uid: number[], role: string) {
await Promise.all([
domain.setUserRole(domainId, uid, role),
@@ -213,7 +213,7 @@ class DomainRoleHandler extends ManageHandler {
this.response.body = { roles, domain: this.domain };
}
@param('role', Types.Name)
@param('role', Types.Role)
async postAdd(domainId: string, role: string) {
const roles = await domain.getRoles(this.domain);
const rdict: Dictionary<any> = {};
@@ -224,7 +224,7 @@ class DomainRoleHandler extends ManageHandler {
}
@requireSudo
@param('roles', Types.ArrayOf(Types.Name))
@param('roles', Types.ArrayOf(Types.Role))
async postDelete(domainId: string, roles: string[]) {
if (Set.intersection(roles, ['root', 'default', 'guest']).size > 0) {
throw new ValidationError('role', null, 'You cannot delete root, default or guest roles');
@@ -251,7 +251,7 @@ class DomainJoinApplicationsHandler extends ManageHandler {
@requireSudo
@post('method', Types.Range([domain.JOIN_METHOD_NONE, domain.JOIN_METHOD_ALL, domain.JOIN_METHOD_CODE]))
@post('role', Types.Name, true)
@post('role', Types.Role, true)
@post('expire', Types.Int, true)
@post('invitationCode', Types.Content, true)
async post(domainId: string, method: number, role: string, expire: number, invitationCode = '') {
+6 -7
View File
@@ -13,7 +13,6 @@ import { DomainDoc, MessageDoc, Setting } from '../interface';
import avatar from '../lib/avatar';
import * as mail from '../lib/mail';
import * as useragent from '../lib/useragent';
import { isDomainId, isEmail, isPassword } from '../lib/validator';
import { verifyTFA } from '../lib/verifyTFA';
import BlackListModel from '../model/blacklist';
import { PERM, PRIV } from '../model/builtin';
@@ -180,8 +179,8 @@ class HomeSecurityHandler extends Handler {
@requireSudo
@param('current', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
@param('password', Types.Password)
@param('verifyPassword', Types.Password)
async postChangePassword(_: string, current: string, password: string, verify: string) {
if (password !== verify) throw new VerifyPasswordError();
this.user.checkPassword(current);
@@ -191,8 +190,8 @@ class HomeSecurityHandler extends Handler {
}
@requireSudo
@param('password', Types.String)
@param('mail', Types.Name, isEmail)
@param('password', Types.Password)
@param('mail', Types.Email)
async postChangeMail(domainId: string, current: string, email: string) {
const mailDomain = email.split('@')[1];
if (await BlackListModel.get(`mail::${mailDomain}`)) throw new BlacklistedError(mailDomain);
@@ -335,7 +334,7 @@ function set(s: Setting, key: string, value: any) {
}
class HomeSettingsHandler extends Handler {
@param('category', Types.Name)
@param('category', Types.Range(['preference', 'account', 'domain']))
async get(domainId: string, category: string) {
this.response.template = 'home_settings.html';
this.response.body = {
@@ -445,7 +444,7 @@ class HomeDomainCreateHandler extends Handler {
this.response.template = 'domain_create.html';
}
@param('id', Types.Name, isDomainId)
@param('id', Types.DomainId)
@param('name', Types.Title)
@param('bulletin', Types.Content)
@param('avatar', Types.Content, true)
+2 -3
View File
@@ -6,7 +6,6 @@ import * as check from '../check';
import {
CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError,
} from '../error';
import { isEmail, isPassword } from '../lib/validator';
import { Logger } from '../logger';
import { PRIV, STATUS } from '../model/builtin';
import domain from '../model/domain';
@@ -230,9 +229,9 @@ class SystemUserImportHandler extends SystemHandler {
let [email, username, password, displayName, extra] = u.split(',').map((t) => t.trim());
if (!email || !username || !password) [email, username, password, displayName, extra] = u.split('\t').map((t) => t.trim());
if (email && username && password) {
if (!isEmail(email)) messages.push(`Line ${+i + 1}: Invalid email.`);
if (!Types.Email[1](email)) messages.push(`Line ${+i + 1}: Invalid email.`);
else if (!Types.Username[1](username)) messages.push(`Line ${+i + 1}: Invalid username`);
else if (!isPassword(password)) messages.push(`Line ${+i + 1}: Invalid password`);
else if (!Types.Password[1](password)) messages.push(`Line ${+i + 1}: Invalid password`);
else if (await user.getByEmail('system', email)) {
messages.push(`Line ${+i + 1}: Email ${email} already exists.`);
} else if (await user.getByUname('system', username)) {
+5 -7
View File
@@ -44,7 +44,7 @@ export class FilesHandler extends Handler {
this.response.template = 'home_files.html';
}
@post('filename', Types.Name, true)
@post('filename', Types.Filename)
async postUploadFile(domainId: string, filename: string) {
this.checkPriv(PRIV.PRIV_CREATE_FILE);
if ((this.user._files?.length || 0) >= system.get('limit.user_files')) {
@@ -57,9 +57,7 @@ export class FilesHandler extends Handler {
if (size >= system.get('limit.user_files_size')) {
if (!this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) throw new FileLimitExceededError('size');
}
filename ||= file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
if (this.user._files.filter((i) => i.name === filename).length) throw new FileExistsError(filename);
if (this.user._files.find((i) => i.name === filename)) throw new FileExistsError(filename);
await storage.put(`user/${this.user._id}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`user/${this.user._id}/${filename}`);
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
@@ -69,7 +67,7 @@ export class FilesHandler extends Handler {
this.back();
}
@post('files', Types.ArrayOf(Types.Name))
@post('files', Types.ArrayOf(Types.Filename))
async postDeleteFiles(domainId: string, files: string[]) {
await Promise.all([
storage.del(files.map((t) => `user/${this.user._id}/${t}`), this.user._id),
@@ -83,7 +81,7 @@ export class FSDownloadHandler extends Handler {
noCheckPermView = true;
@param('uid', Types.Int)
@param('filename', Types.Name)
@param('filename', Types.Filename)
@param('noDisposition', Types.Boolean)
async get(domainId: string, uid: number, filename: string, noDisposition = false) {
const targetUser = await user.getById('system', uid);
@@ -111,7 +109,7 @@ export class StorageHandler extends Handler {
noCheckPermView = true;
@param('target', Types.Name)
@param('filename', Types.Name, true)
@param('filename', Types.Filename, true)
@param('expire', Types.UnsignedInt)
@param('secret', Types.String)
async get(domainId: string, target: string, filename = '', expire: number, secret: string) {
+12 -12
View File
@@ -2,7 +2,7 @@ import AdmZip from 'adm-zip';
import { readFile, statSync } from 'fs-extra';
import { isBinaryFile } from 'isbinaryfile';
import {
escapeRegExp, flattenDeep, intersection, isSafeInteger, pick, uniqBy,
escapeRegExp, flattenDeep, intersection, pick, uniqBy,
} from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
@@ -18,7 +18,6 @@ import {
ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User,
} from '../interface';
import paginate from '../lib/paginate';
import { isPid, parsePid as convertPid } from '../lib/validator';
import { PERM, PRIV, STATUS } from '../model/builtin';
import * as contest from '../model/contest';
import * as discussion from '../model/discussion';
@@ -40,7 +39,6 @@ import { registerResolver, registerValue } from './api';
import { ContestDetailBaseHandler } from './contest';
export const parseCategory = (value: string) => value.replace(/,/g, ',').split(',').map((e) => e.trim());
export const parsePid = (value: string) => (isSafeInteger(value) ? +value : value);
registerValue('FileInfo', [
['_id', 'String!'],
@@ -312,7 +310,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
udoc: User;
psdoc: ProblemStatusDoc;
@route('pid', Types.Name, true, null, parsePid)
@route('pid', Types.ProblemId, true)
@query('tid', Types.ObjectID, true)
async _prepare(domainId: string, pid: number | string, tid?: ObjectID) {
this.pdoc = await problem.get(domainId, pid);
@@ -628,17 +626,18 @@ export class ProblemEditHandler extends ProblemManageHandler {
this.response.template = 'problem_edit.html';
}
@route('pid', Types.Name, null, parsePid)
@route('pid', Types.ProblemId)
@post('title', Types.Title)
@post('content', Types.Content)
@post('pid', Types.Name, isPid, convertPid, true)
@post('pid', Types.ProblemId, true)
@post('hidden', Types.Boolean)
@post('tag', Types.Content, true, null, parseCategory)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
async post(
domainId: string, pid: string | number, title: string, content: string,
newPid: string = '', hidden = false, tag: string[] = [], difficulty = 0,
newPid: string | number = '', hidden = false, tag: string[] = [], difficulty = 0,
) {
if (typeof newPid !== 'string') newPid = `P${newPid}`;
if (newPid !== this.pdoc.pid && await problem.get(domainId, newPid)) throw new ProblemAlreadyExistError(pid);
const $update: Partial<ProblemDoc> = {
title, content, pid: newPid, hidden, tag: tag ?? [], difficulty, html: false,
@@ -722,7 +721,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
this.response.body.links = links;
}
@post('filename', Types.Name, true)
@post('filename', Types.Filename, true)
@post('type', Types.Range(['testdata', 'additional_file']), true)
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
@@ -784,7 +783,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
this.back();
}
@post('files', Types.ArrayOf(Types.Name))
@post('files', Types.ArrayOf(Types.Filename))
@post('type', Types.Range(['testdata', 'additional_file']), true)
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
if (this.pdoc.reference) throw new ProblemIsReferencedError('delete files');
@@ -797,7 +796,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
export class ProblemFileDownloadHandler extends ProblemDetailHandler {
@query('type', Types.Range(['additional_file', 'testdata']), true)
@param('filename', Types.Name)
@param('filename', Types.Filename)
@param('noDisposition', Types.Boolean)
async get(domainId: string, type = 'additional_file', filename: string, noDisposition = false) {
if (this.pdoc.reference) {
@@ -966,14 +965,15 @@ export class ProblemCreateHandler extends Handler {
@post('title', Types.Title)
@post('content', Types.Content)
@post('pid', Types.Name, true, isPid, convertPid)
@post('pid', Types.ProblemId, true)
@post('hidden', Types.Boolean)
@post('difficulty', Types.PositiveInt, (i) => +i <= 10, true)
@post('tag', Types.Content, true, null, parseCategory)
async post(
domainId: string, title: string, content: string, pid: string,
domainId: string, title: string, content: string, pid: string | number = '',
hidden = false, difficulty = 0, tag: string[] = [],
) {
if (typeof pid !== 'string') pid = `P${pid}`;
if (pid && await problem.get(domainId, pid)) throw new ProblemAlreadyExistError(pid);
const docId = await problem.add(domainId, pid, title, content, this.user._id, tag ?? [], hidden);
const files = new Set(Array.from(content.matchAll(/file:\/\/([a-zA-Z0-9_-]+\.[a-zA-Z0-9]+)/g)).map((i) => i[1]));
+8 -8
View File
@@ -25,15 +25,15 @@ class RecordListHandler extends ContestDetailBaseHandler {
tdoc?: Tdoc<30>;
@param('page', Types.PositiveInt, true)
@param('pid', Types.Name, true)
@param('pid', Types.ProblemId, true)
@param('tid', Types.ObjectID, true)
@param('uidOrName', Types.Name, true)
@param('uidOrName', Types.UidOrName, true)
@param('lang', Types.String, true)
@param('status', Types.Int, true)
@param('fullStatus', Types.Boolean)
@param('allDomain', Types.Boolean, true)
async get(
domainId: string, page = 1, pid?: string, tid?: ObjectID,
domainId: string, page = 1, pid?: string | number, tid?: ObjectID,
uidOrName?: string, lang?: string, status?: number, full = false,
all = false,
) {
@@ -64,10 +64,10 @@ class RecordListHandler extends ContestDetailBaseHandler {
if (udoc) q.uid = udoc._id;
else invalid = true;
}
if (pid && tdoc && /^[A-Z]$/.test(pid)) {
if (pid) {
if (typeof pid === 'string' && tdoc && /^[A-Z]$/.test(pid)) {
pid = tdoc.pids[parseInt(pid, 36) - 10];
}
if (pid) {
const pdoc = await problem.get(domainId, pid);
if (pdoc) q.pid = pdoc.docId;
else invalid = true;
@@ -236,13 +236,13 @@ class RecordMainConnectionHandler extends ConnectionHandler {
tdoc: Tdoc<30>;
@param('tid', Types.ObjectID, true)
@param('pid', Types.Name, true)
@param('uidOrName', Types.Name, true)
@param('pid', Types.ProblemId, true)
@param('uidOrName', Types.UidOrName, true)
@param('status', Types.Int, true)
@param('pretest', Types.Boolean)
@param('allDomain', Types.Boolean)
async prepare(
domainId: string, tid?: ObjectID, pid?: string, uidOrName?: string,
domainId: string, tid?: ObjectID, pid?: string | number, uidOrName?: string,
status?: number, pretest = false, all = false,
) {
if (tid) {
+3 -5
View File
@@ -256,7 +256,7 @@ export class TrainingFilesHandler extends Handler {
}
@param('tid', Types.ObjectID)
@post('filename', Types.Name, true)
@post('filename', Types.Filename, true)
async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
throw new FileLimitExceededError('count');
@@ -268,8 +268,6 @@ export class TrainingFilesHandler extends Handler {
if (size >= system.get('limit.contest_files_size')) {
throw new FileLimitExceededError('size');
}
filename ||= file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
await storage.put(`training/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`training/${domainId}/${tid}/${filename}`);
const payload = { _id: filename, name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
@@ -279,7 +277,7 @@ export class TrainingFilesHandler extends Handler {
}
@param('tid', Types.ObjectID)
@post('files', Types.ArrayOf(Types.Name))
@post('files', Types.ArrayOf(Types.Filename))
async postDeleteFiles(domainId: string, tid: ObjectID, files: string[]) {
await Promise.all([
storage.del(files.map((t) => `contest/${domainId}/${tid}/${t}`), this.user._id),
@@ -290,7 +288,7 @@ export class TrainingFilesHandler extends Handler {
}
export class TrainingFileDownloadHandler extends Handler {
@param('tid', Types.ObjectID)
@param('filename', Types.Name)
@param('filename', Types.Filename)
@param('noDisposition', Types.Boolean)
async get(domainId: string, tid: ObjectID, filename: string, noDisposition = false) {
this.response.addHeader('Cache-Control', 'public');
+8 -9
View File
@@ -8,7 +8,6 @@ import {
import { Udoc, User } from '../interface';
import avatar from '../lib/avatar';
import { sendMail } from '../lib/mail';
import { isEmail, isPassword } from '../lib/validator';
import { verifyTFA } from '../lib/verifyTFA';
import BlackListModel from '../model/blacklist';
import { PERM, PRIV, STATUS } from '../model/builtin';
@@ -81,7 +80,7 @@ class UserLoginHandler extends Handler {
}
@param('uname', Types.Username)
@param('password', Types.String)
@param('password', Types.Password)
@param('rememberme', Types.Boolean)
@param('redirect', Types.String, true)
@param('tfa', Types.String, true)
@@ -225,7 +224,7 @@ export class UserRegisterHandler extends Handler {
this.response.template = 'user_register.html';
}
@post('mail', Types.String, true, isEmail)
@post('mail', Types.Email, true)
@post('phone', Types.String, true, (s) => /^\d{11}$/.test(s))
async post(domainId: string, mail: string, phoneNumber: string) {
if (mail) {
@@ -279,8 +278,8 @@ class UserRegisterWithCodeHandler extends Handler {
this.response.body = tdoc;
}
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
@param('password', Types.Password)
@param('verifyPassword', Types.Password)
@param('uname', Types.Username)
@param('code', Types.String)
async post(
@@ -312,7 +311,7 @@ class UserLostPassHandler extends Handler {
this.response.template = 'user_lostpass.html';
}
@param('mail', Types.String, isEmail)
@param('mail', Types.Email)
async post(domainId: string, mail: string) {
if (!system.get('smtp.user')) throw new SystemError('Cannot send mail');
const udoc = await user.getByEmail('system', mail);
@@ -347,8 +346,8 @@ class UserLostPassWithCodeHandler extends Handler {
}
@param('code', Types.String)
@param('password', Types.String, isPassword)
@param('verifyPassword', Types.String)
@param('password', Types.Password)
@param('verifyPassword', Types.Password)
async post(domainId: string, code: string, password: string, verifyPassword: string) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_LOSTPASS], code);
@@ -429,7 +428,7 @@ class UserDeleteHandler extends Handler {
class OauthHandler extends Handler {
noCheckPermView = true;
@param('type', Types.String)
@param('type', Types.Key)
async get(domainId: string, type: string) {
await global.Hydro.module.oauth[type]?.get.call(this);
}
-1
View File
@@ -733,7 +733,6 @@ export interface Lib extends Record<string, any> {
rank: typeof import('./lib/rank');
rating: typeof import('./lib/rating');
testdataConfig: typeof import('./lib/testdataConfig');
validator: typeof import('./lib/validator');
template?: any;
problemSearch: ProblemSearch;
}
-1
View File
@@ -3,7 +3,6 @@ import './mail';
import './paginate';
import './hash.hydro';
import './rank';
import './validator';
import './ui';
import './testdataConfig';
import './difficulty';
-51
View File
@@ -1,51 +0,0 @@
import { ValidationError } from '../error';
const RE_UID = /^-?\d+$/i;
const RE_DOMAINID = /^[a-zA-Z][a-zA-Z0-9_]{3,31}$/i;
const RE_PID = /^[a-zA-Z]+[a-zA-Z0-9]*$/i;
const RE_ROLE = /^[_0-9A-Za-z]{1,31}$/i;
const RE_MAIL = /^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i;
export const isTitle = (s) => typeof s === 'string' && s.trim().length && s.trim().length < 64;
export const checkTitle = (s) => { if (!(s?.trim()?.length && s.trim().length < 64)) throw new ValidationError('title'); else return s; };
export const isDomainId = (s) => RE_DOMAINID.test(s);
export const checkDomainId = (s) => { if (!isDomainId(s)) throw new ValidationError('domainId'); else return s; };
export const isUid = (s) => RE_UID.test(s);
export const checkUid = (s) => { if (!isUid(s)) throw new ValidationError('uid'); else return s; };
export const isRole = (s) => RE_ROLE.test(s);
export const checkRole = (s) => { if (!isRole(s)) throw new ValidationError('role'); else return s; };
export const isPassword = (s) => s.length >= 5;
export const checkPassword = (s) => { if (!(s && s.length >= 5)) throw new ValidationError('password'); else return s; };
export const isEmail = (s) => RE_MAIL.test(s);
export const checkEmail = (s) => { if (!RE_MAIL.test(s)) throw new ValidationError('mail'); else return s; };
export const isContent = (s: any) => s && s.toString().trim().length < 65536;
export const checkContent = (s) => { if (!(s && s.length < 65536)) throw new ValidationError('content'); else return s; };
export const isPid = (s) => RE_PID.test(s.toString());
export const checkPid = (s) => { if (!RE_PID.test(s)) throw new ValidationError('pid'); else return s; };
export const isIntro = () => true;
export const checkIntro = (s) => { if (!isIntro()) throw new ValidationError('intro'); else return s; };
export const parsePid = (s) => (Number.isNaN(+s) ? s : `P${s}`);
global.Hydro.lib.validator = {
isTitle,
checkTitle,
isDomainId,
checkDomainId,
isUid,
checkUid,
isRole,
checkRole,
isPassword,
checkPassword,
isEmail,
checkEmail,
isContent,
checkContent,
isPid,
checkPid,
isIntro,
checkIntro,
parsePid,
};
+2 -2
View File
@@ -96,7 +96,7 @@ class DomainModel {
static async edit(domainId: string, $set: Partial<DomainDoc>) {
domainId = domainId.toLowerCase();
await bus.parallel('domain/before-update', domainId, $set);
const result = await coll.findOneAndUpdate({ _id: domainId }, { $set }, { returnDocument: 'after' });
const result = await coll.findOneAndUpdate({ lower: domainId }, { $set }, { returnDocument: 'after' });
if (result.value) {
await bus.parallel('domain/update', domainId, $set, result.value);
cache.delete(`id::${domainId}`);
@@ -199,7 +199,7 @@ class DomainModel {
static async getDomainUser(domainId: string, udoc: DomainUserArg) {
let dudoc = await collUser.findOne({ domainId, uid: udoc._id });
dudoc ||= {};
dudoc ||= { domainId, uid: udoc._id };
if (!(udoc.priv & PRIV.PRIV_USER_PROFILE)) dudoc.role = 'guest';
if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'root';
dudoc.role ||= 'default';
+22 -28
View File
@@ -161,9 +161,18 @@ function handleMailLower(mail: string) {
return data;
}
async function initAndCache(udoc: Udoc, dudoc, scope: bigint = PERM.PERM_ALL) {
const res = await new User(udoc, dudoc, scope).init();
cache.set(`id/${udoc._id}/${dudoc.domainId}`, res);
cache.set(`name/${udoc.unameLower}/${dudoc.domainId}`, res);
cache.set(`mail/${udoc.mailLower}/${dudoc.domainId}`, res);
return res;
}
class UserModel {
static coll = coll;
static User = User;
static cache = cache;
static defaultUser: Udoc = {
_id: 0,
uname: 'Unknown User',
@@ -183,7 +192,7 @@ class UserModel {
};
@ArgMethod
static async getById(domainId: string, _id: number, scope: bigint | string = PERM.PERM_ALL): Promise<User | null> {
static async getById(domainId: string, _id: number, scope: bigint | string = PERM.PERM_ALL): Promise<User> {
if (cache.has(`id/${_id}/${domainId}`)) return cache.get(`id/${_id}/${domainId}`) || null;
const udoc = await (_id < -999 ? collV : coll).findOne({ _id });
if (!udoc) return null;
@@ -193,11 +202,7 @@ class UserModel {
]);
dudoc.group = groups.map((i) => i.name);
if (typeof scope === 'string') scope = BigInt(scope);
const res = await new User(udoc, dudoc, scope).init();
cache.set(`id/${res._id}/${domainId}`, res);
cache.set(`name/${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`mail/${res.mail.toLowerCase()}/${domainId}`, res);
return res;
return initAndCache(udoc, dudoc, scope);
}
static async getList(domainId: string, uids: number[]): Promise<Udict> {
@@ -211,30 +216,21 @@ class UserModel {
@ArgMethod
static async getByUname(domainId: string, uname: string): Promise<User | null> {
const unameLower = uname.trim().toLowerCase();
if (cache.has(`name/${unameLower}/${domainId}`)) return cache.get(`name/${unameLower}/${domainId}`) || null;
let udoc = await coll.findOne({ unameLower });
udoc ||= await collV.findOne({ unameLower });
if (cache.has(`name/${unameLower}/${domainId}`)) return cache.get(`name/${unameLower}/${domainId}`);
const udoc = (await coll.findOne({ unameLower })) || await collV.findOne({ unameLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`id/${res._id}/${domainId}`, res);
cache.set(`name/${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`mail/${handleMailLower(res.mail)}/${domainId}`, res);
return res;
return initAndCache(udoc, dudoc);
}
@ArgMethod
static async getByEmail(domainId: string, mail: string): Promise<User | null> {
static async getByEmail(domainId: string, mail: string): Promise<User> {
const mailLower = handleMailLower(mail);
if (cache.has(`mail/${mailLower}/${domainId}`)) return cache.get(`mail/${mailLower}/${domainId}`) || null;
if (cache.has(`mail/${mailLower}/${domainId}`)) return cache.get(`mail/${mailLower}/${domainId}`);
const udoc = await coll.findOne({ mailLower });
if (!udoc) return null;
const dudoc = await domain.getDomainUser(domainId, udoc);
const res = await new UserModel.User(udoc, dudoc).init();
cache.set(`id/${res._id}/${domainId}`, res);
cache.set(`name/${res.uname.toLowerCase()}/${domainId}`, res);
cache.set(`mail/${handleMailLower(res.mail)}/${domainId}`, res);
return res;
return initAndCache(udoc, dudoc);
}
@ArgMethod
@@ -260,7 +256,7 @@ class UserModel {
}
@ArgMethod
static async setPassword(uid: number, password: string): Promise<Udoc | null> {
static async setPassword(uid: number, password: string): Promise<Udoc> {
const salt = String.random();
const res = await coll.findOneAndUpdate(
{ _id: uid },
@@ -268,7 +264,7 @@ class UserModel {
{ returnDocument: 'after' },
);
deleteUserCache(res.value);
return res.value || null;
return res.value;
}
@ArgMethod
@@ -373,20 +369,18 @@ class UserModel {
const $regex = `^${escapeRegExp(prefix.toLowerCase())}`;
const udocs = await coll.find({ unameLower: { $regex } })
.limit(limit).project({ _id: 1 }).toArray();
const users = [];
for (const { _id } of udocs) users.push(UserModel.getById(domainId, _id));
return await Promise.all(users);
return await Promise.all(udocs.map(({ _id }) => UserModel.getById(domainId, _id)));
}
@ArgMethod
static async setPriv(uid: number, priv: number): Promise<Udoc | null> {
static async setPriv(uid: number, priv: number): Promise<Udoc> {
const res = await coll.findOneAndUpdate(
{ _id: uid },
{ $set: { priv } },
{ returnDocument: 'after' },
);
deleteUserCache(res.value);
return res.value || null;
return res.value;
}
@ArgMethod
-1
View File
@@ -38,7 +38,6 @@ export { Collections } from './service/db';
// export { Collections } from './interface';
export { Service, Context } from './context';
export { buildContent } from './lib/content';
export * as validator from './lib/validator';
export { default as avatar } from './lib/avatar';
export { default as rank } from './lib/rank';
export { default as paginate } from './lib/paginate';
+47 -12
View File
@@ -6,7 +6,6 @@ import { ObjectID } from 'mongodb';
import saslprep from 'saslprep';
import { Time } from '@hydrooj/utils';
import { ValidationError } from '../error';
import { isContent, isTitle } from '../lib/validator';
import type { Handler } from './server';
type MethodDecorator = (target: any, name: string, obj: any) => any;
@@ -21,51 +20,83 @@ export interface ParamOption<T> {
validate?: Validator,
}
type Type<T> = [Converter<T>, Validator<false>?, boolean?];
type Type<T> = readonly [Converter<T>, Validator<false>?, boolean?];
export interface Types {
// String outputs
Content: Type<string>;
Key: Type<string>;
/** @deprecated */
Name: Type<string>;
Username: Type<string>;
Password: Type<string>;
UidOrName: Type<string>;
Email: Type<string>;
Filename: Type<string>;
DomainId: Type<string>;
ProblemId: Type<string | number>;
Role: Type<string>;
Title: Type<string>;
Emoji: Type<string>;
String: Type<string>;
// Number outputs
Int: Type<number>;
UnsignedInt: Type<number>;
PositiveInt: Type<number>;
Float: Type<number>;
// Other
ObjectID: Type<ObjectID>;
Boolean: Type<boolean>;
Date: Type<string>;
Time: Type<string>;
Range: <T extends string | number>(range: Array<T> | Record<string, any>) => Type<T>;
/** @deprecated */
Array: Type<any[]>;
NumericArray: Type<number[]>;
CommaSeperatedArray: Type<string[]>;
Set: Type<Set<any>>;
Emoji: Type<string>;
Any: Type<any>;
ArrayOf: <T extends Type<any>>(type: T) => (T extends Type<infer R> ? Type<R[]> : never);
AnyOf: <T extends Type<any>>(...type: T[]) => (T extends Type<infer R> ? Type<R> : never);
}
const safeSaslprep = (v) => {
const saslprepString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
convert || ((v) => saslprep(v.toString().trim())),
(v) => {
try {
return saslprep(v.toString().trim());
const res = saslprep(v.toString().trim());
if (regex && !regex.test(res)) return false;
if (cb && !cb(res)) return false;
return !!res.length;
} catch (e) {
return '';
return false;
}
};
},
] as [(v) => string, (v) => boolean];
export const Types: Types = {
Content: [(v) => v.toString().trim(), isContent],
Name: [(v) => saslprep(v.toString().trim()), (v) => /^.{1,255}$/.test(safeSaslprep(v))],
Username: [(v) => saslprep(v.toString().trim()), (v) => /^(.{3,31}|[\u4e00-\u9fa5]{2})$/.test(safeSaslprep(v))],
Title: [(v) => v.toString().trim(), isTitle],
Content: [(v) => v.toString().trim(), (v) => v && v.toString().trim().length < 65536],
Key: saslprepString(/^[a-zA-Z0-9-_]+$/),
/** @deprecated */
Name: saslprepString(/^.{1,255}$/),
Filename: saslprepString(/^[^.\\/?#~]{1,255}$/, (i) => !['con', '.', '..'].includes(i)),
UidOrName: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2}|-?[0-9]+)$/),
Username: saslprepString(/^(.{3,31}|[\u4e00-\u9fa5]{2})$/),
Password: saslprepString(/^.{6,255}$/),
ProblemId: saslprepString(/^[a-zA-Z]+[a-zA-Z0-9]*$/i, () => true, (s) => (Number.isSafeInteger(+s) ? +s : s)),
Email: saslprepString(/^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)+$/i),
DomainId: saslprepString(/^[a-zA-Z][a-zA-Z0-9_]{3,31}$/),
Role: saslprepString(/^[_0-9A-Za-z\u4e00-\u9fa5]{1,31}$/i),
Title: saslprepString(/^.{1,64}$/),
String: [(v) => v.toString(), null],
Int: [(v) => +v, (v) => /^[+-]?[0-9]+$/.test(v.toString().trim()) && isSafeInteger(+v)],
UnsignedInt: [(v) => +v, (v) => /^(-0|\+?[0-9]+)$/.test(v.toString().trim()) && isSafeInteger(+v)],
PositiveInt: [(v) => +v, (v) => /^\+?[1-9][0-9]*$/.test(v.toString().trim()) && isSafeInteger(+v)],
Float: [(v) => +v, (v) => Number.isFinite(+v)],
// eslint-disable-next-line no-shadow
ObjectID: [(v) => new ObjectID(v), ObjectID.isValid],
Boolean: [(v) => v && !['false', 'off'].includes(v), null, true],
Date: [
@@ -153,6 +184,10 @@ export const Types: Types = {
return arr.every((i) => type[1](i));
},
] as any,
AnyOf: (...types) => [
(v) => types.find((type) => type[1](v))[0](v),
(v) => types.some((type) => type[1](v)),
] as any,
};
function _buildParam(name: string, source: 'get' | 'post' | 'all' | 'route', ...args: Array<Type<any> | boolean | Validator | Converter<any>>) {
+1 -1
View File
@@ -118,7 +118,7 @@ export async function buildUI() {
class UiConstantsHandler extends Handler {
noCheckPermView = true;
@param('name', Types.String, true)
@param('name', Types.Filename, true)
async all(domainId: string, name: string) {
this.response.type = 'application/javascript';
name ||= 'entry.js';
+3 -7
View File
@@ -15,17 +15,13 @@ const Root = {
describe('App', () => {
let agent: supertest.SuperAgentTest;
before((done) => {
let timeout;
const resolve = () => setTimeout(() => {
clearTimeout(timeout);
process.send = ((send) => (data) => {
if (data === 'ready') {
agent = supertest.agent(require('hydrooj').httpServer);
done();
}, 2000);
process.send = ((send) => (data) => {
if (data === 'ready') resolve();
}
return send?.(data) || false;
})(process.send);
timeout = setTimeout(resolve, 20000);
}, { timeout: 30000 });
const routes = ['/', '/api', '/p', '/contest', '/homework', '/user/1', '/training'];