core&ui: feat: enhanced system settings (beta)

This commit is contained in:
undefined committed 2022-01-24 19:31:03 +08:00
1 parent f01a8d378e
commit cfdf9fe7a4
9 files changed
+220 -5

No files matched your search

+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "hydrooj",
"version": "3.3.15",
"version": "3.3.16",
"bin": "bin/hydrooj.js",
"main": "src/loader",
"module": "src/loader",
@@ -15,6 +15,7 @@
"@graphql-tools/schema": "^8.3.1",
"@hydrooj/utils": "workspace:*",
"adm-zip": "0.5.5",
"ajv": "^8.9.0",
"ansi_up": "^5.1.0",
"cac": "^6.7.12",
"cookies": "^0.8.0",
+55
View File
@@ -1,4 +1,5 @@
import { inspect } from 'util';
import Ajv from 'ajv';
import * as yaml from 'js-yaml';
import * as check from '../check';
import { ValidationError } from '../error';
@@ -17,9 +18,12 @@ 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<any>(schema);
function set(key: string, value: any) {
if (setting.SYSTEM_SETTINGS_BY_KEY[key]) {
@@ -140,12 +144,55 @@ class SystemSettingHandler extends SystemHandler {
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]) {
@@ -169,6 +216,13 @@ class SystemSettingHandler extends SystemHandler {
}
}
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 = [];
@@ -228,6 +282,7 @@ async function apply() {
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);
}
+136
View File
@@ -0,0 +1,136 @@
import { JSONSchema7Definition } from 'json-schema';
type Def = Exclude<JSONSchema7Definition, boolean>;
function port(examples: number[] = []) {
const res: Def = {
type: 'integer', minimum: 1, maximum: 65535,
};
if (examples.length) {
res.default = examples[0];
res.examples = examples;
}
return res;
}
export const Schema = {
string<T extends Def>(title: string, defaultValue: string, extra?: T) {
return {
type: 'string' as 'string',
default: defaultValue,
title,
...extra,
};
},
boolean<T extends Def>(title: string, defaultValue: boolean, extra?: T) {
return {
type: 'boolean' as 'boolean',
default: defaultValue,
title,
...extra,
};
},
integer<T extends Def>(title: string, defaultValue: number, extra?: T) {
return {
type: 'integer' as 'integer',
default: defaultValue,
title,
...extra,
};
},
};
const definitions: Record<string, Def> = {
smtp: {
type: 'object',
properties: {
user: Schema.string('SMTP Username', 'noreply@hydro.ac'),
from: Schema.string('Mail From', 'Hydro <noreply@hydro.ac>'),
pass: Schema.string('SMTP Password', '', { writeOnly: true }),
host: Schema.string('SMTP Server Host', 'smtp.hydro.ac', { pattern: '^[a-zA-Z0-9\\-\\.]+$' }),
port: Schema.integer('SMTP Server Port', 25, { examples: [25, 465], minimum: 1, maximum: 65535 }),
secure: Schema.boolean('Use SSL', false),
verify: Schema.boolean('Verify register email', false),
},
additionalProperties: false,
},
file: {
type: 'object',
properties: {
endPoint: Schema.string('Storage engine endPoint', 'http://localhost:9000', {
pattern: '^https?://[a-zA-Z0-9\\-\\.]+/?$',
}),
accessKey: Schema.string('Storage engine accessKey', ''),
secretKey: Schema.string('Storage engine secretKey', '', { writeOnly: true }),
bucket: Schema.string('Storage engine bucket', 'hydro'),
region: Schema.string('Storage engine region', 'us-east-1'),
pathStyle: Schema.boolean('pathStyle endpoint', true),
endPointForUser: Schema.string('EndPoint for user', '/fs/'),
endPointForJudge: Schema.string('EndPoint for judge', '/fs/'),
},
required: ['endPoint', 'accessKey', 'secretKey'],
additionalProperties: false,
},
server: {
type: 'object',
properties: {
name: Schema.string('Server Name', 'Hydro'),
url: Schema.string('Self URL', 'https://hydro.ac/', { pattern: '/$' }),
cdn: Schema.string('CDN prefix', '/', {
pattern: '/$', examples: ['/', 'https://cdn.hydro.ac/'],
}),
port: port([8888, 80, 443]),
xff: Schema.string('IP Header', '', { examples: ['x-forwarded-for', 'x-real-ip'], pattern: '^[a-z-]+$' }),
xhost: Schema.string('Host Header', '', { examples: ['x-real-host'], pattern: '^[a-z-]+$' }),
language: { type: 'string', enum: Object.keys(global.Hydro.locales) },
upload: Schema.string('Upload size limit', '256m', { pattern: '^[0-9]+[mkg]b?$' }),
login: Schema.boolean('Enable builtin login', true),
message: Schema.boolean('Enable message', true),
blog: Schema.boolean('Enable blog', true),
checkUpdate: Schema.boolean('Daily update check', true),
},
required: ['url', 'port', 'language'],
},
limit: {
type: 'object',
properties: {
problem_files_max: { type: 'integer', minimum: 0 },
},
},
session: {
type: 'object',
properties: {
keys: {
type: 'array', items: { type: 'string' }, default: [String.random(32)], writeOnly: true,
},
secure: { type: 'boolean', default: false },
saved_expire_seconds: { type: 'integer', minimum: 300, default: 3600 * 24 * 30 },
unsaved_expire_seconds: { type: 'integer', minimum: 60, default: 3600 * 3 },
},
},
user: {
type: 'object',
properties: {
quota: { type: 'integer', minimum: 0 },
},
},
};
export const schema: Def = {
type: 'object',
definitions,
properties: {
smtp: definitions.smtp,
file: definitions.file,
server: definitions.server,
limit: definitions.limit,
session: definitions.session,
user: definitions.user,
},
additionalProperties: true,
};
export function addDef(key: string, def: Def) {
definitions[key] = def;
schema.properties[key] = definitions[key];
}
+1
View File
@@ -14,3 +14,4 @@ export type Projection<O> = (string & keyof O)[];
export type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
export type Filter<T, U> = T extends U ? T : never;
export type MaybeArray<T> = T | T[];
export type UnionToIntersection<U> = (U extends any ? (arg: U) => void : never) extends ((arg: infer I) => void) ? I : never;
@@ -5,7 +5,10 @@ import CmEditor from '.';
function runSubstitute($container: JQuery<Document | HTMLElement>) {
for (const language of ['markdown', 'yaml', 'json', 'plain']) {
$container.find(`textarea[data-${language}]`).get().forEach((element) => {
CmEditor.getOrConstruct($(element), { language });
const config: any = { language };
if ($(element).data('model')) config.model = $(element).data('model');
if ($(element).data('hide')) config.hide = $(element).data('hide');
CmEditor.getOrConstruct($(element), config);
});
}
}
@@ -6,3 +6,7 @@
.vditor ol
list-style: decimal
.decoration-hide
color: grey !important
background-color: grey !important
+12 -1
View File
@@ -1,4 +1,5 @@
import _ from 'lodash';
import { Range } from 'monaco-editor';
import DOMAttachedObject from 'vj/components/DOMAttachedObject';
export const config = {
@@ -32,6 +33,7 @@ interface MonacoOptions {
autoResize?: boolean;
autoLayout?: boolean;
value?: string;
hide?: string[];
}
interface VditorOptions {
theme?: 'classic' | 'dark'
@@ -54,13 +56,14 @@ export default class Editor extends DOMAttachedObject {
async initMonaco() {
const { load } = await import('vj/components/monaco/loader');
const { monaco, registerAction } = await load(['markdown']);
const {
onChange, language = 'markdown',
theme = UserContext.monacoTheme || 'vs-light',
model = `file://model-${Math.random().toString(16)}`,
autoResize = true, autoLayout = true,
hide = [],
} = this.options;
const { monaco, registerAction } = await load([language]);
const { $dom } = this;
const hasFocus = $dom.is(':focus') || $dom.hasClass('autofocus');
const origin = $dom.get(0);
@@ -124,6 +127,14 @@ export default class Editor extends DOMAttachedObject {
cfg.scrollBeyondLastLine = false;
}
this.editor = monaco.editor.create(ele, cfg);
if (hide.length) {
const ranges = [];
for (const text of hide) {
const found = this.model.findMatches(text, true, false, true, '', true);
ranges.push(...found.map((i) => i.range));
}
this.editor.deltaDecorations([], ranges.map((range) => ({ range, options: { inlineClassName: 'decoration-hide' } })));
}
registerAction(this.editor, this.model, this.$dom);
if (autoResize) {
this.editor.onDidChangeModelDecorations(() => {
@@ -88,5 +88,9 @@ monaco.languages.yaml.yamlDefaults.setDiagnosticsOptions({
fileMatch: ['hydro://problem/file/config.yaml'],
schema: problemConfigSchema,
},
{
uri: `${UiContext.cdn_prefix}manage/setting/schema.json`,
fileMatch: ['hydro://system/setting.yaml'],
},
],
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.30.6",
"version": "4.30.7",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -19,7 +19,7 @@
"@types/qrcode": "^1.4.2",
"@types/redux-logger": "^3.0.9",
"@types/sockjs-client": "^1.5.1",
"@undefined-moe/monaco-yaml": "^2.5.0",
"@undefined-moe/monaco-yaml": "^2.5.1",
"ansi_up": "^5.1.0",
"autoprefixer": "^9.8.8",
"chalk": "^4.1.2",