mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 17:36:08 +08:00
core&ui: feat: enhanced system settings (beta)
This commit is contained in:
1 parent
f01a8d378e
commit
cfdf9fe7a4
9 files changed
+220
-5
No files matched your search
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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
|
||||
@@ -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'],
|
||||
},
|
||||
],
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
Reference in new issue
Block a user