import { inspect } from 'util'; import Ajv from 'ajv'; import * as yaml from 'js-yaml'; import * as check from '../check'; import { ValidationError } from '../error'; import { isEmail, isPassword, isUname, validate, } from '../lib/validator'; import { Logger } from '../logger'; import { PRIV, STATUS } from '../model/builtin'; import domain from '../model/domain'; import record from '../model/record'; import * as setting from '../model/setting'; import * as system from '../model/system'; import user from '../model/user'; import * as bus from '../service/bus'; import { Connection, ConnectionHandler, Handler, param, Route, Types, } from '../service/server'; import { schema } from '../settings'; import * as judge from './judge'; const logger = new Logger('manage'); const ajv = new Ajv({ useDefaults: true }); const validator = ajv.compile(schema); function set(key: string, value: any) { if (setting.SYSTEM_SETTINGS_BY_KEY[key]) { const s = setting.SYSTEM_SETTINGS_BY_KEY[key]; if (s.flag & setting.FLAG_DISABLED) return undefined; if ((s.flag & setting.FLAG_SECRET) && !value) return undefined; if (s.type === 'boolean') { if (value === 'on') return true; return false; } if (s.type === 'number') { if (!Number.isSafeInteger(+value)) throw new ValidationError(key); return +value; } if (s.subType === 'yaml') { try { yaml.load(value); } catch (e) { throw new ValidationError(key); } } return value; } return undefined; } class SystemHandler extends Handler { async prepare() { this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); } } class SystemMainHandler extends SystemHandler { async get() { this.response.redirect = '/manage/dashboard'; } } class SystemCheckConnHandler extends ConnectionHandler { id: string; async prepare() { this.checkPriv(PRIV.PRIV_EDIT_SYSTEM); await this.check(); } async check() { const log = (payload: any) => this.send({ type: 'log', payload }); const warn = (payload: any) => this.send({ type: 'warn', payload }); const error = (payload: any) => this.send({ type: 'error', payload }); await check.start(this, log, warn, error, (id) => { this.id = id; }); } async cleanup() { check.cancel(this.id); } } class SystemDashboardHandler extends SystemHandler { async get() { this.response.template = 'manage_dashboard.html'; } } class SystemScriptHandler extends SystemHandler { async get() { this.response.template = 'manage_script.html'; this.response.body.scripts = global.Hydro.script; } @param('id', Types.Name) @param('args', Types.Content, true) async post(domainId: string, id: string, raw = '{}') { if (!global.Hydro.script[id]) throw new ValidationError('id'); const args = JSON.parse(raw); validate(global.Hydro.script[id].validate, args); const rid = await record.add(domainId, -1, this.user._id, '-', id, false, raw); const report = (data) => judge.next({ domainId, rid, ...data }); report({ message: `Running script: ${id} `, status: STATUS.STATUS_JUDGING }); const start = new Date().getTime(); // Maybe async? global.Hydro.script[id].run(args, report) .then((ret: any) => { const time = new Date().getTime() - start; judge.end({ domainId, rid, status: STATUS.STATUS_ACCEPTED, message: inspect(ret, false, 10, true), judger: 1, time, memory: 0, }); }) .catch((err: Error) => { const time = new Date().getTime() - start; logger.error(err); judge.end({ domainId, rid, status: STATUS.STATUS_SYSTEM_ERROR, message: `${err.message} \n${(err as any).params || []} \n${err.stack} `, judger: 1, time, memory: 0, }); }); this.response.body = { rid }; this.response.redirect = this.url('record_detail', { rid }); } } class SystemSettingHandler extends SystemHandler { async get() { this.response.template = 'manage_setting.html'; this.response.body.current = {}; this.response.body.settings = setting.SYSTEM_SETTINGS; for (const s of this.response.body.settings) { this.response.body.current[s.key] = system.get(s.key); } const hide = []; const raw = system.get('_') || ''; let config = yaml.load(raw); const valid = validator(config); if (!(raw.trim() && valid)) { const data = {}; for (const key in schema.properties) { data[key] = {}; for (const subkey in (schema.definitions[key] as any).properties || {}) { if (system.get(`${key}.${subkey}`)) { data[key][subkey] = system.get(`${key}.${subkey}`); if ((schema.definitions[key] as any).properties[subkey]?.writeOnly) { if (data[key][subkey] instanceof Array) { hide.push(...data[key][subkey]); } else hide.push(data[key][subkey]); } } } } validator(data); config = yaml.dump(data); } else config = yaml.dump(config); this.response.body.hide = hide; this.response.body.config = config; } async post(args: any) { const tasks = []; const booleanKeys = args.booleanKeys || {}; delete args.booleanKeys; if (args._) { if (typeof args._ !== 'string') throw new ValidationError('config'); try { const payload = yaml.load(args._); const valid = validator(payload); if (!valid) { throw new ValidationError('config', null, validator.errors[0]); } for (const key in payload) { for (const subkey in payload[key]) { tasks.push(system.set(`${key}.${subkey}`, payload[key][subkey])); } } } catch (e) { throw new ValidationError('config', null, e.message); } await system.set('_', args._); delete args._; } for (const key in args) { if (typeof args[key] === 'object') { for (const subkey in args[key]) { const val = set(`${key}.${subkey}`, args[key][subkey]); if (val !== undefined) { tasks.push(system.set(`${key}.${subkey}`, val)); } } } } for (const key in booleanKeys) { if (typeof booleanKeys[key] === 'object') { for (const subkey in booleanKeys[key]) { if (!args[key][subkey]) tasks.push(system.set(`${key}.${subkey}`, false)); } } } tasks.push(bus.parallel('system/setting', args)); await Promise.all(tasks); this.back(); } } class SystemSettingSchemaHandler extends SystemHandler { async get() { this.response.body = JSON.stringify(schema); this.response.type = 'application/json'; } } class SystemUserImportHandler extends SystemHandler { async get() { this.response.body.users = []; this.response.template = 'manage_user_import.html'; } @param('users', Types.Content) @param('draft', Types.Boolean) async post(domainId: string, _users: string, draft: boolean) { const users = _users.split('\n'); const udocs = []; const messages = []; for (const i in users) { const u = users[i]; if (!u.trim()) continue; let [email, username, password, displayName] = u.split(',').map((t) => t.trim()); if (!email || !username || !password) [email, username, password, displayName] = u.split('\t').map((t) => t.trim()); if (email && username && password) { if (!isEmail(email)) messages.push(`Line ${+i + 1}: Invalid email.`); else if (!isUname(username)) messages.push(`Line ${+i + 1}: Invalid username`); else if (!isPassword(password)) messages.push(`Line ${+i + 1}: Invalid password`); // eslint-disable-next-line no-await-in-loop else if (await user.getByEmail('system', email)) { messages.push(`Line ${+i + 1}: Email ${email} already exists.`); // eslint-disable-next-line no-await-in-loop } else if (await user.getByUname('system', username)) { messages.push(`Line ${+i + 1}: Username ${username} already exists.`); } else { udocs.push({ email, username, password, displayName, }); } } else messages.push(`Line ${+i + 1}: Input invalid.`); } messages.push(`${udocs.length} users found.`); if (!draft) { for (const { email, username, password, displayName, } of udocs) { try { // eslint-disable-next-line no-await-in-loop const uid = await user.create(email, username, password); // eslint-disable-next-line no-await-in-loop if (displayName) await domain.setUserInDomain(domainId, uid, { displayName }); } catch (e) { messages.push(e.message); } } } this.response.body.users = udocs; this.response.body.messages = messages; } } async function apply() { Route('manage', '/manage', SystemMainHandler); Route('manage_dashboard', '/manage/dashboard', SystemDashboardHandler); Route('manage_script', '/manage/script', SystemScriptHandler); Route('manage_setting', '/manage/setting', SystemSettingHandler); Route('manage_setting_schema', '/manage/setting/schema.json', SystemSettingSchemaHandler); Route('manage_user_import', '/manage/userimport', SystemUserImportHandler); Connection('manage_check', '/manage/check-conn', SystemCheckConnHandler); } global.Hydro.handler.manage = apply;