diff --git a/.eslintrc.json b/.eslintrc.json index 6aa2dae5d..983a01c0c 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -31,6 +31,6 @@ ], "no-unused-vars": "warn", "no-unreachable": "warn", - "require-atomic-updates": "warn" + "require-atomic-updates": "off" } } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 4d4681f1d..ab84945a9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -config.yaml \ No newline at end of file +config.yaml +.uibuild/ \ No newline at end of file diff --git a/hydro/development.js b/hydro/development.js index e88733392..743e7da17 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -27,9 +27,10 @@ async function run() { }); }); let server = require('./service/server'); + require('./lib/i18n'); + require('./handler/ui'); require('./handler/base'); require('./handler/home'); - require('./handler/user'); require('./handler/problem'); require('./handler/record'); require('./handler/judge'); diff --git a/hydro/error.js b/hydro/error.js index 46854397d..aec077d0d 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -1,5 +1,3 @@ -const { PRIV_USER_PROFILE } = require('./privilege'); - class UserFacingError extends Error { constructor(type) { super(type); @@ -62,12 +60,6 @@ class VerifyPasswordError extends ForbiddenError { super('VerifyPasswordError'); } } -class PrivilegeError extends ForbiddenError { - constructor(priv) { - super('PrivilegeError'); - this.params = [priv]; - } -} class OpcountExceededError extends ForbiddenError { constructor(op, period_secs, max_operations) { super('OpcountExceededError'); @@ -75,25 +67,6 @@ class OpcountExceededError extends ForbiddenError { } } -class DomainAlreadyExistError extends ForbiddenError { - constructor(domain) { - super('DomainAlreadyExistError'); - this.params = [domain]; - } -} -class DomainNotFoundError extends ForbiddenError { - constructor(domain) { - super('DomainNotFoundError'); - this.params = [domain]; - } -} - -class UserAlreadyDomainMemberError extends ForbiddenError { - constructor(domainId, uid) { - super('UserAlreadyDomainMemberError'); - this.params = [domainId, uid]; - } -} class PermissionError extends ForbiddenError { constructor(perm) { super('PermissionError'); @@ -108,35 +81,34 @@ class ValidationError extends ForbiddenError { } } class ProblemNotFoundError extends NotFoundError { - constructor(domainId, pid) { + constructor(pid) { super('ProblemNotFoundError'); - this.params = [domainId, pid]; + this.params = [pid]; } } class RecordNotFoundError extends NotFoundError { - constructor(domainId, rid) { + constructor(rid) { super('RecordNotFoundError'); - this.params = [domainId, rid]; + this.params = [rid]; } } class TrainingNotFoundError extends NotFoundError { - constructor(domainId, tid) { + constructor(tid) { super('TrainingNotFoundError'); - this.params = [domainId, tid]; + this.params = [tid]; } } class ContestNotFoundError extends NotFoundError { - constructor(domainId, cid) { + constructor(cid) { super('ContestNotFoundError'); - this.params = [domainId, cid]; + this.params = [cid]; } } module.exports = { BadRequestError, ForbiddenError, NotFoundError, LoginError, UserAlreadyExistError, InvalidTokenError, - UserNotFoundError, VerifyPasswordError, PrivilegeError, - OpcountExceededError, DomainAlreadyExistError, DomainNotFoundError, - UserAlreadyDomainMemberError, PermissionError, NoProblemError, + UserNotFoundError, VerifyPasswordError, + OpcountExceededError, PermissionError, NoProblemError, ValidationError, ProblemNotFoundError, TrainingNotFoundError, ContestNotFoundError, RecordNotFoundError }; @@ -176,11 +148,6 @@ class UserFacingError(Error): return 'An error has occurred.' -class BuiltinDomainError(ForbiddenError): - @property - def message(self): - return 'Domain {0} is bulit-in and cannot be modified.' - class BlacklistedError(ForbiddenError): @property @@ -284,20 +251,6 @@ class MessageNotFoundError(NotFoundError): return 'Message {0} not found.' - - -class DomainJoinForbiddenError(ForbiddenError): - @property - def message(self): - return 'You are not allowed to join the domain. The link is either invalid or expired.' - - -class DomainJoinAlreadyMemberError(ForbiddenError): - @property - def message(self): - return 'Failed to join the domain. You are already a member.' - - class InvalidJoinInvitationCodeError(ForbiddenError): @property def message(self): @@ -369,13 +322,6 @@ class UsageExceededError(ForbiddenError): def message(self): return 'Usage exceeded.' - -class DomainRoleAlreadyExistError(ForbiddenError): - @property - def message(self): - return 'Role {1} already exists in domain {0}.' - - class ModifyBuiltinRoleError(ForbiddenError): @property def message(self): diff --git a/hydro/handler/base.js b/hydro/handler/base.js index 38ca61db4..e0d9673b7 100644 --- a/hydro/handler/base.js +++ b/hydro/handler/base.js @@ -1,20 +1,17 @@ const { MIDDLEWARE } = require('../service/server.js'), user = require('../model/user'), - domain = require('../model/domain'), blacklist = require('../model/blacklist'), token = require('../model/token'), options = require('../options'), { UserNotFoundError, BlacklistedError, - PermissionError, - PrivilegeError + PermissionError } = require('../error'); MIDDLEWARE(async (ctx, next) => { try { - ctx.state.domainId = 'system'; let sid = ctx.cookies.get('sid'); let save = ctx.cookies.get('save'); let tokenType, expireSeconds; @@ -33,11 +30,7 @@ MIDDLEWARE(async (ctx, next) => { if (!ctx.session) ctx.session = { uid: 1 }; let bdoc = await blacklist.get(ctx.request.ip); if (bdoc) throw new BlacklistedError(ctx.request.ip); - console.log(ctx.session.uid); - [ctx.state.user, ctx.state.domain] = await Promise.all([ - user.getById(ctx.session.uid, ctx.state.domainId), - domain.get(ctx.state.domainId) - ]); + ctx.state.user = await user.getById(ctx.session.uid); if (!ctx.state.user) throw new UserNotFoundError(ctx.session.uid); ctx.checkPerm = function (perm) { for (let i in arguments) { @@ -55,22 +48,6 @@ MIDDLEWARE(async (ctx, next) => { } } }; - ctx.checkPriv = function (priv) { - for (let i in arguments) { - if (arguments[i] instanceof Array) { - let p = false; - for (let j in arguments) - if (ctx.state.user.hasPriv(arguments[i][j])) { - p = true; - break; - } - if (!p) throw new PrivilegeError([arguments[i]]); - } else { - if (ctx.state.user.hasPriv(arguments[i])) continue; - else throw new PrivilegeError([[arguments[i]]]); - } - } - }; await next(); if (ctx.session.sid) await token.update(ctx.session.sid, tokenType, expireSeconds, Object.assign({ diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index 046127bf1..d535e2a91 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -1,434 +1,35 @@ const - { GET, SOCKET } = require('../service/server'), - { assertQueue, consume } = require('../service/queue'), - { requirePriv } = require('./tools'), + { CONTEXT } = require('../service/server'), + queue = require('../service/queue'), record = require('../model/record'), - token = require('../model/token'), - user = require('../model/user'), - { PRIV_JUDGE } = require('../privilege'); + { requirePerm } = require('./tools'), + { PERM_JUDGE } = require('../permission'); -assertQueue('judge'); +queue.assert('judge'); -GET('/judge/noop', requirePriv(PRIV_JUDGE), async ctx => { +const { MIDDLEWARE, GET, POST } = CONTEXT(); +MIDDLEWARE(requirePerm(PERM_JUDGE)); +GET('/judge/noop', async ctx => { ctx.body = {}; }); -SOCKET('/judge/conn', async conn => { - let sid = await new Promise((resolve, reject) => { - setTimeout(reject, 5000); - conn.once('data', async (data) => { - console.log(data); - resolve(data); - }); - }); - let session = await token.get(sid, token.TYPE_SESSION); - let u = await user.getById(session.uid); - if (!u.hasPriv(PRIV_JUDGE)) conn.close(1000, 'PrivilegeError'); - let h = 1; - let interval = setInterval(() => { - if (!h) h.close(); - h = 0; - conn.write('h'); - }, 30000); - conn.on('data', (data) => { - data = JSON.parse(data); - h++; - if (data.event == 'next') { - // TODO (masnn) - } else if (data.event == 'end') { - // TODO (masnn) - } - }); - consume('judge', async rid => { - rid = rid.toString(); +GET('/judge/fetch', async ctx => { + let rid = await queue.get('judge', false); + if (rid) { let rdoc = await record.get(rid); let data = { event: 'judge', rid, type: 0, - domainId: rdoc.domainId, pid: rdoc.pid, data: rdoc.data, lang: rdoc.lang, code: rdoc.code }; - conn.write(JSON.stringify(data)); - }); - conn.on('close', () => { - clearInterval(interval); - }); + ctx.body = data; + } +}); +POST('/judge/next', async ctx => { + console.log(ctx.request.body); +}); +POST('/judge/end', async ctx => { + console.log(ctx.request.body); }); - -/* - -@app.route('/p/category/{category:[^/]*}/random', 'problem_category_random') -class ProblemCategoryRandomHandler(base.Handler): - @base.requirePerm(builtin.PERM_VIEW_PROBLEM) - @base.get_argument - @base.route_argument - @base.sanitize - async def get(self, *, category: str): - if not self.has_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN): - f = {'hidden': False} - else: - f = {} - query = ProblemCategoryHandler.build_query(category) - pid = await problem.get_random_id(self.domainId, **query, **f) - if pid: - self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) - else: - self.json_or_redirect(self.referer_or_main) - - -@app.route('/p/{pid}/submit', 'problem_submit') -class ProblemSubmitHandler(base.Handler): - @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - # TODO(twd2): check status, eg. test, hidden problem, ... - uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None - pdoc = await problem.get(self.domainId, pid, uid) - if pdoc.get('hidden', False): - self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) - udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), - domain.get_user(self.domainId, pdoc['owner_uid'])) - if uid == None: - rdocs = [] - else: - # TODO(iceboy): needs to be in sync with contest_detail_problem_submit - rdocs = await record \ - .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ - .sort([('_id', -1)]) \ - .limit(10) \ - .to_list() - if not self.prefer_json: - path_components = self.build_path( - (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), - (self.translate('problem_submit'), None)) - self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, - page_title=pdoc['title'], path_components=path_components) - else: - self.json({'rdocs': rdocs}) - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) - @base.route_argument - @base.post_argument - @base.require_csrf_token - @base.sanitize - @base.limitRate('add_record', 60, 100) - async def post(self, *, pid: document.convert_doc_id, lang: str, code: str): - # TODO(twd2): check status, eg. test, hidden problem, ... - pdoc = await problem.get(self.domainId, pid) - if pdoc.get('hidden', False): - self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, - self.user['_id'], lang, code) - self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) - - -@app.route('/p/{pid}/pretest', 'problem_pretest') -class ProblemPretestHandler(base.Handler): - @base.requirePerm(builtin.PERM_SUBMIT_PROBLEM) - @base.route_argument - @base.post_argument - @base.require_csrf_token - @base.sanitize - @base.limitRate('add_record', 60, 100) - async def post(self, *, pid: document.convert_doc_id, lang: str, code: str, - data_input: str, data_output: str): - pdoc = await problem.get(self.domainId, pid) - # don't need to check hidden status - # create zip file, TODO(twd2): check file size - post = await self.request.post() - content = list(zip(post.getall('data_input'), post.getall('data_output'))) - output_buffer = io.BytesIO() - zip_file = zipfile.ZipFile(output_buffer, 'a', zipfile.ZIP_DEFLATED) - config_content = str(len(content)) + '\n' - for i, (data_input, data_output) in enumerate(content): - input_file = 'input{0}.txt'.format(i) - output_file = 'output{0}.txt'.format(i) - config_content += '{0}|{1}|1|10|262144\n'.format(input_file, output_file) - zip_file.writestr('Input/{0}'.format(input_file), data_input) - zip_file.writestr('Output/{0}'.format(output_file), data_output) - zip_file.writestr('Config.ini', config_content) - # mark all files as created in Windows :p - for zfile in zip_file.filelist: - zfile.create_system = 0 - zip_file.close() - fid = await fs.add_data('application/zip', output_buffer.getvalue()) - output_buffer.close() - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, - self.user['_id'], lang, code, fid) - self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) - - -@app.connection_route('/p/{pid}/pretest-conn', 'problem_pretest-conn') -class ProblemPretestConnection(record_handler.RecordVisibilityMixin, base.Connection): - async def on_open(self): - await super(ProblemPretestConnection, self).on_open() - self.pid = document.convert_doc_id(self.request.match_info['pid']) - bus.subscribe(self.on_record_change, ['record_change']) - - async def on_record_change(self, e): - rdoc = e['value'] - if rdoc['uid'] != self.user['_id'] or \ - rdoc['domainId'] != self.domainId or rdoc['pid'] != self.pid: - return - # check permission for visibility: contest - if rdoc['tid']: - show_status, tdoc = await self.rdoc_contest_visible(rdoc) - if not show_status: - return - self.send(rdoc=rdoc) - - async def on_close(self): - bus.unsubscribe(self.on_record_change) - -@app.route('/p/{pid}/data', 'problem_data') -class ProblemDataHandler(base.Handler): - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - # Judges will have PRIV_READ_PROBLEM_DATA, - # domain administrators will have PERM_READ_PROBLEM_DATA, - # problem owner will have PERM_READ_PROBLEM_DATA_SELF. - pdoc = await problem.get(self.domainId, pid) - if type(pdoc['data']) is dict: - return self.redirect(self.reverse_url('problem_data', - domainId=pdoc['data']['domain'], - pid=pdoc['data']['pid'])) - if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) - and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): - self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) - fdoc = await problem.get_data(pdoc) - if not fdoc: - raise error.ProblemDataNotFoundError(self.domainId, pid) - self.redirect(options.cdn_prefix.rstrip('/') + \ - self.reverse_url('fs_get', domainId=builtin.domainId_SYSTEM, - secret=fdoc['metadata']['secret'])) - - -@app.route('/p/copy', 'problem_copy') -class ProblemCopyHandler(base.Handler): - MAX_PROBLEMS_PER_REQUEST = 20 - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.requirePerm(builtin.PERM_CREATE_PROBLEM) - async def get(self): - self.render('problem_copy.html') - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.requirePerm(builtin.PERM_CREATE_PROBLEM) - @base.post_argument - @base.require_csrf_token - @base.sanitize - @base.limitRate('copy_problems', 30, 10) - async def post(self, *, src_domainId: str, src_pids: str, - numeric_pid: bool=False, hidden: bool=False): - src_ddoc, src_dudoc = await asyncio.gather(domain.get(src_domainId), - domain.get_user(src_domainId, self.user['_id'])) - if not src_dudoc: - src_dudoc = {} - if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, - perm=builtin.PERM_VIEW_PROBLEM): - # TODO: This is the source domain's PermissionError. - raise error.PermissionError(builtin.PERM_VIEW_PROBLEM) - - src_pids = misc.dedupe(map(document.convert_doc_id, src_pids.replace('\r\n', '\n').split('\n'))) - if len(src_pids) > self.MAX_PROBLEMS_PER_REQUEST: - raise error.BatchCopyLimitExceededError(self.MAX_PROBLEMS_PER_REQUEST, len(src_pids)) - pdocs = await problem.get_multi(domainId=src_domainId, doc_id={'$in': src_pids}) \ - .sort('doc_id', 1) \ - .to_list() - - exist_pids = [pdoc['doc_id'] for pdoc in pdocs] - if len(src_pids) != len(exist_pids): - for pid in src_pids: - if pid not in exist_pids: - raise error.ProblemNotFoundError(src_domainId, pid) - - for pdoc in pdocs: - if pdoc.get('hidden', False): - if not self.dudoc_has_perm(ddoc=src_ddoc, dudoc=src_dudoc, udoc=self.user, - perm=builtin.PERM_VIEW_PROBLEM_HIDDEN): - # TODO: This is the source domain's PermissionError. - raise error.PermissionError(builtin.PERM_VIEW_PROBLEM_HIDDEN) - - for pdoc in pdocs: - pid = None - if numeric_pid: - pid = await domain.inc_pid_counter(self.domainId) - await problem.copy(pdoc, self.domainId, self.user['_id'], pid, hidden) - - self.redirect(self.reverse_url('problem_main')) - - -@app.route('/p/{pid}/edit', 'problem_edit') -class ProblemEditHandler(base.Handler): - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None - pdoc = await problem.get(self.domainId, pid, uid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), - domain.get_user(self.domainId, pdoc['owner_uid'])) - path_components = self.build_path( - (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), - (self.translate('problem_edit'), None)) - self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, - page_title=pdoc['title'], path_components=path_components) - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.post_argument - @base.require_csrf_token - @base.sanitize - async def post(self, *, pid: document.convert_doc_id, title: str, content: str): - pdoc = await problem.get(self.domainId, pid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) - self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) - - -@app.route('/p/{pid}/settings', 'problem_settings') -class ProblemSettingsHandler(base.Handler): - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None - pdoc = await problem.get(self.domainId, pid, uid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), - domain.get_user(self.domainId, pdoc['owner_uid'])) - path_components = self.build_path( - (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), - (self.translate('problem_settings'), None)) - self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, - categories=problem.get_categories(), - page_title=pdoc['title'], path_components=path_components) - - def split_tags(self, s): - s = s.replace(',', ',') # Chinese ', ' - return list(filter(lambda _: _ != '', map(lambda _: _.strip(), s.split(',')))) - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.post_argument - @base.require_csrf_token - @base.sanitize - async def post(self, *, pid: document.convert_doc_id, hidden: bool=False, - category: str, tag: str, - difficulty_setting: int, difficulty_admin: str=''): - pdoc = await problem.get(self.domainId, pid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - category = self.split_tags(category) - tag = self.split_tags(tag) - for c in category: - if not (c in builtin.PROBLEM_CATEGORIES - or c in builtin.PROBLEM_SUB_CATEGORIES): - raise error.ValidationError('category') - if difficulty_setting not in problem.SETTING_DIFFICULTY_RANGE: - raise error.ValidationError('difficulty_setting') - if difficulty_admin: - try: - difficulty_admin = int(difficulty_admin) - except ValueError: - raise error.ValidationError('difficulty_admin') - else: - difficulty_admin = None - await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, - category=category, tag=tag, - difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) - await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) - self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) - - -@app.route('/p/{pid}/upload', 'problem_upload') -class ProblemUploadHandler(base.Handler): - def get_content_type(self, filename): - if os.path.splitext(filename)[1].lower() != '.zip': - raise error.FileTypeNotAllowedError(filename) - return 'application/zip' - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - pdoc = await problem.get(self.domainId, pid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) - and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): - self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) - md5 = await fs.get_md5(await problem.get_data(pdoc)) - self.render('problem_upload.html', pdoc=pdoc, md5=md5) - - @base.requirePriv(builtin.PRIV_USER_PROFILE) - @base.route_argument - @base.multipart_argument - @base.require_csrf_token - @base.sanitize - async def post(self, *, pid: document.convert_doc_id, file: objectid.ObjectId): - pdoc = await problem.get(self.domainId, pid) - if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): - self.check_perm(builtin.PERM_EDIT_PROBLEM) - if (not self.own(pdoc, builtin.PERM_READ_PROBLEM_DATA_SELF) - and not self.has_perm(builtin.PERM_READ_PROBLEM_DATA)): - self.check_priv(builtin.PRIV_READ_PROBLEM_DATA) - if pdoc.get('data') and type(pdoc['data']) is objectid.ObjectId: - await fs.unlink(pdoc['data']) - await problem.set_data(self.domainId, pid, file) - self.json_or_redirect(self.url) - - -@app.route('/p/{pid}/statistics', 'problem_statistics') -class ProblemStatisticsHandler(base.Handler): - @base.route_argument - @base.sanitize - async def get(self, *, pid: document.convert_doc_id): - # TODO(twd2) - uid = self.user['_id'] if self.has_priv(builtin.PRIV_USER_PROFILE) else None - pdoc = await problem.get(self.domainId, pid, uid) - if pdoc.get('hidden', False): - self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) - udoc, dudoc = await asyncio.gather(user.get_by_uid(pdoc['owner_uid']), - domain.get_user(self.domainId, pdoc['owner_uid'])) - path_components = self.build_path( - (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), - (self.translate('problem_statistics'), None)) - self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, - page_title=pdoc['title'], path_components=path_components) - - -@app.route('/p/search', 'problem_search') -class ProblemSearchHandler(base.Handler): - @base.get_argument - @base.route_argument - @base.sanitize - async def get(self, *, q: str): - q = q.strip() - if not q: - self.json_or_redirect(self.referer_or_main) - return - try: - pdoc = await problem.get(self.domainId, document.convert_doc_id(q)) - except error.ProblemNotFoundError: - pdoc = None - if pdoc: - self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) - return - self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ - .format(parse.quote(q), parse.quote(options.url_prefix))) - -*/ \ No newline at end of file diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index 96776bde2..b3fa21d03 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -1,75 +1,63 @@ const - { GET, POST } = require('../service/server'), - { assertQueue, publish } = require('../service/queue'), - { requirePerm, requirePriv } = require('./tools'), validator = require('../lib/validator'), problem = require('../model/problem'), - training = require('../model/training'), - contest = require('../model/contest'), record = require('../model/record'), - domain = require('../model/domain'), user = require('../model/user'), + { GET, POST } = require('../service/server'), + queue = require('../service/queue'), + { requirePerm } = require('./tools'), { NoProblemError } = require('../error'), - { PRIV_USER_PROFILE } = require('../privilege'), + { constants } = require('../options'), { PERM_VIEW_PROBLEM, PERM_VIEW_PROBLEM_HIDDEN, - PERM_VIEW_TRAINING, - PERM_VIEW_CONTEST, - PERM_VIEW_HOMEWORK, PERM_SUBMIT_PROBLEM, PERM_CREATE_PROBLEM } = require('../permission'); -const PROBLEMS_PER_PAGE = 100; -assertQueue('judge'); +queue.assert('judge'); GET('/p', requirePerm(PERM_VIEW_PROBLEM), async ctx => { - let q = { domainId: ctx.state.domainId }, + let q = {}, page = ctx.query.page || 1; if (!ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; - let pdocs = await problem.getMany(q, { pid: 1 }, page, PROBLEMS_PER_PAGE); - ctx.body = { code: 0, page, pdocs, category: '' }; + let pdocs = await problem.getMany(q, { pid: 1 }, page, constants.PROBLEM_PER_PAGE); + ctx.templateName = 'problem_main.html'; + ctx.body = { page_name: 'problem_list', page, pdocs, category: '' }; }); GET('/problem/random', requirePerm(PERM_VIEW_PROBLEM), async ctx => { - let q = { domainId: ctx.state.domainId }; + let q = {}; if (!ctx.state.user.hasPerm(PERM_VIEW_PROBLEM_HIDDEN)) q.hidden = false; let pid = await problem.random(q); if (!pid) throw new NoProblemError(); ctx.body = { pid }; }); +GET('/p/:pid', requirePerm(PERM_VIEW_PROBLEM), async ctx => { + let uid = ctx.state.user._id, + pid = ctx.params.pid; + let pdoc = await problem.get({ pid, uid }); + if (pdoc.hidden) ctx.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); + let udoc = await user.getById(pdoc.owner); + ctx.body = { pdoc, udoc, title: pdoc.title }; +}); POST('/p/:pid/submit', requirePerm(PERM_SUBMIT_PROBLEM), async ctx => { let rid = await record.add({ creator: ctx.state.user._id, lang: ctx.request.body.language, code: ctx.request.body.code, - domainId: ctx.state.domainId, pid: ctx.params.pid }); - await publish('judge', rid); + await queue.push('judge', rid); ctx.state.user.nSubmit++; ctx.body = { rid }; }); -GET('/p/:pid', requirePerm(PERM_VIEW_PROBLEM), async ctx => { - let uid = null, - pid = Number(ctx.params.pid); - if (ctx.state.user.hasPriv(PRIV_USER_PROFILE)) uid = ctx.state.user._id; - let pdoc = await problem.get({ domainId: ctx.state.domainId, pid, uid }); - if (pdoc.hidden) ctx.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); - let udoc = await user.getById(pdoc.owner); - ctx.body = { pdoc, udoc, title: pdoc.title }; +POST('/problem/create', requirePerm(PERM_CREATE_PROBLEM), async ctx => { + let { title, pid, content, hidden } = ctx.request.body; + validator.checkPid(pid); + pid = pid || await system.incPidCounter(); + await problem.add({ title, content, owner: ctx.state.user._id, pid, hidden }); + ctx.body = { pid }; }); -POST('/problem/create', - requirePriv(PRIV_USER_PROFILE), - requirePerm(PERM_CREATE_PROBLEM), - async ctx => { - let { title, pid, content, hidden } = ctx.request.body; - validator.checkPid(pid); - pid = pid || await domain.incPidCounter(ctx.state.domainId); - await problem.add({ domainId: ctx.state.domainId, title, content, owner: ctx.state.user._id, pid, hidden }); - ctx.body = { pid }; - } -); /* @@ -110,14 +98,14 @@ class ProblemSubmitHandler(base.Handler): else: # TODO(iceboy): needs to be in sync with contest_detail_problem_submit rdocs = await record \ - .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ + .get_user_in_problem_multi(uid, self.domainId, pdoc['_id']) \ .sort([('_id', -1)]) \ .limit(10) \ .to_list() if not self.prefer_json: path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_submit'), None)) self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -136,7 +124,7 @@ class ProblemSubmitHandler(base.Handler): pdoc = await problem.get(self.domainId, pid) if pdoc.get('hidden', False): self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, + rid = await record.add(self.domainId, pdoc['_id'], constant.record.TYPE_SUBMISSION, self.user['_id'], lang, code) self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) @@ -172,7 +160,7 @@ class ProblemPretestHandler(base.Handler): zip_file.close() fid = await fs.add_data('application/zip', output_buffer.getvalue()) output_buffer.close() - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, + rid = await record.add(self.domainId, pdoc['_id'], constant.record.TYPE_PRETEST, self.user['_id'], lang, code, fid) self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) @@ -256,7 +244,7 @@ class ProblemCopyHandler(base.Handler): .sort('doc_id', 1) \ .to_list() - exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + exist_pids = [pdoc['_id'] for pdoc in pdocs] if len(src_pids) != len(exist_pids): for pid in src_pids: if pid not in exist_pids: @@ -292,7 +280,7 @@ class ProblemEditHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_edit'), None)) self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -306,7 +294,7 @@ class ProblemEditHandler(base.Handler): pdoc = await problem.get(self.domainId, pid) if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): self.check_perm(builtin.PERM_EDIT_PROBLEM) - await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) + await problem.edit(self.domainId, pdoc['_id'], title=title, content=content) self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) @@ -324,7 +312,7 @@ class ProblemSettingsHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_settings'), None)) self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, categories=problem.get_categories(), @@ -360,10 +348,10 @@ class ProblemSettingsHandler(base.Handler): raise error.ValidationError('difficulty_admin') else: difficulty_admin = None - await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, + await problem.edit(self.domainId, pdoc['_id'], hidden=hidden, category=category, tag=tag, difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) - await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) + await job.difficulty.update_problem(self.domainId, pdoc['_id']) self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) @@ -419,7 +407,7 @@ class ProblemStatisticsHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_statistics'), None)) self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -440,7 +428,7 @@ class ProblemSearchHandler(base.Handler): except error.ProblemNotFoundError: pdoc = None if pdoc: - self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) + self.redirect(self.reverse_url('problem_detail', pid=pdoc['_id'])) return self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ .format(parse.quote(q), parse.quote(options.url_prefix))) diff --git a/hydro/handler/record.js b/hydro/handler/record.js index 90260207e..9b952d35a 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -1,19 +1,12 @@ const bson = require('bson'), { constants } = require('../options'), - { GET, POST } = require('../service/server'), - { publish } = require('../service/queue'), - { requirePerm, requirePriv } = require('./tools'), - validator = require('../lib/validator'), - problem = require('../model/problem'), + { GET } = require('../service/server'), record = require('../model/record'), - domain = require('../model/domain'), - user = require('../model/user'), - { PRIV_USER_PROFILE } = require('../privilege'), { PERM_READ_RECORD_CODE, PERM_VIEW_CONTEST_HIDDEN_SCOREBOARD } = require('../permission'); GET('/r', async ctx => { - let q = { domainId: ctx.state.domainId }, + let q = {}, page = ctx.query.page || 1; let rdocs = await record.getMany(q, { rid: 1 }, page, constants.RECORD_PER_PAGE); ctx.body = { page, rdocs }; @@ -65,14 +58,14 @@ class ProblemSubmitHandler(base.Handler): else: # TODO(iceboy): needs to be in sync with contest_detail_problem_submit rdocs = await record \ - .get_user_in_problem_multi(uid, self.domainId, pdoc['doc_id']) \ + .get_user_in_problem_multi(uid, self.domainId, pdoc['_id']) \ .sort([('_id', -1)]) \ .limit(10) \ .to_list() if not self.prefer_json: path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_submit'), None)) self.render('problem_submit.html', pdoc=pdoc, udoc=udoc, rdocs=rdocs, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -91,7 +84,7 @@ class ProblemSubmitHandler(base.Handler): pdoc = await problem.get(self.domainId, pid) if pdoc.get('hidden', False): self.check_perm(builtin.PERM_VIEW_PROBLEM_HIDDEN) - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_SUBMISSION, + rid = await record.add(self.domainId, pdoc['_id'], constant.record.TYPE_SUBMISSION, self.user['_id'], lang, code) self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) @@ -127,7 +120,7 @@ class ProblemPretestHandler(base.Handler): zip_file.close() fid = await fs.add_data('application/zip', output_buffer.getvalue()) output_buffer.close() - rid = await record.add(self.domainId, pdoc['doc_id'], constant.record.TYPE_PRETEST, + rid = await record.add(self.domainId, pdoc['_id'], constant.record.TYPE_PRETEST, self.user['_id'], lang, code, fid) self.json_or_redirect(self.reverse_url('record_detail', rid=rid)) @@ -211,7 +204,7 @@ class ProblemCopyHandler(base.Handler): .sort('doc_id', 1) \ .to_list() - exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + exist_pids = [pdoc['_id'] for pdoc in pdocs] if len(src_pids) != len(exist_pids): for pid in src_pids: if pid not in exist_pids: @@ -247,7 +240,7 @@ class ProblemEditHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_edit'), None)) self.render('problem_edit.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -261,7 +254,7 @@ class ProblemEditHandler(base.Handler): pdoc = await problem.get(self.domainId, pid) if not self.own(pdoc, builtin.PERM_EDIT_PROBLEM_SELF): self.check_perm(builtin.PERM_EDIT_PROBLEM) - await problem.edit(self.domainId, pdoc['doc_id'], title=title, content=content) + await problem.edit(self.domainId, pdoc['_id'], title=title, content=content) self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) @@ -279,7 +272,7 @@ class ProblemSettingsHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_settings'), None)) self.render('problem_settings.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, categories=problem.get_categories(), @@ -315,10 +308,10 @@ class ProblemSettingsHandler(base.Handler): raise error.ValidationError('difficulty_admin') else: difficulty_admin = None - await problem.edit(self.domainId, pdoc['doc_id'], hidden=hidden, + await problem.edit(self.domainId, pdoc['_id'], hidden=hidden, category=category, tag=tag, difficulty_setting=difficulty_setting, difficulty_admin=difficulty_admin) - await job.difficulty.update_problem(self.domainId, pdoc['doc_id']) + await job.difficulty.update_problem(self.domainId, pdoc['_id']) self.json_or_redirect(self.reverse_url('problem_detail', pid=pid)) @@ -374,7 +367,7 @@ class ProblemStatisticsHandler(base.Handler): domain.get_user(self.domainId, pdoc['owner_uid'])) path_components = self.build_path( (self.translate('problem_main'), self.reverse_url('problem_main')), - (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['doc_id'])), + (pdoc['title'], self.reverse_url('problem_detail', pid=pdoc['_id'])), (self.translate('problem_statistics'), None)) self.render('problem_statistics.html', pdoc=pdoc, udoc=udoc, dudoc=dudoc, page_title=pdoc['title'], path_components=path_components) @@ -395,7 +388,7 @@ class ProblemSearchHandler(base.Handler): except error.ProblemNotFoundError: pdoc = None if pdoc: - self.redirect(self.reverse_url('problem_detail', pid=pdoc['doc_id'])) + self.redirect(self.reverse_url('problem_detail', pid=pdoc['_id'])) return self.redirect('http://cn.bing.com/search?q={0}+site%3A{1}' \ .format(parse.quote(q), parse.quote(options.url_prefix))) diff --git a/hydro/handler/tools.js b/hydro/handler/tools.js index 3ef5d12ab..386eb4ed3 100644 --- a/hydro/handler/tools.js +++ b/hydro/handler/tools.js @@ -1,6 +1,6 @@ const opcount = require('../model/opcount'), - { PermissionError, PrivilegeError } = require('../error'); + { PermissionError } = require('../error'); module.exports = { requirePerm(perm) { @@ -22,25 +22,6 @@ module.exports = { await next(); }; }, - requirePriv(priv) { - return async (ctx, next) => { - for (let i in arguments) { - if (arguments[i] instanceof Array) { - let p = false; - for (let j in arguments) - if (ctx.state.user.hasPriv(arguments[i][j])) { - p = true; - break; - } - if (!p) throw new PrivilegeError([arguments[i]]); - } else { - if (ctx.state.user.hasPriv(arguments[i])) continue; - else throw new PrivilegeError([arguments[i]]); - } - } - await next(); - }; - }, limitRate(op, period_secs, max_operations) { return async (ctx, next) => { await opcount.inc(op, ctx.request.ip, period_secs, max_operations); diff --git a/hydro/handler/ui.js b/hydro/handler/ui.js new file mode 100644 index 000000000..fc2137a49 --- /dev/null +++ b/hydro/handler/ui.js @@ -0,0 +1,65 @@ +const + path = require('path'), + static = require('koa-static'), + nunjucks = require('nunjucks'), + options = require('../options'), + perm = require('../permission'), + builtin = require('../model/builtin'), + { MIDDLEWARE } = require('../service/server'), + { NotFoundError } = require('../error'); + +class Nunjucks extends nunjucks.Environment { + constructor() { + super( + new nunjucks.FileSystemLoader(path.resolve(options.template.path)), + { autoescape: true, trimBlocks: true } + ); + this.addFilter('json', function (data) { + return JSON.stringify(data); + }, false); + this.addFilter('assign', function (self, data) { + return Object.assign(self, data); + }); + this.addGlobal('static_url', str => `/${str}`); + this.addGlobal('reverse_url', str => str); + this.addGlobal('perm', perm); + this.addGlobal('builtin', builtin); + } +} +const env = new Nunjucks(); +MIDDLEWARE(static(path.resolve(process.cwd(), '.uibuild'))); +MIDDLEWARE(async (ctx, next) => { + ctx.render_title = str => str; + ctx.ui_context = {}; + ctx.user_context = {}; + ctx.render = async (name, context) => { + ctx.user = ctx.state.user; + ctx.translate = str => { + return str.translate(ctx.state.user.language); + }; + ctx.has_perm = perm => ctx.state.user.hasPerm(perm); + ctx.body = await new Promise((resolve, reject) => { + env.render(name, Object.assign(ctx.state, context, { + handler: ctx, + _: ctx.translate + }), (error, res) => { + if (error) reject(error); + else resolve(res); + }); + }); + ctx.response.type = 'text/html'; + }; + try { + try { + await next(); + if (!ctx.body) throw new NotFoundError(); + } catch (error) { + await ctx.render('error.html', { error }); + } + Object.assign(ctx.body, JSON.parse(ctx.query.data || '{}')); + console.log(ctx.body); + if (ctx.query.template || ctx.templateName) await ctx.render(ctx.query.template || ctx.templateName, ctx.body); + } catch (error) { + await ctx.render('bsod.html', { error }); + } +}); \ No newline at end of file diff --git a/hydro/handler/user.js b/hydro/handler/user.js index 80a6e4f14..a653e2744 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -1,13 +1,13 @@ const - { GET, POST } = require('../service/server.js'), + { GET, POST, CONTEXT } = require('../service/server.js'), user = require('../model/user'), token = require('../model/token'), system = require('../model/system'), mail = require('../lib/mail'), validator = require('../lib/validator'), options = require('../options'), - { PRIV_REGISTER_USER, PRIV_USER_PROFILE } = require('../privilege'), - { requirePriv, limitRate } = require('./tools'), + { PERM_REGISTER_USER, PERM_LOGGEDIN } = require('../permission'), + { requirePerm, limitRate } = require('./tools'), { UserAlreadyExistError, InvalidTokenError, VerifyPasswordError, UserNotFoundError, LoginError } = require('../error'); GET('/user', async ctx => { @@ -27,17 +27,20 @@ POST('/login', async ctx => { ctx.session.rememberme = rememberme; ctx.body = { udoc }; }); -POST('/logout', requirePriv(PRIV_USER_PROFILE), async ctx => { +POST('/logout', requirePerm(PERM_LOGGEDIN), async ctx => { ctx.session = { uid: 1 }; ctx.body = {}; }); -GET('/register/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { +let { GET: _GET, POST: _POST, MIDDLEWARE: _MIDDLEWARE } = CONTEXT(); +_MIDDLEWARE(requirePerm(PERM_REGISTER_USER)); + +_GET('/register/:code', async ctx => { let code = ctx.request.body.code; let { mail } = await token.get(code, token.TYPE_REGISTRATION); if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); ctx.body = { mail }; }); -GET('/register/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { +_GET('/register/:code', async ctx => { let { code, password, verify_password, uname } = ctx.request.body; let { mail } = await token.get(code, token.TYPE_REGISTRATION); if (!mail) throw new InvalidTokenError(token.TYPE_REGISTRATION, code); @@ -50,7 +53,7 @@ GET('/register/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { }); if (options.smtp.user) { - POST('/register', requirePriv(PRIV_REGISTER_USER), limitRate('send_mail', 3600, 30), async ctx => { + _POST('/register', limitRate('send_mail', 3600, 30), async ctx => { let email = ctx.request.body.email; validator.check_mail(email); if (await user.get_by_mail(email)) throw new UserAlreadyExistError(email); @@ -59,7 +62,7 @@ if (options.smtp.user) { await mail.send_mail(email, 'Sign Up', 'user_register_mail', m); ctx.body = {}; }); - POST('/lostpass', requirePriv(PRIV_REGISTER_USER), limitRate('send_mail', 3600, 30), async ctx => { + POST('/lostpass', limitRate('send_mail', 3600, 30), async ctx => { let email = ctx.request.body.mail; validator.check_mail(email); let udoc = await user.getByEmail(email); @@ -73,14 +76,14 @@ if (options.smtp.user) { await mail.send_mail(email, 'Lost Password', 'user_lostpass_mail', m); ctx.body = {}; }); - GET('/lostpass/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + GET('/lostpass/:code', async ctx => { let code = ctx.params.code; let tdoc = await token.get(code, token.TYPE_LOSTPASS); if (!tdoc) throw new InvalidTokenError(token.TYPE_LOSTPASS, code); let udoc = await user.getById(tdoc.uid); ctx.body = { uname: udoc.uname }; }); - POST('/lostpass/:code', requirePriv(PRIV_REGISTER_USER), async ctx => { + POST('/lostpass/:code', async ctx => { let code = ctx.params.code; let password = ctx.request.body.password; let verify_password = ctx.request.body.verify_password; @@ -92,7 +95,7 @@ if (options.smtp.user) { ctx.redirect('/'); }); } else - POST('/register', requirePriv(PRIV_REGISTER_USER), async ctx => { + _POST('/register', async ctx => { let email = ctx.request.body.email; validator.check_mail(email); if (await user.get_by_mail(email)) throw new UserAlreadyExistError(email); diff --git a/hydro/install.js b/hydro/install.js index c5a20f0b8..4fe4564ff 100644 --- a/hydro/install.js +++ b/hydro/install.js @@ -1,9 +1,8 @@ const Mongo = require('mongodb'), - { pwhash } = require('./utils'), - priv = require('./privilege'), builtin = require('./model/builtin'), - options = require('./lib/options'); + { pwhash } = require('./utils'), + options = require('./options'); async function run() { let mongourl = 'mongodb://'; @@ -12,8 +11,7 @@ async function run() { let Database = await Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }); let db = Database.db(options.db.name); let coll_user = db.collection('user'); - let coll_domain = db.collection('domain'); - let coll_domain_user = db.collection('domain_user'); + let coll_role = db.collection('role'); let coll_blacklist = db.collection('blacklist'); async function createRootUser() { let salt = pwhash.salt(); @@ -29,15 +27,14 @@ async function run() { regip: '127.0.0.1', loginat: new Date(), loginip: '127.0.0.1', - priv: priv.PRIV_ALL, - gravatar: 'root@hydro' + gravatar: 'root@hydro', + role: 'admin' }); - await coll_domain_user.insertOne({ domainId: 'system', uid: -1, role: 'admin', joinAt: new Date() }); } await coll_user.createIndex('unameLower', { unique: true }); await coll_user.createIndex('emailLower', { sparse: true }); + await coll_role.insertMany(builtin.BUILTIN_ROLES); await coll_user.insertMany(builtin.BUILTIN_USERS); - await coll_domain.insertMany(builtin.BUILTIN_DOMAINS); await coll_blacklist.createIndex('expireAt', { expireAfterSeconds: 0 }); await createRootUser(); console.log('Installed'); diff --git a/hydro/interface.ts b/hydro/interface.ts index beee4c3fb..a705e6faf 100644 --- a/hydro/interface.ts +++ b/hydro/interface.ts @@ -6,17 +6,13 @@ export class User { unameLower: string salt: string hash: string - priv: number - domainId: string = 'system' displayName: string = '' nAccept: number = 0 nSubmit: number = 0 perm: string = '0' - constructor(user, domainUser) { } - hasPriv(priv) { } + constructor(user) { } hasPerm(perm) { } checkPassword(password) { } - async joinDomain(role, joinAt) { } } export interface TestCase { time: number, @@ -26,7 +22,6 @@ export interface TestCase { } export interface Record { _id: import('bson').ObjectID, - domainId: string, pid: string | boolean, creator: number, lang: string, @@ -43,7 +38,6 @@ export interface Record { } export interface Problem { _id: number | string, - domainId: string, title: string, content: string, timeLimit: number, diff --git a/hydro/lib/i18n.js b/hydro/lib/i18n.js new file mode 100644 index 000000000..28f461596 --- /dev/null +++ b/hydro/lib/i18n.js @@ -0,0 +1,37 @@ +const + fs = require('fs'), + yaml = require('js-yaml'); + +let locales = {}; + +String.prototype.format = function (args) { + var result = this; + if (arguments.length > 0) { + if (arguments.length == 1 && typeof (args) == 'object') { + for (var key in args) + if (args[key] != undefined) { + let reg = new RegExp('({' + key + '})', 'g'); + result = result.replace(reg, args[key]); + } + } else for (var i = 0; i < arguments.length; i++) + if (arguments[i] != undefined) { + let reg = new RegExp('({)' + i + '(})', 'g'); + result = result.replace(reg, arguments[i]); + } + } + return result; +}; +String.prototype.rawformat = function (object) { + let res = this.split('{@}'); + return [res[0], object, res[1]]; +}; +String.prototype.translate = function (language = 'zh-CN') { + if (locales[language]) return this; + else return this; +}; + +module.exports = function load(file, language) { + if (!locales[language]) locales[language] = {}; + let content = fs.readFileSync(file).toString(); + Object.assign(locales[language], yaml.safeLoad(content)); +}; \ No newline at end of file diff --git a/hydro/lib/validator.js b/hydro/lib/validator.js index f3cc2f82d..f11326366 100644 --- a/hydro/lib/validator.js +++ b/hydro/lib/validator.js @@ -34,19 +34,8 @@ module.exports = { isPid, checkPid }; /* -domainId_RE = re.compile(r'[_A-Za-z][_0-9A-Za-z]{3,255}') ID_RE = re.compile(r'[^\\/\s\u3000]([^\\/\n\r]*[^\\/\s\u3000])?') ROLE_RE = re.compile(r'[_0-9A-Za-z]{1,256}') -DOMAIN_INVITATION_CODE_RE = re.compile(r'[0-9A-Za-z]{1,64}') - - -def is_domainId(s): - return bool(domainId_RE.fullmatch(s)) - - -def check_domainId(s): - if not is_domainId(s): - raise error.ValidationError('domainId') def is_id(s): diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js index 0a99dc400..1795ce2ae 100644 --- a/hydro/model/builtin.js +++ b/hydro/model/builtin.js @@ -1,6 +1,4 @@ -const - perm = require('../permission'), - priv = require('../privilege'); +const perm = require('../permission'); const BUILTIN_USERS = [ { _id: 0, @@ -11,12 +9,12 @@ const BUILTIN_USERS = [ salt: '', hash: '', gender: 'other', - regat: new Date(0), + regat: new Date(), regip: '127.0.0.1', - priv: priv.PRIV_NONE, gravatar: '', loginat: new Date(), - loginip: '127.0.0.1' + loginip: '127.0.0.1', + role: 'guest' }, { _id: 1, @@ -30,23 +28,155 @@ const BUILTIN_USERS = [ regip: '127.0.0.1', loginat: new Date(), loginip: '127.0.0.1', - priv: priv.PRIV_DEFAULT, - gravatar: 'guest@hydro' + gravatar: 'guest@hydro', + role: 'guest' } ]; -const BUILTIN_DOMAINS = [{ - _id: 'system', - owner: 0, - roles: { - guest: perm.PERM_BASIC, - default: perm.PERM_DEFAULT, - member: perm.PERM_DEFAULT, - admin: perm.PERM_ADMIN - }, - gravatar: '', - name: 'System', - bulletin: '' -}]; +const BUILTIN_ROLES = [ + { _id: 'guest', perm: perm.PERM_BASIC }, + { _id: 'default', perm: perm.PERM_DEFAULT }, + { _id: 'admin', perm: perm.PERM_ADMIN } +]; +const CATEGORIES = { + '动态规划': [ + 'LCS', + 'LIS', + '背包', + '单调性DP', + '环形DP', + '树形DP', + '状态压缩DP' + ], + '搜索': [ + '枚举', + '搜索与剪枝', + '启发式搜索', + 'DLX', + '双向搜索', + '折半搜索', + '记忆化搜索', + '模拟退火' + ], + '计算几何': [ + '半平面交', + '凸包', + '几何图形的交与并', + '旋转卡壳', + '点定位', + '坐标变换', + '离散化与扫描', + '反演', + 'Voronoi图', + '平面图的对偶图', + '三角剖分', + '梯形剖分', + '几何知识' + ], + '贪心': [], + '树结构': [ + '最近公共祖先', + '生成树', + 'DFS序列', + '树上倍增', + '树的分治', + '树链剖分', + 'Link-Cut-Tree' + ], + '图结构': [ + '平面图', + '二分图', + '二分图匹配', + '最短路', + '差分约束', + '拓扑排序', + '网络流', + '强连通分量', + '割点割边', + '欧拉回路', + '2-SAT' + ], + '数论': [ + '素数判定', + '欧几里得算法', + '不定方程', + '数位统计', + '解线性同余方程', + 'baby-step-giant-step', + 'Pell方程', + '大整数质因数分解', + '勾股方程', + '积性函数', + 'Fibonacci数列' + ], + '模拟': [], + '数据结构': [ + '栈', + '队列', + '链表', + '单调队列', + '并查集', + '堆', + '平衡树', + '线段树', + '树状数组', + '树套树', + '四分树', + '划分树', + '归并树', + 'k-d树', + '块状链表', + 'Hashing', + '函数式编程' + ], + '博弈论': [], + '字符串': [ + 'KMP', + '后缀数据结构', + 'Trie树', + 'AC自动机', + 'Manacher', + '表达式处理', + '最小表示法' + ], + '组合数学': [ + '生成函数', + '容斥原理', + '康托展开', + 'Catalan数列', + 'Stirling数', + '差分', + 'Polya定理' + ], + '线性代数': [ + '矩阵乘法', + '高斯消元', + '线性规划' + ], + '高精度': [ + 'FFT' + ], + '递推': [], + '概率论': [ + '随机化' + ], + 'NPC': [], + '其他': [ + '二分查找', + '三分查找', + '双指针扫描', + '分治', + '分块', + 'RMQ', + '快速幂', + '数学', + '排序', + '构造' + ] +}; +const FOOTER_EXTRA_HTMLS = []; +const VIEW_LANGS = [ + { code: 'zh-CN', name: 'zh-CN' } +]; module.exports = { - BUILTIN_USERS, BUILTIN_DOMAINS + BUILTIN_USERS, BUILTIN_ROLES, CATEGORIES, VIEW_LANGS, FOOTER_EXTRA_HTMLS }; \ No newline at end of file diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 3b59edadc..7e5697608 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -1,5 +1,4 @@ const - domain = require('./domain'), validator = require('../lib/validator'), { ValidationError, ContestNotFoundError } = require('../error'), RULE_HOMEWORK = require('../module/contest/homework'), @@ -18,37 +17,37 @@ const RULES = { module.exports = { RULES, - async add(domainId, title, content, owner, rule, + async add(title, content, owner, rule, beginAt = new Date(), endAt = new Date(), pids = [], data) { validator.checkTitle(title); validator.checkContent(content); if (!this.RULES[rule]) throw new ValidationError('rule'); if (beginAt >= endAt) throw new ValidationError('beginAt', 'endAt'); - Object.assign(data, { domainId, content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); + Object.assign(data, { content, owner, title, rule, beginAt, endAt, pids, attend: 0 }); this.RULES[rule].check(data); return await coll.insertOne(data); }, count: query => coll.find(query).count(), - async edit(domainId, cid, $set) { + async edit(cid, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkIntro($set.content); if ($set.rule) if (!this.RULES[$set.rule]) throw new ValidationError('rule'); if ($set.beginAt && $set.endAt) if ($set.beginAt >= $set.endAt) throw new ValidationError('beginAt', 'endAt'); - let cdoc = await coll.findOne({ domainId, cid }); - if (!cdoc) throw new ContestNotFoundError(domainId, cid); + let cdoc = await coll.findOne({ cid }); + if (!cdoc) throw new ContestNotFoundError(cid); this.RULES[$set.rule || cdoc.rule].check(Object.assign(cdoc, $set)); - await coll.findOneAndUpdate({ domainId, cid }, { $set }); + await coll.findOneAndUpdate({ cid }, { $set }); return cdoc; }, - async get(domainId, cid) { - let tdoc = await coll.findOne({ domainId, cid }); - if (!tdoc) throw new ContestNotFoundError(domainId, cid); + async get( cid) { + let tdoc = await coll.findOne({ cid }); + if (!tdoc) throw new ContestNotFoundError(cid); return tdoc; }, - get_multi: (domainId, query, fields) => coll.find(Object.assign({ domainId }, query), { fields }), - get_multi_status: (query, fields) => coll.find(query, { fields }), + get_multi: (query, fields) => coll.find(query, { fields }), + get_multi_status: (query, fields) => coll_status.find(query, { fields }), async get_random_id(query) { let pdocs = coll.find(query); let pcount = await pdocs.count(); @@ -57,8 +56,8 @@ module.exports = { return pdoc.pid; } else return null; }, - get_status: (domainId, cid, uid, fields) => coll_status.findOne({ domainId, cid, uid }, { fields }), - set_status: (domainId, cid, uid, $set) => coll_status.findOneAndUpdate({ domainId, cid, uid }, { $set }) + get_status: (cid, uid, fields) => coll_status.findOne({ cid, uid }, { fields }), + set_status: (cid, uid, $set) => coll_status.findOneAndUpdate({ cid, uid }, { $set }) }; /* @@ -321,7 +320,7 @@ class ContestCommonOperationMixin(object): fields={'doc_id': 1}) \ .sort('doc_id', 1) \ .to_list() - exist_pids = [pdoc['doc_id'] for pdoc in pdocs] + exist_pids = [pdoc['_id'] for pdoc in pdocs] if len(pids) != len(exist_pids): for pid in pids: if pid not in exist_pids: diff --git a/hydro/model/domain.js b/hydro/model/domain.js deleted file mode 100644 index dbc4444d2..000000000 --- a/hydro/model/domain.js +++ /dev/null @@ -1,315 +0,0 @@ -const - validator = require('../utils').validator, - user = require('./user'), - { DomainAlreadyExistError, DomainNotFoundError } = require('../error'), - db = require('../service/db.js'), - coll = db.collection('domain'); - -module.exports = { - /** - * @param {string} domainId - * @param {int} owner - * @param {roles} roles - * @param {str} name - * @param {str} gravatar - * @param {str} bulletin - */ - async add(domainId, owner, roles, name, gravatar, bulletin = '') { - validator.checkDomainId(domainId); - validator.checkName(name); - validator.checkBulletin(bulletin); - try { - let result = await coll.insertOne({ - _id: domainId, pending: true, owner, roles, name, gravatar, bulletin - }); - domainId = result.insertedId; - } catch (e) { - throw new DomainAlreadyExistError(domainId); - } - await (await user.getById(owner, domainId)).joinDomain('root'); - await coll.updateOne({ _id: domainId }, { $unset: { pending: '' } }); - return domainId; - }, - /** - * @param {string} domainId - * @param {object} fields - */ - async get(domainId, fields) { - let ddoc = await coll.findOne({ _id: domainId }, fields); - if (!ddoc) throw new DomainNotFoundError(domainId); - return ddoc; - }, - /** - * Increments the problem ID counter. - * @param {string} domainId - * @returns {number} Integer value before increment. - */ - async incPidCounter(domainId) { - await coll.updateOne({ _id: domainId, pidCounter: { $exists: false } }, - { $set: { pidCounter: 1000 } }); - let doc = await coll.findOneAndUpdate({ _id: domainId }, { $inc: { pidCounter: 1 } }); - return doc.value.pidCounter; - }, - async incUser(domainId, uid, $inc) { - let res = await coll.findOneAndUpdate({ domainId, uid }, { $inc }, { upsert: true }); - return res.value; - } -}; -/* - -async def add_continue(domainId: str, ensure_owner_uid: int=None): - ddoc = await get(domainId) - if 'pending' not in ddoc: - raise error.DomainNotFoundError(domainId) - owner_uid = ddoc['owner_uid'] - if ensure_owner_uid != None and owner_uid != ensure_owner_uid: - raise error.DomainNotFoundError(domainId) - try: - await add_user_role(domainId, owner_uid, builtin.ROLE_ROOT) - except error.UserAlreadyDomainMemberError: - pass - coll = db.coll('domain') - await coll.update_one({'_id': domainId}, - {'$unset': {'pending': ''}}) - -def get_multi(*, fields=None, **kwargs): - coll = db.coll('domain') - return coll.find(kwargs, fields) - - -@argmethod.wrap -async def get_list(*, fields=None, limit: int=None, **kwargs): - coll = db.coll('domain') - return await coll.find(kwargs, fields).limit(limit).to_list(None) - - -def get_pending(**kwargs): - return get_multi(pending=True, **kwargs) - - -@argmethod.wrap -async def edit(domainId: str, **kwargs): - for domain in builtin.DOMAINS: - if domain['_id'] == domainId: - raise error.BuiltinDomainError(domainId) - coll = db.coll('domain') - if 'owner_uid' in kwargs: - del kwargs['owner_uid'] - if 'name' in kwargs: - validator.check_name(kwargs['name']) - # TODO(twd2): check kwargs - return await coll.find_one_and_update(filter={'_id': domainId}, - update={'$set': {**kwargs}}, - return_document=ReturnDocument.AFTER) - - -async def unset(domainId, fields): - # TODO(twd2): check fields - coll = db.coll('domain') - return await coll.find_one_and_update(filter={'_id': domainId}, - update={'$unset': dict((f, '') for f in set(fields))}, - return_document=ReturnDocument.AFTER) - - -@argmethod.wrap -async def set_role(domainId: str, role: str, perm: int): - return await set_roles(domainId, {role: perm}) - - -@argmethod.wrap -async def set_roles(domainId: str, roles): - roles = {str(role): int(perm) for role, perm in roles.items()} - update = {} - for role in roles: - validator.check_role(role) - if role in builtin.BUILTIN_ROLE_DESCRIPTORS: - if not builtin.BUILTIN_ROLE_DESCRIPTORS[role].modifiable: - raise error.ModifyBuiltinRoleError(domainId, role) - update['roles.{0}'.format(role)] = roles[role] - for domain in builtin.DOMAINS: - if domain['_id'] == domainId: - raise error.BuiltinDomainError(domainId) - coll = db.coll('domain') - return await coll.find_one_and_update(filter={'_id': domainId}, - update={'$set': update}, - return_document=ReturnDocument.AFTER) - - -@argmethod.wrap -async def delete_role(domainId: str, role: str): - return await delete_roles(domainId, [role]) - - -async def delete_roles(domainId: str, roles): - roles = list(set(roles)) - for role in roles: - validator.check_role(role) - if role in builtin.BUILTIN_ROLE_DESCRIPTORS: - raise error.ModifyBuiltinRoleError(domainId, role) - for domain in builtin.DOMAINS: - if domain['_id'] == domainId: - raise error.BuiltinDomainError(domainId) - user_coll = db.coll('domain.user') - await user_coll.update_many({'domainId': domainId, 'role': {'$in': list(roles)}}, - {'$unset': {'role': ''}}) - coll = db.coll('domain') - return await coll.find_one_and_update(filter={'_id': domainId}, - update={'$unset': dict(('roles.{0}'.format(role), '') - for role in roles)}, - return_document=ReturnDocument.AFTER) - - -@argmethod.wrap -async def transfer(domainId: str, old_owner_uid: int, new_owner_uid: int): - for domain in builtin.DOMAINS: - if domain['_id'] == domainId: - raise error.BuiltinDomainError(domainId) - coll = db.coll('domain') - return await coll.find_one_and_update(filter={'_id': domainId, 'owner_uid': old_owner_uid}, - update={'$set': {'owner_uid': new_owner_uid}}, - return_document=ReturnDocument.AFTER) - - -async def set_user(domainId, uid, **kwargs): - coll = db.coll('domain.user') - return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid}, - update={'$set': kwargs}, - upsert=True, - return_document=ReturnDocument.AFTER) - - -async def unset_user(domainId, uid, fields): - coll = db.coll('domain.user') - return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid}, - update={'$unset': dict((f, '') for f in set(fields))}, - upsert=True, - return_document=ReturnDocument.AFTER) - - -async def set_users(domainId, uids, **kwargs): - coll = db.coll('domain.user') - await coll.update_many({'domainId': domainId, 'uid': {'$in': list(set(uids))}}, - {'$set': kwargs}, - upsert=False) - - -async def unset_users(domainId, uids, fields): - coll = db.coll('domain.user') - await coll.update_many({'domainId': domainId, 'uid': {'$in': list(set(uids))}}, - {'$unset': dict((f, '') for f in set(fields))}, - upsert=True) - - - -@argmethod.wrap -async def set_user_role(domainId: str, uid: int, role: str): - validator.check_role(role) - # use set_users to utilize "upsert=False" - return await set_users(domainId, [uid], role=role) - - -@argmethod.wrap -async def unset_user_role(domainId: str, uid: int): - return await unset_user(domainId, uid, ['role']) - - -async def set_users_role(domainId: str, uids, role: str): - validator.check_role(role) - await set_users(domainId, uids, role=role) - - -async def unset_users_role(domainId: str, uids): - await unset_users(domainId, uids, ['role']) - - -async def inc_user_usage(domainId: str, uid: int, usage_field: str, usage: int, quota: int): - coll = db.coll('domain.user') - try: - return await coll.find_one_and_update(filter={'domainId': domainId, 'uid': uid, - usage_field: {'$not': {'$gte': quota - usage}}}, - update={'$inc': {usage_field: usage}}, - upsert=True, - return_document=ReturnDocument.AFTER) - except errors.DuplicateKeyError: - raise error.UsageExceededError(domainId, uid, usage_field, usage, quota) - - -def get_multi_user(*, fields=None, **kwargs): - coll = db.coll('domain.user') - return coll.find(kwargs, fields) - - -async def get_dict_user_by_uid(domainId, uids, *, fields=None): - result = dict() - async for dudoc in get_multi_user( - domainId=domainId, uid={'$in': list(set(uids))}, fields=fields): - result[dudoc['uid']] = dudoc - return result - - -async def get_dict_user_by_domainId(uid, *, fields=None): - result = dict() - async for dudoc in get_multi_user(uid=uid, fields=fields): - result[dudoc['domainId']] = dudoc - return result - - -def get_all_roles(ddoc): - builtin_roles = {role: rd.default_permission for role, rd in builtin.BUILTIN_ROLE_DESCRIPTORS.items()} - domain_roles = ddoc['roles'] - return {**builtin_roles, **domain_roles} - - -def get_join_settings(ddoc, now): - if 'join' not in ddoc: - return None - join_settings = ddoc['join'] - if not join_settings: - return None - if join_settings['method'] == constant.domain.JOIN_METHOD_NONE: - return None - if join_settings['role'] not in ddoc['roles']: - return None - if join_settings['expire'] != None and join_settings['expire'] < now: - return None - return join_settings - - -@argmethod.wrap -async def get_prefix_search(prefix: str, fields={}, limit: int=50): - prefix_lower = prefix.lower() - builtin_ddocs = [] - for domain in builtin.DOMAINS: - if domain['_id'].lower().startswith(prefix_lower) or \ - domain['name'].lower().startswith(prefix_lower): - builtin_ddocs.append(domain) - regex = r'\A\Q{0}\E'.format(prefix.replace(r'\E', r'\E\\E\Q')) - coll = db.coll('domain') - ddocs = await coll.find({'$or': [{'_id': {'$regex': regex}}, - {'name': {'$regex': regex}}]}, - projection=fields) \ - .limit(limit) \ - .to_list() - return builtin_ddocs + ddocs - - -@argmethod.wrap -async def ensure_indexes(): - coll = db.coll('domain') - await coll.create_index('owner_uid') - await coll.create_index('name') - user_coll = db.coll('domain.user') - await user_coll.create_index('uid') - await user_coll.create_index([('domainId', 1), - ('uid', 1)], unique=True) - await user_coll.create_index([('domainId', 1), - ('role', 1)], sparse=True) - await user_coll.create_index([('domainId', 1), - ('rp', -1)]) - await user_coll.create_index([('domainId', 1), - ('rank', 1)]) - - -if __name__ == '__main__': - argmethod.invoke_by_args() -*/ \ No newline at end of file diff --git a/hydro/model/problem.js b/hydro/model/problem.js index a24dbc799..7155d344a 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -6,7 +6,6 @@ const coll_status = db.collection('problem.status'); /** - * @param {string} domainId * @param {string} title * @param {string} content * @param {number} owner @@ -17,7 +16,6 @@ const * @param {boolean} hidden */ async function add({ - domainId, title, content, owner, @@ -30,7 +28,6 @@ async function add({ validator.checkTitle(title); validator.checkContent(content); await coll.insertOne({ - domainId, content, owner, pid, @@ -44,24 +41,24 @@ async function add({ }); return pid; } -async function get({ domainId, pid, uid }) { +async function get({ pid, uid }) { pid = parseInt(pid) || pid; - let pdoc = await coll.findOne({ domainId, pid }); - if (!pdoc) throw new ProblemNotFoundError(domainId, pid); + let pdoc = await coll.findOne({ pid }); + if (!pdoc) throw new ProblemNotFoundError(pid); pdoc.psdoc = uid ? - await coll_status.findOne({ domainId, pid, uid }) : + await coll_status.findOne({ pid, uid }) : null; return pdoc; } async function getMany(query, sort, page, limit) { return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); } -async function edit(domainId, pid, $set) { +async function edit(pid, $set) { if ($set.title) validator.checkTitle($set.title); if ($set.content) validator.checkContent($set.content); - await coll.findOneAndUpdate({ domainId, pid }, { $set }); - let pdoc = await coll.findOne({ domainId, pid }); - if (!pdoc) throw new ProblemNotFoundError(domainId, pid); + await coll.findOneAndUpdate({ pid }, { $set }); + let pdoc = await coll.findOne({ pid }); + if (!pdoc) throw new ProblemNotFoundError(pid); return pdoc; } async function count(query) { diff --git a/hydro/model/record.js b/hydro/model/record.js index 8c64e730f..4840d37b0 100644 --- a/hydro/model/record.js +++ b/hydro/model/record.js @@ -36,10 +36,10 @@ async function get(rid) { async function getMany(query, sort, page, limit) { return await coll.find(query).sort(sort).skip((page - 1) * limit).limit(limit).toArray(); } -async function update(domainId, rid, $set) { - await coll.findOneAndUpdate({ domainId, _id: rid }, { $set }); - let rdoc = await coll.findOne({ domainId, _id: rid }); - if (!rdoc) throw new RecordNotFoundError(domainId, rid); +async function update(rid, $set) { + await coll.findOneAndUpdate({ _id: rid }, { $set }); + let rdoc = await coll.findOne({ _id: rid }); + if (!rdoc) throw new RecordNotFoundError(rid); return rdoc; } async function count(query) { diff --git a/hydro/model/system.js b/hydro/model/system.js index 81093f4f3..6f930bd7a 100644 --- a/hydro/model/system.js +++ b/hydro/model/system.js @@ -12,10 +12,17 @@ async function update(_id, update, config) { } /** * Increments the user counter. - * @returns {Number} Integer value after increment. + * @returns {number} Integer value after increment. */ async function incUserCounter() { - return this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); + return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); +} +/** + * Increments the problem ID counter. + * @returns {number} Integer value before increment. + */ +async function incPidCounter() { + return await this.update('userCounter', { $inc: { value: 1 } }, { upsert: true }); } async function acquireLock(name) { let value = Math.floor(Math.random() * 0xFFFFFFFF); @@ -35,6 +42,7 @@ async function releaseLockAnyway(name) { module.exports = { get, update, + incPidCounter, incUserCounter, acquireLock, releaseLock, diff --git a/hydro/model/training.js b/hydro/model/training.js index 384bc6bcd..422923dcd 100644 --- a/hydro/model/training.js +++ b/hydro/model/training.js @@ -1,5 +1,4 @@ const - domain = require('./domain'), validator = require('../lib/validator'), { ValidationError, TrainingNotFoundError } = require('../error'), db = require('../service/db.js'), @@ -11,7 +10,7 @@ module.exports = { SETTING_DIFFICULTY_ADMIN: 1, SETTING_DIFFICULTY_AVERAGE: 2, - async add(domainId, title, content, owner_uid, dag = [], desc = '') { + async add(title, content, owner_uid, dag = [], desc = '') { validator.check_title(title); validator.check_intro(content); validator.check_description(desc); @@ -20,12 +19,12 @@ module.exports = { if (nid >= node._id) throw new ValidationError('dag'); return await coll.insertOne({ - domainId, content, owner_uid, + content, owner_uid, dag, title, desc, enroll: 0 }); }, - count: (domainId, query) => coll.find(Object.assign({ domainId }, query)).count(), - async edit(domainId, tid, $set) { + count: (query) => coll.find(query).count(), + async edit(tid, $set) { if ($set.title) validator.check_title($set.title); if ($set.content) validator.check_intro($set.content); if ($set.desc) validator.check_description($set.desc); @@ -33,28 +32,20 @@ module.exports = { for (let node of $set.dag) for (let nid of node.require_nids) if (nid >= node._id) throw new ValidationError('dag'); - await coll.findOneAndUpdate({ domainId, tid }, { $set }); - let tdoc = await coll.findOne({ domainId, tid }); - if (!tdoc) throw new TrainingNotFoundError(domainId, tid); + await coll.findOneAndUpdate({ tid }, { $set }); + let tdoc = await coll.findOne({ tid }); + if (!tdoc) throw new TrainingNotFoundError(tid); return tdoc; }, - async get(domainId, tid) { - let tdoc = await coll.findOne({ domainId, tid }); - if (!tdoc) throw new TrainingNotFoundError(domainId, tid); + async get(tid) { + let tdoc = await coll.findOne({ tid }); + if (!tdoc) throw new TrainingNotFoundError(tid); return tdoc; }, - get_multi: (domainId, query) => coll.find(Object.assign({ domainId }, query)), - get_multi_status: (query) => coll.find(query), - async get_random_id(query) { - let pdocs = coll.find(query); - let pcount = await pdocs.count(); - if (pcount) { - let pdoc = await pdocs.skip(Math.floor(Math.random() * pcount)).limit(1).toArray()[0]; - return pdoc.pid; - } else return null; - }, - get_status: (domainId, tid, uid) => coll_status.findOne({ domainId, tid, uid }), - set_status: (domainId, tid, uid, $set) => coll_status.findOneAndUpdate({ domainId, tid, uid }, { $set }) + get_multi: (query) => coll.find( query), + get_multi_status: (query) => coll_status.find(query), + get_status: (tid, uid) => coll_status.findOne({ tid, uid }), + set_status: (tid, uid, $set) => coll_status.findOneAndUpdate({ tid, uid }, { $set }) }; diff --git a/hydro/model/user.js b/hydro/model/user.js index 648557329..ca1df64d5 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -1,28 +1,19 @@ const - { UserNotFoundError, UserAlreadyExistError, DomainNotFoundError, - UserAlreadyDomainMemberError } = require('../error'), + { UserNotFoundError, UserAlreadyExistError } = require('../error'), system = require('./system'), - domain = require('./domain'), { pwhash, validator } = require('../utils'), db = require('../service/db.js'), coll = db.collection('user'), - coll_domain = db.collection('domain_user'); + coll_role = db.collection('role'); class USER { - constructor(user, domainUser = {}) { - if (!user) throw new UserNotFoundError(); + constructor(user) { this._id = user._id; this.email = user.email; this.uname = user.uname; - this.priv = user.priv; this.salt = user.salt; this.hash = user.hash; - this.perm = domainUser.perm || '0'; - this.displayName = domainUser.displayName || null; - this.domainId = domainUser.domainId || 'system'; - } - hasPriv(priv) { - return this.priv & priv; + this.perm = user.perm; } hasPerm(perm) { console.log('PERM', perm, this.perm); @@ -31,43 +22,28 @@ class USER { checkPassword(password) { return pwhash.check(password, this.salt, this.hash); } - /** - * @param {string} role - * @param {date} joinAt - */ - async joinDomain(role, joinAt) { - validator.checkRole(role); - try { - await coll_domain.updateOne( - { domainId: this.domainId, uid: this._id, role: { $exists: false } }, - { $set: { role, joinAt: joinAt || new Date() } }, - { upsert: true } - ); - } catch (e) { - throw new UserAlreadyDomainMemberError(this.domainId, this._id); - } - return true; - } } -async function getById(_id, domainId) { - let dudoc, udoc = await coll.findOne({ _id }); - if (domainId) { - let ddoc = await domain.get(domainId); - if (!ddoc) throw new DomainNotFoundError(domainId); - dudoc = (await coll_domain.findOne({ uid: _id, domainId })) || {}; - console.log(dudoc.role, ddoc.roles, { uid: _id, domainId }); - dudoc.perm = ddoc.roles[dudoc.role || 'default']; - } - return new USER(udoc, dudoc); +async function getById(_id) { + let udoc = await coll.findOne({ _id }); + if (!udoc) throw new UserNotFoundError(_id); + let role = await coll_role.findOne({ _id: udoc.role || 'default' }); + udoc.perm = role.perm; + return new USER(udoc); } async function getByUname(uname) { let unameLower = uname.trim().toLowerCase(); let udoc = await coll.findOne({ unameLower }); + if (!udoc) throw new UserNotFoundError(uname); + let role = await coll_role.findOne({ _id: udoc.role || 'default' }); + udoc.perm = role.perm; return new USER(udoc); } async function getByEmail(email) { let emailLower = email.trim().toLowerCase(); let udoc = await coll.findOne({ emailLower }); + if (!udoc) throw new UserNotFoundError(email); + let role = await coll_role.findOne({ _id: udoc.role || 'default' }); + udoc.perm = role.perm; return new USER(udoc); } async function setPassword(uid, password) { @@ -84,10 +60,6 @@ async function setEmail(uid, email) { function setById(uid, args) { coll.findOneAndUpdate({ _id: uid }, { $set: args }); } - -function setPriv(uid, priv) { - setById(uid, { priv }); -} async function changePassword(uid, currentPassword, newPassword) { validator.checkPassword(newPassword); let udoc = await getById(uid); @@ -133,5 +105,5 @@ module.exports = { changePassword, create, getByEmail, getById, getByUname, getMany, setById, setEmail, - setPassword, setPriv + setPassword }; \ No newline at end of file diff --git a/hydro/options.js b/hydro/options.js index c4358ebe9..3c00a308f 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -12,7 +12,9 @@ let options = { username: '', password: '' }, - mq: '', + template: { + path: path.join(__dirname, '..', 'templates') + }, listen: { host: '127.0.0.1', port: 8888, diff --git a/hydro/permission.js b/hydro/permission.js index 4214d0c68..6e04a7553 100644 --- a/hydro/permission.js +++ b/hydro/permission.js @@ -1,6 +1,8 @@ let permissions = { PERM_NONE: '0', PERM_ALL: '-', + PERM_LOGGEDIN: '~', + PERM_REGISTER_USER: '+', PERM_VIEW: '1', PERM_EDIT_PERM: '2', PERM_MOD_BADGE: '3', @@ -38,7 +40,7 @@ let permissions = { PERM_EDIT_CONTEST: 'Z', PERM_VIEW_TRAINING: 'a', PERM_CREATE_TRAINING: 'b', - PERM_EDIT_TRAINING: 'c' + PERM_EDIT_TRAINING: 'c', }; permissions.PERM_BASIC = permissions.PERM_VIEW + @@ -46,7 +48,8 @@ permissions.PERM_BASIC = permissions.PERM_VIEW_PROBLEM_SOLUTION + permissions.PERM_VIEW_DISCUSSION + permissions.PERM_VIEW_CONTEST + - permissions.PERM_VIEW_CONTEST_SCOREBOARD; + permissions.PERM_VIEW_CONTEST_SCOREBOARD + + permissions.PERM_REGISTER_USER; permissions.PERM_DEFAULT = permissions.PERM_VIEW + permissions.PERM_VIEW_PROBLEM + @@ -62,6 +65,7 @@ permissions.PERM_DEFAULT = permissions.PERM_VIEW_CONTEST_SCOREBOARD + permissions.PERM_ATTEND_CONTEST + permissions.PERM_VIEW_TRAINING + - permissions.PERM_CREATE_TRAINING; + permissions.PERM_CREATE_TRAINING + + permissions.PERM_LOGGEDIN; permissions.PERM_ADMIN = permissions.PERM_ALL; module.exports = permissions; diff --git a/hydro/privilege.js b/hydro/privilege.js deleted file mode 100644 index 2f39dcc45..000000000 --- a/hydro/privilege.js +++ /dev/null @@ -1,32 +0,0 @@ -let privileges = { - PRIV_NONE: 0, - PRIV_ALL: -1, - PRIV_SET_PRIV: 1 << 0, - PRIV_SET_PERM: 1 << 1, - PRIV_USER_PROFILE: 1 << 2, - PRIV_REGISTER_USER: 1 << 3, - PRIV_READ_PROBLEM_DATA: 1 << 4, - PRIV_READ_PRETEST_DATA: 1 << 5, - PRIV_READ_PRETEST_DATA_SELF: 1 << 6, - PRIV_READ_RECORD_CODE: 1 << 7, - PRIV_VIEW_HIDDEN_RECORD: 1 << 8, - PRIV_WRITE_RECORD: 1 << 9, - PRIV_CREATE_DOMAIN: 1 << 10, - PRIV_VIEW_ALL_DOMAIN: 1 << 11, - PRIV_MANAGE_ALL_DOMAIN: 1 << 12, - PRIV_REJUDGE: 1 << 13, - PRIV_VIEW_USER_SECRET: 1 << 14, - PRIV_VIEW_JUDGE_STATISTICS: 1 << 15, - PRIV_CREATE_FILE: 1 << 16, - PRIV_UNLIMITED_QUOTA: 1 << 17, - PRIV_DELETE_FILE: 1 << 18, - PRIV_DELETE_FILE_SELF: 1 << 19 -}; -privileges.PRIV_DEFAULT = - privileges.PRIV_USER_PROFILE | privileges.PRIV_CREATE_DOMAIN - | privileges.PRIV_CREATE_FILE | privileges.PRIV_DELETE_FILE_SELF; -privileges.PRIV_JUDGE = - privileges.PRIV_USER_PROFILE | privileges.PRIV_VIEW_ALL_DOMAIN - | privileges.PRIV_READ_PROBLEM_DATA | privileges.PRIV_READ_PRETEST_DATA - | privileges.PRIV_READ_RECORD_CODE | privileges.PRIV_WRITE_RECORD; -module.exports = privileges; \ No newline at end of file diff --git a/hydro/service/queue.js b/hydro/service/queue.js index 9b9ee6ea6..09514862f 100644 --- a/hydro/service/queue.js +++ b/hydro/service/queue.js @@ -1,53 +1,28 @@ -const options = require('../options'); -if (options.mq) { - const amqplib = require('amqplib'); - /** - * @type {import('amqplib').Connection} - */ - let conn = null; - let ch = null; - - module.exports = { - async connect() { - conn = await amqplib.connect(options.mq.url); - ch = conn.createChannel(); - }, - async assertQueue(queue) { - return await ch.assertQueue(queue); - }, - async publish(queue, data) { - return await ch.sendToQueue(queue, Buffer.from(data)); - }, - async consume(queue, callback) { - ch.consume(queue, function (msg) { - if (msg !== null) { - callback(msg.content.toString(), () => { ch.ack(msg); }); - ch.ack(msg); - } - }); - } - }; -} else { - let q = {}; - module.exports = { - async connect() { }, - async assertQueue(queue) { - q[queue] = q[queue] || []; - }, - async publish(queue, data) { - q[queue].push(data); - }, - async consume(queue, callback) { - while (1) { - if (q[queue].length) { - let data = q[queue][0]; - q[queue].splice(0, 1); - callback(data, () => { }); - } +let q = {}; +module.exports = { + async connect() { }, + async assert(queue) { + q[queue] = q[queue] || []; + }, + async push(queue, data) { + q[queue].push(data); + }, + async get(queue, wait = true) { + if (wait) { + while (!q[queue].length) await new Promise(resolve => { - setTimeout(resolve, 500); + setTimeout(resolve, 100); }); + let data = q[queue][0]; + q[queue].splice(0, 1); + return data; + } else { + if (!q[queue].length) return null; + else { + let data = q[queue][0]; + q[queue].splice(0, 1); + return data; } } - }; -} \ No newline at end of file + } +}; diff --git a/hydro/service/server.js b/hydro/service/server.js index c1a930939..0e137dbb8 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -1,12 +1,50 @@ +const + Koa = require('koa'), + morgan = require('koa-morgan'), + Body = require('koa-body'), + Router = require('koa-router'), + SockJS = require('sockjs'); + const options = require('../options'); + let http = options.listen.https ? require('https') : require('http'); -let app = new (require('koa'))(); +let app = new Koa(); let server = http.createServer(app.callback()); app.keys = options.session.keys; -app.use(require('koa-morgan')(':method :url :status :res[content-length] - :response-time ms')); -app.use(require('koa-body')()); -let router = (new require('koa-router'))(); -const sockjs = require('sockjs'); +app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); +app.use(Body()); + +let router = new Router(); +let contexts = []; +function CONTEXT() { + this.router = new Router(); + /** + * @param {import('koa').Middleware} middleware + */ + function MIDDLEWARE(middleware) { + router.use(middleware); + } + /** + * @param {string} route + * @param {...import('koa').Middleware} handler + */ + function GET(route, ...handler) { + router.get(route, ...handler); + } + /** + * @param {string} route + * @param {...import('koa').Middleware} handler + */ + function POST(route, ...handler) { + router.post(route, ...handler); + } + this.MIDDLEWARE = MIDDLEWARE; + this.GET = GET; + this.POST = POST; + contexts.push(this); + return this; +} + /** * @param {import('koa').Middleware} middleware */ @@ -36,16 +74,19 @@ function POST(route, ...handler) { * @param {SockJSHandler} handler */ function SOCKET(prefix, handler) { - const sock = sockjs.createServer({ prefix }); + const sock = SockJS.createServer({ prefix }); sock.on('connection', handler); sock.installHandlers(server); } +exports.CONTEXT = CONTEXT; exports.MIDDLEWARE = MIDDLEWARE; exports.GET = GET; exports.POST = POST; exports.SOCKET = SOCKET; exports.start = function start() { app.use(router.routes()).use(router.allowedMethods()); + for (let c of contexts) + app.use(c.router.routes()).use(c.router.allowedMethods()); app.listen(options.listen.port); console.log('Server listening at: %s', options.listen.port); -}; \ No newline at end of file +}; diff --git a/hydro/uninstall.js b/hydro/uninstall.js index 89a35f23b..7d6c78067 100644 --- a/hydro/uninstall.js +++ b/hydro/uninstall.js @@ -1,6 +1,6 @@ const Mongo = require('mongodb'), - options = require('./lib/options'); + options = require('./options'); async function run() { let mongourl = 'mongodb://'; diff --git a/package.json b/package.json index 0d2d8e4c0..aa708227c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "author": "masnn", "license": "MIT", "dependencies": { - "amqplib": "^0.5.5", "axios": "^0.19.0", "bson": "^4.0.2", "js-yaml": "^3.13.1", @@ -14,9 +13,11 @@ "koa-body": "^4.1.1", "koa-morgan": "^1.0.1", "koa-router": "^7.4.0", + "koa-static": "^5.0.0", "lodash": "^4.17.15", "mongodb": "^3.3.4", "nodemailer": "^6.3.1", + "nunjucks": "^3.2.1", "sockjs": "^0.3.20" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index a8063384c..a1350591e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.12.14.tgz#1c1d6e3c75dba466e0326948d56e8bd72a1903d2" integrity sha512-u/SJDyXwuihpwjXy7hOOghagLEV1KdAST6syfnOk6QZAMzZuWZqXy5aYYZbh8Jdpd4escVFP0MvftHNDb9pruA== +a-sync-waterfall@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz#75b6b6aa72598b497a125e7a2770f14f4c8a1fa7" + integrity sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA== + accepts@^1.3.5: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -64,18 +69,6 @@ ajv@^6.10.0, ajv@^6.10.2: json-schema-traverse "^0.4.1" uri-js "^4.2.2" -amqplib@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/amqplib/-/amqplib-0.5.5.tgz#698f0cb577e0591954a90572fcb3b8998a76fd40" - integrity sha512-sWx1hbfHbyKMw6bXOK2k6+lHL8TESWxjAx5hG8fBtT7wcxoXNIsFxZMnFyBjxt3yL14vn7WqBDe5U6BGOadtLg== - dependencies: - bitsyntax "~0.1.0" - bluebird "^3.5.2" - buffer-more-ints "~1.0.0" - readable-stream "1.x >=1.1.9" - safe-buffer "~5.1.2" - url-parse "~1.4.3" - ansi-escapes@^4.2.1: version "4.3.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.0.tgz#a4ce2b33d6b214b7950d8595c212f12ac9cc569d" @@ -100,11 +93,19 @@ ansi-styles@^3.2.0, ansi-styles@^3.2.1: dependencies: color-convert "^1.9.0" -any-promise@^1.1.0: +any-promise@^1.0.0, any-promise@^1.1.0: version "1.3.0" resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" integrity sha1-q8av7tzqUugJzcA3au0845Y10X8= +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -112,6 +113,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +asap@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY= + astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" @@ -142,19 +148,10 @@ basic-auth@~2.0.0: dependencies: safe-buffer "5.1.2" -bitsyntax@~0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/bitsyntax/-/bitsyntax-0.1.0.tgz#b0c59acef03505de5a2ed62a2f763c56ae1d6205" - integrity sha512-ikAdCnrloKmFOugAfxWws89/fPc+nw0OOG1IzIE72uSOg/A3cYptKCjSUhDTuj7fhsJtzkzlv7l3b8PzRHLN0Q== - dependencies: - buffer-more-ints "~1.0.0" - debug "~2.6.9" - safe-buffer "~5.1.2" - -bluebird@^3.5.2: - version "3.7.2" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" - integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== brace-expansion@^1.1.7: version "1.1.11" @@ -164,6 +161,13 @@ brace-expansion@^1.1.7: balanced-match "^1.0.0" concat-map "0.0.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + bson@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/bson/-/bson-1.1.3.tgz#aa82cb91f9a453aaa060d6209d0675114a8154d3" @@ -177,11 +181,6 @@ bson@^4.0.2: buffer "^5.1.0" long "^4.0.0" -buffer-more-ints@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz#ef4f8e2dddbad429ed3828a9c55d44f05c611422" - integrity sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg== - buffer@^5.1.0: version "5.4.3" resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.4.3.tgz#3fbc9c69eb713d323e3fc1a895eee0710c072115" @@ -222,6 +221,21 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +chokidar@^3.3.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.3.1.tgz#c84e5b3d18d9a4d77558fef466b1bf16bbeb3450" + integrity sha512-4QYCEWOcK3OJrxwvyyAOxFuhpvOVCYkr33LPfFNBjAD/w3sEzWsp2BUOkI4l9bHvWioAd0rc6NlHUOEaWkTeqg== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.3.0" + optionalDependencies: + fsevents "~2.1.2" + cli-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" @@ -261,6 +275,11 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= +commander@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.2.tgz#6837c3fb677ad9933d1cfba42dd14d5117d6b39e" + integrity sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow== + concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -286,11 +305,6 @@ cookies@~0.8.0: depd "~2.0.0" keygrip "~1.1.0" -core-util-is@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" - integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= - cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -302,7 +316,7 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" -debug@2.6.9, debug@~2.6.9: +debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== @@ -548,6 +562,13 @@ file-entry-cache@^5.0.1: dependencies: flat-cache "^2.0.1" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + flat-cache@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" @@ -584,6 +605,11 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= +fsevents@~2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" + integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== + functional-red-black-tree@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" @@ -596,6 +622,13 @@ glob-parent@^5.0.0: dependencies: is-glob "^4.0.1" +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@^7.1.3: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -637,6 +670,16 @@ http-errors@1.7.3, http-errors@^1.3.1, http-errors@^1.6.3, http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0= + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + "http-parser-js@>=0.4.0 <0.4.11": version "0.4.10" resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" @@ -685,11 +728,16 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@~2.0.1: +inherits@2, inherits@2.0.4: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4= + inquirer@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" @@ -709,6 +757,13 @@ inquirer@^7.0.0: strip-ansi "^5.1.0" through "^2.3.6" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^2.0.2: version "2.0.4" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" @@ -734,13 +789,18 @@ is-generator-function@^1.0.7: resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.7.tgz#d2132e529bb0000a7f80794d4bdf5cd5e5813522" integrity sha512-YZc5EwyO4f2kWCax7oegfuSr9mFz1ZvieNYBEjmukLxgXfBUbxAWGVF7GZf0zidYtoBl3WvC07YK0wT76a+Rtw== -is-glob@^4.0.0, is-glob@^4.0.1: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== dependencies: is-extglob "^2.1.1" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -834,6 +894,24 @@ koa-router@^7.4.0: path-to-regexp "^1.1.1" urijs "^1.19.0" +koa-send@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-send/-/koa-send-5.0.0.tgz#5e8441e07ef55737734d7ced25b842e50646e7eb" + integrity sha512-90ZotV7t0p3uN9sRwW2D484rAaKIsD8tAVtypw/aBU+ryfV+fR2xrcAwhI8Wl6WRkojLUs/cB9SBSCuIb+IanQ== + dependencies: + debug "^3.1.0" + http-errors "^1.6.3" + mz "^2.7.0" + resolve-path "^1.4.0" + +koa-static@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/koa-static/-/koa-static-5.0.0.tgz#5e92fc96b537ad5219f425319c95b64772776943" + integrity sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ== + dependencies: + debug "^3.1.0" + koa-send "^5.0.0" + koa@^2.11.0: version "2.11.0" resolved "https://registry.yarnpkg.com/koa/-/koa-2.11.0.tgz#fe5a51c46f566d27632dd5dc8fd5d7dd44f935a4" @@ -970,6 +1048,15 @@ mute-stream@0.0.8: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + natural-compare@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" @@ -990,6 +1077,27 @@ nodemailer@^6.3.1: resolved "https://registry.yarnpkg.com/nodemailer/-/nodemailer-6.3.1.tgz#2784beebac6b9f014c424c54dbdcc5c4d1221346" integrity sha512-j0BsSyaMlyadEDEypK/F+xlne2K5m6wzPYMXS/yxKI0s7jmT1kBx6GEKRVbZmyYfKOsjkeC/TiMVDJBI/w5gMQ== +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +nunjucks@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/nunjucks/-/nunjucks-3.2.1.tgz#f229539281e92c6ad25d8c578c9bdb41655caf83" + integrity sha512-LYlVuC1ZNSalQQkLNNPvcgPt2M9FTY9bs39mTCuFXtqh7jWbYzhDlmz2M6onPiXEhdZo+b9anRhc+uBGuJZ2bQ== + dependencies: + a-sync-waterfall "^1.0.0" + asap "^2.0.3" + commander "^3.0.2" + optionalDependencies: + chokidar "^3.3.0" + +object-assign@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= + on-finished@^2.3.0, on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" @@ -1050,7 +1158,7 @@ parseurl@^1.3.2: resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== -path-is-absolute@^1.0.0: +path-is-absolute@1.0.1, path-is-absolute@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= @@ -1067,6 +1175,11 @@ path-to-regexp@^1.1.1: dependencies: isarray "0.0.1" +picomatch@^2.0.4, picomatch@^2.0.7: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -1087,11 +1200,6 @@ qs@^6.4.0: resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.1.tgz#20082c65cb78223635ab1a9eaca8875a29bf8ec9" integrity sha512-Cxm7/SS/y/Z3MHWSxXb8lIFqgqBowP5JMlTUFyJN88y0SGQhVmZnqFK/PeuMX9LzUyWsqqhNxIyg0jlzq946yA== -querystringify@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" - integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== - raw-body@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.4.1.tgz#30ac82f98bb5ae8c152e67149dac8d55153b168c" @@ -1102,15 +1210,12 @@ raw-body@^2.2.0: iconv-lite "0.4.24" unpipe "1.0.0" -"readable-stream@1.x >=1.1.9": - version "1.1.14" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9" - integrity sha1-fPTFTvZI44EwhMY23SB54WbAgdk= +readdirp@~3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.3.0.tgz#984458d13a1e42e2e9f5841b129e162f369aff17" + integrity sha512-zz0pAkSPOXXm1viEwygWIPSPkcBYjW1xU5j/JBh5t9bGCJwa6f9+BJa6VaB2g+b55yVrmXzqkyLf4xaWYM0IkQ== dependencies: - core-util-is "~1.0.0" - inherits "~2.0.1" - isarray "0.0.1" - string_decoder "~0.10.x" + picomatch "^2.0.7" regexpp@^2.0.1: version "2.0.1" @@ -1125,11 +1230,6 @@ require_optional@^1.0.1: resolve-from "^2.0.0" semver "^5.1.0" -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8= - resolve-from@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57" @@ -1140,6 +1240,14 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-path@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/resolve-path/-/resolve-path-1.4.0.tgz#c4bda9f5efb2fce65247873ab36bb4d834fe16f7" + integrity sha1-xL2p9e+y/OZSR4c6s2u02DT+Fvc= + dependencies: + http-errors "~1.6.2" + path-is-absolute "1.0.1" + restore-cursor@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" @@ -1169,7 +1277,7 @@ rxjs@^6.4.0: dependencies: tslib "^1.9.0" -safe-buffer@5.1.2, safe-buffer@~5.1.2: +safe-buffer@5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -1201,6 +1309,11 @@ semver@^6.1.2: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + setprototypeof@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.1.tgz#7e95acb24aa92f5885e0abef5ba131330d4ae683" @@ -1253,7 +1366,7 @@ sprintf-js@~1.0.2: resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= -"statuses@>= 1.5.0 < 2", statuses@^1.5.0: +"statuses@>= 1.4.0 < 2", "statuses@>= 1.5.0 < 2", statuses@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" integrity sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow= @@ -1276,11 +1389,6 @@ string-width@^4.1.0: is-fullwidth-code-point "^3.0.0" strip-ansi "^6.0.0" -string_decoder@~0.10.x: - version "0.10.31" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" - integrity sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ= - strip-ansi@^5.1.0, strip-ansi@^5.2.0: version "5.2.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" @@ -1322,6 +1430,20 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha1-GhkY1ALY/D+Y+/I02wvMjMEOlyY= + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.0" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.0.tgz#e69e38a1babe969b0108207978b9f62b88604839" + integrity sha1-5p44obq+lpsBCCB5eLn2K4hgSDk= + dependencies: + any-promise "^1.0.0" + through@^2.3.6: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -1334,6 +1456,13 @@ tmp@^0.0.33: dependencies: os-tmpdir "~1.0.2" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + toidentifier@1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" @@ -1386,14 +1515,6 @@ urijs@^1.19.0: resolved "https://registry.yarnpkg.com/urijs/-/urijs-1.19.2.tgz#f9be09f00c4c5134b7cb3cf475c1dd394526265a" integrity sha512-s/UIq9ap4JPZ7H1EB5ULo/aOUbWqfDi7FKzMC2Nz+0Si8GiT1rIEaprt8hy3Vy2Ex2aJPpOQv4P4DuOZ+K1c6w== -url-parse@~1.4.3: - version "1.4.7" - resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" - integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== - dependencies: - querystringify "^2.1.1" - requires-port "^1.0.0" - uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"