From 75ebc1a746702bdf2abdadba7855b686a3468a98 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 29 Apr 2020 19:08:03 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=EF=BC=8C=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0webpack=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + build/buildModule.js | 53 +++++++ build/index.js | 8 +- build/locales.js | 5 +- build/webpack.js | 56 ++++--- hydro/development.js | 29 ++-- hydro/error.js | 2 +- hydro/handler/contest.js | 30 +++- hydro/handler/discussion.js | 30 +++- hydro/handler/home.js | 229 ++++++++++++++++++++++++++- hydro/handler/import.js | 59 ++----- hydro/lib/axios.js | 2 +- hydro/lib/download.js | 3 +- hydro/lib/loader.js | 43 +++++ hydro/lib/rank.js | 6 +- hydro/model/contest.js | 8 +- hydro/model/setting.js | 60 +++++++ hydro/model/token.js | 8 + hydro/module/contest/acm.js | 111 ------------- hydro/module/contest/homework.js | 95 ----------- hydro/module/contest/oi.js | 63 -------- hydro/options.js | 2 +- hydro/service/server.js | 4 +- module/.gitkeep | 0 package.json | 2 +- templates/components/home.html | 14 +- templates/home_security.html | 68 ++++++-- templates/home_settings.html | 84 +++++----- templates/partials/login_dialog.html | 4 +- ui/build/config/webpack.js | 1 - 30 files changed, 622 insertions(+), 459 deletions(-) create mode 100644 build/buildModule.js create mode 100644 hydro/lib/loader.js create mode 100644 hydro/model/setting.js delete mode 100644 hydro/module/contest/acm.js delete mode 100644 hydro/module/contest/homework.js delete mode 100644 hydro/module/contest/oi.js create mode 100644 module/.gitkeep diff --git a/.gitignore b/.gitignore index 46fe8fa7..1cb1d2ba 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ ui/misc/.iconfont ui/static/locale .cache .webpackStats.json +module/** +!.gitkeep diff --git a/build/buildModule.js b/build/buildModule.js new file mode 100644 index 00000000..3c03d835 --- /dev/null +++ b/build/buildModule.js @@ -0,0 +1,53 @@ +const fs = require('fs'); +const webpack = require('webpack'); +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); +const root = require('./root'); +const exist = (name) => { + try { + fs.statSync(root(name)); + } catch (e) { + return false; + } + return true; +} +const build = async () => { + const modules = fs.readdirSync(root('module')); + const config = { + mode: 'production', + entry: {}, + output: { + filename: 'module/[name].js', + path: root('.build') + }, + target: 'node', + module: {}, + plugins: [ + new webpack.ProgressPlugin(), + new FriendlyErrorsPlugin(), + ] + }; + for (let i of modules) { + if (!i.startsWith('.')) { + if (exist(`module/${i}/model.js`)) { + config.entry[`${i}/model`] = root(`module/${i}/model.js`); + } + if (exist(`module/${i}/handler.js`)) { + config.entry[`${i}/handler`] = root(`module/${i}/handler.js`); + } + } + } + const compiler = webpack(config); + await new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) console.error(err.details); + reject(); + } + if (stats.hasErrors()) process.exitCode = 1; + resolve(); + }); + }) +} + +module.exports = build; diff --git a/build/index.js b/build/index.js index 5390bbb6..928cd64b 100644 --- a/build/index.js +++ b/build/index.js @@ -4,8 +4,6 @@ try { fs.rmdirSync(root('.build')); } catch (e) { } fs.mkdirSync(root('.build')); -let progress = (name) => ((prog) => { - console.log(name, prog); -}); -require('./locales')('locales', progress); -require('./webpack')('webpack', progress); +require('./locales')(); +require('./buildModule')(); +require('./webpack')(); diff --git a/build/locales.js b/build/locales.js index a6c64e2b..fcd6388e 100644 --- a/build/locales.js +++ b/build/locales.js @@ -1,15 +1,12 @@ const yaml = require('js-yaml'); const fs = require('fs'); const root = require('./root'); -const build = async (next) => { +const build = async () => { let langs = fs.readdirSync(root('locales')); - next({ total: langs.length }); let lang = {}; - let count = 0; for (let i of langs) { const content = fs.readFileSync(root(`locales/${i}`)).toString(); lang[i.split('.')[0]] = yaml.safeLoad(content); - next({ progress: ++count }); } fs.writeFileSync(root('.build/locales.json'), JSON.stringify(lang)); } diff --git a/build/webpack.js b/build/webpack.js index 34e4f33f..18c12194 100644 --- a/build/webpack.js +++ b/build/webpack.js @@ -1,11 +1,24 @@ -const path = require('path'); const fs = require('fs'); const webpack = require('webpack'); +const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin'); const root = require('./root'); -const build = async (next) => { - const modules = fs.readdirSync(root('hydro', 'module')); +const build = async () => { + function hackNodeModuleFormidable() { + const tasks = ['incoming_form', 'file', 'json_parser', 'querystring_parser']; + for (let task of tasks) { + let file = fs.readFileSync(root(`node_modules/formidable/lib/${task}.js`)).toString(); + if (file.startsWith('if (global.GENTLY) require = GENTLY.hijack(require);')) { + file = file.split('\n'); + file[0] = ''; + file = file.join('\n'); + fs.writeFileSync(root(`node_modules/formidable/lib/${task}.js`), file); + } + } + } + hackNodeModuleFormidable(); + const config = { - mode: 'production', + mode: 'development', entry: { development: root('hydro/development.js'), install: root('hydro/install.js'), @@ -16,25 +29,24 @@ const build = async (next) => { path: root('.build') }, target: 'node', - module: {} + module: {}, + plugins: [ + new webpack.ProgressPlugin(), + new FriendlyErrorsPlugin(), + ] }; - for (let i of modules) { - if (fs.statSync(path.resolve(__dirname, 'hydro', 'module', i)).isDirectory()) { - config.entry[root(`.build/module/${i}`)] = root(`./hydro/module/${i}/index.js`); - } else { - config.entry[root(`.build/module/${i}`)] = root(`./hydro/module/${i}`); - } - } const compiler = webpack(config); - function compilerCallback(err, stats) { - if (err) { - console.error(err.stack || err); - if (err.details) console.error(err.details); - process.exit(1); - } - } - if (!watch && stats.hasErrors()) process.exitCode = 1; - compiler.run(compilerCallback); - next({ total: 100 }); + await new Promise((resolve, reject) => { + compiler.run((err, stats) => { + if (err) { + console.error(err.stack || err); + if (err.details) console.error(err.details); + reject(1); + } + if (stats.hasErrors()) process.exitCode = 1; + resolve(); + }); + }) } + module.exports = build; diff --git a/hydro/development.js b/hydro/development.js index f7e1573d..8c7b2f13 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -9,12 +9,16 @@ process.stdin.on('data', async (input) => { } }); +global.Hydro = {}; + require('./lib/i18n'); require('./utils'); const bus = require('./service/bus'); +const loader = require('./lib/loader'); async function run() { + await loader.prepare(); await new Promise((resolve) => { const h = () => { console.log('Database connected'); @@ -27,16 +31,21 @@ async function run() { require('./service/gridfs'); require('./service/queue'); const server = require('./service/server'); - require('./handler/home'); - require('./handler/problem'); - require('./handler/record'); - require('./handler/judge'); - require('./handler/user'); - require('./handler/contest'); - require('./handler/training'); - require('./handler/discussion'); - require('./handler/manage'); - require('./handler/import'); + const HandlerHome = require('./handler/home'); + const HandlerProblem = require('./handler/problem'); + const HandlerRecord = require('./handler/record'); + const HandlerJudge = require('./handler/judge'); + const HandlerUser = require('./handler/user'); + const HandlerContest = require('./handler/contest'); + const HandlerTraining = require('./handler/training'); + const HandlerDiscussion = require('./handler/discussion'); + const HandlerManage = require('./handler/manage'); + const HandlerImport = require('./handler/import'); + await loader.model(); + await loader.handler(); + HandlerContest.apply(); + HandlerDiscussion.apply(); + HandlerImport.apply(); server.start(); } run().catch((e) => { diff --git a/hydro/error.js b/hydro/error.js index 4275f6cb..d140501c 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -225,7 +225,7 @@ class DiscussionNotFoundError extends NotFoundError { } } -module.exports = { +global.Hydro.error = module.exports = { BadRequestError, BlacklistedError, ForbiddenError, diff --git a/hydro/handler/contest.js b/hydro/handler/contest.js index e499bdd8..a52d4d11 100644 --- a/hydro/handler/contest.js +++ b/hydro/handler/contest.js @@ -267,11 +267,25 @@ class ContestCreateHandler extends ContestHandler { } } -Route('/c', ContestListHandler); -Route('/c/:tid', ContestDetailHandler); -Route('/c/:tid/edit', ContestEditHandler); -Route('/c/:tid/scoreboard', ContestScoreboardHandler); -Route('/c/:tid/export/:ext', ContestScoreboardDownloadHandler); -Route('/c/:tid/p/:pid', ContestProblemHandler); -Route('/c/:tid/p/:pid/submit', ContestProblemSubmitHandler); -Route('/contest/create', ContestCreateHandler); +function apply() { + Route('/c', module.exports.ContestListHandler); + Route('/c/:tid', module.exports.ContestDetailHandler); + Route('/c/:tid/edit', module.exports.ContestEditHandler); + Route('/c/:tid/scoreboard', module.exports.ContestScoreboardHandler); + Route('/c/:tid/export/:ext', module.exports.ContestScoreboardDownloadHandler); + Route('/c/:tid/p/:pid', module.exports.ContestProblemHandler); + Route('/c/:tid/p/:pid/submit', module.exports.ContestProblemSubmitHandler); + Route('/contest/create', module.exports.ContestCreateHandler); +} + +module.exports = { + ContestListHandler, + ContestDetailHandler, + ContestEditHandler, + ContestScoreboardHandler, + ContestScoreboardDownloadHandler, + ContestProblemHandler, + ContestProblemSubmitHandler, + ContestCreateHandler, + apply, +}; diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.js index d21f70b9..c44566ed 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.js @@ -257,11 +257,25 @@ class DiscussionEditHandler extends DiscussionHandler { } } -Route('/discuss', DiscussionMainHandler); -Route('/discuss/:did', DiscussionDetailHandler); -Route('/discuss/:did/edit', DiscussionEditHandler); -Route('/discuss/:did/raw', DiscussionDetailRawHandler); -Route('/discuss/:did/:drid/raw', DiscussionReplyRawHandler); -Route('/discuss/:did/:drid/:drrid/raw', DiscussionTailReplyRawHandler); -Route('/discuss/:type/:docId', DiscussionNodeHandler); -Route('/discuss/:type/:docId/create', DiscussionCreateHandler); +function apply() { + Route('/discuss', module.exports.DiscussionMainHandler); + Route('/discuss/:did', module.exports.DiscussionDetailHandler); + Route('/discuss/:did/edit', module.exports.DiscussionEditHandler); + Route('/discuss/:did/raw', module.exports.DiscussionDetailRawHandler); + Route('/discuss/:did/:drid/raw', module.exports.DiscussionReplyRawHandler); + Route('/discuss/:did/:drid/:drrid/raw', module.exports.DiscussionTailReplyRawHandler); + Route('/discuss/:type/:docId', module.exports.DiscussionNodeHandler); + Route('/discuss/:type/:docId/create', module.exports.DiscussionCreateHandler); +} + +module.exports = { + DiscussionMainHandler, + DiscussionDetailHandler, + DiscussionEditHandler, + DiscussionDetailRawHandler, + DiscussionReplyRawHandler, + DiscussionTailReplyRawHandler, + DiscussionNodeHandler, + DiscussionCreateHandler, + apply, +}; diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 6ce6dff8..a7a226a8 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -1,8 +1,19 @@ +const { + VerifyPasswordError, UserAlreadyExistError, InvalidTokenError, + NotFoundError, +} = require('../error'); +const options = require('../options'); const { Route, Handler } = require('../service/server'); +const md5 = require('../lib/md5'); const contest = require('../model/contest'); const user = require('../model/user'); +const setting = require('../model/setting'); +const token = require('../model/token'); const training = require('../model/training'); -const { PERM_VIEW_TRAINING, PERM_VIEW_CONTEST, PERM_VIEW_DISCUSSION } = require('../permission'); +const { + PERM_VIEW_TRAINING, PERM_VIEW_CONTEST, PERM_VIEW_DISCUSSION, + PERM_LOGGEDIN, +} = require('../permission'); const { CONTESTS_ON_MAIN, TRAININGS_ON_MAIN, DISCUSSIONS_ON_MAIN } = require('../options').constants; class HomeHandler extends Handler { @@ -57,4 +68,220 @@ class HomeHandler extends Handler { } } +class HomeSecurityHandler extends Handler { + async prepare() { + this.checkPerm(PERM_LOGGEDIN); + } + + async get() { + // TODO(iceboy): pagination? or limit session count for uid? + const sessions = await token.getSessionListByUid(this.user._id); + const parsed = sessions.map((session) => ({ + ...session, + updateUa: useragent.parse(session.updateUa || session.createUa || ''), + updateGeoip: geoip.ip2geo(session.updateIp || session.createIp, this.user.viewLang), + _id: md5(session._id), + isCurrent: session._id === this.session._id, + })); + this.response.template = 'home_security.html'; + this.response.body = { sessions: parsed }; + } + + async postChangePassword({ current, password, verifyPassword }) { + if (password !== verifyPassword) throw new VerifyPasswordError(); + await user.changePassword(this.user._id, current, password); + this.back(); + } + + async postChangeMail({ currentPassword, mail }) { + this.limitRate('send_mail', 3600, 30); + this.user.checkPassword(currentPassword); + const udoc = await user.getByMail(mail); + if (udoc) throw new UserAlreadyExistError(mail); + const [rid] = await token.add( + token.TYPE_CHANGEMAIL, + options.changemail_token_expire_seconds, + { uid: this.udoc._id, mail }, + ); + await mail.sendMail(mail, 'Change Email', 'user_changemail_mail.html', { + url: `/changeMail/${rid}`, uname: this.udoc.uname, + }); + this.response.template = 'user_changemail_mail_sent.html'; + } + + async postDeleteToken({ tokenDigest }) { + const sessions = await token.getSessionListByUid(this.user._id); + for (const session in sessions) { + if (tokenDigest === md5(session._id)) { + await token.delete(session._id, token.TYPE_SESSION); + return this.back(); + } + } + throw new InvalidTokenError(tokenDigest); + } + + async postDeleteAllTokens() { + await token.deleteByUid(this.user._id); + this.back(); + } +} + +class HomeSettingsHandler extends Handler { + async prepare() { + this.checkPerm(PERM_LOGGEDIN); + } + + async get({ category }) { + this.response.template = 'home_settings.html'; + if (category === 'preference') { + this.response.body = { + category, + page_name: `home_${category}`, + settings: setting.PREFERENCE_SETTINGS, + }; + } else if (category === 'account') { + this.response.body = { + category, + page_name: `home_${category}`, + settings: setting.ACCOUNT_SETTINGS, + }; + } else throw new NotFoundError(); + } + + async post(args) { + // FIXME validation + await user.setById(this.user._id, args); + this.back(); + } +} + +class UserChangemailWithCodeHandler extends Handler { + async get({ code }) { + const tdoc = await token.get(code, token.TYPE_CHANGEMAIL); + if (!tdoc || tdoc.uid !== this.user._id) { + throw new InvalidTokenError(code); + } + const udoc = await user.getByEmail(tdoc.mail); + if (udoc) throw new UserAlreadyExistError(tdoc.mail); + // TODO(twd2): Ensure mail is unique. + await user.set_mail(this.user._id, tdoc.mail); + await token.delete(code, token.TYPE_CHANGEMAIL); + this.response.body = {}; + this.response.redirect = '/home/security'; + } +} + Route('/', HomeHandler); +Route('/home/security', HomeSecurityHandler); +Route('/home/changeMail/:code', UserChangemailWithCodeHandler); +Route('/home/settings/:category', HomeSettingsHandler); + +/* +@app.route('/home/messages', 'home_messages', global_route=True) +class HomeMessagesHandler(base.OperationHandler): + def modify_udoc(this, udict, key): + udoc = udict.get(key) + if not udoc: + return + gravatar_url = misc.gravatar_url(udoc.get('gravatar')) + if 'gravatar' in udoc and udoc['gravatar']: + udict[key] = {**udoc, + 'gravatar_url': gravatar_url, + 'gravatar': ''} + + @base.require_priv(builtin.PRIV_USER_PROFILE) + async def get(this): + # TODO(iceboy): projection, pagination. + mdocs = await message.get_multi(this.user['_id']).sort([('_id', -1)]).limit(50).to_list() + udict = await user.get_dict( + itertools.chain.from_iterable((mdoc['sender_uid'], mdoc['sendee_uid']) for mdoc in mdocs), + fields=user.PROJECTION_PUBLIC) + # TODO(twd2): improve here: + for mdoc in mdocs: + this.modify_udoc(udict, mdoc['sender_uid']) + this.modify_udoc(udict, mdoc['sendee_uid']) + this.json_or_render('home_messages.html', messages=mdocs, udict=udict) + + @base.require_priv(builtin.PRIV_USER_PROFILE) + @base.require_csrf_token + @base.sanitize + async def post_send_message(this, *, uid: int, content: str): + udoc = await user.get_by_uid(uid, user.PROJECTION_PUBLIC) + if not udoc: + raise error.UserNotFoundError(uid) + mdoc = await message.add(this.user['_id'], udoc['_id'], content) + # TODO(twd2): improve here: + # projection + sender_udoc = await user.get_by_uid(this.user['_id'], user.PROJECTION_PUBLIC) + mdoc['sender_udoc'] = sender_udoc + this.modify_udoc(mdoc, 'sender_udoc') + mdoc['sendee_udoc'] = udoc + this.modify_udoc(mdoc, 'sendee_udoc') + if this.user['_id'] != uid: + await bus.publish('message_received-' + str(uid), {'type': 'new', 'data': mdoc}) + this.json_or_redirect(this.url, mdoc=mdoc) + + @base.require_priv(builtin.PRIV_USER_PROFILE) + @base.require_csrf_token + @base.sanitize + async def post_reply_message(this, *, message_id: objectid.ObjectId, content: str): + mdoc, reply = await message.add_reply(message_id, this.user['_id'], content) + if not mdoc: + return error.MessageNotFoundError(message_id) + if mdoc['sender_uid'] != mdoc['sendee_uid']: + if mdoc['sender_uid'] == this.user['_id']: + other_uid = mdoc['sendee_uid'] + else: + other_uid = mdoc['sender_uid'] + mdoc['reply'] = [reply] + await bus.publish('message_received-' + str(other_uid), {'type': 'reply', 'data': mdoc}) + this.json_or_redirect(this.url, reply=reply) + + @base.require_priv(builtin.PRIV_USER_PROFILE) + @base.require_csrf_token + @base.sanitize + async def post_delete_message(this, *, message_id: objectid.ObjectId): + await message.delete(message_id, this.user['_id']) + this.back(); + + +@app.connection_route('/home/messages-conn', 'home_messages-conn', global_route=True) +class HomeMessagesConnection(base.Connection): + @base.require_priv(builtin.PRIV_USER_PROFILE) + async def on_open(this): + await super(HomeMessagesConnection, this).on_open() + bus.subscribe(this.on_message_received, ['message_received-' + str(this.user['_id'])]) + + async def on_message_received(this, e): + this.send(**e['value']) + + async def on_close(this): + bus.unsubscribe(this.on_message_received) + + +@app.route('/home/file', 'home_file', global_route=True) +class HomeFileHandler(base.OperationHandler): + def file_url(this, fdoc): + return options.cdn_prefix.rstrip('/') + \ + this.reverse_url('fs_get', domain_id=builtin.DOMAIN_ID_SYSTEM, + secret=fdoc['metadata']['secret']) + + @base.require_priv(builtin.PRIV_USER_PROFILE) + async def get(this): + ufdocs = await userfile.get_multi(owner_uid=this.user['_id']).to_list() + fdict = await fs.get_meta_dict(ufdoc.get('file_id') for ufdoc in ufdocs) + this.render('home_file.html', ufdocs=ufdocs, fdict=fdict) + + @base.require_priv(builtin.PRIV_USER_PROFILE) + @base.post_argument + @base.require_csrf_token + @base.sanitize + async def post_delete(this, *, ufid: document.convert_doc_id): + ufdoc = await userfile.get(ufid) + if not this.own(ufdoc, priv=builtin.PRIV_DELETE_FILE_this): + this.check_priv(builtin.PRIV_DELETE_FILE) + result = await userfile.delete(ufdoc['doc_id']) + if result: + await userfile.dec_usage(this.user['_id'], ufdoc['length']) + this.redirect(this.referer_or_main) +*/ diff --git a/hydro/handler/import.js b/hydro/handler/import.js index 1d727945..1e943165 100644 --- a/hydro/handler/import.js +++ b/hydro/handler/import.js @@ -1,57 +1,10 @@ -const assert = require('assert'); -const download = require('../lib/download'); + const problem = require('../model/problem'); const { Route, Handler } = require('../service/server'); const { PERM_CREATE_PROBLEM } = require('../permission'); -const axios = require('../lib/axios'); -const { ValidationError, RemoteOnlineJudgeError } = require('../error'); +const { ValidationError } = require('../error'); class ProblemImportHandler extends Handler { - async syzoj(url) { - const RE_SYZOJ = /https?:\/\/([a-zA-Z0-9.]+)\/problem\/([0-9]+)\/?/i; - assert(url.match(RE_SYZOJ), new ValidationError('url')); - if (!url.endsWith('/')) url += '/'; - const [, host, pid] = RE_SYZOJ.exec(url); - const res = await axios.get(`${url}export`); - assert(res.status === 200, new RemoteOnlineJudgeError('Cannot connect to target server')); - assert(res.data.success, new RemoteOnlineJudgeError((res.data.error || {}).message)); - const p = res.data.obj; - const content = [ - this.translate('problem.import.problem_description'), - p.description, - this.translate('problem.import.input_format'), - p.input_format, - this.translate('problem.import.output_format'), - p.output_format, - this.translate('problem.import.hint'), - p.hint, - this.translate('problem.import.limit_and_hint'), - p.limit_and_hint, - ]; - if (p.have_additional_file) { - content.push( - this.translate('problem.import.additional_file'), - `${url}download/additional_file`, - ); - } - const pdoc = { - title: p.title, - content: content.join(' \n'), - owner: this.user._id, - from: url, - pid: `${host}_${pid}`, - config: { - time: p.time_limit, - memory: p.memory_limit * 1024, - filename: p.file_io_input_name, - type: p.type, - tags: p.tags, - }, - }; - const r = await download(`${url}testdata/download`); - return [pdoc, r]; - } - async prepare() { this.checkPerm(PERM_CREATE_PROBLEM); } @@ -82,4 +35,10 @@ class ProblemImportHandler extends Handler { } } -Route('/problem/import', ProblemImportHandler); +async function apply() { + Route('/problem/import', module.exports.ProblemImportHandler); +} + +global.Hydro['handler.import'] = module.exports = { + ProblemImportHandler, apply, +}; diff --git a/hydro/lib/axios.js b/hydro/lib/axios.js index fa4a548e..1714e373 100644 --- a/hydro/lib/axios.js +++ b/hydro/lib/axios.js @@ -20,4 +20,4 @@ async function post(url, data, options) { return res; } -module.exports = { get, post }; +global.Hydro['lib.axios'] = module.exports = { get, post }; diff --git a/hydro/lib/download.js b/hydro/lib/download.js index b0c4d5b4..23dd26d3 100644 --- a/hydro/lib/download.js +++ b/hydro/lib/download.js @@ -19,4 +19,5 @@ async function download(url, path, retry = 3) { } return r; } -module.exports = download; + +global.Hydro['lib.download'] = module.exports = download; diff --git a/hydro/lib/loader.js b/hydro/lib/loader.js new file mode 100644 index 00000000..b8004269 --- /dev/null +++ b/hydro/lib/loader.js @@ -0,0 +1,43 @@ +const fs = require('fs'); +const path = require('path'); + +let installed; +function root(name) { + return path.resolve(process.cwd(), name); +} +function exist(name) { + try { + fs.statSync(root(name)); + } catch (e) { + return false; + } + return true; +} + +const superRequire = (name) => (__non_webpack_require__ + ? __non_webpack_require__(root(name)) + : require(root(name))); + +async function prepare() { + installed = fs.readdirSync(root('.build/module')); +} + +async function handler() { + for (const i of installed) { + if (exist(`.build/module/${i}/handler.js`)) { + superRequire(`.build/module/${i}/handler.js`); + } + console.log(`Handler init: ${i}`); + } +} + +async function model() { + for (const i of installed) { + if (exist(`.build/module/${i}/model.js`)) { + superRequire(`.build/module/${i}/model.js`); + } + console.log(`Model init: ${i}`); + } +} + +global.Hydro['lib.loader'] = module.exports = { prepare, handler, model }; diff --git a/hydro/lib/rank.js b/hydro/lib/rank.js index 76ccef2d..7b5d287b 100644 --- a/hydro/lib/rank.js +++ b/hydro/lib/rank.js @@ -1,4 +1,4 @@ -module.exports = function* ranked(diter, equ = (a, b) => a === b) { +function* ranked(diter, equ = (a, b) => a === b) { let last = null; let r = 0; let count = 0; @@ -8,4 +8,6 @@ module.exports = function* ranked(diter, equ = (a, b) => a === b) { last = doc; yield [r, doc]; } -}; +} + +global.Hydro['lib.rank'] = module.exports = ranked; diff --git a/hydro/model/contest.js b/hydro/model/contest.js index 6fe7daef..cc329f78 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -11,11 +11,7 @@ const db = require('../service/db'); const coll = db.collection('contest'); const collStatus = db.collection('contest.status'); -const RULES = { - homework: require('../module/contest/homework'), - oi: require('../module/contest/oi'), - acm: require('../module/contest/acm'), -}; +const RULES = {}; /** * @typedef {import('bson').ObjectID} ObjectID @@ -197,6 +193,8 @@ module.exports = { : 'finished'), }; +global.Hydro['model.contest'] = module.exports; + /* def _get_status_journal(tsdoc): diff --git a/hydro/model/setting.js b/hydro/model/setting.js new file mode 100644 index 00000000..9d7ada57 --- /dev/null +++ b/hydro/model/setting.js @@ -0,0 +1,60 @@ +const builtin = require('./builtin'); +const options = require('../options'); +const i18n = require('../lib/i18n'); + +const Setting = ( + family, key, range = null, + value = null, ui = 'text', name = '', + desc = '', imageClass = '', +) => ({ + family, key, range, value, ui, name, desc, imageClass, +}); + +const PREFERENCE_SETTINGS = [ + Setting('setting_display', 'view_lang', i18n, + options.default_locale, 'select', 'UI Language'), + Setting('setting_display', 'timezone', [], // TODO(masnn) timezone + 'Asia/Shanghai', 'select', 'Timezone'), + Setting('setting_usage', 'code_lang', builtin.LANG_TEXTS, + null, 'select', 'Default Code Language'), + Setting('setting_usage', 'code_template', null, + null, 'textarea', 'Default Code Template', + 'If left blank, the built-in template of the corresponding language will be used.'), +]; + +const ACCOUNT_SETTINGS = [ + Setting('setting_info', 'gravatar', null, + null, null, 'Gravatar Email', + 'We use Gravatar to present your avatar icon.'), + Setting('setting_info', 'qq', null, + null, null, 'QQ'), + Setting('setting_info', 'wechat', null, + null, null, 'WeChat'), + Setting('setting_info', 'gender', builtin.USER_GENDER_RANGE, + null, 'select', 'Gender'), + Setting('setting_info', 'bio', null, + null, 'markdown', 'Bio'), + Setting('setting_privacy', 'show_mail', builtin.PRIVACY_RANGE, + null, 'select', 'Email Visibility'), + Setting('setting_privacy', 'show_qq', builtin.PRIVACY_RANGE, + null, 'select', 'QQ Visibility'), + Setting('setting_privacy', 'show_wechat', builtin.PRIVACY_RANGE, + null, 'select', 'WeChat Visibility'), + Setting('setting_privacy', 'show_gender', builtin.PRIVACY_RANGE, + null, 'select', 'Gender Visibility'), + Setting('setting_privacy', 'show_bio', builtin.PRIVACY_RANGE, + null, 'select', 'Bio Visibility'), + Setting('setting_customize', 'background_img', builtin.BACKGROUND_RANGE, + null, 'image_radio', 'Profile Background Image', + 'Choose the background image in your profile page.', + 'user-profile-bg--thumbnail-{0}'), +]; + +const SETTINGS = [...PREFERENCE_SETTINGS, ...ACCOUNT_SETTINGS]; +const SETTINGS_BY_KEY = {}; + +for (const setting in SETTINGS) SETTINGS_BY_KEY[setting.key] = setting; + +module.exports = { + PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, SETTINGS_BY_KEY, +}; diff --git a/hydro/model/token.js b/hydro/model/token.js index 8a0d5946..07c80c79 100644 --- a/hydro/model/token.js +++ b/hydro/model/token.js @@ -7,6 +7,8 @@ module.exports = { TYPE_SESSION: 0, TYPE_CSRF_TOKEN: 1, TYPE_REGISTER: 2, + TYPE_CHANGEMAIL: 3, + /** * Add a token. * @param {number} tokenType type of the token. @@ -71,7 +73,13 @@ module.exports = { const result = await coll.deleteOne({ _id: tokenId, tokenType }); return !!result.deletedCount; }, + deleteByUid(uid) { + return coll.deleteMany({ uid }); + }, getMostRecentSessionByUid(uid) { return coll.findOne({ uid, token_type: this.TYPE_SESSION }, { sort: { updateAt: -1 } }); }, + getSessionListByUid(uid) { + return coll.find({ uid, token_type: this.TYPE_SESSION }).sort('updateAt', -1).toArray(); + }, }; diff --git a/hydro/module/contest/acm.js b/hydro/module/contest/acm.js deleted file mode 100644 index b6c8d55f..00000000 --- a/hydro/module/contest/acm.js +++ /dev/null @@ -1,111 +0,0 @@ -module.exports = { - TEXT: 'ACM/ICPC', - check: () => { }, - showScoreboard: () => true, - showRecord: (tdoc, now) => now > tdoc.endAt, - stat: (tdoc, journal) => { - const naccept = {}; - const effective = {}; - const detail = []; - let accept = 0; - let time = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid) - && !(effective.includes(j.pid) && effective[j.pid].accept)) { - effective[j.pid] = j; - } - if (!j.accept) naccept[j.pid]++; - } - function _time(jdoc) { - const real = jdoc.rid.generationTime - Math.floor(tdoc.begin_at / 1000); - const penalty = 20 * 60 * naccept[jdoc.pid]; - return real + penalty; - } - for (const j of effective) detail.push({ ...j, naccept: naccept[j.pid], time: _time(j) }); - for (const d of detail) { - accept += d.accept; - if (d.accept) time += d.time; - } - return { accept, time, detail }; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'solved_problems', value: _('Solved Problems') }, - ]; - if (isExport) { - columns.push({ type: 'total_time', value: _('Total Time (Seconds)') }); - columns.push({ type: 'total_time_str', value: _('Total Time') }); - } - for (const i in tdoc.pids) { - if (isExport) { - columns.push({ - type: 'problem_flag', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); - columns.push({ - type: 'problem_time', - value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')), - }); - columns.push({ - type: 'problem_time_str', - value: '#{0} {1}'.format(i + 1, _('Time')), - }); - } else { - columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], - }); - } - } - const rows = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - if (tdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } - const row = []; - row.push( - { type: 'string', value: rank }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'string', value: tsdoc.accept || 0 }, - ); - if (isExport) { - row.push( - { type: 'string', value: tsdoc.time || 0.0 }, - { type: 'string', value: tsdoc.time || 0.0 }, - ); - } - for (const pid of tdoc.pids) { - let rdoc; - let colAccepted; - let colTime; - let colTimeStr; - if ((tsddict[pid] || {}).accept) { - rdoc = tsddict[pid].rid; - colAccepted = _('Accepted'); - colTime = tsddict[pid].time; - colTimeStr = colTime; - } else { - rdoc = null; - colAccepted = '-'; - colTime = '-'; - colTimeStr = '-'; - } - if (isExport) { - row.push({ type: 'string', value: colAccepted }); - row.push({ type: 'string', value: colTime }); - row.push({ type: 'string', value: colTimeStr }); - } else { - row.push({ - type: 'record', - value: '{0}\n{1}'.format(colAccepted, colTimeStr), - raw: rdoc, - }); - } - rows.push(row); - } - } - return rows; - }, -}; diff --git a/hydro/module/contest/homework.js b/hydro/module/contest/homework.js deleted file mode 100644 index 7e24c5a2..00000000 --- a/hydro/module/contest/homework.js +++ /dev/null @@ -1,95 +0,0 @@ -module.exports = { - TEXT: 'Assignment', - check() {}, - stat(tdoc, journal) { - const effective = {}; - const detail = []; - let score = 0; - let time = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid) - && !(effective.includes(j.pid) && effective[j.pid].accept)) { - effective[j.pid] = j; - } - } - const _time = (jdoc) => jdoc.rid.generationTime - Math.floor(tdoc.beginAt / 1000); - for (const j in effective) { - detail.push({ - ...effective[j], - time: _time(effective[j]), - }); - } - for (const d of detail) { - score += d.score; - time += d.time; - } - return { - score, time, detail, - }; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'display_name', value: _('Display Name') }, - { type: 'total_score', value: _('Score') }, - ]; - if (isExport) { - columns.push( - { type: 'total_original_score', value: _('Original Score') }, - { type: 'total_time', value: _('Total Time (Seconds)') }, - ); - } - columns.push({ type: 'total_time_str', value: _('Total Time') }); - for (const i in tdoc.pids) { - if (isExport) { - columns.push( - { type: 'problem_score', value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title) }, - { type: 'problem_original_score', value: '#{0} {1}'.format(i + 1, _('Original Score')) }, - { type: 'problem_time', value: '#{0} {1}'.format(i + 1, _('Time (Seconds)')) }, - { type: 'problem_time_str', value: '#{0} {1}'.format(i + 1, _('Time')) }, - ); - } else columns.push({ type: 'problem_detail', value: '#{0}'.format(i + 1), raw: pdict[tdoc.pids[i]] }); - } - const rows = [columns]; - for (const [rank, tsdoc] in rankedTsdocs) { - const tsddict = {}; - if (tsdoc.detail) { for (const item of tsdoc.detail) tsddict[item.pid] = item; } - const row = [ - { type: 'string', value: rank }, - { type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }, - { type: 'string', value: tsdoc.penalty_score || 0 }, - ]; - if (isExport) { - row.push( - { type: 'string', value: tsdoc.score || 0 }, - { type: 'string', value: tsdoc.time || 0.0 }, - ); - } - row.push({ type: 'string', value: tsdoc.time || 0 }); - for (const pid of tdoc.pids) { - const rdoc = (tsddict[pid] || {}).rid || null; - const colScore = (tsddict[pid] || {}).penalty_score || '-'; - const colOriginalScore = (tsddict[pid] || {}).score || '-'; - const colTime = (tsddict[pid] || {}).time || '-'; - const colTimeStr = colTime !== '-' ? colTime : '-'; - if (isExport) { - row.push( - { type: 'string', value: colScore }, - { type: 'string', value: colOriginalScore }, - { type: 'string', value: colTime }, - { type: 'string', value: colTimeStr }, - ); - } else { - row.push({ - type: 'record', - value: '{0} / {1}\n{2}'.format(colScore, colOriginalScore, colTimeStr), - raw: rdoc, - }); - } - } - rows.push(row); - } - return rows; - }, -}; diff --git a/hydro/module/contest/oi.js b/hydro/module/contest/oi.js deleted file mode 100644 index 8623b69d..00000000 --- a/hydro/module/contest/oi.js +++ /dev/null @@ -1,63 +0,0 @@ -const ranked = require('../../lib/rank'); - -module.exports = { - TEXT: 'OI', - check: () => { }, - stat: (tdoc, journal) => { - const detail = {}; - let score = 0; - for (const j in journal) { - if (tdoc.pids.includes(j.pid)) { - detail[j.pid] = j; - score += j.score; - } - } - return { score, detail }; - }, - showScoreboard(tdoc, now) { - return now > tdoc.endAt; - }, - showRecord(tdoc, now) { - return now > tdoc.endAt; - }, - scoreboard(isExport, _, tdoc, rankedTsdocs, udict, pdict) { - const columns = [ - { type: 'rank', value: _('Rank') }, - { type: 'user', value: _('User') }, - { type: 'total_score', value: _('Total Score') }, - ]; - for (const i in tdoc.pids) { - if (isExport) { - columns.push({ - type: 'problem_score', - value: '#{0} {1}'.format(i + 1, pdict[tdoc.pids[i]].title), - }); - } else { - columns.push({ - type: 'problem_detail', - value: '#{0}'.format(i + 1), - raw: pdict[tdoc.pids[i]], - }); - } - } - const rows = [columns]; - for (const [rank, tsdoc] of rankedTsdocs) { - const tsddict = {}; - if (tsdoc.journal) { for (const item of tsdoc.journal) tsddict[item.pid] = item; } - const row = []; - row.push({ type: 'string', value: rank }); - row.push({ type: 'user', value: udict[tsdoc.uid].uname, raw: udict[tsdoc.uid] }); - row.push({ type: 'string', value: tsdoc.score || 0 }); - for (const pid of tdoc.pids) { - row.push({ - type: 'record', - value: (tsddict[pid] || {}).score || '-', - raw: (tsddict[pid] || {}).rid || null, - }); - } - rows.push(row); - } - return rows; - }, - rank: (tdocs) => ranked(tdocs, (a, b) => a.score === b.score), -}; diff --git a/hydro/options.js b/hydro/options.js index 65708113..89c16431 100644 --- a/hydro/options.js +++ b/hydro/options.js @@ -13,7 +13,7 @@ let options = { password: '', }, template: { - path: path.join(__dirname, '..', 'templates'), + path: path.join(process.cwd(), 'templates'), }, listen: { host: '127.0.0.1', diff --git a/hydro/service/server.js b/hydro/service/server.js index 450e2066..cb3d9049 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -240,7 +240,9 @@ class Handler { console.error(error.message, error.params); console.error(error.stack); this.response.template = error instanceof UserFacingError ? 'error.html' : 'bsod.html'; - this.response.body = { error: { message: error.message, params: error.params, stack: error.stack } }; + this.response.body = { + error: { message: error.message, params: error.params, stack: error.stack }, + }; await this.___cleanup().catch(() => { }); } } diff --git a/module/.gitkeep b/module/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/package.json b/package.json index 3214b6d9..a87a193b 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "scripts": { "build": "node build/index.js", "pack": "pkg .", - "lint": "eslint hydro --fix" + "lint": "eslint hydro module --fix" }, "pkg": { "scripts": [ diff --git a/templates/components/home.html b/templates/components/home.html index 0eaf984c..2bc7c23b 100644 --- a/templates/components/home.html +++ b/templates/components/home.html @@ -2,16 +2,14 @@ {% macro render_sidebar() %}
{% endmacro %} diff --git a/templates/home_security.html b/templates/home_security.html index 80b47c31..8cb98817 100644 --- a/templates/home_security.html +++ b/templates/home_security.html @@ -1,3 +1,4 @@ +{% set page_name = "home_security" %} {% extends "layout/home_base.html" %} {% block home_content %}
@@ -9,9 +10,28 @@
- {{ form.form_text(type='password', label='Current Password', columns=10, name='current_password', required=true) }} - {{ form.form_text(type='text', label='Current Email', columns=10, name='current_email', value=handler.user['mail'], disabled=true) }} - {{ form.form_text(type='text', label='New Email', columns=10, name='mail', required=true) }} + {{ form.form_text({ + type:'password', + label:'Current Password', + columns:10, + name:'password', + required:true + }) }} + {{ form.form_text({ + type:'text', + label:'Current Email', + columns:10, + name:'currentEmail', + value:this.user.email, + disabled:true + }) }} + {{ form.form_text({ + type:'text', + label:'New Email', + columns:10, + name:'mail', + required:true + }) }}
@@ -28,9 +48,27 @@
- {{ form.form_text(type='password', label='Current Password', columns=10, name='current_password', required=true) }} - {{ form.form_text(type='password', label='New Password', columns=10, name='new_password', required=true) }} - {{ form.form_text(type='password', label='Repeat Password', columns=10, name='verify_password', required=true) }} + {{ form.form_text({ + type:'password', + label:'Current Password', + columns:10, + name:'current', + required:true + }) }} + {{ form.form_text({ + type:'password', + label:'New Password', + columns:10, + name:'password', + required:true + }) }} + {{ form.form_text({ + type:'password', + label:'Repeat Password', + columns:10, + name:'verifyPassword', + required:true + }) }}
@@ -53,25 +91,23 @@
  • - +
    -

    {{ _('Last Update At') }}: {{ datetime_span(session['update_at']) }}

    -

    {{ _('Location') }}: {{ _(session['update_geoip']) }} ({{ session['update_ip'] }})

    -

    {{ _('Operating System') }}: {{ session['update_ua']['os'] }}

    -

    {{ _('Browser') }}: {{ session['update_ua']['browser'] }}

    +

    {{ _('Last Update At') }}: {{ datetime_span(session.updateAt)|safe }}

    +

    {{ _('Location') }}: {{ _(session.updateGeoip) }} ({{ session.updateIp }})

    +

    {{ _('Operating System') }}: {{ session.updateUa.os }}

    +

    {{ _('Browser') }}: {{ session.updateUa.browser }}

    -

    {{ _('Type') }}: {{ _(vj4.handler.home.TOKEN_TYPE_TEXTS[session['token_type']]) }}

    - {% if session['is_current'] %} + {% if session.isCurrent %}

    {{ _('This is the current session') }}

    {% endif %}
    - {% if not session['is_current'] %} + {% if not session.isCurrent %}
    - - + diff --git a/templates/home_settings.html b/templates/home_settings.html index 90520471..4f4801e2 100644 --- a/templates/home_settings.html +++ b/templates/home_settings.html @@ -9,53 +9,53 @@
    {% for setting in family_settings %} {% if setting.ui == 'text' or setting.ui == 'password' %} - {{ form.form_text( - type=setting.ui, - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key)) - }} + {{ form.form_text({ + type:setting.ui, + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value + }) }} {% elif setting.ui == 'select' %} - {{ form.form_select( - options=setting.range.items(), - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key)) - }} + {{ form.form_select({ + options:setting.range, + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value + }) }} {% elif setting.ui == 'radio' %} - {{ form.form_radio( - options=setting.range.items(), - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key)) - }} + {{ form.form_radio({ + options:setting.range, + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value + }) }} {% elif setting.ui == 'image_radio' %} - {{ form.form_image_radio( - options=setting.range.items(), - image_class=setting.image_class, - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key)) - }} + {{ form.form_image_radio({ + options:setting.range, + image_class:setting.image_class, + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value + }) }} {% elif setting.ui == 'textarea' %} - {{ form.form_textarea( - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key)) - }} + {{ form.form_textarea({ + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value + }) }} {% elif setting.ui == 'markdown' %} - {{ form.form_textarea( - label=setting.name, - help_text=setting.desc, - name=setting.key, - value=handler.get_setting(setting.key), - markdown=true) - }} + {{ form.form_textarea({ + label:setting.name, + help_text:setting.desc, + name:setting.key, + value:handler.user[setting.key] or setting.value, + markdown:true + }) }} {% endif %} {% endfor %}
    diff --git a/templates/partials/login_dialog.html b/templates/partials/login_dialog.html index 559a0a83..efdbeb67 100644 --- a/templates/partials/login_dialog.html +++ b/templates/partials/login_dialog.html @@ -3,7 +3,7 @@ @@ -13,7 +13,7 @@ {{ _('CLOSE') }}

    {{ _('SIGN IN') }}

    - +