From 57f711ed4fdb5b6846d1770e0c8147ee480da3c1 Mon Sep 17 00:00:00 2001 From: undefined Date: Fri, 3 Jul 2020 13:37:04 +0800 Subject: [PATCH] system check & setting_secret --- .eslintrc.js | 1 + hydro/check.js | 69 ++++++++ hydro/handler/home.js | 8 +- hydro/handler/manage.js | 32 +++- hydro/handler/notfound.js | 8 +- hydro/lib/md5.js | 2 +- hydro/lib/template.js | 1 + hydro/model/builtin.js | 1 - hydro/model/setting.js | 23 ++- hydro/model/user.js | 41 ++--- hydro/service/server.js | 160 ++++++++---------- locales/zh_CN.yaml | 2 +- locales/zh_TW.yaml | 2 +- templates/manage_dashboard.html | 12 +- ...nage_settings.html => manage_setting.html} | 0 templates/partials/setting.html | 18 +- ui/misc/typography.styl | 15 ++ ui/pages/manage_dashboard.page.js | 34 ++++ 18 files changed, 295 insertions(+), 134 deletions(-) create mode 100644 hydro/check.js rename templates/{manage_settings.html => manage_setting.html} (100%) create mode 100644 ui/pages/manage_dashboard.page.js diff --git a/.eslintrc.js b/.eslintrc.js index 0f7faa20b..459254b0b 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -28,5 +28,6 @@ module.exports = { 'no-nested-ternary': 'off', 'no-multi-assign': 'off', 'no-return-await': 'off', + 'no-bitwise': 'off', }, }; diff --git a/hydro/check.js b/hydro/check.js new file mode 100644 index 000000000..f0ab293aa --- /dev/null +++ b/hydro/check.js @@ -0,0 +1,69 @@ +// TODO check more + +const os = require('os'); +const superagent = require('superagent'); +const system = require('./model/system'); +const db = require('./service/db'); + +const c = {}; + +async function checkDb(log, warn, error) { + try { + const coll = db.collection('check'); + const d = await coll.findOne({ _id: 'check' }); + if (!d) await coll.insertOne({ _id: 'check', value: 'check' }); + await coll.createIndex('check'); + await coll.deleteOne({ _id: 'check' }); + } catch (e) { + error(`Mongo Error: Database read-write failed.\n${e.message}`); + } +} + +async function checkPerm(log, warn) { + const { username } = os.userInfo(); + if (username === 'root') warn('Hydro should not be run as root.'); + // TODO check cwd read-write +} + +async function checkMail(log, warn) { + const from = await system.get('smtp.from'); + if (!from) warn('SMTP account was not provided, email verification disabled.'); +} + +async function checkProxy(log, warn) { + const [github, google, proxy] = await system.getMany([ + 'oauth.githubappid', 'oauth.googleappid', 'proxy', + ]); + if (proxy) { + superagent.get('https://www.google.com/').timeout(10000).proxy(proxy) + .catch(() => warn('The proxy configured seem to be invalid.')); + } else if (github || google) { + warn('OAuth enabled. But for well-known reasons, a proxy is required.'); + } +} + +const checks = [ + checkDb, + checkPerm, + checkMail, + checkProxy, +]; + +async function start(log, warn, error, cb) { + const id = String.random(6); + cb(id); + for (const check of checks) { + if (c[id]) { + delete c[id]; + return; + } + // eslint-disable-next-line no-await-in-loop + await check(log, warn, error); + } +} + +async function cancel(id) { + c[id] = true; +} + +module.exports = { start, cancel }; diff --git a/hydro/handler/home.js b/hydro/handler/home.js index 995dfbd6e..7294c9162 100644 --- a/hydro/handler/home.js +++ b/hydro/handler/home.js @@ -132,8 +132,12 @@ class HomeSecurityHandler extends Handler { ); } } + const path = [ + ['Hydro', 'homepage'], + ['home_security', null], + ]; this.response.template = 'home_security.html'; - this.response.body = { sessions, geoipProvider: (geoip || {}).provider }; + this.response.body = { sessions, geoipProvider: (geoip || {}).provider, path }; if (useragent) this.response.body.icon = useragent.icon; } @@ -202,7 +206,7 @@ class HomeSettingsHandler extends Handler { async post(args) { const $set = {}; for (const key in args) { - if (setting.SETTINGS_BY_KEY[key]) { + if (setting.SETTINGS_BY_KEY[key] && !setting.SETTINGS_BY_KEY[key].disabled) { $set[key] = args[key]; } } diff --git a/hydro/handler/manage.js b/hydro/handler/manage.js index 6178a34bd..83bed29fe 100644 --- a/hydro/handler/manage.js +++ b/hydro/handler/manage.js @@ -3,9 +3,12 @@ const system = require('../model/system'); const { STATUS, PRIV } = require('../model/builtin'); const record = require('../model/record'); const judge = require('./judge'); -const { Route, Handler } = require('../service/server'); +const { + Route, Connection, Handler, ConnectionHandler, +} = require('../service/server'); const hpm = require('../lib/hpm'); const loader = require('../loader'); +const check = require('../check'); class SystemHandler extends Handler { async prepare() { @@ -24,6 +27,24 @@ class SystemMainHandler extends SystemHandler { } } +class SystemCheckConnHandler extends ConnectionHandler { + async prepare() { + this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); + await this.check(); + } + + async check() { + const log = (payload) => this.send({ type: 'log', payload }); + const warn = (payload) => this.send({ type: 'warn', payload }); + const error = (payload) => this.send({ type: 'error', payload }); + await check.start(log, warn, error, (id) => { this.id = id; }); + } + + async cleanup() { + check.cancel(this.token); + } +} + class SystemDashboardHandler extends SystemHandler { async get() { this.response.template = 'manage_dashboard.html'; @@ -111,8 +132,8 @@ class SystemScriptHandler extends SystemHandler { class SystemSettingHandler extends SystemHandler { async get() { - this.response.template = 'manage_settings.html'; - this.response.body.path.push(['manage_settings', null]); + this.response.template = 'manage_setting.html'; + this.response.body.path.push(['manage_setting', null]); this.response.body.current = {}; this.response.body.settings = setting.SYSTEM_SETTINGS; for (const s of this.response.body.settings) { @@ -129,7 +150,7 @@ class SystemSettingHandler extends SystemHandler { const subtasks = []; for (const sub in args[key]) { const s = setting.SYSTEM_SETTINGS_BY_KEY[`${key}.${sub}`]; - if (s) { + if (s && !((s.flag & setting.FLAG_SECRET) && args[key][sub] === '')) { if (s.ui === 'number') args[key][sub] = Number(args[key][sub]); subtasks.push(system.set(`${key}.${sub}`, args[key][sub])); } @@ -137,7 +158,7 @@ class SystemSettingHandler extends SystemHandler { tasks.push(Promise.all(subtasks)); } else { const s = setting.SYSTEM_SETTINGS_BY_KEY[key]; - if (s) { + if (s && !((s.flag & setting.FLAG_SECRET) && args[key] === '')) { if (s.ui === 'number') args[key] = Number(args[key]); tasks.push(system.set(key, args[key])); } @@ -154,6 +175,7 @@ async function apply() { Route('manage_script', '/manage/script', SystemScriptHandler); Route('manage_module', '/manage/module', SystemModuleHandler); Route('manage_setting', '/manage/setting', SystemSettingHandler); + Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } global.Hydro.handler.manage = module.exports = apply; diff --git a/hydro/handler/notfound.js b/hydro/handler/notfound.js index 9ef3108d5..d761366c2 100644 --- a/hydro/handler/notfound.js +++ b/hydro/handler/notfound.js @@ -1,10 +1,16 @@ +/* eslint-disable no-empty-function */ +/* eslint-disable class-methods-use-this */ const { Route, Handler } = require('../service/server'); const { NotFoundError } = require('../error'); class NotFoundHandler extends Handler { - async prepare() { // eslint-disable-line class-methods-use-this + async prepare() { throw new NotFoundError(); } + + async get() { } + + async post() { } } async function apply() { diff --git a/hydro/lib/md5.js b/hydro/lib/md5.js index 27cff470a..ce026606d 100644 --- a/hydro/lib/md5.js +++ b/hydro/lib/md5.js @@ -1,7 +1,7 @@ // From https://github.com/pvorb/node-md5/blob/master/md5.js // BSD 3-Clause "New" or "Revised" License -/* eslint-disable no-bitwise */ /* eslint-disable max-len */ +/* eslint-disable max-len */ function safeAdd(x, y) { const lsw = (x & 0xffff) + (y & 0xffff); const msw = (x >> 16) + (y >> 16) + (lsw >> 16); diff --git a/hydro/lib/template.js b/hydro/lib/template.js index 27438b29c..9977097d0 100644 --- a/hydro/lib/template.js +++ b/hydro/lib/template.js @@ -23,6 +23,7 @@ class Nunjucks extends nunjucks.Environment { }); this.addFilter('ansi', (self) => misc.ansiToHtml(self)); this.addFilter('base64_encode', (s) => Buffer.from(s).toString('base64')); + this.addFilter('bitand', (self, val) => self & val); } } const env = new Nunjucks(); diff --git a/hydro/model/builtin.js b/hydro/model/builtin.js index ba9a8f1ae..2ade5d280 100644 --- a/hydro/model/builtin.js +++ b/hydro/model/builtin.js @@ -1,4 +1,3 @@ -/* eslint-disable no-bitwise */ const PERM = { PERM_NONE: '', diff --git a/hydro/model/setting.js b/hydro/model/setting.js index 93d5c9db0..ca395065f 100644 --- a/hydro/model/setting.js +++ b/hydro/model/setting.js @@ -9,6 +9,10 @@ for (const country of countries) { } const timezones = Array.from(tzs).sort().map((tz) => [tz, tz]); +const FLAG_HIDDEN = 1; +const FLAG_DISABLED = 2; +const FLAG_SECRET = 4; + const PREFERENCE_SETTINGS = []; const ACCOUNT_SETTINGS = []; const DOMAIN_SETTINGS = []; @@ -23,9 +27,9 @@ const SYSTEM_SETTINGS_BY_KEY = {}; const Setting = ( family, key, range = null, value = null, type = 'text', name = '', - desc = '', hidden = false, disabled = false, + desc = '', flag = 0, ) => ({ - family, key, range, value, type, name, desc, hidden, disabled, + family, key, range, value, type, name, desc, flag, }); const PreferenceSetting = (...settings) => { @@ -93,11 +97,15 @@ DomainSetting( Setting('setting_domain', 'name', null, 'New domain', 'text', 'name'), Setting('setting_domain', 'gravatar', null, '', 'text', 'gravatar', 'Will be used as the domain icon.'), Setting('setting_domain', 'bulletin', null, '', 'markdown', 'Bulletin'), + Setting('storage', 'nAccept', null, 0, 'number', 'nAccept', null, FLAG_HIDDEN & FLAG_DISABLED), + Setting('storage', 'nSubmit', null, 0, 'number', 'nSubmit', null, FLAG_HIDDEN & FLAG_DISABLED), + Setting('storage', 'nLike', null, 0, 'number', 'nLike', null, FLAG_HIDDEN & FLAG_DISABLED), + Setting('storage', 'rating', null, 1500, 'number', 'rating', null, FLAG_HIDDEN & FLAG_DISABLED), ); SystemSetting( Setting('setting_smtp', 'smtp.user', null, null, 'text', 'SMTP Username'), - Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password'), + Setting('setting_smtp', 'smtp.pass', null, null, 'password', 'SMTP Password', null, FLAG_SECRET), Setting('setting_smtp', 'smtp.host', null, null, 'text', 'SMTP Server Host'), Setting('setting_smtp', 'smtp.port', null, 465, 'number', 'SMTP Server Port'), Setting('setting_smtp', 'smtp.from', null, null, 'text', 'Mail From'), @@ -109,9 +117,9 @@ SystemSetting( Setting('setting_server', 'server.port', null, 8888, 'number', 'Server Port'), Setting('setting_server', 'server.log', null, false, 'boolean', 'Disable Access Log'), Setting('setting_oauth', 'oauth.githubappid', null, null, 'text', 'Github Oauth AppID'), - Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret'), - Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID'), - Setting('setting_oauth', 'oauth.googlesecret', null, null, 'text', 'Google Oauth Secret'), + Setting('setting_oauth', 'oauth.githubsecret', null, null, 'text', 'Github Oauth Secret', null, FLAG_SECRET), + Setting('setting_oauth', 'oauth.googleappid', null, null, 'text', 'Google Oauth ClientID', null), + Setting('setting_oauth', 'oauth.googlesecret', null, null, 'text', 'Google Oauth Secret', null, FLAG_SECRET), Setting('setting_proxy', 'proxy', null, null, 'text', 'Proxy Server URL'), Setting('setting_constant', 'PROBLEM_PER_PAGE', null, 100, 'number', 'Problems per Page'), Setting('setting_constant', 'CONTEST_PER_PAGE', null, 20, 'number', 'Contests per Page'), @@ -129,6 +137,9 @@ global.Hydro.model.setting = module.exports = { DomainSetting, DomainUserSetting, SystemSetting, + FLAG_HIDDEN, + FLAG_DISABLED, + FLAG_SECRET, PREFERENCE_SETTINGS, ACCOUNT_SETTINGS, SETTINGS, diff --git a/hydro/model/user.js b/hydro/model/user.js index d80c5fdaa..2ba29fa52 100644 --- a/hydro/model/user.js +++ b/hydro/model/user.js @@ -9,6 +9,14 @@ const db = require('../service/db'); const coll = db.collection('user'); +function setPassword(uid, password) { + const salt = String.random(); + return coll.findOneAndUpdate( + { _id: uid }, + { $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } }, + ); +} + class USER { constructor(udoc, dudoc) { this.udoc = () => udoc; @@ -18,7 +26,12 @@ class USER { this.uname = udoc.uname; this.salt = () => udoc.salt; this.hash = () => udoc.hash; + this.hashType = udoc.hashType || 'hydro'; this.priv = udoc.priv; + this.regat = udoc.regat; + this.loginat = udoc.loginat; + this.perm = dudoc.perm; + this.role = dudoc.role || 'default'; for (const key in setting.SETTINGS_BY_KEY) { if (udoc[key]) this[key] = udoc[key]; @@ -27,14 +40,12 @@ class USER { } } - this.regat = udoc.regat; - this.loginat = udoc.loginat; - this.ban = udoc.ban || false; - this.nAccept = dudoc.nAccept || 0; - this.nSubmit = dudoc.nSubmit || 0; - this.rating = dudoc.rating || 1500; - this.perm = dudoc.perm; - this.role = dudoc.role || 'default'; + for (const key in setting.DOMAIN_USER_SETTINGS_BY_KEY) { + if (dudoc[key]) this[key] = dudoc[key]; + else if (setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value) { + this[key] = setting.DOMAIN_USER_SETTINGS_BY_KEY[key].value; + } + } } hasPerm(p) { @@ -42,15 +53,16 @@ class USER { } hasPriv(p) { - // eslint-disable-next-line no-bitwise return this.priv & p; } checkPassword(password) { - const h = global.Hydro.lib[`hash.${this.hashType || 'hydro'}`]; + const h = global.Hydro.lib[`hash.${this.hashType}`]; if (!h) throw new Error('Unknown hash method'); if (!(h(password, this.salt(), this) === this.hash())) { throw new LoginError(this.uname); + } else if (this.hashType !== 'hydro') { + setPassword(this._id, password); } } } @@ -59,7 +71,6 @@ async function getInDomain(domainId, udoc) { let dudoc = await document.getStatus(domainId, document.TYPE_DOMAIN_USER, 0, udoc._id); dudoc = dudoc || {}; if (udoc._id === 1) dudoc.role = 'guest'; - // eslint-disable-next-line no-bitwise if (udoc.priv & PRIV.PRIV_MANAGE_ALL_DOMAIN) dudoc.role = 'admin'; const p = await document.get(domainId, document.TYPE_DOMAIN_USER, dudoc.role || 'default'); dudoc.perm = p ? p.content : BUILTIN_ROLES[dudoc.role || 'default'].perm; @@ -116,14 +127,6 @@ async function getByEmail(domainId, mail, ignoreMissing = false) { return new USER(udoc, dudoc); } -function setPassword(uid, password) { - const salt = String.random(); - return coll.findOneAndUpdate( - { _id: uid }, - { $set: { salt, hash: pwhash(password, salt), hashType: 'hydro' } }, - ); -} - function setById(uid, $set, $unset) { const op = {}; if ($set && Object.keys($set).length) op.$set = $set; diff --git a/hydro/service/server.js b/hydro/service/server.js index 4012d3a66..d9c04b409 100644 --- a/hydro/service/server.js +++ b/hydro/service/server.js @@ -121,41 +121,7 @@ async function prepare() { })); } -class Handler { - /** - * @param {import('koa').Context} ctx - */ - 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, - body: ctx.request.body, - files: ctx.request.files, - query: ctx.query, - path: ctx.path, - params: ctx.params, - referer: ctx.request.headers.referer || '/', - }; - this.response = { - body: '', - type: '', - status: null, - template: null, - redirect: null, - attachment: (name) => ctx.attachment(name), - }; - this.UIContext = { - cdn_prefix: '/', - url_prefix: '/', - }; - this._handler = {}; - this.session = {}; - } - +const HandlerMixin = (Class) => class extends Class { async renderHTML(name, context) { if (enableLog) console.time(name); this.hasPerm = (perm) => this.user.hasPerm(perm); @@ -171,9 +137,12 @@ class Handler { return res; } - async render(name, context) { - this.response.body = await this.renderHTML(name, context); - this.response.type = 'text/html'; + async limitRate(op, periodSecs, maxOperations) { + await opcount.inc(op, this.request.ip, periodSecs, maxOperations); + } + + translate(str) { + return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''; } renderTitle(str) { @@ -214,26 +183,6 @@ class Handler { } } - async limitRate(op, periodSecs, maxOperations) { - await opcount.inc(op, this.request.ip, periodSecs, maxOperations); - } - - back(body) { - this.response.body = body || this.response.body || {}; - this.response.redirect = this.request.headers.referer || '/'; - } - - translate(str) { - return str ? str.toString().translate(this.user.viewLang || this.session.viewLang) : ''; - } - - binary(data, name) { - this.response.body = data; - this.response.template = null; - this.response.type = 'application/octet-stream'; - this.response.disposition = `attachment; filename="${name}"`; - } - url(name, kwargs = {}) { let res = '#'; const args = { ...kwargs }; @@ -252,8 +201,61 @@ class Handler { } return res; } +}; - async ___prepare({ domainId }) { +const Handler = HandlerMixin(class { + /** + * @param {import('koa').Context} ctx + */ + 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, + body: ctx.request.body, + files: ctx.request.files, + query: ctx.query, + path: ctx.path, + params: ctx.params, + referer: ctx.request.headers.referer || '/', + }; + this.response = { + body: '', + type: '', + status: null, + template: null, + redirect: null, + attachment: (name) => ctx.attachment(name), + }; + this.UIContext = { + cdn_prefix: '/', + url_prefix: '/', + }; + this._handler = {}; + this.session = {}; + } + + async render(name, context) { + this.response.body = await this.renderHTML(name, context); + this.response.type = 'text/html'; + } + + back(body) { + this.response.body = body || this.response.body || {}; + this.response.redirect = this.request.headers.referer || '/'; + } + + binary(data, name) { + this.response.body = data; + this.response.template = null; + this.response.type = 'application/octet-stream'; + this.response.disposition = `attachment; filename="${name}"`; + } + + async init({ domainId }) { this.response.body = {}; this.now = new Date(); this._handler.sid = this.request.cookies.get('sid'); @@ -377,7 +379,7 @@ class Handler { }; await this.___cleanup().catch(() => { }); } -} +}); async function handle(ctx, HandlerClass, checker) { global.Hydro.stat.reqCount++; @@ -394,7 +396,7 @@ async function handle(ctx, HandlerClass, checker) { .replace(/_([a-z])/gm, (s) => s[1].toUpperCase()); } - if (h.___prepare) await h.___prepare(args); + if (h.init) await h.init(args); if (checker) checker.call(h); if (method === 'post') { await h.checkCsrfToken(args.csrfToken); @@ -437,7 +439,7 @@ async function handle(ctx, HandlerClass, checker) { if (h.__cleanup) await h.__cleanup(args); if (h.___cleanup) await h.___cleanup(args); } catch (e) { - if (h.onerror) await h.onerror(e); + await h.onerror(e); } } @@ -471,7 +473,7 @@ function Route(name, route, RouteHandler, ...permPrivChecker) { router.all(`${name}_with_domainId`, `/d/:domainId${route}`, (ctx) => handle(ctx, RouteHandler, checker)); } -class ConnectionHandler { +const ConnectionHandler = HandlerMixin(class { /** * @param {import('sockjs').Connection} conn */ @@ -486,25 +488,6 @@ class ConnectionHandler { for (const i in p) this.request.params[p[i][0]] = decodeURIComponent(p[i][1]); } - url(name, kwargs = {}) { - let res = '#'; - const args = { ...kwargs }; - try { - if (this.args.domainId !== 'system') { - name += '_with_domainId'; - args.domainId = args.domainId || this.args.domainId; - } - const { anchor, query } = args; - if (query) res = router.url(name, args, { query }); - else res = router.url(name, args); - if (anchor) return `${res}#${anchor}`; - } catch (e) { - console.error(e.message); - console.error(name, kwargs); - } - return res; - } - async renderHTML(name, context) { this._user = { ...this.user, gravatar: misc.gravatar(this.user.gravatar, 128) }; const res = await template.render(name, Object.assign(context, { @@ -524,7 +507,12 @@ class ConnectionHandler { this.conn.close(code, reason); } - async ___prepare({ domainId }) { + onerror(err) { + console.error(err); + this.close(1001, err.toString()); + } + + async init({ domainId }) { try { this.session = await token.get(this.request.params.token, token.TYPE_CSRF_TOKEN); await token.del(this.request.params.token, token.TYPE_CSRF_TOKEN); @@ -536,7 +524,7 @@ class ConnectionHandler { this.user = await user.getById(domainId, this.session.uid); if (!this.user) throw new UserNotFoundError(this.session.uid); } -} +}); function Connection(name, prefix, RouteConnHandler, permission) { const sock = sockjs.createServer({ prefix }); @@ -545,7 +533,7 @@ function Connection(name, prefix, RouteConnHandler, permission) { try { const args = { domainId: 'system', ...h.request.params }; h.args = args; - if (h.___prepare) await h.___prepare(args); + if (h.init) await h.init(args); if (permission) h.checkPerm(permission); let checking = ''; try { @@ -575,7 +563,7 @@ function Connection(name, prefix, RouteConnHandler, permission) { }); } catch (e) { console.log(e); - if (h.onerror) await h.onerror(e); + await h.onerror(e); } }); sock.installHandlers(server); diff --git a/locales/zh_CN.yaml b/locales/zh_CN.yaml index f67350585..0c63b741d 100644 --- a/locales/zh_CN.yaml +++ b/locales/zh_CN.yaml @@ -405,7 +405,7 @@ Mark Yellow: 标黄 Memory Cost: 内存占用 Memory: 内存 Message {0} not found.: 消息 {0} 不存在。 -Messages: 站内消息 +Messages: 消息 Method: 方法 Month: 月 Monthly Popular: 月度最受欢迎 diff --git a/locales/zh_TW.yaml b/locales/zh_TW.yaml index 5df1c3ce4..cbf25d37e 100644 --- a/locales/zh_TW.yaml +++ b/locales/zh_TW.yaml @@ -266,7 +266,7 @@ Manage: 管理 Memory Cost: 記憶體佔用 Memory: 記憶體 Message {0} not found.: 訊息 {0} 不存在。 -Messages: 站內訊息 +Messages: 訊息 Most Upvoted Solutions: 最熱題解 My Domains: 我的域 My Profile: 我的資料 diff --git a/templates/manage_dashboard.html b/templates/manage_dashboard.html index c21875942..136145618 100644 --- a/templates/manage_dashboard.html +++ b/templates/manage_dashboard.html @@ -1,13 +1,17 @@ {% import "components/user.html" as user %} {% extends "manage_base.html" %} {% block manage_content %} -{% if domain.bulletin %} +
-
- {{ domain.bulletin|markdown|safe }} +
+

