core&ui: add error & fix error notification (#464)

This commit is contained in:
panda authored and GitHub committed 2022-11-26 20:30:18 +08:00
1 parent a2ea3812df
commit 534a634028
14 files changed
+123 -76

No files matched your search

+40 -1
View File
@@ -54,9 +54,11 @@ export const RemoteOnlineJudgeError = Err('RemoteOnlineJudgeError', UserFacingEr
export const SendMailError = Err('SendMailError', UserFacingError, 'Failed to send mail to {0}. (1)', 500);
export const AlreadyVotedError = Err('AlreadyVotedError', ForbiddenError, "You've already voted.");
export const BuiltinLoginError = Err('BuiltinLoginError', ForbiddenError, 'Builtin login is disabled.');
export const LoginError = Err('LoginError', ForbiddenError, 'Invalid password for user {0}.');
export const AccessDeniedError = Err('AccessDeniedError', ForbiddenError, 'Access denied.');
export const UserAlreadyExistError = Err('UserAlreadyExistError', ForbiddenError, 'User {0} already exists.');
export const InvalidTokenError = Err('InvalidTokenError', ForbiddenError);
export const InvalidTokenError = Err('InvalidTokenError', ForbiddenError, 'The {0} Token is invalid.');
export const BlacklistedError = Err('BlacklistedError', ForbiddenError, 'Address or user {0} is blacklisted.');
export const VerifyPasswordError = Err('VerifyPasswordError', ForbiddenError, "Passwords don't match.");
export const OpcountExceededError = Err('OpcountExceededError', ForbiddenError, 'Too frequent operations of {0} (limit: {2} operations in {1} seconds).');
@@ -99,6 +101,23 @@ export const DomainJoinAlreadyMemberError = Err('DomainJoinAlreadyMemberError',
export const InvalidJoinInvitationCodeError = Err('InvalidJoinInvitationCodeError', ForbiddenError, 'The invitation code you provided is invalid.');
export const CurrentPasswordError = Err('CurrentPasswordError', ForbiddenError, "Current password doesn't match.");
export const DiscussionLockedError = Err('DiscussionLockedError', ForbiddenError, 'The discussion is locked, you can not reply anymore.');
export const NotAssignedError = Err('NotAssignedError', ForbiddenError, 'You are not assigned to this {0}.');
export const FileLimitExceededError = Err('FileLimitExceededError', ForbiddenError, 'File {0} limit exceeded.');
export const FileUploadError = Err('FileUploadError', ForbiddenError, 'File upload failed.');
export const FileExistsError = Err('FileExistsError', ForbiddenError, 'File {0} already exists.');
export const HackFailedError = Err('HackFailedError', ForbiddenError, 'Hack failed: {0}');
export const ProblemAlreadyExistError = Err('ProblemAlreadyExistError', ForbiddenError, 'Problem {0} already exists.');
export const ProblemAlreadyUsedByContestError = Err('ProblemAlreadyUsedByContestError', ForbiddenError, 'Problem {0} is already used by contest {1}.');
export const ProblemNotAllowPretestError = Err('ProblemNotAllowPretestError', ForbiddenError, 'This {0} is not allow run pretest.');
export const ProblemNotAllowLanguageError = Err('ProblemNotAllowSubmitError', ForbiddenError, 'This language is not allow to submit.');
export const HackRejudgeFailedError = Err('HackRejudgeFailedError', BadRequestError, 'Cannot rejudge a hack record.');
export const CannotDeleteSystemDomainError = Err('CannotDeleteSystemDomainError', BadRequestError, 'You are not allowed to delete system domain.');
export const OnlyOwnerCanDeleteDomainError = Err('OnlyOwnerCanDeleteDomainError', BadRequestError, 'You are not the owner of this domain.');
export const CannotEditSuperAdminError = Err('CannotEditSuperAdminError', BadRequestError, 'You are not allowed to edit super admin in web.');
export const ProblemConfigError = Err('ProblemConfigError', BadRequestError, 'Invalid problem config.');
export const ProblemIsReferencedError = Err('ProblemIsReferencedError', BadRequestError, 'Cannot {0} of a referenced problem.');
export const TFAOperationError = Err('TFAOperationError', BadRequestError, '2FA is already {0}.');
export const UserNotFoundError = Err('UserNotFoundError', NotFoundError, 'User {0} not found.');
export const NoProblemError = Err('NoProblemError', NotFoundError, 'No problem.');
@@ -115,6 +134,7 @@ export const DiscussionNotFoundError = Err('DiscussionNotFoundError', DocumentNo
export const DiscussionNodeNotFoundError = Err('DiscussionNodeNotFoundError', DocumentNotFoundError, 'Discussion node {1} not found.');
export const InvalidOperationError = Err('InvalidOperationError', MethodNotAllowedError);
export const NotLaunchedByPM2Error = Err('NotLaunchedByPM2Error', BadRequestError, 'Not launched by PM2.');
export const FileTooLargeError = Err('FileTooLargeError', ValidationError, 'The uploaded file is too long.');
global.Hydro.error = {
@@ -168,6 +188,25 @@ global.Hydro.error = {
FileTooLargeError,
CurrentPasswordError,
DiscussionLockedError,
FileLimitExceededError,
FileUploadError,
FileExistsError,
HackFailedError,
HackRejudgeFailedError,
CannotDeleteSystemDomainError,
OnlyOwnerCanDeleteDomainError,
NotAssignedError,
BuiltinLoginError,
ProblemAlreadyExistError,
ProblemAlreadyUsedByContestError,
ProblemConfigError,
ProblemIsReferencedError,
ProblemNotAllowPretestError,
ProblemNotAllowLanguageError,
TFAOperationError,
CannotEditSuperAdminError,
NotLaunchedByPM2Error,
AccessDeniedError,
};
/*
+2 -2
View File
@@ -1,5 +1,5 @@
import { ObjectID } from 'mongodb';
import { DiscussionNotFoundError, ForbiddenError } from '../error';
import { DiscussionNotFoundError } from '../error';
import { BlogDoc } from '../interface';
import paginate from '../lib/paginate';
import * as blog from '../model/blog';
@@ -14,7 +14,6 @@ class BlogHandler extends Handler {
@param('did', Types.ObjectID, true)
async _prepare(domainId: string, did: ObjectID) {
if (!system.get('server.blog')) throw new ForbiddenError('Blog is disabled');
if (did) {
this.ddoc = await blog.get(did);
if (!this.ddoc) throw new DiscussionNotFoundError(domainId, did);
@@ -118,6 +117,7 @@ class BlogEditHandler extends BlogHandler {
}
export async function apply(ctx) {
if (!system.get('server.blog')) return;
ctx.Route('blog_main', '/blog/:uid', BlogUserHandler);
ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
+7 -8
View File
@@ -6,9 +6,8 @@ import { ObjectID } from 'mongodb';
import { Counter, sortFiles, Time } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
ContestScoreboardHiddenError,
ForbiddenError, InvalidTokenError, PermissionError,
ValidationError,
ContestScoreboardHiddenError, FileLimitExceededError, FileUploadError, InvalidTokenError,
NotAssignedError, PermissionError, ValidationError,
} from '../error';
import { Tdoc } from '../interface';
import paginate from '../lib/paginate';
@@ -101,7 +100,7 @@ export class ContestDetailBaseHandler extends Handler {
if (this.tdoc.assign?.length && !this.user.own(this.tdoc)) {
const groups = await user.listGroup(domainId, this.user._id);
if (!Set.intersection(this.tdoc.assign, groups.map((i) => i.name)).size) {
throw new ForbiddenError('You are not assigned.');
throw new NotAssignedError('contest', tid);
}
}
if (this.tdoc.duration && this.tsdoc?.startAt) {
@@ -191,7 +190,7 @@ export class ContestDetailHandler extends ContestDetailBaseHandler {
@param('code', Types.String, true)
async postAttend(domainId: string, tid: ObjectID, code = '') {
if (contest.isDone(this.tdoc)) throw new ContestNotLiveError(tid);
if (this.tdoc._code && code !== this.tdoc._code) throw new InvalidTokenError(code);
if (this.tdoc._code && code !== this.tdoc._code) throw new InvalidTokenError('Contest Invitation', code);
await contest.attend(domainId, tid, this.user._id);
this.back();
}
@@ -485,21 +484,21 @@ export class ContestFilesHandler extends ContestDetailBaseHandler {
@post('filename', Types.Name, true)
async postUploadFile(domainId: string, tid: ObjectID, filename: string) {
if ((this.tdoc.files?.length || 0) >= system.get('limit.contest_files')) {
throw new ForbiddenError('File limit exceeded.');
throw new FileLimitExceededError('count');
}
const file = this.request.files?.file;
if (!file) throw new ValidationError('file');
const f = statSync(file.filepath);
const size = Math.sum((this.tdoc.files || []).map((i) => i.size)) + f.size;
if (size >= system.get('limit.contest_files_size')) {
throw new ForbiddenError('File size limit exceeded.');
throw new FileLimitExceededError('size');
}
if (!filename) filename = file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
await storage.put(`contest/${domainId}/${tid}/${filename}`, file.filepath, this.user._id);
const meta = await storage.getMeta(`contest/${domainId}/${tid}/${filename}`);
const payload = { name: filename, ...pick(meta, ['size', 'lastModified', 'etag']) };
if (!meta) throw new Error('Upload failed');
if (!meta) throw new FileUploadError();
await contest.edit(domainId, tid, { files: [...(this.tdoc.files || []), payload] });
this.back();
}
+4 -4
View File
@@ -3,8 +3,8 @@ import { Dictionary } from 'lodash';
import moment from 'moment-timezone';
import { Context } from '../context';
import {
DomainJoinAlreadyMemberError, DomainJoinForbiddenError, ForbiddenError,
InvalidJoinInvitationCodeError, PermissionError, RoleAlreadyExistError, ValidationError,
CannotDeleteSystemDomainError, DomainJoinAlreadyMemberError, DomainJoinForbiddenError,
InvalidJoinInvitationCodeError, OnlyOwnerCanDeleteDomainError, PermissionError, RoleAlreadyExistError, ValidationError,
} from '../error';
import type { DomainDoc } from '../interface';
import avatar from '../lib/avatar';
@@ -102,8 +102,8 @@ class DomainEditHandler extends ManageHandler {
@requireSudo
async postDelete({ domainId }) {
if (domainId === 'system') throw new ForbiddenError('You are not allowed to delete system domain');
if (this.domain.owner !== this.user._id) throw new ForbiddenError('You are not the owner of this domain.');
if (domainId === 'system') throw new CannotDeleteSystemDomainError();
if (this.domain.owner !== this.user._id) throw new OnlyOwnerCanDeleteDomainError();
await domain.del(domainId);
this.response.redirect = this.url('home_domain', { domainId: 'system' });
}
+2 -2
View File
@@ -217,7 +217,7 @@ class HomeSecurityHandler extends Handler {
return this.back();
}
}
throw new InvalidTokenError(tokenDigest);
throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_SESSION], tokenDigest);
}
async postDeleteAllTokens() {
@@ -293,7 +293,7 @@ class UserChangemailWithCodeHandler extends Handler {
async get(domainId: string, code: string) {
const tdoc = await token.get(code, token.TYPE_CHANGEMAIL);
if (!tdoc || tdoc.uid !== this.user._id) {
throw new InvalidTokenError(code);
throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_CHANGEMAIL], code);
}
const udoc = await user.getByEmail(domainId, tdoc.email);
if (udoc) throw new UserAlreadyExistError(tdoc.email);
+2 -3
View File
@@ -3,8 +3,7 @@ import moment from 'moment-timezone';
import { ObjectID } from 'mongodb';
import { Time } from '@hydrooj/utils/lib/utils';
import {
ContestNotFoundError, ForbiddenError, HomeworkNotLiveError,
ValidationError,
ContestNotFoundError, HomeworkNotLiveError, NotAssignedError, ValidationError,
} from '../error';
import { PenaltyRules, Tdoc } from '../interface';
import paginate from '../lib/paginate';
@@ -49,7 +48,7 @@ class HomeworkDetailHandler extends Handler {
if (tdoc.rule !== 'homework') throw new ContestNotFoundError(domainId, tid);
if (tdoc.assign?.length && !this.user.own(tdoc)) {
if (!Set.intersection(tdoc.assign, this.user.group).size) {
throw new ForbiddenError('You are not assigned.');
throw new NotAssignedError('homework', tdoc.docId);
}
}
}
+3 -3
View File
@@ -4,7 +4,7 @@ import * as yaml from 'js-yaml';
import Schema from 'schemastery';
import * as check from '../check';
import {
BadRequestError, ForbiddenError, UserNotFoundError, ValidationError,
CannotEditSuperAdminError, NotLaunchedByPM2Error, UserNotFoundError, ValidationError,
} from '../error';
import { isEmail, isPassword } from '../lib/validator';
import { Logger } from '../logger';
@@ -86,7 +86,7 @@ class SystemDashboardHandler extends SystemHandler {
}
async postRestart() {
if (!process.env.pm_cwd) throw new BadRequestError('Not launched by pm2');
if (!process.env.pm_cwd) throw new NotLaunchedByPM2Error();
exec(`pm2 reload "${process.env.name}"`);
this.back();
}
@@ -293,7 +293,7 @@ class SystemUserPrivHandler extends SystemHandler {
if (!editSystem) {
const udoc = await user.getById(domainId, uid);
if (!udoc) throw new UserNotFoundError(uid);
if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new ForbiddenError('you can not edit user as SU in web.');
if (udoc.priv === -1 || priv === -1 || priv === allPriv) throw new CannotEditSuperAdminError();
await user.setPriv(uid, priv);
} else {
const defaultPriv = system.get('default.priv');
+9 -8
View File
@@ -3,7 +3,8 @@ import { statSync } from 'fs';
import { pick } from 'lodash';
import { lookup } from 'mime-types';
import {
BadRequestError, ForbiddenError, NotFoundError, ValidationError,
AccessDeniedError, FileExistsError, FileLimitExceededError, FileUploadError, NotFoundError,
ValidationError,
} from '../error';
import { PRIV } from '../model/builtin';
import * as oplog from '../model/oplog';
@@ -57,22 +58,22 @@ export class FilesHandler extends Handler {
async postUploadFile(domainId: string, filename: string) {
this.checkPriv(PRIV.PRIV_CREATE_FILE);
if ((this.user._files?.length || 0) >= system.get('limit.user_files')) {
if (!this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) throw new ForbiddenError('File limit exceeded.');
if (!this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) throw new FileLimitExceededError('count');
}
const file = this.request.files?.file;
if (!file) throw new ValidationError('file');
const f = statSync(file.filepath);
const size = Math.sum((this.user._files || []).map((i) => i.size)) + f.size;
if (size >= system.get('limit.user_files_size')) {
if (!this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) throw new ForbiddenError('File size limit exceeded.');
if (!this.user.hasPriv(PRIV.PRIV_UNLIMITED_QUOTA)) throw new FileLimitExceededError('size');
}
if (!filename) 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 BadRequestError('file exists');
if (this.user._files.filter((i) => i.name === filename).length) 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']) };
if (!meta) throw new Error('Upload failed');
if (!meta) throw new FileUploadError();
this.user._files.push({ _id: filename, ...payload });
await user.setById(this.user._id, { _files: this.user._files });
this.back();
@@ -97,7 +98,7 @@ export class FSDownloadHandler extends Handler {
async get(domainId: string, uid: number, filename: string, noDisposition = false) {
const targetUser = await user.getById('system', uid);
if (!targetUser) throw new NotFoundError(uid);
if (this.user._id !== uid && !targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new ForbiddenError('Access denied');
if (this.user._id !== uid && !targetUser.hasPriv(PRIV.PRIV_CREATE_FILE)) throw new AccessDeniedError();
this.response.addHeader('Cache-Control', 'public');
const target = `user/${uid}/${filename}`;
const file = await storage.getMeta(target);
@@ -120,8 +121,8 @@ export class StorageHandler extends Handler {
@param('secret', Types.String)
async get(domainId: string, target: string, filename = '', expire: number, secret: string) {
const expected = md5(`${target}/${expire}/${builtinConfig.file.secret}`);
if (expire < Date.now()) throw new ForbiddenError('Link expired');
if (secret !== expected) throw new ForbiddenError('Invalid secret');
if (expire < Date.now()) throw new AccessDeniedError();
if (secret !== expected) throw new AccessDeniedError();
this.response.body = await storage.get(target);
this.response.type = (target.endsWith('.out') || target.endsWith('.ans'))
? 'text/plain'
+26 -26
View File
@@ -8,11 +8,11 @@ import { FilterQuery, ObjectID } from 'mongodb';
import { nanoid } from 'nanoid';
import { sortFiles, streamToBuffer } from '@hydrooj/utils/lib/utils';
import {
BadRequestError, ContestNotAttendedError, ContestNotEndedError,
ContestNotFoundError, ContestNotLiveError,
ForbiddenError, NoProblemError, NotFoundError,
PermissionError, ProblemNotFoundError, RecordNotFoundError, SolutionNotFoundError,
ValidationError,
ContestNotAttendedError, ContestNotEndedError, ContestNotFoundError, ContestNotLiveError,
FileLimitExceededError, HackFailedError, NoProblemError, NotFoundError,
PermissionError, ProblemAlreadyExistError, ProblemAlreadyUsedByContestError, ProblemConfigError,
ProblemIsReferencedError, ProblemNotAllowLanguageError, ProblemNotAllowPretestError, ProblemNotFoundError,
RecordNotFoundError, SolutionNotFoundError, ValidationError,
} from '../error';
import {
ProblemDoc, ProblemSearchOptions, ProblemStatusDoc, RecordDoc, User,
@@ -94,7 +94,7 @@ registerResolver(
'ProblemManage', 'delete', 'Boolean!',
async (arg, ctx) => {
const tdocs = await contest.getRelated(ctx.args.domainId, ctx.pdoc.docId);
if (tdocs.length) throw new BadRequestError('Problem already used by contest {0}', tdocs[0]._id);
if (tdocs.length) throw new ProblemAlreadyUsedByContestError(ctx.pdoc.docId, tdocs[0]._id);
return problem.del(ctx.pdoc.domainId, ctx.pdoc.docId);
},
);
@@ -467,7 +467,7 @@ export class ProblemDetailHandler extends ContestDetailBaseHandler {
async postDelete() {
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
const tdocs = await contest.getRelated(this.args.domainId, this.pdoc.docId);
if (tdocs.length) throw new BadRequestError('Problem already used by contest {0}', tdocs[0]._id);
if (tdocs.length) throw new ProblemAlreadyUsedByContestError(this.pdoc.docId, tdocs[0]._id);
await problem.del(this.pdoc.domainId, this.pdoc.docId);
this.response.redirect = this.url('problem_main');
}
@@ -477,7 +477,7 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
@param('tid', Types.ObjectID, true)
async prepare(domainId: string, tid?: ObjectID) {
if (tid && !contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId);
if (typeof this.pdoc.config === 'string') throw new BadRequestError('Invalid problem config');
if (typeof this.pdoc.config === 'string') throw new ProblemConfigError();
}
async get() {
@@ -505,17 +505,17 @@ export class ProblemSubmitHandler extends ProblemDetailHandler {
@param('tid', Types.ObjectID, true)
async post(domainId: string, lang: string, code: string, pretest = false, input = '', tid?: ObjectID) {
const config = this.pdoc.config;
if (typeof config === 'string' || config === null) throw new BadRequestError('Problem configuration incorrect');
if (typeof config === 'string' || config === null) throw new ProblemConfigError();
if (config.type === 'objective') {
lang = '_';
} else if (config.langs && !config.langs.includes(lang)) {
throw new BadRequestError('Language not allowed.');
throw new ProblemNotAllowLanguageError();
}
if (pretest) {
if (setting.langs[lang]?.pretest) lang = setting.langs[lang].pretest as string;
if (setting.langs[lang]?.pretest === false) throw new BadRequestError('Cannot run pretest for this language.');
if (setting.langs[lang]?.pretest === false) throw new ProblemNotAllowPretestError('language');
if (!['default', 'fileio', 'remote_judge'].includes(this.response.body.pdoc.config?.type)) {
throw new BadRequestError('unable to run pretest');
throw new ProblemNotAllowPretestError('type');
}
}
await this.limitRate('add_record', 60, system.get('limit.submission_user'), true);
@@ -565,17 +565,17 @@ export class ProblemHackHandler extends ProblemDetailHandler {
@param('rid', Types.ObjectID)
@param('tid', Types.ObjectID, true)
async prepare(domainId: string, rid: ObjectID, tid?: ObjectID) {
if (typeof this.pdoc.config !== 'object' || !this.pdoc.config.hackable) throw new ForbiddenError('This problem is not hackable.');
if (typeof this.pdoc.config !== 'object' || !this.pdoc.config.hackable) throw new HackFailedError('This problem is not hackable.');
this.rdoc = await record.get(domainId, rid);
if (!this.rdoc || this.rdoc.pid !== this.pdoc.docId
|| this.rdoc.contest?.toString() !== tid?.toString()) throw new RecordNotFoundError(domainId, rid);
if (tid) {
if (this.tdoc.rule !== 'codeforces') throw new ForbiddenError('This contest is not hackable.');
if (this.tdoc.rule !== 'codeforces') throw new HackFailedError('This contest is not hackable.');
if (!contest.isOngoing(this.tdoc, this.tsdoc)) throw new ContestNotLiveError(this.tdoc.docId);
}
if (this.rdoc.uid === this.user._id) throw new BadRequestError('You cannot hack your own submission');
if (this.psdoc?.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You must accept this problem before hacking.');
if (this.rdoc.status !== STATUS.STATUS_ACCEPTED) throw new ForbiddenError('You cannot hack a unsuccessful submission.');
if (this.rdoc.uid === this.user._id) throw new HackFailedError('You cannot hack your own submission');
if (this.psdoc?.status !== STATUS.STATUS_ACCEPTED) throw new HackFailedError('You must accept this problem before hacking.');
if (this.rdoc.status !== STATUS.STATUS_ACCEPTED) throw new HackFailedError('You cannot hack a unsuccessful submission.');
}
async get() {
@@ -638,7 +638,7 @@ export class ProblemEditHandler extends ProblemManageHandler {
domainId: string, pid: string | number, title: string, content: string,
newPid: string = '', hidden = false, tag: string[] = [], difficulty = 0,
) {
if (newPid !== this.pdoc.pid && await problem.get(domainId, newPid)) throw new BadRequestError('new pid exists');
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,
};
@@ -649,7 +649,7 @@ export class ProblemEditHandler extends ProblemManageHandler {
export class ProblemConfigHandler extends ProblemManageHandler {
async get() {
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit config of a referenced problem.');
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit config');
this.response.body.testdata = sortFiles(this.pdoc.data || []);
const configFile = (this.pdoc.data || []).filter((i) => i.name.toLowerCase() === 'config.yaml');
this.response.body.config = '';
@@ -692,7 +692,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
@post('type', Types.Range(['testdata', 'additional_file']), true)
async postGetLinks(domainId: string, files: Set<string>, type = 'testdata') {
if (type === 'testdata' && !this.user.own(this.pdoc)) {
if (this.pdoc.reference) throw new BadRequestError('Cannot download testdata.');
if (this.pdoc.reference) throw new ProblemIsReferencedError('download testdata.');
if (!this.user.hasPriv(PRIV.PRIV_READ_PROBLEM_DATA)) this.checkPerm(PERM.PERM_READ_PROBLEM_DATA);
if (this.tdoc && !contest.isDone(this.tdoc)) throw new ContestNotEndedError(this.tdoc.domainId, this.tdoc.docId);
}
@@ -720,7 +720,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
@post('filename', Types.Name, true)
@post('type', Types.Range(['testdata', 'additional_file']), true)
async postUploadFile(domainId: string, filename: string, type = 'testdata') {
if (this.pdoc.reference) throw new ForbiddenError('Cannot edit files of a referenced problem.');
if (this.pdoc.reference) throw new ProblemIsReferencedError('edit files');
if (!this.request.files.file) throw new ValidationError('file');
if (!filename) filename = this.request.files.file.originalFilename || String.random(16);
if (filename.includes('/') || filename.includes('..')) throw new ValidationError('filename', null, 'Bad filename');
@@ -756,7 +756,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
+ (this.pdoc.additional_file?.length || 0)
+ files.length
>= system.get('limit.problem_files_max')) {
throw new ForbiddenError('File limit exceeded.');
throw new FileLimitExceededError('count');
}
const size = Math.sum(
(this.pdoc.data || []).map((i) => i.size),
@@ -764,7 +764,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
files.map((i) => i.size),
);
if (size >= system.get('limit.problem_files_max_size')) {
throw new ForbiddenError('File size limit exceeded.');
throw new FileLimitExceededError('size');
}
}
for (const entry of files) {
@@ -782,7 +782,7 @@ export class ProblemFilesHandler extends ProblemDetailHandler {
@post('files', Types.Array)
@post('type', Types.Range(['testdata', 'additional_file']), true)
async postDeleteFiles(domainId: string, files: string[], type = 'testdata') {
if (this.pdoc.reference) throw new ForbiddenError('Cannot delete files of a referenced problem.');
if (this.pdoc.reference) throw new ProblemIsReferencedError('delete files');
if (!this.user.own(this.pdoc, PERM.PERM_EDIT_PROBLEM_SELF)) this.checkPerm(PERM.PERM_EDIT_PROBLEM);
if (type === 'testdata') await problem.delTestdata(domainId, this.pdoc.docId, files, this.user._id);
else await problem.delAdditionalFile(domainId, this.pdoc.docId, files, this.user._id);
@@ -796,7 +796,7 @@ export class ProblemFileDownloadHandler extends ProblemDetailHandler {
@param('noDisposition', Types.Boolean)
async get(domainId: string, type = 'additional_file', filename: string, noDisposition = false) {
if (this.pdoc.reference) {
if (type === 'testdata') throw new ForbiddenError('Cannot download testdata');
if (type === 'testdata') throw new ProblemIsReferencedError('download testdata');
this.pdoc = await problem.get(this.pdoc.reference.domainId, this.pdoc.reference.pid);
if (!this.pdoc) throw new ProblemNotFoundError();
}
@@ -969,7 +969,7 @@ export class ProblemCreateHandler extends Handler {
domainId: string, title: string, content: string, pid: string,
hidden = false, difficulty = 0, tag: string[] = [],
) {
if (pid && await problem.get(domainId, pid)) throw new BadRequestError('ProblemID already exists');
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]));
const tasks = [];
+2 -2
View File
@@ -1,7 +1,7 @@
import { omit, pick, throttle } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import {
ContestNotAttendedError, ContestNotFoundError, ForbiddenError, PermissionError,
ContestNotAttendedError, ContestNotFoundError, HackRejudgeFailedError, PermissionError,
ProblemNotFoundError, RecordNotFoundError, UserNotFoundError,
} from '../error';
import { RecordDoc, Tdoc } from '../interface';
@@ -186,7 +186,7 @@ class RecordDetailHandler extends ContestDetailBaseHandler {
@param('rid', Types.ObjectID)
async post() {
this.checkPerm(PERM.PERM_REJUDGE);
if (this.rdoc.files?.hack) throw new ForbiddenError('Cannot rejudge a hack record.');
if (this.rdoc.files?.hack) throw new HackRejudgeFailedError();
}
@param('rid', Types.ObjectID)
+14 -14
View File
@@ -2,8 +2,8 @@ import moment from 'moment-timezone';
import notp from 'notp';
import b32 from 'thirty-two';
import {
BlacklistedError, ForbiddenError, InvalidTokenError, LoginError,
SystemError, UserAlreadyExistError, UserFacingError,
BlacklistedError, BuiltinLoginError, ForbiddenError, InvalidTokenError,
SystemError, TFAOperationError, UserAlreadyExistError, UserFacingError,
UserNotFoundError, ValidationError, VerifyPasswordError,
} from '../error';
import { Udoc, User } from '../interface';
@@ -53,8 +53,8 @@ registerResolver(
registerResolver(
'TFAContext', 'enable(secret: String!, code: String!)', 'Boolean!',
async (arg, ctx) => {
if (ctx.user._tfa) throw new Error('2FA is already enabled');
if (!verifyToken(arg.secret, arg.code)) throw new Error('Invalid 2FA code');
if (ctx.user._tfa) throw new TFAOperationError('enabled');
if (!verifyToken(arg.secret, arg.code)) throw new InvalidTokenError('2FA');
await user.setById(ctx.user._id, { tfa: arg.secret });
// TODO: return backup codes
return true;
@@ -64,8 +64,8 @@ registerResolver(
registerResolver(
'TFAContext', 'disable(code: String!)', 'Boolean!',
async (arg, ctx) => {
if (!ctx.user._tfa) throw new Error('2FA is already disabled');
if (!verifyToken(ctx.user._tfa, arg.code)) throw new Error('Invalid 2FA code');
if (!ctx.user._tfa) throw new TFAOperationError('disabled');
if (!verifyToken(ctx.user._tfa, arg.code)) throw new InvalidTokenError('2FA');
await user.setById(ctx.user._id, undefined, { tfa: '' });
return true;
},
@@ -106,7 +106,7 @@ class UserLoginHandler extends Handler {
noCheckPermView = true;
async get() {
if (!system.get('server.login')) throw new LoginError('Builtin login disabled.');
if (!system.get('server.login')) throw new BuiltinLoginError();
this.response.template = 'user_login.html';
}
@@ -116,7 +116,7 @@ class UserLoginHandler extends Handler {
@param('redirect', Types.String, true)
@param('tfa', Types.String, true)
async post(domainId: string, uname: string, password: string, rememberme = false, redirect = '', tfa = '') {
if (!system.get('server.login')) throw new LoginError('Builtin login disabled.');
if (!system.get('server.login')) throw new BuiltinLoginError();
let udoc = await user.getByEmail(domainId, uname);
if (!udoc) udoc = await user.getByUname(domainId, uname);
if (!udoc) throw new UserNotFoundError(uname);
@@ -125,7 +125,7 @@ class UserLoginHandler extends Handler {
this.limitRate(`user_login_${uname}`, 60, 5, false),
oplog.log(this, 'user.login', { redirect }),
]);
if (udoc._tfa && !verifyToken(udoc._tfa, tfa)) throw new InvalidTokenError('2FA token invalid.');
if (udoc._tfa && !verifyToken(udoc._tfa, tfa)) throw new InvalidTokenError('2FA');
udoc.checkPassword(password);
await user.setById(udoc._id, { loginat: new Date(), loginip: this.request.ip });
if (!udoc.hasPriv(PRIV.PRIV_USER_PROFILE)) throw new BlacklistedError(uname);
@@ -156,7 +156,7 @@ class UserSudoHandler extends Handler {
oplog.log(this, 'user.sudo', {}),
]);
if (tfa) {
if (!this.user._tfa || !verifyToken(this.user._tfa, tfa)) throw new InvalidTokenError('2FA token invalid.');
if (!this.user._tfa || !verifyToken(this.user._tfa, tfa)) throw new InvalidTokenError('2FA');
} else {
this.user.checkPassword(password);
}
@@ -241,7 +241,7 @@ class UserRegisterWithCodeHandler extends Handler {
async get(domainId: string, code: string) {
this.response.template = 'user_register_with_code.html';
const tdoc = await token.get(code, token.TYPE_REGISTRATION);
if (!tdoc) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_REGISTRATION], code);
this.response.body = tdoc;
}
@@ -254,7 +254,7 @@ class UserRegisterWithCodeHandler extends Handler {
uname: string, code: string,
) {
const tdoc = await token.get(code, token.TYPE_REGISTRATION);
if (!tdoc || (!tdoc.mail && !tdoc.phone)) throw new InvalidTokenError(token.TYPE_REGISTRATION, code);
if (!tdoc || (!tdoc.mail && !tdoc.phone)) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_REGISTRATION], code);
if (password !== verify) throw new VerifyPasswordError();
if (tdoc.phone) tdoc.mail = `${tdoc.phone}@hydro.local`;
const uid = await user.create(tdoc.mail, uname, password, undefined, this.request.ip);
@@ -305,7 +305,7 @@ class UserLostPassWithCodeHandler extends Handler {
async get({ domainId, code }) {
const tdoc = await token.get(code, token.TYPE_LOSTPASS);
if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code);
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_LOSTPASS], code);
const udoc = await user.getById(domainId, tdoc.uid);
this.response.body = { uname: udoc.uname };
this.response.template = 'user_lostpass_with_code.html';
@@ -316,7 +316,7 @@ class UserLostPassWithCodeHandler extends Handler {
@param('verifyPassword', Types.String)
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_LOSTPASS, code);
if (!tdoc) throw new InvalidTokenError(token.TYPE_TEXTS[token.TYPE_LOSTPASS], code);
if (password !== verifyPassword) throw new VerifyPasswordError();
await user.setPassword(tdoc.uid, password);
await token.del(code, token.TYPE_LOSTPASS);
+2 -2
View File
@@ -2,7 +2,7 @@ import { escapeRegExp, pick } from 'lodash';
import { FilterQuery, ObjectID } from 'mongodb';
import type { Readable } from 'stream';
import { streamToBuffer } from '@hydrooj/utils/lib/utils';
import { ProblemNotFoundError, ValidationError } from '../error';
import { FileUploadError, ProblemNotFoundError, ValidationError } from '../error';
import type {
Document, ProblemDict, ProblemStatusDoc, User,
} from '../interface';
@@ -245,7 +245,7 @@ export class ProblemModel {
storage.put(`problem/${domainId}/${pid}/testdata/${name}`, f, operator),
]);
const meta = await storage.getMeta(`problem/${domainId}/${pid}/testdata/${name}`);
if (!meta) throw new Error('Upload failed');
if (!meta) throw new FileUploadError();
const payload = { name, ...pick(meta, ['size', 'lastModified', 'etag']) };
payload.lastModified ||= new Date();
if (!fileinfo) await ProblemModel.push(domainId, pid, 'data', { _id: name, ...payload });
+9
View File
@@ -13,6 +13,15 @@ class TokenModel {
static TYPE_LOSTPASS = 5;
static TYPE_EXPORT = 6;
static TYPE_IMPORT = 7;
static TYPE_TEXTS = {
[TokenModel.TYPE_SESSION]: 'Session',
[TokenModel.TYPE_REGISTRATION]: 'Registration',
[TokenModel.TYPE_CHANGEMAIL]: 'Change Email',
[TokenModel.TYPE_OAUTH]: 'OAuth',
[TokenModel.TYPE_LOSTPASS]: 'Lost Password',
[TokenModel.TYPE_EXPORT]: 'Export',
[TokenModel.TYPE_IMPORT]: 'Import',
};
static async add(
tokenType: number, expireSeconds: number, data: any, id = String.random(32),
+1 -1
View File
@@ -26,7 +26,7 @@ const request = {
const { error } = jqXHR.responseJSON;
if (error.params) {
const message = i18n(error.message, ...error.params);
const err = new Error(message);
const err = new Error(message === error.message && error.params.length ? `${error.message}: ${error.params.join(' ')}` : message);
err.rawMessage = error.message;
err.params = error.params;
reject(err);