@podosoft/podokit 0.9.0 → 0.10.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/dist/add.d.ts +20 -0
- package/dist/add.js +121 -8
- package/dist/assemble.d.ts +4 -1
- package/dist/assemble.js +35 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +20 -4
- package/dist/inspect.d.ts +5 -0
- package/dist/inspect.js +5 -0
- package/dist/lockfile.d.ts +20 -1
- package/dist/lockfile.js +48 -6
- package/dist/remove.js +3 -2
- package/dist/templates/fullstack-nest-svelte/README.md +8 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/Dockerfile +12 -8
- package/dist/templates/fullstack-nest-svelte/apps/api/src/health/health.controller.ts +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/Dockerfile +12 -8
- package/dist/templates/fullstack-nest-svelte/apps/web/src/app.css +42 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/api.ts +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +4 -2
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/fullstack-nest-svelte/compose.dev.yaml +4 -0
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +6 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/configmap.yaml +2 -0
- package/dist/templates/fullstack-nest-svelte/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/fullstack-nest-svelte/infra/traefik/dynamic.yml +4 -0
- package/dist/templates/fullstack-nest-svelte/package.json +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.controller.ts +60 -1
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/site-settings/site-settings.service.ts +3 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +11 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-data.server.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte +46 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +730 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/appearance-card.svelte +371 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +12 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/apply-theme.ts +124 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site/themes.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/site.svelte.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +8 -717
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +17 -9
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/general-settings.svelte +0 -7
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +4 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +56 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +10 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +9 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +64 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +42 -13
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +21 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +14 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +12 -4
- package/dist/templates/modules/admin-dashboard/module.manifest.json +1 -1
- package/dist/templates/modules/api-key-auth/files/tests/api/machine.api.spec.ts +25 -0
- package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +2 -2
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +22 -0
- package/dist/templates/modules/auth/module.manifest.json +5 -1
- package/dist/templates/modules/bullmq/files/infra/docker/worker.compose.example.yml +2 -1
- package/dist/templates/modules/bullmq/files/tests/api/jobs.api.spec.ts +38 -0
- package/dist/templates/modules/file-upload/files/tests/api/files.api.spec.ts +31 -0
- package/dist/templates/modules/job-progress/files/tests/api/progress.api.spec.ts +22 -0
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +28 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +38 -3
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +35 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +6 -3
- package/dist/templates/modules/redis/files/tests/api/redis.api.spec.ts +30 -0
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +46 -0
- package/dist/templates/todo/apps/api/Dockerfile +12 -8
- package/dist/templates/todo/apps/api/src/health/health.controller.ts +2 -2
- package/dist/templates/todo/apps/web/Dockerfile +12 -8
- package/dist/templates/todo/apps/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/server/api.ts +3 -1
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +4 -2
- package/dist/templates/todo/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/todo/compose.dev.yaml +4 -0
- package/dist/templates/todo/dot-gitignore +6 -0
- package/dist/templates/todo/infra/k3s/configmap.yaml +2 -0
- package/dist/templates/todo/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/todo/infra/traefik/dynamic.yml +4 -0
- package/dist/templates/todo/package.json +1 -1
- package/dist/update.js +74 -8
- package/package.json +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +0 -61
|
@@ -23,6 +23,14 @@
|
|
|
23
23
|
--border: oklch(0.92 0.004 286.32);
|
|
24
24
|
--input: oklch(0.92 0.004 286.32);
|
|
25
25
|
--ring: oklch(0.705 0.015 286.067);
|
|
26
|
+
--sidebar: oklch(0.985 0 0);
|
|
27
|
+
--sidebar-foreground: oklch(0.141 0.005 285.823);
|
|
28
|
+
--sidebar-primary: oklch(0.21 0.006 285.885);
|
|
29
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
30
|
+
--sidebar-accent: oklch(0.967 0.001 286.375);
|
|
31
|
+
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
|
|
32
|
+
--sidebar-border: oklch(0.92 0.004 286.32);
|
|
33
|
+
--sidebar-ring: oklch(0.705 0.015 286.067);
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
.dark {
|
|
@@ -44,6 +52,14 @@
|
|
|
44
52
|
--border: oklch(1 0 0 / 10%);
|
|
45
53
|
--input: oklch(1 0 0 / 15%);
|
|
46
54
|
--ring: oklch(0.552 0.016 285.938);
|
|
55
|
+
--sidebar: oklch(0.21 0.006 285.885);
|
|
56
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
57
|
+
--sidebar-primary: oklch(0.92 0.004 286.32);
|
|
58
|
+
--sidebar-primary-foreground: oklch(0.21 0.006 285.885);
|
|
59
|
+
--sidebar-accent: oklch(0.274 0.006 286.033);
|
|
60
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
61
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
62
|
+
--sidebar-ring: oklch(0.552 0.016 285.938);
|
|
47
63
|
}
|
|
48
64
|
|
|
49
65
|
@theme inline {
|
|
@@ -69,6 +85,14 @@
|
|
|
69
85
|
--color-border: var(--border);
|
|
70
86
|
--color-input: var(--input);
|
|
71
87
|
--color-ring: var(--ring);
|
|
88
|
+
--color-sidebar: var(--sidebar);
|
|
89
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
90
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
91
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
92
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
93
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
94
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
95
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
72
96
|
}
|
|
73
97
|
|
|
74
98
|
@layer base {
|
|
@@ -79,3 +103,21 @@
|
|
|
79
103
|
@apply bg-background text-foreground;
|
|
80
104
|
}
|
|
81
105
|
}
|
|
106
|
+
|
|
107
|
+
/* shadcn-svelte sidebar active-state correction.
|
|
108
|
+
* Upstream (shadcn-svelte registry, `nova` style) styles the active item with
|
|
109
|
+
* the `data-active:` Tailwind variant, which matches attribute PRESENCE
|
|
110
|
+
* (`[data-active]`). The menu buttons always render `data-active="true|false"`,
|
|
111
|
+
* so the active look (accent background, accent text, medium weight) leaks onto
|
|
112
|
+
* EVERY item — nearly invisible in the default palette but glaring once a theme
|
|
113
|
+
* raises the contrast. We keep `ui/sidebar/*` as a pristine mirror of upstream
|
|
114
|
+
* (so `shadcn-svelte` updates apply cleanly) and correct the behaviour here:
|
|
115
|
+
* neutralize the resting state of non-active items, leaving hover/press feedback
|
|
116
|
+
* (their `:hover`/`:active` rules) untouched. Drop this once shadcn-svelte ships
|
|
117
|
+
* value-matching `data-[active=true]:` (canonical shadcn/ui already does). */
|
|
118
|
+
[data-sidebar="menu-button"][data-active="false"]:not(:hover):not(:active),
|
|
119
|
+
[data-sidebar="menu-sub-button"][data-active="false"]:not(:hover):not(:active) {
|
|
120
|
+
background-color: transparent;
|
|
121
|
+
color: var(--sidebar-foreground);
|
|
122
|
+
font-weight: 400;
|
|
123
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import type { RequestEvent } from "@sveltejs/kit";
|
|
2
2
|
import { createApiClient, type ApiClient } from "@podosoft/podokit-api-client";
|
|
3
|
-
import { backendBaseUrl } from "./backend-proxy";
|
|
3
|
+
import { backendBaseUrl, normalizeClientIp } from "./backend-proxy";
|
|
4
4
|
|
|
5
5
|
// Server-side client for load functions and hooks: points at the internal
|
|
6
6
|
// backend URL and forwards the incoming request's cookies.
|
|
7
7
|
export function serverApiClient(event: RequestEvent): ApiClient {
|
|
8
8
|
const cookie = event.request.headers.get("cookie") ?? "";
|
|
9
|
+
const clientIp = normalizeClientIp(event.getClientAddress());
|
|
9
10
|
const fetchWithCookies: typeof fetch = (input, init) => {
|
|
10
11
|
const headers = new Headers(init?.headers);
|
|
11
12
|
if (cookie) headers.set("cookie", cookie);
|
|
13
|
+
if (clientIp) headers.set("x-forwarded-for", clientIp);
|
|
12
14
|
return event.fetch(input, { ...init, headers });
|
|
13
15
|
};
|
|
14
16
|
return createApiClient({
|
|
@@ -12,7 +12,7 @@ export function backendBaseUrl(): string {
|
|
|
12
12
|
// IPv4 as-is but anonymizes IPv6 (masks the host bits), so an IPv6 loopback would
|
|
13
13
|
// be recorded as "::" — map loopback to 127.0.0.1 for a clean local value, and
|
|
14
14
|
// unwrap IPv4-mapped addresses.
|
|
15
|
-
function normalizeClientIp(address: string | undefined): string | undefined {
|
|
15
|
+
export function normalizeClientIp(address: string | undefined): string | undefined {
|
|
16
16
|
if (!address) return undefined;
|
|
17
17
|
if (address === "::1" || address === "::") return "127.0.0.1";
|
|
18
18
|
if (address.startsWith("::ffff:")) return address.slice("::ffff:".length);
|
|
@@ -37,7 +37,9 @@ export async function proxyRequest(
|
|
|
37
37
|
const upstream = await fetch(targetUrl, {
|
|
38
38
|
method: request.method,
|
|
39
39
|
headers,
|
|
40
|
-
|
|
40
|
+
// Preserve multipart uploads and other binary bodies byte-for-byte. Reading
|
|
41
|
+
// them as text corrupts bytes that are not valid UTF-8 before forwarding.
|
|
42
|
+
body: hasBody ? await request.arrayBuffer() : undefined,
|
|
41
43
|
redirect: "manual",
|
|
42
44
|
});
|
|
43
45
|
|
|
@@ -48,6 +48,10 @@ services:
|
|
|
48
48
|
environment:
|
|
49
49
|
- VITE_DOCKER=1
|
|
50
50
|
- BACKEND_INTERNAL_URL=http://api:5002
|
|
51
|
+
# One trusted Traefik proxy sits in front of SvelteKit. This keeps
|
|
52
|
+
# getClientAddress() tied to the visitor instead of the proxy container.
|
|
53
|
+
- ADDRESS_HEADER=x-forwarded-for
|
|
54
|
+
- XFF_DEPTH=1
|
|
51
55
|
# Match Traefik's published port so HMR's WebSocket targets the right port
|
|
52
56
|
# (default 80). Without this, HMR connects to :80 and a non-80 stack falls
|
|
53
57
|
# back to a full page reload on every edit.
|
|
@@ -1,8 +1,18 @@
|
|
|
1
|
+
apiVersion: traefik.io/v1alpha1
|
|
2
|
+
kind: Middleware
|
|
3
|
+
metadata:
|
|
4
|
+
name: compression
|
|
5
|
+
namespace: podokit
|
|
6
|
+
spec:
|
|
7
|
+
compress: {}
|
|
8
|
+
---
|
|
1
9
|
apiVersion: networking.k8s.io/v1
|
|
2
10
|
kind: Ingress
|
|
3
11
|
metadata:
|
|
4
12
|
name: app
|
|
5
13
|
namespace: podokit
|
|
14
|
+
annotations:
|
|
15
|
+
traefik.ingress.kubernetes.io/router.middlewares: podokit-compression@kubernetescrd
|
|
6
16
|
spec:
|
|
7
17
|
rules:
|
|
8
18
|
- host: example.com
|
|
@@ -26,6 +26,65 @@ const ALLOWED = new Set<string>(PUBLIC_SITE_KEYS);
|
|
|
26
26
|
const MAX_ICON_BYTES = 1024 * 1024; // 1 MB
|
|
27
27
|
const ICON_TYPES = new Set(["image/svg+xml", "image/png", "image/x-icon", "image/vnd.microsoft.icon"]);
|
|
28
28
|
|
|
29
|
+
// Theme values are injected into a client-side <style>, so validate them to
|
|
30
|
+
// prevent CSS injection. Colors must be hex, radius a bounded rem number,
|
|
31
|
+
// preset a slug, and overrides a JSON map of hex colors under known token keys.
|
|
32
|
+
const HEX = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
33
|
+
const PRESET = /^[a-z0-9-]{1,32}$/;
|
|
34
|
+
const THEME_TOKEN_KEYS = new Set([
|
|
35
|
+
"background",
|
|
36
|
+
"card",
|
|
37
|
+
"foreground",
|
|
38
|
+
"mutedForeground",
|
|
39
|
+
"border",
|
|
40
|
+
"secondary",
|
|
41
|
+
"accent",
|
|
42
|
+
"primary",
|
|
43
|
+
"primaryForeground",
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** Validate/normalize a single theme setting value. Empty string clears it. */
|
|
47
|
+
function validateThemeValue(key: string, value: string): string {
|
|
48
|
+
if (value === "") return "";
|
|
49
|
+
if (key === "brandColor") {
|
|
50
|
+
if (!HEX.test(value)) throw new BadRequestException("brandColor must be a hex color");
|
|
51
|
+
return value;
|
|
52
|
+
}
|
|
53
|
+
if (key === "themePreset") {
|
|
54
|
+
if (!PRESET.test(value)) throw new BadRequestException("Invalid themePreset");
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
if (key === "themeRadius") {
|
|
58
|
+
const n = Number(value);
|
|
59
|
+
if (!Number.isFinite(n) || n < 0 || n > 4) throw new BadRequestException("themeRadius must be 0–4");
|
|
60
|
+
return String(n);
|
|
61
|
+
}
|
|
62
|
+
if (key === "themeOverrides") {
|
|
63
|
+
let parsed: unknown;
|
|
64
|
+
try {
|
|
65
|
+
parsed = JSON.parse(value);
|
|
66
|
+
} catch {
|
|
67
|
+
throw new BadRequestException("themeOverrides must be valid JSON");
|
|
68
|
+
}
|
|
69
|
+
if (!parsed || typeof parsed !== "object") throw new BadRequestException("themeOverrides must be an object");
|
|
70
|
+
const out: Record<string, Record<string, string>> = {};
|
|
71
|
+
for (const mode of ["light", "dark"] as const) {
|
|
72
|
+
const block = (parsed as Record<string, unknown>)[mode];
|
|
73
|
+
if (block === undefined) continue;
|
|
74
|
+
if (!block || typeof block !== "object") throw new BadRequestException(`themeOverrides.${mode} must be an object`);
|
|
75
|
+
const clean: Record<string, string> = {};
|
|
76
|
+
for (const [k, v] of Object.entries(block as Record<string, unknown>)) {
|
|
77
|
+
if (!THEME_TOKEN_KEYS.has(k)) throw new BadRequestException(`Unknown theme token: ${k}`);
|
|
78
|
+
if (typeof v !== "string" || !HEX.test(v)) throw new BadRequestException(`themeOverrides.${mode}.${k} must be a hex color`);
|
|
79
|
+
clean[k] = v;
|
|
80
|
+
}
|
|
81
|
+
if (Object.keys(clean).length) out[mode] = clean;
|
|
82
|
+
}
|
|
83
|
+
return JSON.stringify(out);
|
|
84
|
+
}
|
|
85
|
+
return value;
|
|
86
|
+
}
|
|
87
|
+
|
|
29
88
|
@ApiTags("site")
|
|
30
89
|
@Controller("site")
|
|
31
90
|
export class SiteSettingsController {
|
|
@@ -53,7 +112,7 @@ export class SiteSettingsController {
|
|
|
53
112
|
const update: Record<string, string> = {};
|
|
54
113
|
for (const [key, value] of Object.entries(body ?? {})) {
|
|
55
114
|
if (!ALLOWED.has(key)) throw new BadRequestException(`Unknown setting: ${key}`);
|
|
56
|
-
update[key] = String(value ?? "");
|
|
115
|
+
update[key] = validateThemeValue(key, String(value ?? ""));
|
|
57
116
|
}
|
|
58
117
|
return this.site.setMany(update);
|
|
59
118
|
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import type { Handle } from "@sveltejs/kit";
|
|
2
2
|
import { serverApiClient } from "$lib/server/api";
|
|
3
3
|
import { resolveLocale } from "$lib/i18n/messages";
|
|
4
|
+
import { applySearchIndexingHeaders } from "$lib/server/search-indexing";
|
|
4
5
|
import type { SiteSettings } from "$lib/site.svelte";
|
|
5
6
|
|
|
6
7
|
export const handle: Handle = async ({ event, resolve }) => {
|
|
8
|
+
// Static assets and API proxy requests do not render application layouts.
|
|
9
|
+
// Avoid multiplying settings/session API calls for every asset and proxied
|
|
10
|
+
// request; page and data routes still receive the complete runtime context.
|
|
11
|
+
if (event.route.id === null || event.url.pathname.startsWith("/api/")) {
|
|
12
|
+
const response = await resolve(event);
|
|
13
|
+
return applySearchIndexingHeaders(response, event.url.pathname);
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
// Public site settings (branding, locale/timezone defaults, maintenance, …),
|
|
8
17
|
// loaded once here and reused by the layout load. A visitor with no locale
|
|
9
18
|
// cookie falls back to the admin-configured site locale, then the app default.
|
|
@@ -25,7 +34,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
25
34
|
event.locals.user = null;
|
|
26
35
|
event.locals.session = null;
|
|
27
36
|
}
|
|
28
|
-
|
|
37
|
+
const response = await resolve(event, {
|
|
29
38
|
transformPageChunk: ({ html }) => html.replace('lang="en"', `lang="${event.locals.locale}"`),
|
|
30
39
|
});
|
|
40
|
+
return applySearchIndexingHeaders(response, event.url.pathname);
|
|
31
41
|
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
2
|
+
|
|
3
|
+
type ServerFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
4
|
+
|
|
5
|
+
const defaultCapabilities: Capabilities = {
|
|
6
|
+
twoFactor: false,
|
|
7
|
+
providers: [],
|
|
8
|
+
deleteAccount: false,
|
|
9
|
+
auditLog: false,
|
|
10
|
+
emailVerification: false,
|
|
11
|
+
passwordBreachCheck: false,
|
|
12
|
+
magicLink: false,
|
|
13
|
+
emailOtp: false,
|
|
14
|
+
username: false,
|
|
15
|
+
multiSession: false,
|
|
16
|
+
phoneNumber: false,
|
|
17
|
+
apiKey: false,
|
|
18
|
+
passkey: false,
|
|
19
|
+
organization: false,
|
|
20
|
+
oidcProvider: false,
|
|
21
|
+
roles: ["admin", "user"],
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export async function loadAccountData(locals: App.Locals, fetch: ServerFetch): Promise<{
|
|
25
|
+
user: App.Locals["user"];
|
|
26
|
+
currentSessionId: string | null;
|
|
27
|
+
capabilities: Capabilities;
|
|
28
|
+
}> {
|
|
29
|
+
let capabilities = defaultCapabilities;
|
|
30
|
+
try {
|
|
31
|
+
const response = await fetch("/api/account/capabilities");
|
|
32
|
+
if (response.ok) capabilities = (await response.json()) as Capabilities;
|
|
33
|
+
} catch {
|
|
34
|
+
/* Keep safe defaults if the capability endpoint is unavailable. */
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
user: locals.user,
|
|
39
|
+
currentSessionId: locals.session?.id ?? null,
|
|
40
|
+
capabilities,
|
|
41
|
+
};
|
|
42
|
+
}
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-menu.svelte
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { Button } from "$lib/components/ui/button";
|
|
4
|
+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
|
5
|
+
import LayoutDashboardIcon from "@lucide/svelte/icons/layout-dashboard";
|
|
6
|
+
import LogOutIcon from "@lucide/svelte/icons/log-out";
|
|
7
|
+
import UserIcon from "@lucide/svelte/icons/user";
|
|
8
|
+
import { api } from "$lib/api";
|
|
9
|
+
import { getI18n } from "$lib/i18n";
|
|
10
|
+
import type { SessionUser } from "../../app.d.ts";
|
|
11
|
+
import UserAvatar from "./user-avatar.svelte";
|
|
12
|
+
|
|
13
|
+
let { user }: { user: SessionUser | null } = $props();
|
|
14
|
+
const i18n = getI18n();
|
|
15
|
+
|
|
16
|
+
async function signOut(): Promise<void> {
|
|
17
|
+
await api.auth.signOut();
|
|
18
|
+
await goto("/login", { invalidateAll: true });
|
|
19
|
+
}
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
{#if user}
|
|
23
|
+
<DropdownMenu.Root>
|
|
24
|
+
<DropdownMenu.Trigger
|
|
25
|
+
class="hover:bg-muted focus-visible:border-ring focus-visible:ring-ring/50 inline-flex size-8 items-center justify-center rounded-full outline-none focus-visible:ring-3"
|
|
26
|
+
aria-label={i18n.t.nav.account}
|
|
27
|
+
>
|
|
28
|
+
<UserAvatar {user} class="size-8" />
|
|
29
|
+
</DropdownMenu.Trigger>
|
|
30
|
+
<DropdownMenu.Content align="end" class="w-56">
|
|
31
|
+
<DropdownMenu.Label>
|
|
32
|
+
<span class="block truncate font-medium">{user.name}</span>
|
|
33
|
+
<span class="text-muted-foreground block truncate text-xs font-normal">{user.email}</span>
|
|
34
|
+
</DropdownMenu.Label>
|
|
35
|
+
<DropdownMenu.Separator />
|
|
36
|
+
<DropdownMenu.Item onSelect={() => goto("/account")}><UserIcon class="mr-2 size-4" /> {i18n.t.nav.account}</DropdownMenu.Item>
|
|
37
|
+
{#if user.role === "admin"}
|
|
38
|
+
<DropdownMenu.Item onSelect={() => goto("/admin")}><LayoutDashboardIcon class="mr-2 size-4" /> {i18n.t.common.appName}</DropdownMenu.Item>
|
|
39
|
+
{/if}
|
|
40
|
+
<DropdownMenu.Separator />
|
|
41
|
+
<DropdownMenu.Item onSelect={signOut}><LogOutIcon class="mr-2 size-4" /> {i18n.t.nav.signOut}</DropdownMenu.Item>
|
|
42
|
+
</DropdownMenu.Content>
|
|
43
|
+
</DropdownMenu.Root>
|
|
44
|
+
{:else}
|
|
45
|
+
<Button href="/login" variant="ghost">{i18n.t.auth.signIn}</Button>
|
|
46
|
+
{/if}
|