{{ _('Messages') }}

+
-{% endif %}

{{ _('Information') }}

diff --git a/templates/manage_settings.html b/templates/manage_setting.html similarity index 100% rename from templates/manage_settings.html rename to templates/manage_setting.html diff --git a/templates/partials/setting.html b/templates/partials/setting.html index 2a9bd6736..f7eccd573 100644 --- a/templates/partials/setting.html +++ b/templates/partials/setting.html @@ -5,15 +5,17 @@
{% for setting in family_settings %} - {% if not setting.hidden %} + {% if not setting.flag|bitand(model.setting.FLAG_HIDDEN) %} + {% set secret = setting.flag|bitand(model.setting.FLAG_SECRET) != 0 %} {% if setting.type == 'text' or setting.type == 'password' or setting.type == 'number' %} {{ form.form_text({ type:setting.type, label:setting.name, help_text:setting.desc, name:setting.key, - value:current[setting.key] or setting.value, - disabled:setting.disabled + value:'' if secret else (current[setting.key]|default(setting.value)), + disabled:setting.disabled, + placeholder:_('(Not changed)') if secret else '' }) }} {% elif setting.type == 'select' %} {{ form.form_select({ @@ -48,17 +50,19 @@ label:setting.name, help_text:setting.desc, name:setting.key, - value:current[setting.key] or setting.value, - disabled:setting.disabled + value:'' if secret else (current[setting.key]|default(setting.value)), + disabled:setting.disabled, + placeholder:_('(Not changed)') if secret else '' }) }} {% elif setting.type == 'markdown' %} {{ form.form_textarea({ label:setting.name, help_text:setting.desc, name:setting.key, - value:current[setting.key] or setting.value, + value:'' if secret else (current[setting.key]|default(setting.value)), markdown:true, - disabled:setting.disabled + disabled:setting.disabled, + placeholder:_('(Not changed)') if secret else '' }) }} {% elif setting.type == 'boolean' %} {{ form.form_checkbox({ diff --git a/ui/misc/typography.styl b/ui/misc/typography.styl index 4253e22e1..3a00b43c4 100644 --- a/ui/misc/typography.styl +++ b/ui/misc/typography.styl @@ -105,6 +105,21 @@ blockquote &:before content: 'WARNING:' color: red + + &.red + border-color: red + background: #F8F8F8 + color: lighten($text-1-color, 10%) + + &.yellow + border-color: yellow + background: #F8F8F8 + color: lighten($text-1-color, 10%) + + &.blue + border-color: blue + background: #F8F8F8 + color: lighten($text-1-color, 10%) acronym, abbr border-bottom: 1px dotted diff --git a/ui/pages/manage_dashboard.page.js b/ui/pages/manage_dashboard.page.js new file mode 100644 index 000000000..d6feea14d --- /dev/null +++ b/ui/pages/manage_dashboard.page.js @@ -0,0 +1,34 @@ +import { NamedPage } from 'vj/misc/PageLoader'; + +const page = new NamedPage('manage_dashboard', async () => { + const { default: SockJs } = await import('sockjs-client'); + + const sock = new SockJs(Context.socketUrl); + sock.onopen = () => { + $('

Connection opened.

').appendTo('#messages'); + }; + + sock.onmessage = (message) => { + const msg = JSON.parse(message.data); + if (msg.type === 'log') { + const div = $('
').appendTo('#messages'); + $(`

${msg.payload}

`).appendTo(div); + } else if (msg.type === 'warn') { + const div = $('
').appendTo('#messages'); + $(`

${msg.payload}

`).appendTo(div); + } else if (msg.type === 'error') { + const div = $('
').appendTo('#messages'); + $(`

${msg.payload}

`).appendTo(div); + } + }; + + sock.onclose = (message) => { + $(` +
+

Connection closed,reason=${JSON.stringify(message.reason)}

+
+ `).appendTo('#messages'); + }; +}); + +export default page;