@valentinkolb/cloud 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +18 -8
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +119 -47
- package/src/_internal/runtime-context.ts +1 -0
- package/src/api/accounts-entities.ts +4 -0
- package/src/api/admin-core-settings.ts +98 -0
- package/src/api/announcements.ts +131 -0
- package/src/api/auth/schemas.ts +24 -0
- package/src/api/auth.ts +113 -10
- package/src/api/index.ts +15 -25
- package/src/api/me.ts +203 -14
- package/src/api/search/schemas.ts +1 -0
- package/src/api/search.ts +62 -8
- package/src/config/ssr.ts +2 -9
- package/src/contracts/announcements.test.ts +37 -0
- package/src/contracts/announcements.ts +121 -0
- package/src/contracts/app.ts +4 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +4 -0
- package/src/contracts/shared.ts +108 -1
- package/src/desktop/index.ts +704 -0
- package/src/desktop/solid.tsx +938 -0
- package/src/server/api/index.ts +1 -1
- package/src/server/api/respond.ts +50 -10
- package/src/server/index.ts +44 -38
- package/src/server/middleware/auth.ts +98 -9
- package/src/server/middleware/index.ts +2 -1
- package/src/server/middleware/settings.ts +26 -0
- package/src/server/services/access.test.ts +197 -0
- package/src/server/services/access.ts +254 -6
- package/src/server/services/index.ts +14 -11
- package/src/server/services/pagination.ts +22 -0
- package/src/server/time.ts +45 -0
- package/src/services/account-lifecycle/index.ts +142 -18
- package/src/services/accounts/app.ts +658 -170
- package/src/services/accounts/authz.test.ts +77 -0
- package/src/services/accounts/authz.ts +22 -0
- package/src/services/accounts/entities.ts +84 -5
- package/src/services/accounts/groups.ts +30 -24
- package/src/services/accounts/model.test.ts +30 -0
- package/src/services/accounts/switching.test.ts +14 -0
- package/src/services/accounts/switching.ts +15 -6
- package/src/services/accounts/users.ts +75 -52
- package/src/services/announcements/index.test.ts +32 -0
- package/src/services/announcements/index.ts +224 -0
- package/src/services/audit/index.test.ts +84 -0
- package/src/services/audit/index.ts +431 -0
- package/src/services/auth-flows/index.ts +9 -2
- package/src/services/auth-flows/ipa.ts +0 -2
- package/src/services/auth-flows/magic-link.ts +3 -2
- package/src/services/auth-flows/password-reset.ts +284 -0
- package/src/services/auth-flows/proxy-return.test.ts +24 -0
- package/src/services/auth-flows/proxy-return.ts +49 -0
- package/src/services/gateway.ts +162 -0
- package/src/services/index.ts +44 -2
- package/src/services/ipa/effective-groups.test.ts +33 -0
- package/src/services/ipa/effective-groups.ts +70 -0
- package/src/services/ipa/profile.ts +45 -3
- package/src/services/ipa/search.ts +3 -5
- package/src/services/ipa/service-account.ts +15 -0
- package/src/services/ipa/sync-planning.test.ts +32 -0
- package/src/services/ipa/sync-planning.ts +22 -0
- package/src/services/ipa/sync.ts +110 -38
- package/src/services/oauth-tokens.ts +104 -0
- package/src/services/postgres.ts +21 -6
- package/src/services/providers/local/auth.test.ts +22 -0
- package/src/services/providers/local/auth.ts +46 -3
- package/src/services/secrets.ts +10 -0
- package/src/services/service-account-credentials.test.ts +210 -0
- package/src/services/service-account-credentials.ts +715 -0
- package/src/services/service-accounts.ts +188 -0
- package/src/services/session/index.ts +7 -8
- package/src/services/settings/app.ts +4 -20
- package/src/services/settings/defaults.ts +64 -22
- package/src/services/settings/store.ts +47 -0
- package/src/services/weather/forecast.ts +40 -7
- package/src/services/webauthn.test.ts +36 -0
- package/src/services/webauthn.ts +384 -0
- package/src/shared/icons.ts +391 -100
- package/src/shared/index.ts +7 -0
- package/src/shared/markdown/extensions/code.ts +38 -1
- package/src/shared/markdown/extensions/images.ts +39 -3
- package/src/shared/markdown/extensions/info-blocks.ts +5 -5
- package/src/shared/markdown/extensions/mark.ts +48 -0
- package/src/shared/markdown/extensions/sub-sup.ts +60 -0
- package/src/shared/markdown/extensions/tables.ts +79 -58
- package/src/shared/markdown/formula.test.ts +1089 -0
- package/src/shared/markdown/formula.ts +1187 -0
- package/src/shared/markdown/index.ts +76 -2
- package/src/shared/mock-cover.ts +130 -0
- package/src/shared/redirect.test.ts +49 -0
- package/src/shared/redirect.ts +52 -0
- package/src/shared/theme.test.ts +24 -0
- package/src/shared/theme.ts +68 -0
- package/src/shared/time.ts +13 -0
- package/src/ssr/AdminLayout.tsx +7 -3
- package/src/ssr/AdminSidebar.tsx +115 -49
- package/src/ssr/AppLaunchpad.island.tsx +176 -0
- package/src/ssr/Footer.island.tsx +3 -8
- package/src/ssr/GlobalAnnouncements.island.tsx +141 -0
- package/src/ssr/GlobalSearchDialog.tsx +545 -117
- package/src/ssr/HotkeysHelpRail.island.tsx +3 -70
- package/src/ssr/Layout.tsx +74 -66
- package/src/ssr/LayoutBreadcrumbs.island.tsx +44 -0
- package/src/ssr/LayoutHelp.tsx +266 -0
- package/src/ssr/NavMenu.island.tsx +0 -39
- package/src/ssr/ThemeToggleRail.island.tsx +3 -3
- package/src/ssr/TimezoneCookie.island.tsx +23 -0
- package/src/ssr/islands/index.ts +13 -0
- package/src/styles/base-popover.css +5 -2
- package/src/styles/effects.css +87 -6
- package/src/styles/global.css +146 -9
- package/src/styles/input.css +3 -1
- package/src/styles/utilities-buttons.css +133 -27
- package/src/styles/utilities-code-display.css +67 -0
- package/src/styles/utilities-completion.css +223 -0
- package/src/styles/utilities-detail.css +73 -0
- package/src/styles/utilities-feedback.css +16 -15
- package/src/styles/utilities-layout.css +42 -2
- package/src/styles/utilities-markdown-editor.css +472 -0
- package/src/styles/utilities-navigation.css +63 -8
- package/src/styles/utilities-script.css +84 -0
- package/src/styles/utilities-table-tile.css +229 -0
- package/src/types/ambient.d.ts +9 -0
- package/src/ui/completion/behaviors.test.ts +95 -0
- package/src/ui/completion/behaviors.ts +205 -0
- package/src/ui/completion/engine.ts +368 -0
- package/src/ui/completion/index.ts +40 -0
- package/src/ui/completion/overlay.ts +92 -0
- package/src/ui/dialog-core.ts +173 -45
- package/src/ui/filter/FilterChip.tsx +42 -40
- package/src/ui/index.ts +11 -12
- package/src/ui/input/AutocompleteEditor.tsx +656 -0
- package/src/ui/input/CheckboxCard.tsx +91 -0
- package/src/ui/input/Combobox.tsx +375 -0
- package/src/ui/input/DatePicker.tsx +846 -0
- package/src/ui/input/DateTimeInput.tsx +29 -4
- package/src/ui/input/FileDropzone.tsx +116 -0
- package/src/ui/input/IconInput.tsx +116 -0
- package/src/ui/input/ImageInput.tsx +19 -2
- package/src/ui/input/MultiSelectInput.tsx +448 -0
- package/src/ui/input/NumberInput.tsx +417 -61
- package/src/ui/input/SegmentedControl.tsx +2 -2
- package/src/ui/input/Select.tsx +172 -10
- package/src/ui/input/Slider.tsx +3 -4
- package/src/ui/input/Switch.tsx +3 -2
- package/src/ui/input/TemplateEditor.tsx +212 -0
- package/src/ui/input/TextInput.tsx +144 -13
- package/src/ui/input/index.ts +53 -8
- package/src/ui/input/markdown/MarkdownEditor.tsx +774 -0
- package/src/ui/input/markdown/Toolbar.tsx +90 -0
- package/src/ui/input/markdown/actions.ts +233 -0
- package/src/ui/input/markdown/active-formats.ts +94 -0
- package/src/ui/input/markdown/behaviors.ts +193 -0
- package/src/ui/input/markdown/code-zone.ts +23 -0
- package/src/ui/input/markdown/highlight.ts +316 -0
- package/src/ui/layout.ts +22 -0
- package/src/ui/misc/AppOverview.tsx +105 -0
- package/src/ui/misc/AppWorkspace.tsx +607 -0
- package/src/ui/misc/Calendar.tsx +1291 -0
- package/src/ui/misc/Chart.tsx +162 -0
- package/src/ui/misc/CodeDisplay.tsx +54 -0
- package/src/ui/misc/ContextMenu.tsx +2 -2
- package/src/ui/misc/DataTable.tsx +269 -0
- package/src/ui/misc/DockWorkspace.tsx +425 -0
- package/src/ui/misc/Docs.tsx +153 -0
- package/src/ui/misc/Dropdown.tsx +2 -2
- package/src/ui/misc/EntitySearch.tsx +260 -129
- package/src/ui/misc/LinkCard.tsx +14 -2
- package/src/ui/misc/LogEntriesTable.tsx +34 -31
- package/src/ui/misc/Pagination.tsx +31 -12
- package/src/ui/misc/PanelDialog.tsx +109 -0
- package/src/ui/misc/Panes.tsx +873 -0
- package/src/ui/misc/PermissionEditor.tsx +358 -262
- package/src/ui/misc/Placeholder.tsx +40 -0
- package/src/ui/misc/ProgressBar.tsx +1 -1
- package/src/ui/misc/ResourceApiKeys.tsx +260 -0
- package/src/ui/misc/SettingsModal.tsx +150 -0
- package/src/ui/misc/StatCell.tsx +182 -40
- package/src/ui/misc/StatGrid.tsx +149 -0
- package/src/ui/misc/StructuredDataPreview.tsx +107 -0
- package/src/ui/misc/code-highlight.ts +213 -0
- package/src/ui/misc/index.ts +93 -12
- package/src/ui/prompts.tsx +362 -312
- package/src/ui/toast.ts +384 -0
- package/src/ui/widgets/Widget.tsx +12 -4
- package/src/ssr/MoreAppsDropdown.island.tsx +0 -61
- package/src/ui/ipa/GroupView.tsx +0 -36
- package/src/ui/ipa/LoginBtn.tsx +0 -16
- package/src/ui/ipa/UserView.tsx +0 -58
- package/src/ui/ipa/index.ts +0 -4
- package/src/ui/navigation.ts +0 -32
- package/src/ui/sidebar.tsx +0 -468
- /package/src/ui/{ipa → misc}/Avatar.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valentinkolb/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Modular Hono+SolidJS framework for building per-app docker services behind a dynamic gateway. Powers cloud.stuve-ulm.de.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -19,39 +19,49 @@
|
|
|
19
19
|
"exports": {
|
|
20
20
|
".": "./src/index.ts",
|
|
21
21
|
"./ui": "./src/ui/index.ts",
|
|
22
|
+
"./ui/styles.css": "./src/styles/global.css",
|
|
23
|
+
"./desktop": "./src/desktop/index.ts",
|
|
24
|
+
"./desktop/solid": "./src/desktop/solid.tsx",
|
|
22
25
|
"./server": "./src/server/index.ts",
|
|
23
26
|
"./browser": "./src/server/api-client.ts",
|
|
24
27
|
"./shared": "./src/shared/index.ts",
|
|
25
28
|
"./services": "./src/services/index.ts",
|
|
29
|
+
"./services/ipa/service-account": null,
|
|
26
30
|
"./services/*": "./src/services/*",
|
|
27
31
|
"./ssr": "./src/ssr/index.ts",
|
|
28
32
|
"./ssr/islands": "./src/ssr/islands/index.ts",
|
|
29
33
|
"./config": "./src/config/index.ts",
|
|
30
34
|
"./config/*": "./src/config/*",
|
|
31
35
|
"./contracts": "./src/contracts/index.ts",
|
|
36
|
+
"./api": "./src/api/index.ts",
|
|
37
|
+
"./clients/core": "./src/clients/core.ts",
|
|
32
38
|
"./styles/global.css": "./src/styles/global.css"
|
|
33
39
|
},
|
|
34
40
|
"scripts": {
|
|
41
|
+
"test": "bun test",
|
|
35
42
|
"typecheck": "node ../../node_modules/typescript/bin/tsc -p tsconfig.typecheck.json --noEmit --pretty false 2>&1 | grep -v node_modules | (! grep -q 'error TS')"
|
|
36
43
|
},
|
|
37
44
|
"dependencies": {
|
|
38
|
-
"@fontsource
|
|
39
|
-
"@
|
|
40
|
-
"@
|
|
45
|
+
"@fontsource/ibm-plex-mono": "^5.2.7",
|
|
46
|
+
"@fontsource/ibm-plex-sans": "^5.2.8",
|
|
47
|
+
"@fontsource/ibm-plex-sans-condensed": "^5.2.8",
|
|
48
|
+
"@simplewebauthn/server": "^13.3.1",
|
|
41
49
|
"@tabler/icons-webfont": "^3.36.1",
|
|
42
50
|
"@tailwindcss/typography": "^0.5.19",
|
|
43
|
-
"@valentinkolb/ssr": "0.
|
|
44
|
-
"@valentinkolb/stdlib": "0.
|
|
51
|
+
"@valentinkolb/ssr": "0.10.0",
|
|
52
|
+
"@valentinkolb/stdlib": "0.12.0",
|
|
45
53
|
"@valentinkolb/sync": "^5.0.0",
|
|
46
54
|
"bun-plugin-tailwind": "^0.1.2",
|
|
47
55
|
"hono": "^4.11.1",
|
|
48
56
|
"hono-openapi": "^1.1.2",
|
|
57
|
+
"jose": "^6.1.3",
|
|
49
58
|
"katex": "^0.16.28",
|
|
50
59
|
"marked": "^17.0.1",
|
|
51
60
|
"mermaid": "^11.12.2",
|
|
52
61
|
"mustache": "^4.2.0",
|
|
53
|
-
"nodemailer": "^
|
|
54
|
-
"sanitize-html": "^2.17.
|
|
62
|
+
"nodemailer": "^8.0.10",
|
|
63
|
+
"sanitize-html": "^2.17.4",
|
|
64
|
+
"seroval": "^1.5.4",
|
|
55
65
|
"solid-js": "^1.9.10",
|
|
56
66
|
"tailwindcss": "^4.1.18",
|
|
57
67
|
"zod": "^4.3.4"
|
package/scripts/preload.ts
CHANGED
|
@@ -7,10 +7,11 @@
|
|
|
7
7
|
* uses /app as projectRoot → auto-detect scans all packages → all classes generated.
|
|
8
8
|
* No @source directives needed in CSS files.
|
|
9
9
|
*/
|
|
10
|
-
import
|
|
10
|
+
import { existsSync, watch } from "node:fs";
|
|
11
11
|
import { cp, mkdir } from "node:fs/promises";
|
|
12
|
-
import {
|
|
12
|
+
import { dirname, resolve } from "node:path";
|
|
13
13
|
import { fileURLToPath } from "node:url";
|
|
14
|
+
import tailwind from "bun-plugin-tailwind";
|
|
14
15
|
|
|
15
16
|
const appId = process.env.APP_ID ?? "core";
|
|
16
17
|
const { plugin } = await import(`../../${appId}/src/config`);
|
|
@@ -21,8 +22,7 @@ const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..
|
|
|
21
22
|
const publicDir = resolve(workspaceRoot, "public");
|
|
22
23
|
await mkdir(publicDir, { recursive: true });
|
|
23
24
|
|
|
24
|
-
|
|
25
|
-
if (appId === "core") {
|
|
25
|
+
const buildGlobalCss = async () => {
|
|
26
26
|
// Use styles.css at workspace root as entrypoint so the oxide scanner
|
|
27
27
|
// walks up to /app/package.json and scans ALL packages for utility classes.
|
|
28
28
|
// (If the CSS file is inside packages/lib/, it only scans packages/lib/.)
|
|
@@ -32,24 +32,74 @@ if (appId === "core") {
|
|
|
32
32
|
naming: "global.css",
|
|
33
33
|
plugins: [tailwind],
|
|
34
34
|
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const buildAppCss = async () => {
|
|
38
|
+
// `naming: "app.css"` is essential — without it, Bun.build with `root: workspaceRoot`
|
|
39
|
+
// preserves the directory structure (packages/<id>/src/styles/app.css) inside outdir,
|
|
40
|
+
// so Layout's `<link href="/public/<id>/app.css">` 404s and only global.css's classes
|
|
41
|
+
// reach the browser. That's why responsive grid utilities went missing on dashboard.
|
|
42
|
+
await Bun.build({
|
|
43
|
+
entrypoints: [resolve(workspaceRoot, `packages/${appId}/src/styles/app.css`)],
|
|
44
|
+
outdir: resolve(publicDir, appId),
|
|
45
|
+
naming: "app.css",
|
|
46
|
+
root: workspaceRoot, // same: auto-detect from /app
|
|
47
|
+
plugins: [tailwind],
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const watchDevCss = (label: string, paths: string[], build: () => Promise<void>) => {
|
|
52
|
+
if (process.env.NODE_ENV !== "development") return;
|
|
53
|
+
|
|
54
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
55
|
+
let running = false;
|
|
56
|
+
let queued = false;
|
|
57
|
+
|
|
58
|
+
const rebuild = () => {
|
|
59
|
+
if (running) {
|
|
60
|
+
queued = true;
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
running = true;
|
|
65
|
+
void build()
|
|
66
|
+
.then(() => console.log(`[preload] rebuilt ${label}`))
|
|
67
|
+
.catch((error) => console.error(`[preload] failed to rebuild ${label}`, error))
|
|
68
|
+
.finally(() => {
|
|
69
|
+
running = false;
|
|
70
|
+
if (queued) {
|
|
71
|
+
queued = false;
|
|
72
|
+
rebuild();
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const schedule = () => {
|
|
78
|
+
if (timer) clearTimeout(timer);
|
|
79
|
+
timer = setTimeout(rebuild, 75);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
for (const path of paths) {
|
|
83
|
+
if (!existsSync(path)) continue;
|
|
84
|
+
watch(path, { persistent: true }, schedule);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// global.css + branding are only served by core (Traefik routes them there)
|
|
89
|
+
if (appId === "core") {
|
|
90
|
+
await buildGlobalCss();
|
|
35
91
|
|
|
36
92
|
// Default branding asset: copy the tracked logo.svg into the runtime
|
|
37
93
|
// public dir so serveBranding can fall back to it when no admin-uploaded
|
|
38
94
|
// logo (data URI) is configured. User uploads are stored as base64 data
|
|
39
95
|
// URIs in settings — no image processing (sharp etc.) needed.
|
|
40
|
-
await cp(
|
|
41
|
-
resolve(workspaceRoot, "packages/cloud/public/logo.svg"),
|
|
42
|
-
resolve(publicDir, "logo.svg"),
|
|
43
|
-
);
|
|
96
|
+
await cp(resolve(workspaceRoot, "packages/cloud/public/logo.svg"), resolve(publicDir, "logo.svg"));
|
|
44
97
|
}
|
|
45
98
|
|
|
46
99
|
// katex.css is only needed by notebooks (served by core via Traefik /public/katex.css)
|
|
47
100
|
if (appId === "notebooks") {
|
|
48
101
|
try {
|
|
49
|
-
await cp(
|
|
50
|
-
resolve(workspaceRoot, "node_modules/katex/dist/katex.min.css"),
|
|
51
|
-
resolve(publicDir, "katex.css"),
|
|
52
|
-
);
|
|
102
|
+
await cp(resolve(workspaceRoot, "node_modules/katex/dist/katex.min.css"), resolve(publicDir, "katex.css"));
|
|
53
103
|
} catch {
|
|
54
104
|
console.warn("[preload] katex.css not found, skipping");
|
|
55
105
|
}
|
|
@@ -60,14 +110,19 @@ const appCssPath = resolve(workspaceRoot, `packages/${appId}/src/styles/app.css`
|
|
|
60
110
|
const appPublicDir = resolve(publicDir, appId);
|
|
61
111
|
await mkdir(appPublicDir, { recursive: true });
|
|
62
112
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
});
|
|
113
|
+
await buildAppCss();
|
|
114
|
+
|
|
115
|
+
watchDevCss("app.css", [resolve(appCssPath, "..")], buildAppCss);
|
|
116
|
+
if (appId === "core") {
|
|
117
|
+
watchDevCss("global.css", [resolve(workspaceRoot, "styles.css"), resolve(workspaceRoot, "packages/cloud/src/styles")], buildGlobalCss);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Optional app-owned dev assets. Production builds already have
|
|
121
|
+
// scripts/build-extras.ts; this mirrors that hook for watch mode without
|
|
122
|
+
// forcing app-specific asset logic into the framework preload.
|
|
123
|
+
const devExtras = resolve(workspaceRoot, `packages/${appId}/scripts/dev-extras.ts`);
|
|
124
|
+
if (existsSync(devExtras)) {
|
|
125
|
+
process.env.WORKSPACE_ROOT = workspaceRoot;
|
|
126
|
+
process.env.PUBLIC_DIR = publicDir;
|
|
127
|
+
await import(devExtras);
|
|
128
|
+
}
|
|
@@ -4,26 +4,23 @@
|
|
|
4
4
|
* Merges SSR config, app meta, and server bootstrap into one call.
|
|
5
5
|
* Returns `{ ssr, plugin, config, meta, start }`.
|
|
6
6
|
*/
|
|
7
|
+
|
|
8
|
+
import type { SsrConfig } from "@valentinkolb/ssr";
|
|
7
9
|
import { createConfig as createSsrConfig } from "@valentinkolb/ssr";
|
|
8
10
|
import { createSSRHandler, routes } from "@valentinkolb/ssr/hono";
|
|
9
11
|
import { Hono } from "hono";
|
|
10
12
|
import { serveStatic } from "hono/bun";
|
|
11
|
-
import
|
|
12
|
-
import type {
|
|
13
|
-
AppMeta,
|
|
14
|
-
AppLifecycle,
|
|
15
|
-
AppCapabilities,
|
|
16
|
-
AppSearchContext,
|
|
17
|
-
CloudContext,
|
|
18
|
-
} from "../contracts/app";
|
|
13
|
+
import { generateSpecs } from "hono-openapi";
|
|
14
|
+
import type { AppCapabilities, AppLifecycle, AppMeta, AppSearchContext, CloudContext } from "../contracts/app";
|
|
19
15
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
20
|
-
import type { Role } from "../contracts/shared";
|
|
21
16
|
import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
17
|
+
import type { Role } from "../contracts/shared";
|
|
18
|
+
import { themeBootstrapScript } from "../shared/theme";
|
|
24
19
|
import { auth } from "../server/middleware/auth";
|
|
25
20
|
import { logger } from "../services/logging";
|
|
26
|
-
import { get,
|
|
21
|
+
import { get, loadCache as loadSettingsCache, set } from "../services/settings";
|
|
22
|
+
import { createSettingsAPI, type SettingsAPI } from "../services/settings/api";
|
|
23
|
+
import { registerSettings, type SettingDef } from "../services/settings/defaults";
|
|
27
24
|
import { createHeartbeat } from "./heartbeat";
|
|
28
25
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
29
26
|
|
|
@@ -80,7 +77,7 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
80
77
|
/**
|
|
81
78
|
* Legal/info links contributed by this app — aggregated app-wide via
|
|
82
79
|
* `listLegalLinks()` and rendered in login footer, app Footer, rail more
|
|
83
|
-
* dropdown. Each app contributes its own (e.g.
|
|
80
|
+
* dropdown. Each app contributes its own (e.g. core owns
|
|
84
81
|
* Imprint/Privacy/Terms; faq owns FAQ). KISS: no `external` flag, links
|
|
85
82
|
* always open in a new tab from the login footer.
|
|
86
83
|
*/
|
|
@@ -101,12 +98,29 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
101
98
|
* `/admin/<id>` — admin SSR pages
|
|
102
99
|
* `/public/<id>` — built CSS and other static assets
|
|
103
100
|
*
|
|
104
|
-
* Apps with non-standard URLs (core's `/auth`, `/me
|
|
105
|
-
* `/.well-known
|
|
101
|
+
* Apps with non-standard URLs (core's `/auth`, `/me`, `/legal/*`, `/impressum`;
|
|
102
|
+
* oauth's `/oauth`, `/.well-known/...`) list whatever
|
|
106
103
|
* top-level paths they own. The gateway is dumb — it just builds a
|
|
107
104
|
* prefix-trie from these strings.
|
|
108
105
|
*/
|
|
109
106
|
routes: readonly string[];
|
|
107
|
+
/**
|
|
108
|
+
* Gateway-relative URL at which this app's OpenAPI 3.x JSON spec is
|
|
109
|
+
* served, e.g. `"/api/notebooks/openapi.json"`. Opt-in: only set this
|
|
110
|
+
* for apps whose api router is documented with `middleware.openapi()`
|
|
111
|
+
* (i.e. `describeRoute()`) and worth surfacing in the api-docs aggregator.
|
|
112
|
+
*
|
|
113
|
+
* Pair this with `app.start({ openapi: <api router> })` — `defineApp`
|
|
114
|
+
* generates the spec from that router at boot, mounts it on the
|
|
115
|
+
* framework server (before the user's fetch, so it bypasses any
|
|
116
|
+
* auth/rate-limit middleware), and advertises the URL via the registry
|
|
117
|
+
* so `app-api-docs` picks it up automatically.
|
|
118
|
+
*
|
|
119
|
+
* The path must be reachable through the gateway — usually that means
|
|
120
|
+
* the standard form `"/api/<id>/openapi.json"` (covered by the
|
|
121
|
+
* `/api/<id>` entry in `routes`).
|
|
122
|
+
*/
|
|
123
|
+
openapi?: string;
|
|
110
124
|
/**
|
|
111
125
|
* Project root used by the SSR plugin to discover island/client files.
|
|
112
126
|
* Defaults to `process.cwd()`. Override only if you run the entrypoint
|
|
@@ -133,7 +147,19 @@ export type StartOptions = {
|
|
|
133
147
|
* before this fetch — they take precedence over any catch-all the app
|
|
134
148
|
* might register.
|
|
135
149
|
*/
|
|
136
|
-
fetch: (req: Request) => Response | Promise<Response>;
|
|
150
|
+
fetch: (req: Request, env?: unknown) => Response | Promise<Response>;
|
|
151
|
+
/**
|
|
152
|
+
* Hono router to scan for OpenAPI route metadata. When set together with
|
|
153
|
+
* `defineApp({ openapi: "..." })`, the framework generates an OpenAPI
|
|
154
|
+
* spec from this router and mounts it at the configured URL on the
|
|
155
|
+
* framework server (before the user's fetch, so the spec stays public).
|
|
156
|
+
*
|
|
157
|
+
* Pass the BARE api router — the one with `describeRoute()` annotations.
|
|
158
|
+
* `generateSpecs` walks the route tree without executing middleware, so
|
|
159
|
+
* the inner auth/rate-limit `.use(...)` calls don't matter for spec
|
|
160
|
+
* generation; they only run if the router is hit by an actual request.
|
|
161
|
+
*/
|
|
162
|
+
openapi?: Hono<any>;
|
|
137
163
|
lifecycle?: AppLifecycle;
|
|
138
164
|
capabilities?: AppCapabilities;
|
|
139
165
|
port?: number;
|
|
@@ -142,6 +168,7 @@ export type StartOptions = {
|
|
|
142
168
|
|
|
143
169
|
export type StartResult = {
|
|
144
170
|
port: number;
|
|
171
|
+
development: boolean;
|
|
145
172
|
fetch: Hono["fetch"];
|
|
146
173
|
};
|
|
147
174
|
|
|
@@ -174,6 +201,8 @@ export type AppDefinition<S extends AppSettingsMap = {}> = {
|
|
|
174
201
|
// ── Implementation ──────────────────────────────────────────────────────────
|
|
175
202
|
|
|
176
203
|
export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<S>): AppDefinition<S> => {
|
|
204
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
205
|
+
|
|
177
206
|
// ── 0. Register declared settings into the runtime registry ──────────
|
|
178
207
|
// SETTINGS_MAP is the single source of truth for validation in store.ts
|
|
179
208
|
// (writeKey checks SETTINGS_MAP.get(key)) and for snapshot.ts (allKnownKeys
|
|
@@ -207,7 +236,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
207
236
|
|
|
208
237
|
// ── 1. SSR config ─────────────────────────────────────────────────────
|
|
209
238
|
const { config, plugin, html } = createSsrConfig<PageOptions>({
|
|
210
|
-
dev:
|
|
239
|
+
dev: isDevelopment,
|
|
211
240
|
verbose: true,
|
|
212
241
|
rootDir: opts.appRoot ?? process.cwd(),
|
|
213
242
|
basePath: opts.basePath,
|
|
@@ -222,17 +251,10 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
222
251
|
<meta name="theme-color" content="#09090b">
|
|
223
252
|
<meta name="mobile-web-app-capable" content="yes">
|
|
224
253
|
<link rel="icon" href="/branding/favicon">
|
|
225
|
-
<
|
|
254
|
+
<style data-cloud-css-layers>@layer theme, base, components, utilities;</style>
|
|
226
255
|
<link rel="stylesheet" href="/public/${opts.id}/app.css?v=${v}">
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
var el = document.documentElement;
|
|
230
|
-
if (!el.hasAttribute('data-theme-fixed')) {
|
|
231
|
-
var theme = document.cookie.match(/theme=([^;]+)/)?.[1] || 'light';
|
|
232
|
-
el.classList.add(theme);
|
|
233
|
-
}
|
|
234
|
-
})();
|
|
235
|
-
</script>
|
|
256
|
+
<link rel="stylesheet" href="/public/global.css?v=${v}">
|
|
257
|
+
<script>${themeBootstrapScript}</script>
|
|
236
258
|
</head>
|
|
237
259
|
<body>
|
|
238
260
|
${body}
|
|
@@ -258,6 +280,8 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
258
280
|
nav: opts.nav,
|
|
259
281
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
260
282
|
widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
|
|
283
|
+
settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
|
|
284
|
+
openapi: opts.openapi,
|
|
261
285
|
};
|
|
262
286
|
|
|
263
287
|
// ── 3. start() — builds and boots the Hono server ────────────────────
|
|
@@ -266,6 +290,11 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
266
290
|
const baseUrl = opts.baseUrl;
|
|
267
291
|
const log = logger("app");
|
|
268
292
|
|
|
293
|
+
// OpenAPI advertised in the registry only when there's a router to
|
|
294
|
+
// derive the spec from. The mount block lower down uses the same
|
|
295
|
+
// flag so the registry never points at a URL that 404s.
|
|
296
|
+
const advertiseOpenapi = !!(opts.openapi && startOpts.openapi);
|
|
297
|
+
|
|
269
298
|
// Registry entry
|
|
270
299
|
const entry: AppRegistryEntry = {
|
|
271
300
|
id: meta.id,
|
|
@@ -274,16 +303,17 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
274
303
|
description: meta.description,
|
|
275
304
|
baseUrl,
|
|
276
305
|
routes: [...meta.routes],
|
|
277
|
-
nav:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
306
|
+
nav:
|
|
307
|
+
meta.nav || meta.adminHref
|
|
308
|
+
? {
|
|
309
|
+
href: meta.nav?.href ?? "",
|
|
310
|
+
match: meta.nav?.match,
|
|
311
|
+
section: meta.nav?.section ?? "hidden",
|
|
312
|
+
requiresAuth: meta.nav?.requiresAuth,
|
|
313
|
+
requiresRoles: meta.nav?.requiresRoles,
|
|
314
|
+
adminHref: meta.adminHref,
|
|
315
|
+
}
|
|
316
|
+
: undefined,
|
|
287
317
|
search: startOpts.capabilities?.search
|
|
288
318
|
? {
|
|
289
319
|
tags: [...(startOpts.capabilities.search.tags ?? [])],
|
|
@@ -294,6 +324,8 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
294
324
|
: undefined,
|
|
295
325
|
legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
296
326
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
327
|
+
settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
|
|
328
|
+
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
297
329
|
};
|
|
298
330
|
|
|
299
331
|
// Heartbeat
|
|
@@ -306,21 +338,26 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
306
338
|
// watcher is a module-level singleton, only one runs per process.
|
|
307
339
|
await ensureRuntimeWatcher();
|
|
308
340
|
|
|
309
|
-
// Build Hono server. Framework owns
|
|
341
|
+
// Build Hono server. Framework owns these mounts (registered first so
|
|
310
342
|
// they take precedence over any catch-all in the user's fetch):
|
|
311
343
|
// /_ssr/* island chunks (SSR adapter)
|
|
312
344
|
// /public/* serveStatic + terminal 404
|
|
313
345
|
// /api/_internal/search only when capabilities.search is declared
|
|
346
|
+
// <opts.openapi> OpenAPI JSON spec, when both opts.openapi
|
|
347
|
+
// and startOpts.openapi are set
|
|
314
348
|
const ssrMountPath = config.basePath ? `${config.basePath}/_ssr` : "/_ssr";
|
|
315
349
|
|
|
316
350
|
const server = new Hono()
|
|
317
351
|
.route(ssrMountPath, routes(config))
|
|
318
|
-
.use(
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
352
|
+
.use(
|
|
353
|
+
"/public/*",
|
|
354
|
+
serveStatic({
|
|
355
|
+
root: "./",
|
|
356
|
+
onFound: (_path, c) => {
|
|
357
|
+
c.header("Cache-Control", isDevelopment ? "no-store" : "public, max-age=31536000, immutable");
|
|
358
|
+
},
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
324
361
|
// serveStatic calls next() on miss — terminate /public/* here so a
|
|
325
362
|
// missing asset is a clean 404 instead of falling through to the app
|
|
326
363
|
// fetch (which might render an HTML page for the missing path).
|
|
@@ -329,6 +366,9 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
329
366
|
if (startOpts.capabilities?.search) {
|
|
330
367
|
const searchRun = startOpts.capabilities.search.run;
|
|
331
368
|
server.post("/api/_internal/search", auth.requireRole("authenticated"), async (c) => {
|
|
369
|
+
if (!c.get("user")) {
|
|
370
|
+
return c.json({ message: "Search providers require a user-backed actor", code: "FORBIDDEN" }, 403);
|
|
371
|
+
}
|
|
332
372
|
const body = await c.req.json<{ query: string; tags: string[]; limit: number }>();
|
|
333
373
|
const ctx: AppSearchContext = { get: (key) => c.get(key) as never };
|
|
334
374
|
const results = await searchRun({ query: body.query, tags: body.tags, limit: body.limit, ctx });
|
|
@@ -336,10 +376,40 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
336
376
|
});
|
|
337
377
|
}
|
|
338
378
|
|
|
379
|
+
// OpenAPI spec mount. Registered on the framework server (before the
|
|
380
|
+
// user-fetch catch-all below) so it bypasses any auth / rate-limit
|
|
381
|
+
// middleware on the api router — the spec must stay reachable without
|
|
382
|
+
// a session for `app-api-docs` to render it.
|
|
383
|
+
//
|
|
384
|
+
// The `servers` override is load-bearing: hono-openapi walks the
|
|
385
|
+
// BARE api router and emits paths relative to its own root (e.g.
|
|
386
|
+
// `/{id}`, `/{id}/notes`), without the `/api/<id>` prefix it ends
|
|
387
|
+
// up under in the user's outer router. We derive that prefix from
|
|
388
|
+
// `opts.openapi` (everything before the trailing `/openapi.json`)
|
|
389
|
+
// so combined Scalar URLs resolve to the real public paths.
|
|
390
|
+
if (advertiseOpenapi) {
|
|
391
|
+
const apiPrefix = opts.openapi!.replace(/\/openapi\.json$/, "") || "/";
|
|
392
|
+
const spec = await generateSpecs(startOpts.openapi!, {
|
|
393
|
+
documentation: {
|
|
394
|
+
info: {
|
|
395
|
+
title: meta.name,
|
|
396
|
+
version: "0.0.1",
|
|
397
|
+
description: meta.description,
|
|
398
|
+
},
|
|
399
|
+
servers: [{ url: apiPrefix }],
|
|
400
|
+
},
|
|
401
|
+
});
|
|
402
|
+
server.get(opts.openapi!, (c) => c.json(spec));
|
|
403
|
+
}
|
|
404
|
+
|
|
339
405
|
// User's fetch handles everything else. The framework doesn't inject any
|
|
340
406
|
// context vars here — the user's router is expected to register the
|
|
341
407
|
// middlewares it needs (middleware.runtime, middleware.settings, …).
|
|
342
|
-
|
|
408
|
+
//
|
|
409
|
+
// env is threaded through so Bun-specific helpers inside the user's
|
|
410
|
+
// router still work — most importantly `upgradeWebSocket` from hono/bun,
|
|
411
|
+
// which reads the Bun server off `c.env`.
|
|
412
|
+
server.all("*", (c) => Promise.resolve(startOpts.fetch(c.req.raw, c.env)));
|
|
343
413
|
|
|
344
414
|
// Lifecycle
|
|
345
415
|
const cloudCtx: CloudContext = {
|
|
@@ -366,7 +436,9 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
366
436
|
if (stopping) return;
|
|
367
437
|
stopping = true;
|
|
368
438
|
log.info(`Stopping: ${meta.id}`);
|
|
369
|
-
try {
|
|
439
|
+
try {
|
|
440
|
+
if (startOpts.lifecycle?.stop) await startOpts.lifecycle.stop(cloudCtx);
|
|
441
|
+
} catch {}
|
|
370
442
|
await stopRuntimeWatcher();
|
|
371
443
|
await heartbeat.stop();
|
|
372
444
|
};
|
|
@@ -374,7 +446,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
374
446
|
process.on("SIGTERM", () => void shutdown().then(() => process.exit(0)));
|
|
375
447
|
process.on("SIGINT", () => void shutdown().then(() => process.exit(0)));
|
|
376
448
|
|
|
377
|
-
return { port, fetch: server.fetch };
|
|
449
|
+
return { port, development: isDevelopment, fetch: server.fetch };
|
|
378
450
|
};
|
|
379
451
|
|
|
380
452
|
return {
|
|
@@ -33,6 +33,7 @@ export const buildRuntimeFromRegistry = (entries: AppRegistryEntry[]): CloudRunt
|
|
|
33
33
|
searchHelp: e.search?.help,
|
|
34
34
|
searchTagHelp: e.search?.tagHelp,
|
|
35
35
|
legalLinks: e.legalLinks ? e.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
36
|
+
openapi: e.openapi,
|
|
36
37
|
}),
|
|
37
38
|
),
|
|
38
39
|
});
|
|
@@ -32,6 +32,7 @@ const QuerySchema = z
|
|
|
32
32
|
profile: UserProfileSchema.optional(),
|
|
33
33
|
exclude_user_ids: z.string().optional(),
|
|
34
34
|
exclude_group_ids: z.string().optional(),
|
|
35
|
+
exclude_service_account_ids: z.string().optional(),
|
|
35
36
|
user_member_of_group_ids: z.string().optional(),
|
|
36
37
|
member_of_group_id: z.uuid().optional(),
|
|
37
38
|
manager_of_group_id: z.uuid().optional(),
|
|
@@ -102,6 +103,8 @@ const app = new Hono()
|
|
|
102
103
|
if (!excludeUserIds.ok) return respond(c, fail(err.badInput(excludeUserIds.message)));
|
|
103
104
|
const excludeGroupIds = parseUuidList(query.exclude_group_ids, "exclude_group_ids");
|
|
104
105
|
if (!excludeGroupIds.ok) return respond(c, fail(err.badInput(excludeGroupIds.message)));
|
|
106
|
+
const excludeServiceAccountIds = parseUuidList(query.exclude_service_account_ids, "exclude_service_account_ids");
|
|
107
|
+
if (!excludeServiceAccountIds.ok) return respond(c, fail(err.badInput(excludeServiceAccountIds.message)));
|
|
105
108
|
const userMemberOfGroupIds = parseUuidList(query.user_member_of_group_ids, "user_member_of_group_ids");
|
|
106
109
|
if (!userMemberOfGroupIds.ok) return respond(c, fail(err.badInput(userMemberOfGroupIds.message)));
|
|
107
110
|
|
|
@@ -113,6 +116,7 @@ const app = new Hono()
|
|
|
113
116
|
profile: query.profile,
|
|
114
117
|
excludeUserIds: excludeUserIds.value,
|
|
115
118
|
excludeGroupIds: excludeGroupIds.value,
|
|
119
|
+
excludeServiceAccountIds: excludeServiceAccountIds.value,
|
|
116
120
|
userMemberOfGroupIds: userMemberOfGroupIds.value,
|
|
117
121
|
memberOfGroupId: query.member_of_group_id,
|
|
118
122
|
managerOfGroupId: query.manager_of_group_id,
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin API for platform-wide runtime settings.
|
|
3
|
+
*
|
|
4
|
+
* This route lives in cloud-lib because `/admin/settings` is a platform page
|
|
5
|
+
* and needs a typed client without depending on the core app package. The
|
|
6
|
+
* core app mounts it under `/api/admin/core/settings`.
|
|
7
|
+
*/
|
|
8
|
+
import { sql } from "bun";
|
|
9
|
+
import { Hono } from "hono";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { listApps } from "../_internal/registry";
|
|
12
|
+
import { auth, v, type AuthContext } from "../server";
|
|
13
|
+
import { settingsDeleteLegacyKeys, settingsListLegacyKeys } from "../services";
|
|
14
|
+
import * as settings from "../services/settings";
|
|
15
|
+
import { SETTINGS_MAP } from "../services/settings/defaults";
|
|
16
|
+
|
|
17
|
+
const BulkUpdateSchema = z.record(z.string(), z.unknown());
|
|
18
|
+
|
|
19
|
+
type FieldErrors = Record<string, string>;
|
|
20
|
+
|
|
21
|
+
const isKnownSetting = (key: string): boolean => SETTINGS_MAP.has(key);
|
|
22
|
+
const liveSettingKeys = async () => (await listApps()).flatMap((app) => [...(app.settingKeys ?? [])]);
|
|
23
|
+
|
|
24
|
+
const app = new Hono<AuthContext>()
|
|
25
|
+
.get("/legacy", auth.requireRole("admin"), async (c) => {
|
|
26
|
+
return c.json(await settingsListLegacyKeys(await liveSettingKeys()));
|
|
27
|
+
})
|
|
28
|
+
.delete("/legacy", auth.requireRole("admin"), async (c) => {
|
|
29
|
+
return c.json(await settingsDeleteLegacyKeys(await liveSettingKeys()));
|
|
30
|
+
})
|
|
31
|
+
.put(
|
|
32
|
+
"/",
|
|
33
|
+
auth.requireRole("admin"),
|
|
34
|
+
v("json", BulkUpdateSchema),
|
|
35
|
+
async (c) => {
|
|
36
|
+
const updates = c.req.valid("json");
|
|
37
|
+
const keys = Object.keys(updates);
|
|
38
|
+
|
|
39
|
+
if (keys.length === 0) {
|
|
40
|
+
return c.body(null, 204);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ownership: FieldErrors = {};
|
|
44
|
+
for (const key of keys) {
|
|
45
|
+
if (!isKnownSetting(key)) {
|
|
46
|
+
ownership[key] = `Unknown setting "${key}"`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (Object.keys(ownership).length > 0) {
|
|
50
|
+
return c.json({ message: "Invalid keys", errors: ownership }, 400);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const fieldErrors: FieldErrors = {};
|
|
54
|
+
try {
|
|
55
|
+
await sql.begin(async () => {
|
|
56
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
57
|
+
try {
|
|
58
|
+
await settings.set(key, value);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
fieldErrors[key] = error instanceof Error ? error.message : `Failed to update ${key}`;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : "Save failed";
|
|
67
|
+
return c.json(
|
|
68
|
+
{
|
|
69
|
+
message,
|
|
70
|
+
errors: Object.keys(fieldErrors).length > 0 ? fieldErrors : { _form: message },
|
|
71
|
+
},
|
|
72
|
+
400,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return c.body(null, 204);
|
|
77
|
+
},
|
|
78
|
+
)
|
|
79
|
+
.delete(
|
|
80
|
+
"/:key{.+}",
|
|
81
|
+
auth.requireRole("admin"),
|
|
82
|
+
async (c) => {
|
|
83
|
+
const key = c.req.param("key");
|
|
84
|
+
if (!isKnownSetting(key)) {
|
|
85
|
+
return c.json({ message: `Unknown setting "${key}"` }, 400);
|
|
86
|
+
}
|
|
87
|
+
try {
|
|
88
|
+
await settings.remove(key);
|
|
89
|
+
return c.body(null, 204);
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const message = error instanceof Error ? error.message : "Reset failed";
|
|
92
|
+
return c.json({ message }, 500);
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
export default app;
|
|
98
|
+
export type ApiType = typeof app;
|