mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core: drop legacy validator (#497)
This commit is contained in:
1 parent
d8f5193a24
commit
1f585968a4
19 files changed
+135
-171
No files matched your search
@@ -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');
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = '') {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]));
|
||||
|
||||
@@ -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)) {
|
||||
pid = tdoc.pids[parseInt(pid, 36) - 10];
|
||||
}
|
||||
if (pid) {
|
||||
if (typeof pid === 'string' && tdoc && /^[A-Z]$/.test(pid)) {
|
||||
pid = tdoc.pids[parseInt(pid, 36) - 10];
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import './mail';
|
||||
import './paginate';
|
||||
import './hash.hydro';
|
||||
import './rank';
|
||||
import './validator';
|
||||
import './ui';
|
||||
import './testdataConfig';
|
||||
import './difficulty';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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) => {
|
||||
try {
|
||||
return saslprep(v.toString().trim());
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
const saslprepString = <T = string>(regex?: RegExp, cb?: (i: string) => boolean, convert?: (i: string) => T) => [
|
||||
convert || ((v) => saslprep(v.toString().trim())),
|
||||
(v) => {
|
||||
try {
|
||||
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 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>>) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
+4
-8
@@ -15,17 +15,13 @@ const Root = {
|
||||
describe('App', () => {
|
||||
let agent: supertest.SuperAgentTest;
|
||||
before((done) => {
|
||||
let timeout;
|
||||
const resolve = () => setTimeout(() => {
|
||||
clearTimeout(timeout);
|
||||
agent = supertest.agent(require('hydrooj').httpServer);
|
||||
done();
|
||||
}, 2000);
|
||||
process.send = ((send) => (data) => {
|
||||
if (data === 'ready') resolve();
|
||||
if (data === 'ready') {
|
||||
agent = supertest.agent(require('hydrooj').httpServer);
|
||||
done();
|
||||
}
|
||||
return send?.(data) || false;
|
||||
})(process.send);
|
||||
timeout = setTimeout(resolve, 20000);
|
||||
}, { timeout: 30000 });
|
||||
|
||||
const routes = ['/', '/api', '/p', '/contest', '/homework', '/user/1', '/training'];
|
||||
|
||||
Reference in new issue
Block a user