mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
core&ui: add error & fix error notification (#464)
This commit is contained in:
1 parent
a2ea3812df
commit
534a634028
14 files changed
+123
-76
No files matched your search
@@ -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,
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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' });
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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 });
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user