@valentinkolb/cloud 0.4.0 → 0.5.1
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 -6
- package/scripts/preload.ts +78 -23
- package/src/_internal/define-app.ts +53 -46
- 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 +116 -13
- package/src/api/index.ts +7 -2
- 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 +2 -0
- package/src/contracts/index.ts +3 -2
- package/src/contracts/registry.ts +2 -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 +47 -7
- package/src/services/auth-flows/magic-link.ts +92 -20
- 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/notifications/index.ts +82 -11
- 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 +79 -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 +58 -0
- package/src/shared/redirect.ts +56 -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.1",
|
|
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,37 +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
|
|
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",
|
|
39
49
|
"@tabler/icons-webfont": "^3.36.1",
|
|
40
50
|
"@tailwindcss/typography": "^0.5.19",
|
|
41
|
-
"@valentinkolb/ssr": "0.
|
|
42
|
-
"@valentinkolb/stdlib": "0.
|
|
51
|
+
"@valentinkolb/ssr": "0.10.0",
|
|
52
|
+
"@valentinkolb/stdlib": "0.12.0",
|
|
43
53
|
"@valentinkolb/sync": "^5.0.0",
|
|
44
54
|
"bun-plugin-tailwind": "^0.1.2",
|
|
45
55
|
"hono": "^4.11.1",
|
|
46
56
|
"hono-openapi": "^1.1.2",
|
|
57
|
+
"jose": "^6.1.3",
|
|
47
58
|
"katex": "^0.16.28",
|
|
48
59
|
"marked": "^17.0.1",
|
|
49
60
|
"mermaid": "^11.12.2",
|
|
50
61
|
"mustache": "^4.2.0",
|
|
51
|
-
"nodemailer": "^
|
|
52
|
-
"sanitize-html": "^2.17.
|
|
62
|
+
"nodemailer": "^8.0.10",
|
|
63
|
+
"sanitize-html": "^2.17.4",
|
|
64
|
+
"seroval": "^1.5.4",
|
|
53
65
|
"solid-js": "^1.9.10",
|
|
54
66
|
"tailwindcss": "^4.1.18",
|
|
55
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,27 +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
13
|
import { generateSpecs } from "hono-openapi";
|
|
12
|
-
import type {
|
|
13
|
-
import type {
|
|
14
|
-
AppMeta,
|
|
15
|
-
AppLifecycle,
|
|
16
|
-
AppCapabilities,
|
|
17
|
-
AppSearchContext,
|
|
18
|
-
CloudContext,
|
|
19
|
-
} from "../contracts/app";
|
|
14
|
+
import type { AppCapabilities, AppLifecycle, AppMeta, AppSearchContext, CloudContext } from "../contracts/app";
|
|
20
15
|
import type { AppRegistryEntry } from "../contracts/registry";
|
|
21
|
-
import type { Role } from "../contracts/shared";
|
|
22
16
|
import type { AppSettingsMap, KindToType } from "../contracts/settings-types";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
17
|
+
import type { Role } from "../contracts/shared";
|
|
18
|
+
import { themeBootstrapScript } from "../shared/theme";
|
|
25
19
|
import { auth } from "../server/middleware/auth";
|
|
26
20
|
import { logger } from "../services/logging";
|
|
27
|
-
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";
|
|
28
24
|
import { createHeartbeat } from "./heartbeat";
|
|
29
25
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
30
26
|
|
|
@@ -81,7 +77,7 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
81
77
|
/**
|
|
82
78
|
* Legal/info links contributed by this app — aggregated app-wide via
|
|
83
79
|
* `listLegalLinks()` and rendered in login footer, app Footer, rail more
|
|
84
|
-
* dropdown. Each app contributes its own (e.g.
|
|
80
|
+
* dropdown. Each app contributes its own (e.g. core owns
|
|
85
81
|
* Imprint/Privacy/Terms; faq owns FAQ). KISS: no `external` flag, links
|
|
86
82
|
* always open in a new tab from the login footer.
|
|
87
83
|
*/
|
|
@@ -102,8 +98,8 @@ export type AppOptions<S extends AppSettingsMap = {}> = {
|
|
|
102
98
|
* `/admin/<id>` — admin SSR pages
|
|
103
99
|
* `/public/<id>` — built CSS and other static assets
|
|
104
100
|
*
|
|
105
|
-
* Apps with non-standard URLs (core's `/auth`, `/me
|
|
106
|
-
* `/.well-known
|
|
101
|
+
* Apps with non-standard URLs (core's `/auth`, `/me`, `/legal/*`, `/impressum`;
|
|
102
|
+
* oauth's `/oauth`, `/.well-known/...`) list whatever
|
|
107
103
|
* top-level paths they own. The gateway is dumb — it just builds a
|
|
108
104
|
* prefix-trie from these strings.
|
|
109
105
|
*/
|
|
@@ -151,7 +147,7 @@ export type StartOptions = {
|
|
|
151
147
|
* before this fetch — they take precedence over any catch-all the app
|
|
152
148
|
* might register.
|
|
153
149
|
*/
|
|
154
|
-
fetch: (req: Request) => Response | Promise<Response>;
|
|
150
|
+
fetch: (req: Request, env?: unknown) => Response | Promise<Response>;
|
|
155
151
|
/**
|
|
156
152
|
* Hono router to scan for OpenAPI route metadata. When set together with
|
|
157
153
|
* `defineApp({ openapi: "..." })`, the framework generates an OpenAPI
|
|
@@ -172,6 +168,7 @@ export type StartOptions = {
|
|
|
172
168
|
|
|
173
169
|
export type StartResult = {
|
|
174
170
|
port: number;
|
|
171
|
+
development: boolean;
|
|
175
172
|
fetch: Hono["fetch"];
|
|
176
173
|
};
|
|
177
174
|
|
|
@@ -204,6 +201,8 @@ export type AppDefinition<S extends AppSettingsMap = {}> = {
|
|
|
204
201
|
// ── Implementation ──────────────────────────────────────────────────────────
|
|
205
202
|
|
|
206
203
|
export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<S>): AppDefinition<S> => {
|
|
204
|
+
const isDevelopment = process.env.NODE_ENV === "development";
|
|
205
|
+
|
|
207
206
|
// ── 0. Register declared settings into the runtime registry ──────────
|
|
208
207
|
// SETTINGS_MAP is the single source of truth for validation in store.ts
|
|
209
208
|
// (writeKey checks SETTINGS_MAP.get(key)) and for snapshot.ts (allKnownKeys
|
|
@@ -237,7 +236,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
237
236
|
|
|
238
237
|
// ── 1. SSR config ─────────────────────────────────────────────────────
|
|
239
238
|
const { config, plugin, html } = createSsrConfig<PageOptions>({
|
|
240
|
-
dev:
|
|
239
|
+
dev: isDevelopment,
|
|
241
240
|
verbose: true,
|
|
242
241
|
rootDir: opts.appRoot ?? process.cwd(),
|
|
243
242
|
basePath: opts.basePath,
|
|
@@ -252,17 +251,10 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
252
251
|
<meta name="theme-color" content="#09090b">
|
|
253
252
|
<meta name="mobile-web-app-capable" content="yes">
|
|
254
253
|
<link rel="icon" href="/branding/favicon">
|
|
255
|
-
<
|
|
254
|
+
<style data-cloud-css-layers>@layer theme, base, components, utilities;</style>
|
|
256
255
|
<link rel="stylesheet" href="/public/${opts.id}/app.css?v=${v}">
|
|
257
|
-
<
|
|
258
|
-
|
|
259
|
-
var el = document.documentElement;
|
|
260
|
-
if (!el.hasAttribute('data-theme-fixed')) {
|
|
261
|
-
var theme = document.cookie.match(/theme=([^;]+)/)?.[1] || 'light';
|
|
262
|
-
el.classList.add(theme);
|
|
263
|
-
}
|
|
264
|
-
})();
|
|
265
|
-
</script>
|
|
256
|
+
<link rel="stylesheet" href="/public/global.css?v=${v}">
|
|
257
|
+
<script>${themeBootstrapScript}</script>
|
|
266
258
|
</head>
|
|
267
259
|
<body>
|
|
268
260
|
${body}
|
|
@@ -288,6 +280,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
288
280
|
nav: opts.nav,
|
|
289
281
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
290
282
|
widgets: opts.widgets ? opts.widgets.map((w) => ({ ...w })) : undefined,
|
|
283
|
+
settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
|
|
291
284
|
openapi: opts.openapi,
|
|
292
285
|
};
|
|
293
286
|
|
|
@@ -310,16 +303,17 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
310
303
|
description: meta.description,
|
|
311
304
|
baseUrl,
|
|
312
305
|
routes: [...meta.routes],
|
|
313
|
-
nav:
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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,
|
|
323
317
|
search: startOpts.capabilities?.search
|
|
324
318
|
? {
|
|
325
319
|
tags: [...(startOpts.capabilities.search.tags ?? [])],
|
|
@@ -330,6 +324,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
330
324
|
: undefined,
|
|
331
325
|
legalLinks: meta.legalLinks ? meta.legalLinks.map((l) => ({ ...l })) : undefined,
|
|
332
326
|
widgets: meta.widgets ? meta.widgets.map((w) => ({ ...w })) : undefined,
|
|
327
|
+
settingKeys: meta.settingKeys ? [...meta.settingKeys] : undefined,
|
|
333
328
|
openapi: advertiseOpenapi ? opts.openapi : undefined,
|
|
334
329
|
};
|
|
335
330
|
|
|
@@ -354,12 +349,15 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
354
349
|
|
|
355
350
|
const server = new Hono()
|
|
356
351
|
.route(ssrMountPath, routes(config))
|
|
357
|
-
.use(
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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
|
+
)
|
|
363
361
|
// serveStatic calls next() on miss — terminate /public/* here so a
|
|
364
362
|
// missing asset is a clean 404 instead of falling through to the app
|
|
365
363
|
// fetch (which might render an HTML page for the missing path).
|
|
@@ -368,6 +366,9 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
368
366
|
if (startOpts.capabilities?.search) {
|
|
369
367
|
const searchRun = startOpts.capabilities.search.run;
|
|
370
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
|
+
}
|
|
371
372
|
const body = await c.req.json<{ query: string; tags: string[]; limit: number }>();
|
|
372
373
|
const ctx: AppSearchContext = { get: (key) => c.get(key) as never };
|
|
373
374
|
const results = await searchRun({ query: body.query, tags: body.tags, limit: body.limit, ctx });
|
|
@@ -404,7 +405,11 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
404
405
|
// User's fetch handles everything else. The framework doesn't inject any
|
|
405
406
|
// context vars here — the user's router is expected to register the
|
|
406
407
|
// middlewares it needs (middleware.runtime, middleware.settings, …).
|
|
407
|
-
|
|
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)));
|
|
408
413
|
|
|
409
414
|
// Lifecycle
|
|
410
415
|
const cloudCtx: CloudContext = {
|
|
@@ -431,7 +436,9 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
431
436
|
if (stopping) return;
|
|
432
437
|
stopping = true;
|
|
433
438
|
log.info(`Stopping: ${meta.id}`);
|
|
434
|
-
try {
|
|
439
|
+
try {
|
|
440
|
+
if (startOpts.lifecycle?.stop) await startOpts.lifecycle.stop(cloudCtx);
|
|
441
|
+
} catch {}
|
|
435
442
|
await stopRuntimeWatcher();
|
|
436
443
|
await heartbeat.stop();
|
|
437
444
|
};
|
|
@@ -439,7 +446,7 @@ export const defineApp = <const S extends AppSettingsMap = {}>(opts: AppOptions<
|
|
|
439
446
|
process.on("SIGTERM", () => void shutdown().then(() => process.exit(0)));
|
|
440
447
|
process.on("SIGINT", () => void shutdown().then(() => process.exit(0)));
|
|
441
448
|
|
|
442
|
-
return { port, fetch: server.fetch };
|
|
449
|
+
return { port, development: isDevelopment, fetch: server.fetch };
|
|
443
450
|
};
|
|
444
451
|
|
|
445
452
|
return {
|
|
@@ -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;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { ok } from "@valentinkolb/stdlib";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { describeRoute } from "hono-openapi";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import {
|
|
6
|
+
ActiveAnnouncementsResponseSchema,
|
|
7
|
+
AnnouncementEntrySchema,
|
|
8
|
+
AnnouncementListResponseSchema,
|
|
9
|
+
CreateAnnouncementSchema,
|
|
10
|
+
ErrorResponseSchema,
|
|
11
|
+
MessageResponseSchema,
|
|
12
|
+
parseAnnouncementCookieHeader,
|
|
13
|
+
UpdateAnnouncementSchema,
|
|
14
|
+
} from "../contracts";
|
|
15
|
+
import { type AuthContext, auth, jsonResponse, requiresAdmin, requiresAuth, respond, v } from "../server";
|
|
16
|
+
import { announcements } from "../services";
|
|
17
|
+
|
|
18
|
+
const IdParamSchema = z.object({ id: z.uuid() });
|
|
19
|
+
|
|
20
|
+
const withMessage = async <T>(operation: Promise<import("@valentinkolb/stdlib").Result<T>>, message: string) => {
|
|
21
|
+
const result = await operation;
|
|
22
|
+
if (!result.ok) return result;
|
|
23
|
+
return ok({ message });
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const announcementRoutes = new Hono<AuthContext>().get(
|
|
27
|
+
"/active",
|
|
28
|
+
auth.requireRole("authenticated"),
|
|
29
|
+
describeRoute({
|
|
30
|
+
tags: ["Announcements"],
|
|
31
|
+
summary: "List active user announcements",
|
|
32
|
+
description: "Returns active banners and unseen announcements for the current request cookie state.",
|
|
33
|
+
...requiresAuth,
|
|
34
|
+
responses: {
|
|
35
|
+
200: jsonResponse(ActiveAnnouncementsResponseSchema, "Active announcements"),
|
|
36
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
37
|
+
},
|
|
38
|
+
}),
|
|
39
|
+
async (c) => {
|
|
40
|
+
const state = parseAnnouncementCookieHeader(c.req.header("Cookie"));
|
|
41
|
+
return respond(c, ok(await announcements.active.forState({ state })));
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
export const adminAnnouncementRoutes = new Hono<AuthContext>()
|
|
46
|
+
.use(auth.requireRole("admin"))
|
|
47
|
+
.get(
|
|
48
|
+
"/",
|
|
49
|
+
describeRoute({
|
|
50
|
+
tags: ["Admin Announcements"],
|
|
51
|
+
summary: "List announcements",
|
|
52
|
+
...requiresAdmin,
|
|
53
|
+
responses: {
|
|
54
|
+
200: jsonResponse(AnnouncementListResponseSchema, "Announcements"),
|
|
55
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
56
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
57
|
+
},
|
|
58
|
+
}),
|
|
59
|
+
v(
|
|
60
|
+
"query",
|
|
61
|
+
z.object({
|
|
62
|
+
kind: z.enum(["announcement", "banner"]).optional(),
|
|
63
|
+
search: z.string().optional(),
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
async (c) => {
|
|
67
|
+
const query = c.req.valid("query");
|
|
68
|
+
const items = await announcements.admin.list({
|
|
69
|
+
filter: { kind: query.kind, query: query.search },
|
|
70
|
+
});
|
|
71
|
+
return respond(c, ok({ items }));
|
|
72
|
+
},
|
|
73
|
+
)
|
|
74
|
+
.post(
|
|
75
|
+
"/",
|
|
76
|
+
describeRoute({
|
|
77
|
+
tags: ["Admin Announcements"],
|
|
78
|
+
summary: "Create announcement",
|
|
79
|
+
...requiresAdmin,
|
|
80
|
+
responses: {
|
|
81
|
+
201: jsonResponse(AnnouncementEntrySchema, "Created announcement"),
|
|
82
|
+
400: jsonResponse(ErrorResponseSchema, "Validation error"),
|
|
83
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
84
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
v("json", CreateAnnouncementSchema),
|
|
88
|
+
async (c) => respond(c, announcements.admin.create({ data: c.req.valid("json"), actorId: c.get("user").id }), 201),
|
|
89
|
+
)
|
|
90
|
+
.patch(
|
|
91
|
+
"/:id",
|
|
92
|
+
describeRoute({
|
|
93
|
+
tags: ["Admin Announcements"],
|
|
94
|
+
summary: "Update announcement",
|
|
95
|
+
...requiresAdmin,
|
|
96
|
+
responses: {
|
|
97
|
+
200: jsonResponse(AnnouncementEntrySchema, "Updated announcement"),
|
|
98
|
+
400: jsonResponse(ErrorResponseSchema, "Validation error"),
|
|
99
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
100
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
101
|
+
404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
|
|
102
|
+
},
|
|
103
|
+
}),
|
|
104
|
+
v("param", IdParamSchema),
|
|
105
|
+
v("json", UpdateAnnouncementSchema),
|
|
106
|
+
async (c) =>
|
|
107
|
+
respond(
|
|
108
|
+
c,
|
|
109
|
+
announcements.admin.update({
|
|
110
|
+
id: c.req.valid("param").id,
|
|
111
|
+
data: c.req.valid("json"),
|
|
112
|
+
actorId: c.get("user").id,
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
.delete(
|
|
117
|
+
"/:id",
|
|
118
|
+
describeRoute({
|
|
119
|
+
tags: ["Admin Announcements"],
|
|
120
|
+
summary: "Delete announcement",
|
|
121
|
+
...requiresAdmin,
|
|
122
|
+
responses: {
|
|
123
|
+
200: jsonResponse(MessageResponseSchema, "Announcement deleted"),
|
|
124
|
+
401: jsonResponse(ErrorResponseSchema, "Authentication required"),
|
|
125
|
+
403: jsonResponse(ErrorResponseSchema, "Admin access required"),
|
|
126
|
+
404: jsonResponse(ErrorResponseSchema, "Announcement not found"),
|
|
127
|
+
},
|
|
128
|
+
}),
|
|
129
|
+
v("param", IdParamSchema),
|
|
130
|
+
async (c) => respond(c, withMessage(announcements.admin.remove({ id: c.req.valid("param").id }), "Announcement deleted.")),
|
|
131
|
+
);
|