From 508f5644c399ffd6948ad7c336d980f935908387 Mon Sep 17 00:00:00 2001 From: masnn Date: Sun, 7 Jun 2020 13:59:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=AE=89=E8=A3=85=E6=B5=81?= =?UTF-8?q?=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 4 +- build/index.js | 7 +-- hydro/error.js | 4 +- hydro/handler/discussion.js | 56 +++++++++++------- hydro/handler/home.js | 10 +++- hydro/handler/manage.js | 15 ++++- hydro/handler/record.js | 3 +- hydro/lib/nav.js | 2 +- hydro/model/contest.js | 2 +- hydro/model/discussion.js | 40 ++++++++++--- hydro/model/file.js | 8 ++- hydro/model/index.js | 1 + hydro/model/problem.js | 13 +++-- hydro/model/setting.js | 35 +++++++---- hydro/script/install.js | 15 ++--- hydro/service/db.js | 6 +- hydro/service/server.js | 13 +++-- hydro/service/setup.js | 41 +++++++------ locales/zh_CN.yaml | 3 +- locales/zh_TW.yaml | 3 +- manager/daemon.js | 7 +++ module/migrate-vijos/script.js | 77 +++++++++++++++++++++---- templates/components/form.html | 1 + templates/components/user.html | 2 +- templates/discussion_main_or_node.html | 8 +-- templates/domain_edit.html | 7 +++ templates/manage_module.html | 11 +--- templates/partials/discussion_list.html | 10 ++-- templates/partials/setting.html | 2 +- templates/setup.html | 12 ++-- 30 files changed, 279 insertions(+), 139 deletions(-) create mode 100644 manager/daemon.js diff --git a/Dockerfile b/Dockerfile index e1432cf6b..bfdd73b3c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,5 @@ FROM mhart/alpine-node:slim-10 WORKDIR /app -ADD .uibuild .build /app/ -RUN mkdir /config -VOLUME [ "/config" ] +ADD .build /app EXPOSE 8888 CMD ["node", ".build/development.js"] diff --git a/build/index.js b/build/index.js index 4606d633c..02f7db1a4 100644 --- a/build/index.js +++ b/build/index.js @@ -56,14 +56,11 @@ async function build(type) { rmdir(root(`.build/module/${f}`)); } } - const modules = [ - 'builtin', 'contest-rule-homework', 'judger', 'migrate-vijos', 'module-pastebin', - 'problem-import-syzoj', 'wiki', - ]; + const modules = await fsp.readdir(root('.build/module')); const j = {}; for (const m of modules) { try { - const d = await fsp.readFile(root(`.build/module/${m}.hydro`)); + const d = await fsp.readFile(root(`.build/module/${m}`)); j[m] = d.toString('base64'); } catch (e) { console.error(`Module pack failed: ${m}`); diff --git a/hydro/error.js b/hydro/error.js index a9836bb17..6a050911c 100644 --- a/hydro/error.js +++ b/hydro/error.js @@ -163,9 +163,9 @@ class RoleAlreadyExistError extends ForbiddenError { } class ProblemNotFoundError extends NotFoundError { - constructor(pid) { + constructor(domainId, pid) { super('ProblemNotFoundError'); - this.params = [pid]; + this.params = [domainId, pid]; } } diff --git a/hydro/handler/discussion.js b/hydro/handler/discussion.js index f46730cd0..9f5056d78 100644 --- a/hydro/handler/discussion.js +++ b/hydro/handler/discussion.js @@ -1,7 +1,9 @@ +const { ObjectID } = require('bson'); const paginate = require('../lib/paginate'); const system = require('../model/system'); const user = require('../model/user'); const discussion = require('../model/discussion'); +const document = require('../model/document'); const { Route, Handler } = require('../service/server'); const { DiscussionNotFoundError, DocumentNotFoundError } = require('../error'); const { @@ -10,17 +12,24 @@ const { PERM_LOGGEDIN, PERM_CREATE_DISCUSSION, PERM_REPLY_DISCUSSION, } = require('../permission'); +const typeMapper = { + problem: document.TYPE_PROBLEM, + contest: document.TYPE_CONTEST, + node: document.TYPE_DISCUSSION_NODE, + training: document.TYPE_TRAINING, + homework: document.TYPE_HOMEWORK, +}; + class DiscussionHandler extends Handler { async _prepare({ - domainId, type = 'node', docId, name, did, drid, drrid, + domainId, type, name, did, drid, drrid, }) { this.checkPerm(PERM_VIEW_DISCUSSION); - docId = docId || name; if (did) { this.ddoc = await discussion.get(domainId, did); if (!this.ddoc) throw new DiscussionNotFoundError(did); - type = this.ddoc.parentType; - docId = this.ddoc.parentId; + type = discussion.typeDisplay[this.ddoc.parentType]; + name = this.ddoc.parentId; if (drid) { this.drdoc = await discussion.getReply(domainId, drid, did); if (!this.drdoc) throw new DiscussionNotFoundError(drid); @@ -37,7 +46,11 @@ class DiscussionHandler extends Handler { // TODO(twd2): do more visibility check eg. contest // TODO(twd2): exclude problem/contest discussions? // TODO(iceboy): continuation based pagination. - this.vnode = await discussion.getVnode(domainId, { parentType: type, parentId: docId }, this); + if (ObjectID.isValid(name)) name = new ObjectID(name); + this.vnode = await discussion.getVnode(domainId, { + parentType: typeMapper[type], + parentId: name, + }, this); if (this.ddoc) { this.ddoc.parentType = this.ddoc.parentType || this.vnode.type; this.ddoc.parentId = this.ddoc.parentId || this.vnode.id; @@ -45,7 +58,7 @@ class DiscussionHandler extends Handler { } } -class DiscussionMainHandler extends DiscussionHandler { +class DiscussionMainHandler extends Handler { async get({ domainId, page = 1 }) { const [ddocs, dpcount] = await paginate( discussion.getMulti(domainId), @@ -57,20 +70,20 @@ class DiscussionMainHandler extends DiscussionHandler { ['Hydro', '/'], ['discussion_main', null], ]; + const vndict = await discussion.getListVnodes(domainId, ddocs, this); this.response.template = 'discussion_main_or_node.html'; this.response.body = { - ddocs, dpcount, udict, page, page_name: 'discussion_main', vnode: {}, path, + ddocs, dpcount, udict, page, page_name: 'discussion_main', vndict, vnode: {}, path, }; } } class DiscussionNodeHandler extends DiscussionHandler { async get({ - domainId, type = 'node', docId, name, page = 1, + domainId, type, name, page = 1, }) { - docId = docId || name; const [ddocs, dpcount] = await paginate( - discussion.getMulti(domainId, { parentType: type, parentId: docId }), + discussion.getMulti(domainId, { parentType: typeMapper[type], parentId: name }), page, await system.get('DISCUSSION_PER_PAGE'), ); @@ -80,6 +93,7 @@ class DiscussionNodeHandler extends DiscussionHandler { ['discussion_main', '/discuss'], [this.vnode.title, null, true], ]; + const vndict = { [typeMapper[type]]: { [name]: this.vnode } }; this.response.template = 'discussion_main_or_node.html'; this.response.body = { ddocs, @@ -87,6 +101,7 @@ class DiscussionNodeHandler extends DiscussionHandler { udict, path, page, + vndict, vnode: this.vnode, page_name: 'discussion_node', }; @@ -99,12 +114,11 @@ class DiscussionCreateHandler extends DiscussionHandler { this.checkPerm(PERM_CREATE_DISCUSSION); } - async get({ type = 'node', docId, name }) { - docId = docId || name; + async get({ type, name }) { const path = [ ['Hydro', '/'], ['discussion_main', '/discuss'], - [this.vnode.title, `/discuss/${type}/${docId}`, true], + [this.vnode.title, `/discuss/${type}/${name}`, true], ['discussion_create', null], ]; this.response.template = 'discussion_create.html'; @@ -112,13 +126,13 @@ class DiscussionCreateHandler extends DiscussionHandler { } async post({ - domainId, type = 'node', docId, name, title, content, highlight, + domainId, type, name, title, content, highlight, }) { - docId = docId || name; this.limitRate('add_discussion', 3600, 30); + if (ObjectID.isValid(name)) name = new ObjectID(name); if (highlight) this.checkPerm(PERM_HIGHLIGHT_DISCUSSION); const did = await discussion.add( - domainId, type, docId, this.user._id, + domainId, typeMapper[type], name, this.user._id, title, content, this.request.ip, highlight, ); this.response.body = { did }; @@ -136,7 +150,7 @@ class DiscussionDetailHandler extends DiscussionHandler { page, await system.get('REPLY_PER_PAGE'), ); - const uids = drdocs.map(domainId, (drdoc) => drdoc.owner); + const uids = drdocs.map((drdoc) => drdoc.owner); uids.push(this.ddoc.owner); for (const drdoc of drdocs) { if (drdoc.reply) { @@ -149,7 +163,7 @@ class DiscussionDetailHandler extends DiscussionHandler { const path = [ ['Hydro', '/'], ['discussion_main', '/discuss'], - [this.vnode.title, `/discuss/${this.ddoc.parentType}`, true], + [this.vnode.title, `/discuss/${discussion.typeDisplay[this.ddoc.parentType]}/${this.ddoc.parentId}`, true], [this.ddoc.title, null, true], ]; this.response.template = 'discussion_detail.html'; @@ -275,10 +289,8 @@ async function apply() { 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/node/:name', module.exports.DiscussionNodeHandler); - Route('/discuss/node/:name/create', module.exports.DiscussionCreateHandler); - Route('/discuss/:type/:docId', module.exports.DiscussionNodeHandler); - Route('/discuss/:type/:docId/create', module.exports.DiscussionCreateHandler); + Route('/discuss/:type/:name', module.exports.DiscussionNodeHandler); + Route('/discuss/:type/:name/create', module.exports.DiscussionCreateHandler); } global.Hydro.handler.discussion = module.exports = { diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 0abfc1e95..ec9c120af 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -100,7 +100,6 @@ class HomeSecurityHandler extends Handler { async get() { // TODO(iceboy): pagination? or limit session count for uid? - // TODO(masnn): UA & IP const sessions = await token.getSessionListByUid(this.user._id); const parsed = []; for (const session of sessions) { @@ -173,8 +172,13 @@ class HomeSettingsHandler extends Handler { } async post(args) { - // FIXME validation - await user.setById(args.domainId, this.user._id, args); + const $set = {}; + for (const key in args) { + if (setting.SETTINGS_BY_KEY[args]) { + $set[key] = args[key]; + } + } + await user.setById(args.domainId, this.user._id, $set); this.back(); } } diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index 79590aa8d..467c6bc92 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -78,10 +78,21 @@ class SystemSettingHandler extends SystemHandler { if (typeof args[key] === 'object') { const subtasks = []; for (const sub in args[key]) { - subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); + const s = setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`]; + if (s) { + if (s.ui === 'number') args[key][sub] = Number(args[key][sub]); + console.log(s); + subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); + } } tasks.push(Promise.all(subtasks)); - } else tasks.push(system.set(key, args[key])); + } else { + const s = setting.SYSTEM_SETTINGS_BY_KEY[key]; + if (s) { + if (s.ui === 'number') args[key] = Number(args[key]); + tasks.push(system.set(key, args[key])); + } + } } await Promise.all(tasks); this.back(); diff --git a/hydro/handler/record.js b/hydro/handler/record.js index 60142483c..a516e473b 100644 --- a/hydro/handler/record.js +++ b/hydro/handler/record.js @@ -25,9 +25,10 @@ class RecordListHandler extends RecordHandler { ]; } const rdocs = await record.getMany(domainId, q, { _id: -1 }, page, await system.get('RECORD_PER_PAGE')); + console.log(rdocs); const [udict, pdict] = await Promise.all([ user.getList(domainId, rdocs.map((rdoc) => rdoc.uid)), - problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid)), + problem.getList(domainId, rdocs.map((rdoc) => rdoc.pid), false), ]); const path = [ ['Hydro', '/'], diff --git a/hydro/lib/nav.js b/hydro/lib/nav.js index 56869bc56..941d43784 100644 --- a/hydro/lib/nav.js +++ b/hydro/lib/nav.js @@ -22,6 +22,6 @@ Item('/homework', 'homework_main', 'homework', permission.PERM_VIEW_HOMEWORK); Item('/discuss', 'discussion_main', 'discussion', permission.PERM_VIEW_DISCUSSION); Item('/c', 'contest_main', 'contest', permission.PERM_VIEW_CONTEST); Item('/domain/dashboard', 'domain_dashboard', 'domain', permission.PERM_MANAGE); -Item('/manage/dashboard', 'manage_dashboard', 'manage', null, (handler) => handler.user.priv === 1); +Item('/manage/dashboard', 'manage', 'manage', null, (handler) => handler.user.priv === 1); global.Hydro.lib.nav = module.exports = Item; diff --git a/hydro/model/contest.js b/hydro/model/contest.js index a83526d20..4576a2b14 100644 --- a/hydro/model/contest.js +++ b/hydro/model/contest.js @@ -242,7 +242,7 @@ async function edit(domainId, tid, $set, type = document.TYPE_CONTEST) { * @param {string} domainId * @param {ObjectID} tid * @param {number} type - * @returns {Tdoc} + * @returns {Promise} */ async function get(domainId, tid, type = document.TYPE_CONTEST) { let tdoc; diff --git a/hydro/model/discussion.js b/hydro/model/discussion.js index d17e10627..c0a4115e0 100644 --- a/hydro/model/discussion.js +++ b/hydro/model/discussion.js @@ -2,9 +2,18 @@ const { ObjectID } = require('bson'); const problem = require('./problem'); const document = require('./document'); const contest = require('./contest'); +const training = require('./training'); const { PERM_VIEW_PROBLEM_HIDDEN } = require('../permission'); const { DocumentNotFoundError } = require('../error'); +const typeDisplay = { + [document.TYPE_PROBLEM]: 'problem', + [document.TYPE_CONTEST]: 'contest', + [document.TYPE_DISCUSSION_NODE]: 'node', + [document.TYPE_TRAINING]: 'training', + [document.TYPE_HOMEWORK]: 'homework', +}; + function add(domainId, parentType, parentId, owner, title, content, ip = null, highlight = false) { return document.add( domainId, content, owner, document.TYPE_DISCUSSION, @@ -115,9 +124,7 @@ function getStatus(domainId, did, uid) { } function addNode(domainId, _id, category) { - return document.add( - domainId, _id, 1, document.TYPE_DISCUSSION_NODE, null, null, null, { category }, - ); + return document.add(domainId, category, 1, document.TYPE_DISCUSSION_NODE, _id); } function getNode(domainId, _id) { @@ -125,17 +132,32 @@ function getNode(domainId, _id) { } async function getVnode(domainId, ddoc, handler) { - if (ddoc.parentType === 'problem') { + if (ddoc.parentType === document.TYPE_PROBLEM) { const pdoc = await problem.getById(domainId, ddoc.parentId); if (!pdoc) return null; if (pdoc.hidden && handler) handler.checkPerm(PERM_VIEW_PROBLEM_HIDDEN); return { ...pdoc, type: ddoc.parentType, id: ddoc.parentId }; - } if (ddoc.parentType === 'contest') { + } + if (ddoc.parentType === document.TYPE_CONTEST) { const tdoc = await contest.get(domainId, ddoc.parentId); return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId }; - } if (ddoc.parentType === 'node') { + } + if (ddoc.parentType === document.TYPE_DISCUSSION_NODE) { const ndoc = await getNode(domainId, ddoc.parentId); - return { title: ndoc._id, type: ddoc.parentType, id: ddoc.parentId }; + return { + ...ndoc, + title: ddoc.parentId, + type: ddoc.parentType, + id: ddoc.parentId, + }; + } + if (ddoc.parentType === document.TYPE_TRAINING) { + const tdoc = await training.get(domainId, ddoc.parentId); + return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId }; + } + if (ddoc.parentType === document.TYPE_HOMEWORK) { + const tdoc = await contest.get(domainId, ddoc.parentId, document.TYPE_HOMEWORK); + return { ...tdoc, type: ddoc.parentType, id: ddoc.parentId }; } return { title: 'Missing Node', @@ -147,13 +169,15 @@ async function getVnode(domainId, ddoc, handler) { async function getListVnodes(domainId, ddocs, handler) { const res = {}; for (const ddoc of ddocs) { + if (!res[ddoc.parentType]) res[ddoc.parentType] = {}; // FIXME no-await-in-loop - res[ddoc._id] = await getVnode(domainId, ddoc, handler); // eslint-disable-line no-await-in-loop + res[ddoc.parentType][ddoc.parentId] = await getVnode(domainId, ddoc, handler); // eslint-disable-line no-await-in-loop } return res; } global.Hydro.model.discussion = module.exports = { + typeDisplay, add, get, edit, diff --git a/hydro/model/file.js b/hydro/model/file.js index 18f589e11..73e513a16 100644 --- a/hydro/model/file.js +++ b/hydro/model/file.js @@ -30,8 +30,12 @@ function del(_id) { } async function inc(_id) { - const file = await coll.findOne({ _id }); - file.count++; + const doc = await coll.findOneAndUpdate( + { _id }, + { $inc: { count: 1 } }, + { returnOriginal: false }, + ); + return (doc.value || {}).count; } async function dec(_id) { diff --git a/hydro/model/index.js b/hydro/model/index.js index 822237a68..45eb42bcf 100644 --- a/hydro/model/index.js +++ b/hydro/model/index.js @@ -14,5 +14,6 @@ module.exports = { task: require('./task'), message: require('./message'), setting: require('./setting'), + discussion: require('./discussion'), file: require('./file'), }; diff --git a/hydro/model/problem.js b/hydro/model/problem.js index 8df78f35d..b9724df42 100644 --- a/hydro/model/problem.js +++ b/hydro/model/problem.js @@ -45,7 +45,7 @@ async function get(domainId, pid, uid = null) { const pdoc = Number.isInteger(pid) ? await document.get(domainId, document.TYPE_PROBLEM, pid) : (await document.getMulti(domainId, document.TYPE_PROBLEM, { pid }).toArray())[0]; - if (!pdoc) throw new ProblemNotFoundError(pid); + if (!pdoc) throw new ProblemNotFoundError(domainId, pid); if (uid) { pdoc.psdoc = await document.getStatus(domainId, document.TYPE_PROBLEM, pdoc.docId, uid); } @@ -110,10 +110,15 @@ async function random(domainId, query) { } return null; } -async function getList(domainId, pids) { +async function getList(domainId, pids, doThrow = true) { const r = {}; - // eslint-disable-next-line no-await-in-loop - for (const pid of pids) r[pid] = await get(domainId, pid); + if (doThrow) { + // eslint-disable-next-line no-await-in-loop + for (const pid of pids) r[pid] = await get(domainId, pid); + } else { + // eslint-disable-next-line no-await-in-loop + for (const pid of pids) r[pid] = await get(domainId, pid).catch((e) => e); + } return r; } diff --git a/hydro/model/setting.js b/hydro/model/setting.js index 951a9bbd6..d317bfab7 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.js @@ -41,7 +41,7 @@ const ACCOUNT_SETTINGS = [ const SETTINGS = [...PREFERENCE_SETTINGS, ...ACCOUNT_SETTINGS]; const SETTINGS_BY_KEY = {}; -for (const setting in SETTINGS) SETTINGS_BY_KEY[setting.key] = setting; +for (const setting of SETTINGS) SETTINGS_BY_KEY[setting.key] = setting; const SYSTEM_SETTINGS = [ Setting('setting_basic', 'user.register', null, false, 'checkbox', 'Allow register'), @@ -49,19 +49,32 @@ const SYSTEM_SETTINGS = [ Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'), Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password'), Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'), - Setting('setting_smtp', 'smtp.port', null, 465, 'text', 'SMTP Server Port'), + Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'), Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'), Setting('setting_smtp', 'smtp.secure', null, false, 'checkbox', 'SSL'), - Setting('setting_server', 'listen.port', null, 8888, 'text', 'Server Port', ''), - Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'text', 'Problems per Page'), - Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'text', 'Contests per Page'), - Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'text', 'Discussion per Page'), - Setting('setting_constant', 'RECORD_PER_PAGE', null, 100, 'text', 'Record per Page'), - Setting('setting_constant', 'SOLUTION_PER_PAGE', null, 20, 'text', 'Solutions per Page'), - Setting('setting_constant', 'TRAINING_PER_PAGE', null, 10, 'text', 'Training per Page'), - Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'text', 'Reply per Page'), + Setting('setting_server', 'server.hostname', null, null, 'text', 'Server Hostname'), + Setting('setting_server', 'server.host', null, null, 'text', 'Server Host'), + Setting('setting_server', 'server.url', null, null, 'text', 'Server BaseURL'), + Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'), + Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page'), + Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page'), + Setting('setting_constant', 'DISCUSSION_PER_PAGE', null, 50, 'number', 'Discussion per Page'), + Setting('setting_constant', 'RECORD_PER_PAGE', null, 100, 'number', 'Record per Page'), + Setting('setting_constant', 'SOLUTION_PER_PAGE', null, 20, 'number', 'Solutions per Page'), + Setting('setting_constant', 'TRAINING_PER_PAGE', null, 10, 'number', 'Training per Page'), + Setting('setting_constant', 'REPLY_PER_PAGE', null, 50, 'number', 'Reply per Page'), ]; +const SYSTEM_SETTINGS_BY_KEY = {}; + +for (const setting of SYSTEM_SETTINGS) SYSTEM_SETTINGS_BY_KEY[setting.key] = setting; + global.Hydro.model.setting = module.exports = { - Setting, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, SETTINGS_BY_KEY, SYSTEM_SETTINGS, + Setting, + PREFERENCE_SETTINGS, + ACCOUNT_SETTINGS, + SETTINGS, + SETTINGS_BY_KEY, + SYSTEM_SETTINGS, + SYSTEM_SETTINGS_BY_KEY, }; diff --git a/hydro/script/install.js b/hydro/script/install.js index 3dc4d1daa..151504b6a 100644 --- a/hydro/script/install.js +++ b/hydro/script/install.js @@ -19,7 +19,7 @@ const allowFail = async (func, ...args) => { return res; }; -async function run(username, password) { +async function run({ username, password } = {}) { const def = { PROBLEM_PER_PAGE: 100, RECORD_PER_PAGE: 100, @@ -39,7 +39,7 @@ async function run(username, password) { 'smtp.pass': '', 'smtp.secure': false, 'db.ver': 1, - 'listen.port': 8888, + 'server.port': 8888, 'session.keys': ['Hydro'], 'session.secure': false, 'session.saved_expire_seconds': 3600 * 24, @@ -49,7 +49,10 @@ async function run(username, password) { }; let tasks = []; for (const key in def) { - tasks.push(system.set(key, def[key])); + tasks.push(system.get(key).then((value) => { + if (!value) return system.set(key, def[key]); + return Promise.resolve(); + })); } await Promise.all(tasks); tasks = [ @@ -91,12 +94,6 @@ async function run(username, password) { }, { upsert: true }), ); } - for (const category in builtin.DEFAULT_NODES) { - const nodes = builtin.DEFAULT_NODES[category]; - for (const node of nodes) { - tasks.push(discussion.addNode(node.name, category)); - } - } await Promise.all(tasks); } diff --git a/hydro/service/db.js b/hydro/service/db.js index dd5019d9d..e6c4b533f 100644 --- a/hydro/service/db.js +++ b/hydro/service/db.js @@ -10,9 +10,9 @@ mongourl += `${options.host}:${options.port}/${options.name}`; */ let db = null; Mongo.MongoClient.connect(mongourl, { useNewUrlParser: true, useUnifiedTopology: true }) - .then((Database) => { - db = Database.db(options.name); - db.Db = Database; + .then((Client) => { + db = Client.db(options.name); + db.Client = Client; bus.publish('system_database_connected', null); }); diff --git a/hydro/service/server.js b/hydro/service/server.js index 4df0421ca..cf1d65311 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -98,6 +98,8 @@ class Handler { constructor(ctx) { this.ctx = ctx; this.request = { + host: ctx.request.host, + hostname: ctx.request.hostname, ip: ctx.request.ip, headers: ctx.request.headers, cookies: ctx.cookies, @@ -193,8 +195,8 @@ class Handler { token.TYPE_SESSION, this._handler.expireSeconds, { - update_ip: this.request.ip, - update_ua: this.request.headers['user-agent'] || '', + updateIp: this.request.ip, + updateUa: this.request.headers['user-agent'] || '', }, ) : { uid: 1 }; if (!this.session) this.session = { uid: 1 }; @@ -302,6 +304,9 @@ function Route(route, RouteHandler, permission = null) { const args = { ...ctx.params, ...ctx.query, ...ctx.request.body, domainId: 'system', }; + if (h.request.host !== await system.get('server.host')) { + args.domainId = h.request.url.split('//')[1].split(`.${await system.get('server.host')}`)[0] || ''; + } if (h.___prepare) await h.___prepare(args); if (permission) h.checkPerm(permission); @@ -437,8 +442,8 @@ async function start() { app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); Route('*', Handler); - server.listen(await system.get('listen.port')); - console.log('Server listening at: %s', await system.get('listen.port')); + server.listen(await system.get('server.port')); + console.log('Server listening at: %s', await system.get('server.port')); } global.Hydro.service.server = module.exports = { diff --git a/hydro/service/setup.js b/hydro/service/setup.js index 448048ab7..d23dd848c 100644 --- a/hydro/service/setup.js +++ b/hydro/service/setup.js @@ -1,8 +1,8 @@ const fs = require('fs'); +const os = require('os'); const path = require('path'); const http = require('http'); const Koa = require('koa'); -const morgan = require('koa-morgan'); const Body = require('koa-body'); const Router = require('koa-router'); const cache = require('koa-static-cache'); @@ -19,20 +19,11 @@ class Loader { } } -class Nunjucks extends nunjucks.Environment { - constructor() { - super(new Loader(), { autoescape: true, trimBlocks: true }); - this.addFilter('json', (self) => JSON.stringify(self), false); - this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64')); - } -} -const env = new Nunjucks(); +const env = new nunjucks.Environment(new Loader(), { autoescape: true, trimBlocks: true }); + function render(name) { return new Promise((resolve, reject) => { env.render(name, { - typeof: (o) => typeof o, - static_url: (str) => `/${str}`, - handler: { renderTitle: (str) => str }, _: (str) => str, }, (err, res) => { if (err) reject(err); @@ -40,15 +31,16 @@ function render(name) { }); }); } + async function setup() { const app = new Koa(); const server = http.createServer(app.callback()); const router = new Router(); app.keys = ['Hydro']; - app.use(cache(path.join(process.cwd(), '.uibuild'), { + app.use(cache(path.join(os.tmpdir(), 'hydro', 'builtin'), { maxAge: 365 * 24 * 60 * 60, })); - app.use(Body({ formidable: { maxFileSize: 256 * 1024 * 1024 } })); + app.use(Body()); router.get('/', async (ctx) => { ctx.body = await render('setup.html'); ctx.response.type = 'text/html'; @@ -66,9 +58,23 @@ async function setup() { useNewUrlParser: true, useUnifiedTopology: true, }); const db = Database.db(name); - // Check Write Access - await db.collection('test').insertOne({ test: 'test' }); - await db.collection('test').deleteOne({ test: 'test' }); + await Promise.all([ + db.collection('system').updateOne( + { _id: 'server.host' }, + { $set: { value: ctx.request.host } }, + { upsert: true }, + ), + db.collection('system').updateOne( + { _id: 'server.hostname' }, + { $set: { value: ctx.request.hostname } }, + { upsert: true }, + ), + db.collection('system').updateOne( + { _id: 'server.url' }, + { $set: { value: ctx.request.href } }, + { upsert: true }, + ), + ]); fs.writeFileSync(path.resolve(process.cwd(), 'config.json'), JSON.stringify({ host, port, name, username, password, })); @@ -79,7 +85,6 @@ async function setup() { } }); }); - app.use(morgan(':method :url :status :res[content-length] - :response-time ms')); app.use(router.routes()).use(router.allowedMethods()); server.listen(8888); console.log('Server listening at: 8888'); diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index 87b70e021..58f67a794 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -307,7 +307,8 @@ Create Domain: 创建域 Properties: 属性 Access Control: 访问控制 domain_main: 首页 -manage: 管理域 +manage: 管理 +manage_domain: 管理域 manage_dashboard: 概况 manage_discussion: 讨论节点 manage_edit: 编辑域资料 diff --git a/locales/zh_TW.yaml b/locales/zh_TW.yaml index a03ea6455..93fce61ff 100644 --- a/locales/zh_TW.yaml +++ b/locales/zh_TW.yaml @@ -301,7 +301,8 @@ Create Domain: 創建域 Properties: 屬性 Access Control: 訪問控制 domain_main: 首頁 -manage: 管理域 +manage: 管理 +manage_domain: 管理域 manage_dashboard: 概況 manage_edit: 編輯域資料 manage_join_applications: 加域申請 diff --git a/manager/daemon.js b/manager/daemon.js new file mode 100644 index 000000000..09cf49ff9 --- /dev/null +++ b/manager/daemon.js @@ -0,0 +1,7 @@ +const fs = require('fs'); +const path = require('path'); +const child = require('child_process'); + +const appPath = path.resolve(__dirname, '..', 'hydro', 'development.js'); + +const app = child.fork(appPath); diff --git a/module/migrate-vijos/script.js b/module/migrate-vijos/script.js index 1c7681feb..a3d4c9d6c 100644 --- a/module/migrate-vijos/script.js +++ b/module/migrate-vijos/script.js @@ -1,13 +1,12 @@ /* eslint-disable no-await-in-loop */ const { mongodb } = global.Hydro.nodeModules; const dst = global.Hydro.service.db; +const { file, problem, discussion } = global.Hydro.model; const map = {}; const pid = (id) => { if (map[id.toString()]) return map[id.toString()]; - if (id.generationTime) return id; - id = id.toString(); return id; }; @@ -114,6 +113,8 @@ const tasks = { res[mapper[key]] = doc[key]; } else if (typeof mapper[key] === 'object') { res[mapper[key].field] = mapper[key].processer(doc[key]); + } else if (mapper[key] === null) { + // Ignore this key } else { console.log('Unknown key:', key); } @@ -214,7 +215,7 @@ const tasks = { 'fs.chunks': async (doc) => doc, file: async (doc) => ({ _id: doc._id, - count: doc.metadata.link, + count: 0, secret: doc.metadata.secret, size: doc.length, md5: doc.md5, @@ -223,7 +224,7 @@ const tasks = { const cursor = { user: (s) => s.collection('user').find(), - document: (s) => s.collection('document').find(), + document: (s) => s.collection('document').find({ doc_type: { $ne: 20 } }), 'document.status': (s) => s.collection('document.status').find(), record: (s) => s.collection('record').find(), 'fs.files': (s) => s.collection('fs.files').find(), @@ -231,6 +232,29 @@ const cursor = { file: (s) => s.collection('fs.files').find(), }; +async function discussionNode(src, report) { + const count = await src.collection('document').find({ doc_type: 20 }).count(); + await report({ progress: 1, message: `discussion.node: ${count}` }); + const total = Math.floor(count / 5); + for (let i = 0; i <= total; i++) { + const docs = await src.collection('document') + .find({ doc_type: 20 }).skip(i * 5).limit(5) + .toArray(); + for (const doc of docs) { + const t = []; + for (const item of doc.content || []) { + const category = item[0]; + const nodes = item[1]; + for (const node of nodes || []) { + t.push(discussion.addNode(doc.domain_id, node.name, category)); + } + } + await Promise.all(t).catch((e) => e); + } + await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); + } +} + async function fixPid(report) { const count = await dst.collection('document').find({ docType: 10 }).count(); await report({ progress: 1, message: `Fix pid: ${count}` }); @@ -239,12 +263,28 @@ async function fixPid(report) { const docs = await dst.collection('document') .find({ docType: 10 }).skip(i * 50).limit(50) .toArray(); + const t = []; for (const doc of docs) { - dst.collection('document').updateOne( - { _id: doc._id }, - { $set: { pid: doc.pid || doc.docId.toString() } }, + t.push( + dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { pid: doc.pid || doc.docId.toString() } }, + ), ); + if (doc.data && doc.data.generationTime) { + t.push(file.inc(doc.data)); + } else if (doc.data) { + const pdoc = await problem.get(doc.data.domain, pid(doc.data.pid)); + t.push( + file.inc(pdoc.data), + dst.collection('document').updateOne( + { _id: doc._id }, + { $set: { data: pdoc.data } }, + ), + ); + } } + await Promise.all(t); await report({ progress: Math.round(100 * ((i + 1) / (total + 1))) }); } } @@ -273,9 +313,23 @@ async function task(name, src, report) { if (d.domainId && d.docId && d.docType) { const query = { domainId: d.domainId, docId: d.docId, docType: d.docType }; if (d.uid) query.uid = d.uid; - res.push(dst.collection(name).updateOne( - query, { $set: docWithoutDid }, { upsert: true }, - )); + res.push((async () => { + const data = await dst.collection(name).findOne(query); + if (data) { + await dst.collection(name).updateOne(query, { $set: docWithoutId }); + } else if (d._id) { + const dat = await dst.collection(name).findOne({ _id: d._id }); + if (dat) { + await dst.collection(name).updateOne({ + _id: d._id, + }, { $set: docWithoutId }); + } else { + await dst.collection(name).insertOne(d); + } + } else { + await dst.collection(name).insertOne(d); + } + })()); } else if (d._id) { res.push(dst.collection(name).updateOne({ _id: d._id, @@ -290,7 +344,6 @@ async function task(name, src, report) { lastProgress = progress; } } - await fixPid(report); } async function migrateVijos({ @@ -325,6 +378,8 @@ async function migrateVijos({ await dst.collection('user').deleteMany({ _id: { $nin: [0, 1] } }); const d = ['user', 'document', 'document.status', 'record', 'file']; for (const i of d) await task(i, src, report); + await fixPid(report); + await discussionNode(src, report); return true; } diff --git a/templates/components/form.html b/templates/components/form.html index 9f445f845..0da63dcb7 100644 --- a/templates/components/form.html +++ b/templates/components/form.html @@ -1,3 +1,4 @@ +{# No Compression #} {% macro form_begin(args) %} {% set label = args.label %} {% set columns = args.columns %} diff --git a/templates/components/user.html b/templates/components/user.html index be6203157..41279e8dd 100644 --- a/templates/components/user.html +++ b/templates/components/user.html @@ -13,7 +13,7 @@ {% if badge %} -{% if modbadge and handler.hasPerm(perm.PERM_MOD_BADGE) %} +{% if modbadge and udoc.hasPerm(perm.PERM_MOD_BADGE) %} {% endif %} {% endif %} diff --git a/templates/discussion_main_or_node.html b/templates/discussion_main_or_node.html index 02c215a8a..8c06a9f8f 100644 --- a/templates/discussion_main_or_node.html +++ b/templates/discussion_main_or_node.html @@ -7,15 +7,15 @@
- {% if vnode.parentType == 'problem' %} + {% if vnode.parentType == model.document.TYPE_PROBLEM %} {% set pdoc = vnode %} {% set owner_udoc = udict[vnode.owner] %} {% include "partials/problem_sidebar.html" %} - {% elif vnode.parentType == 'contest' %} + {% elif vnode.parentType == model.document.TYPE_CONTEST %} {% set tdoc = vnode %} {% set owner_udoc = udict[vnode.owner] %} {% include "partials/contest_sidebar.html" %} - {% elif vnode.parentType == 'node' %} + {% elif vnode.parentType == model.document.TYPE_DISCUSSION_NODE %}
{% if vnode.pic %}
{% endif %}
@@ -24,7 +24,7 @@
{% if vnode %} {% if handler.hasPerm(perm.PERM_CREATE_DISCUSSION) %} -

{{ _('Create a Discussion') }}

+

{{ _('Create a Discussion') }}

{% else %} {% if not handler.hasPerm(perm.PERM_LOGGEDIN) %}

{{ _('Login to Create a Discussion') }}

diff --git a/templates/domain_edit.html b/templates/domain_edit.html index 0bd0f7235..63b4fd8a9 100644 --- a/templates/domain_edit.html +++ b/templates/domain_edit.html @@ -3,6 +3,13 @@
+ {{ form.form_text({ + label:'ID', + name:'id', + value:domain._id, + disabled:true, + required:true + }) }} {{ form.form_text({ label:'Name', name:'name', diff --git a/templates/manage_module.html b/templates/manage_module.html index b01ce8611..ab5fe8366 100644 --- a/templates/manage_module.html +++ b/templates/manage_module.html @@ -6,18 +6,12 @@

{{ _('Add Module') }}

-
+
-
- -
@@ -55,10 +49,9 @@ {{ size(module.size) if module.size else '?' }} {{ module.version|default('?') }} - {{ _('Download') }} - + diff --git a/templates/partials/discussion_list.html b/templates/partials/discussion_list.html index 37763ee1f..ddb06a3f4 100644 --- a/templates/partials/discussion_list.html +++ b/templates/partials/discussion_list.html @@ -15,18 +15,18 @@
{% for setting in family_settings %} - {% if setting.ui == 'text' or setting.ui == 'password' %} + {% if setting.ui == 'text' or setting.ui == 'password' or setting.ui == 'number' %} {{ form.form_text({ type:setting.ui, label:setting.name, diff --git a/templates/setup.html b/templates/setup.html index 85b8d3976..9b2e16ec6 100644 --- a/templates/setup.html +++ b/templates/setup.html @@ -7,8 +7,7 @@ - - + {{ _('Setup') }}