diff --git a/hydro/development.js b/hydro/development.js index 743e7da17..c27e6c99e 100644 --- a/hydro/development.js +++ b/hydro/development.js @@ -16,6 +16,11 @@ process.on('restart', async () => { delete require.cache; run(); }); +const path = require('path'); +const i18n = require('./lib/i18n'); +i18n(path.resolve(__dirname, '..', 'locales', 'zh_CN.yaml'), 'zh_CN'); + + const EventEmitter = require('events'); global.bus = new EventEmitter(); async function run() { @@ -27,13 +32,14 @@ async function run() { }); }); let server = require('./service/server'); - require('./lib/i18n'); + require('./handler/ui'); require('./handler/base'); require('./handler/home'); require('./handler/problem'); require('./handler/record'); require('./handler/judge'); + require('./handler/user'); server.start(); } run().catch(e => { diff --git a/hydro/handler/base.js b/hydro/handler/base.js index e0d9673b7..d36a2d603 100644 --- a/hydro/handler/base.js +++ b/hydro/handler/base.js @@ -15,13 +15,9 @@ MIDDLEWARE(async (ctx, next) => { let sid = ctx.cookies.get('sid'); let save = ctx.cookies.get('save'); let tokenType, expireSeconds; - if (save) { - tokenType = token.TYPE_SAVED_SESSION; - expireSeconds = options.session.saved_expire_seconds; - } else { - tokenType = token.TYPE_UNSAVED_SESSION; - expireSeconds = options.session.unsaved_expire_seconds; - } + tokenType = token.TYPE_SESSION; + if (save) expireSeconds = options.session.saved_expire_seconds; + else expireSeconds = options.session.unsaved_expire_seconds; ctx.session = sid ? await token.update(sid, tokenType, expireSeconds, Object.assign({ update_ip: ctx.request.ip, @@ -48,6 +44,7 @@ MIDDLEWARE(async (ctx, next) => { } } }; + console.log(ctx.session.uid); await next(); if (ctx.session.sid) await token.update(ctx.session.sid, tokenType, expireSeconds, Object.assign({ @@ -61,7 +58,8 @@ MIDDLEWARE(async (ctx, next) => { update_ip: ctx.request.ip, update_ua: ctx.request.headers['user-agent'] || '' }, ctx.session)); - let cookie = { domain: options.session.domain, secure: options.session.secure, httponly: true }; + console.log(ctx.session.sid, ctx.session.uid); + let cookie = { secure: options.session.secure, httponly: true }; if (save) { cookie.expires = ctx.session.expireAt, cookie.maxAge = expireSeconds; ctx.cookies.set('save', 'true', cookie); diff --git a/hydro/handler/judge.js b/hydro/handler/judge.js index d535e2a91..8c172b44e 100644 --- a/hydro/handler/judge.js +++ b/hydro/handler/judge.js @@ -1,5 +1,5 @@ const - { CONTEXT } = require('../service/server'), + { GET, POST } = require('../service/server'), queue = require('../service/queue'), record = require('../model/record'), { requirePerm } = require('./tools'), @@ -7,12 +7,10 @@ const queue.assert('judge'); -const { MIDDLEWARE, GET, POST } = CONTEXT(); -MIDDLEWARE(requirePerm(PERM_JUDGE)); -GET('/judge/noop', async ctx => { +GET('/judge/noop', requirePerm(PERM_JUDGE), async ctx => { ctx.body = {}; }); -GET('/judge/fetch', async ctx => { +GET('/judge/fetch', requirePerm(PERM_JUDGE), async ctx => { let rid = await queue.get('judge', false); if (rid) { let rdoc = await record.get(rid); @@ -27,9 +25,9 @@ GET('/judge/fetch', async ctx => { ctx.body = data; } }); -POST('/judge/next', async ctx => { +POST('/judge/next', requirePerm(PERM_JUDGE), async ctx => { console.log(ctx.request.body); }); -POST('/judge/end', async ctx => { +POST('/judge/end', requirePerm(PERM_JUDGE), async ctx => { console.log(ctx.request.body); }); diff --git a/hydro/handler/problem.js b/hydro/handler/problem.js index b3fa21d03..56803a995 100644 --- a/hydro/handler/problem.js +++ b/hydro/handler/problem.js @@ -38,6 +38,7 @@ GET('/p/:pid', requirePerm(PERM_VIEW_PROBLEM), async ctx => { let pdoc = await problem.get({ pid, uid }); if (pdoc.hidden) ctx.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); let udoc = await user.getById(pdoc.owner); + ctx.templateName = 'problem_detail.html'; ctx.body = { pdoc, udoc, title: pdoc.title }; }); POST('/p/:pid/submit', requirePerm(PERM_SUBMIT_PROBLEM), async ctx => { diff --git a/hydro/handler/ui.js b/hydro/handler/ui.js index fc2137a49..7858a78b0 100644 --- a/hydro/handler/ui.js +++ b/hydro/handler/ui.js @@ -1,25 +1,58 @@ const path = require('path'), + md5 = require('blueimp-md5'), static = require('koa-static'), nunjucks = require('nunjucks'), + hljs = require('hljs'), + MarkdownIt = require('markdown-it'), + katex = require('markdown-it-katex'), options = require('../options'), perm = require('../permission'), builtin = require('../model/builtin'), { MIDDLEWARE } = require('../service/server'), - { NotFoundError } = require('../error'); + { NotFoundError } = require('../error'), + MD5_REGEX = /^[0-9a-f]{32}$/; +function getHash(email) { + email = (typeof email === 'string') ? email.trim().toLowerCase() : 'unspecified'; + return email.match(MD5_REGEX) ? email : md5(email); +} + +class Markdown extends MarkdownIt { + constructor() { + super({ + linkify: true, + highlight: function (str, lang) { + if (lang && hljs.getLanguage(lang)) + try { + return hljs.highlight(lang, str).value; + } catch (__) { } // eslint-disable-line no-empty + return ''; + } + }); + this.linkify.tlds('.py', false); + this.use(katex); + } +} +const md = new Markdown(); 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); + this.addFilter('json', function (self) { + return JSON.stringify(self); }, false); this.addFilter('assign', function (self, data) { return Object.assign(self, data); }); + this.addFilter('markdown', function (self) { + return md.render(self); + }); + this.addFilter('gravatar_url', function (email, size) { + return `//gravatar.loli.net/avatar/${getHash(email)}?d=mm&s=${size}`; + }); this.addGlobal('static_url', str => `/${str}`); this.addGlobal('reverse_url', str => str); this.addGlobal('perm', perm); @@ -30,8 +63,10 @@ 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.UIContext = { + cdn_prefix: '/', + url_prefix: '/' + }; ctx.render = async (name, context) => { ctx.user = ctx.state.user; ctx.translate = str => { @@ -53,12 +88,17 @@ MIDDLEWARE(async (ctx, next) => { try { await next(); if (!ctx.body) throw new NotFoundError(); + if (ctx.query.template || ctx.templateName) { + Object.assign(ctx.body, JSON.parse(ctx.query.data || '{}')); + await ctx.render(ctx.query.template || ctx.templateName, ctx.body); + } else if (ctx.setRedirect) { + ctx.response.type = 'application/octet-stream'; + ctx.redirect(ctx.setRedirect); + } } catch (error) { + if (error.toString().startsWith('Template render error')) throw 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 }); } diff --git a/hydro/handler/user.js b/hydro/handler/user.js index a653e2744..7405dedf5 100644 --- a/hydro/handler/user.js +++ b/hydro/handler/user.js @@ -12,12 +12,16 @@ const GET('/user', async ctx => { let udoc = await user.getById(ctx.session.uid); + ctx.templateName = 'user_detail.html'; ctx.body = { udoc }; }); +GET('/login', async ctx => { + ctx.templateName = 'user_login.html'; +}); POST('/login', async ctx => { - let { username, password, rememberme } = ctx.request.body; - let udoc = await user.getByUname(username); - if (!udoc) throw new LoginError(username); + let { uname, password, rememberme = false } = ctx.request.body; + let udoc = await user.getByUname(uname); + if (!udoc) throw new LoginError(uname); if (udoc) udoc.checkPassword(password); await user.setById(udoc._id, { loginat: new Date(), loginip: ctx.request.ip }); udoc.salt = ''; @@ -25,22 +29,20 @@ POST('/login', async ctx => { console.log(udoc); ctx.session.uid = udoc._id; ctx.session.rememberme = rememberme; - ctx.body = { udoc }; + ctx.body = {}; + ctx.setRedirect = ctx.request.headers.referer || '/'; }); POST('/logout', requirePerm(PERM_LOGGEDIN), async ctx => { ctx.session = { uid: 1 }; ctx.body = {}; }); -let { GET: _GET, POST: _POST, MIDDLEWARE: _MIDDLEWARE } = CONTEXT(); -_MIDDLEWARE(requirePerm(PERM_REGISTER_USER)); - -_GET('/register/:code', async ctx => { +GET('/register/:code', requirePerm(PERM_REGISTER_USER), 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', async ctx => { +GET('/register/:code', requirePerm(PERM_REGISTER_USER), 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); @@ -53,7 +55,7 @@ _GET('/register/:code', async ctx => { }); if (options.smtp.user) { - _POST('/register', limitRate('send_mail', 3600, 30), async ctx => { + POST('/register', requirePerm(PERM_REGISTER_USER), 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); @@ -95,7 +97,7 @@ if (options.smtp.user) { ctx.redirect('/'); }); } else - _POST('/register', async ctx => { + POST('/register', requirePerm(PERM_REGISTER_USER), 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/lib/i18n.js b/hydro/lib/i18n.js index 28f461596..495b20d35 100644 --- a/hydro/lib/i18n.js +++ b/hydro/lib/i18n.js @@ -25,8 +25,8 @@ 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; +String.prototype.translate = function (language = 'zh_CN') { + if (locales[language]) return locales[language][this] || this; else return this; }; diff --git a/hydro/service/server.js b/hydro/service/server.js index 0e137dbb8..0b59fafc9 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -13,37 +13,7 @@ let server = http.createServer(app.callback()); app.keys = options.session.keys; 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 @@ -78,15 +48,12 @@ function SOCKET(prefix, handler) { 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); }; diff --git a/package.json b/package.json index aa708227c..4734db4ba 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "license": "MIT", "dependencies": { "axios": "^0.19.0", + "blueimp-md5": "^2.13.0", "bson": "^4.0.2", + "hljs": "^6.2.3", "js-yaml": "^3.13.1", "koa": "^2.11.0", "koa-body": "^4.1.1", @@ -15,6 +17,8 @@ "koa-router": "^7.4.0", "koa-static": "^5.0.0", "lodash": "^4.17.15", + "markdown-it": "^10.0.0", + "markdown-it-katex": "^2.0.3", "mongodb": "^3.3.4", "nodemailer": "^6.3.1", "nunjucks": "^3.2.1", diff --git a/templates/components/comments_solution.html b/templates/components/comments_solution.html index a9af76c41..9ad99982e 100644 --- a/templates/components/comments_solution.html +++ b/templates/components/comments_solution.html @@ -34,7 +34,7 @@
{{ _('Hint') }}: {{ _('Dataset Format') }}, {{ _('An example of dataset') }}: {{ _('Download') }}
- {% if pdoc and (handler.own(pdoc, vj4.model.builtin.PERM_READ_PROBLEM_DATA_SELF) or handler.has_perm(vj4.model.builtin.PERM_READ_PROBLEM_DATA) or handler.has_priv(vj4.model.builtin.PRIV_READ_PROBLEM_DATA)) %} + {% if pdoc and (handler.own(pdoc, perm.PERM_READ_PROBLEM_DATA_SELF) or handler.has_perm(perm.PERM_READ_PROBLEM_DATA) or handler.has_priv(vj4.model.builtin.PRIV_READ_PROBLEM_DATA)) %} {% endif %}