From caa4ddc414a868aa44b099f9baf3d1aa650233c6 Mon Sep 17 00:00:00 2001 From: undefined Date: Wed, 12 Mar 2025 17:54:32 +0800 Subject: [PATCH] core: schema: hide secret value --- packages/hydrooj/src/handler/manage.ts | 36 +++++++++++-- packages/hydrooj/src/model/setting.ts | 3 +- packages/hydrooj/src/settings.ts | 5 +- packages/ui-default/pages/setting.page.tsx | 63 ++++++++++++++++------ 4 files changed, 85 insertions(+), 22 deletions(-) diff --git a/packages/hydrooj/src/handler/manage.ts b/packages/hydrooj/src/handler/manage.ts index 8fe56d59f..c12f28e9f 100644 --- a/packages/hydrooj/src/handler/manage.ts +++ b/packages/hydrooj/src/handler/manage.ts @@ -184,23 +184,53 @@ class SystemConfigHandler extends SystemHandler { async get() { this.response.template = 'manage_config.html'; let value = this.ctx.config.configSource; + + const processNode = (node: any, schema: Schema, parent?: any, accessKey?: string) => { + if (['union', 'intersect'].includes(schema.type)) { + for (const item of schema.list) processNode(node, item, parent, accessKey); + } + if (parent && (schema.meta.secret === true || schema.meta.role === 'secret')) { + if (schema.type === 'string') parent[accessKey] = '[hidden]'; + // TODO support more types + } + if (schema.type === 'object') { + for (const key in schema.dict) processNode(node[key], schema.dict[key], node, key); + } + }; + try { - value = yaml.dump(Schema.intersect(this.ctx.config.settings)(yaml.load(value))); + value = Schema.intersect(this.ctx.config.settings)(yaml.load(value)); + for (const schema of this.ctx.config.settings) processNode(value, schema); } catch (e) { } this.response.body = { schema: Schema.intersect(this.ctx.config.settings).toJSON(), - value, + value: yaml.dump(value), }; } @requireSudo @param('value', Types.String) async post({ }, value: string) { + const oldConfig = yaml.load(this.ctx.config.configSource); let config; + const processNode = (node: any, old: any, schema: Schema, parent?: any, accessKey?: string) => { + if (['union', 'intersect'].includes(schema.type)) { + for (const item of schema.list) processNode(node, old, item, parent, accessKey); + } + if (parent && (schema.meta.secret === true || schema.meta.role === 'secret')) { + if (node === '[hidden]') parent[accessKey] = old[accessKey]; + // TODO support more types + } + if (schema.type === 'object') { + for (const key in schema.dict) processNode(node[key] || {}, old[key] || {}, schema.dict[key], node, key); + } + }; + try { config = yaml.load(value); + for (const schema of this.ctx.config.settings) processNode(config, oldConfig, schema, null, ''); } catch (e) { - throw new ValidationError('value'); + throw new ValidationError('value', '', e.message); } await this.ctx.config.saveConfig(config); } diff --git a/packages/hydrooj/src/model/setting.ts b/packages/hydrooj/src/model/setting.ts index b2d03f7bf..0921692aa 100644 --- a/packages/hydrooj/src/model/setting.ts +++ b/packages/hydrooj/src/model/setting.ts @@ -74,7 +74,8 @@ export const Setting = ( declare global { namespace Schemastery { interface Meta { // eslint-disable-line @typescript-eslint/no-unused-vars - family: string; + family?: string; + secret?: boolean; } } } diff --git a/packages/hydrooj/src/settings.ts b/packages/hydrooj/src/settings.ts index 1435c3baa..3919b74c0 100644 --- a/packages/hydrooj/src/settings.ts +++ b/packages/hydrooj/src/settings.ts @@ -40,7 +40,7 @@ export class ConfigService extends Service { applyDelta(source: any, key: string, value: any) { const path = key.split('.'); - if (path.filter((i) => ['__proto__', 'prototype', 'constructor'].includes(i)).length) return false; + if (path.some((i) => ['__proto__', 'prototype', 'constructor'].includes(i))) return false; const t = path.pop(); const root = JSON.parse(JSON.stringify(source)); let cursor = root; @@ -96,11 +96,12 @@ export class ConfigService extends Service { } const that = this; const getAccess = (path: (string | symbol)[]) => { + if (path.some((p) => ['__proto__', 'prototype', 'constructor'].includes(String(p)))) throw new Error('Invalid path'); let currentValue = curValue; for (const p of path) { currentValue = currentValue[p]; } - if (typeof currentValue !== 'object') return curValue; + if (typeof currentValue !== 'object' || !currentValue) return currentValue; return new Proxy(currentValue, { get(self, key: string) { return getAccess(path.concat(key)); diff --git a/packages/ui-default/pages/setting.page.tsx b/packages/ui-default/pages/setting.page.tsx index 3b2065e4f..eae694ad0 100644 --- a/packages/ui-default/pages/setting.page.tsx +++ b/packages/ui-default/pages/setting.page.tsx @@ -1,5 +1,6 @@ import 'schemastery-react/lib/schemastery-react.css'; +import { diffLines } from 'diff'; import yaml from 'js-yaml'; import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -34,6 +35,10 @@ const page = new NamedPage('manage_config', async () => { }, }); + setInterval(() => { + monaco.editor.remeasureFonts(); + }, 1000); + const initialConfigString = UiContext.config; const schema = new Schema(UiContext.schema); const initial = yaml.load(UiContext.config); @@ -42,6 +47,7 @@ const page = new NamedPage('manage_config', async () => { const editorRef = React.useRef(null); const [loading, setLoading] = React.useState(false); const [editor, setEditor] = React.useState(null); + const [model, setModel] = React.useState(null); React.useEffect(() => { const listener = () => { @@ -51,10 +57,27 @@ const page = new NamedPage('manage_config', async () => { window.addEventListener('resize', listener); return () => window.removeEventListener('resize', listener); }, [editor]); + React.useEffect(() => { + if (!editor) return; + const disposable = editor.onDidChangeModelContent(() => { + const val = editor.getValue({ lineEnding: '\n', preserveBOM: false }); + try { + const loaded = yaml.load(val); + schema(loaded); + setValue(val); + setError(''); + } catch (err) { + setError(err.message); + } + }); + return () => disposable.dispose(); // eslint-disable-line + }, [editor, setValue, setError]); React.useEffect(() => { if (!editorRef.current || loading) return; setLoading(true); + // eslint-disable-next-line const model = monaco.editor.createModel(config, 'yaml', monaco.Uri.parse('hydro://system/setting.yaml')); + setModel(model); const e = monaco.editor.create(editorRef.current, { theme: 'vs-light', lineNumbers: 'off', @@ -72,23 +95,29 @@ const page = new NamedPage('manage_config', async () => { }, }); registerAction(e, model, editorRef.current); - e.onDidChangeModelContent(() => { - const val = e.getValue({ lineEnding: '\n', preserveBOM: false }); - try { - const loaded = yaml.load(val); - schema(loaded); - setValue(val); - setError(''); - } catch (err) { - setError(err.message); - } - }); setEditor(e); }, [editorRef.current]); React.useEffect(() => { if (!editor) return; const current = editor.getValue({ lineEnding: '\n', preserveBOM: false }); - if (current !== config) editor.setValue(config); + const diff = diffLines(current, config); + const ops = []; + let cursor = 1; + for (const line of diff) { + if (line.added) { + let range = model.getFullModelRange(); + range = range.setStartPosition(cursor, 0); + range = range.setEndPosition(cursor, 0); + ops.push({ range, text: line.value }); + } else if (line.removed) { + let range = model.getFullModelRange(); + range = range.setStartPosition(cursor, 0); + cursor += line.count; + range = range.setEndPosition(cursor, 0); + ops.push({ range, text: '' }); + } else cursor += line.count; + } + model.pushEditOperations([], ops, undefined); }, [editor, config]); return (
{ return (<>
-
-
+
{info}
+
+ +