@podosoft/podokit 0.8.0 → 0.10.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/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/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/backend-proxy.ts +3 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +6 -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/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 +3 -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 +90 -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)/+layout.server.ts +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +8 -680
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +10 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +30 -11
- 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/(auth)/login/+page.svelte +96 -14
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +1 -0
- 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/apps/web/src/routes/setup-2fa/+page.server.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/setup-2fa/+page.svelte +122 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +106 -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-flows.ui.spec.ts +74 -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 +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +13 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +21 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +23 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +41 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +12 -8
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +35 -0
- 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 +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +23 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +137 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/verify-email.ui.spec.ts +17 -0
- 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/account/account.controller.ts +8 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/two-factor-required.guard.ts +68 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +5 -1
- package/dist/templates/modules/auth/module.manifest.json +10 -0
- 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/tests/api/rate-limit.api.spec.ts +35 -0
- 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/web/src/lib/components/site-runtime.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +3 -1
- package/dist/templates/todo/apps/web/src/routes/+layout.svelte +2 -2
- package/dist/templates/todo/dot-gitignore +6 -0
- package/dist/templates/todo/infra/k3s/ingress.yaml +10 -0
- package/dist/templates/todo/infra/traefik/dynamic.yml +4 -0
- 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
|
@@ -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,6 +1,7 @@
|
|
|
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 }) => {
|
|
@@ -25,7 +26,8 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
25
26
|
event.locals.user = null;
|
|
26
27
|
event.locals.session = null;
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
const response = await resolve(event, {
|
|
29
30
|
transformPageChunk: ({ html }) => html.replace('lang="en"', `lang="${event.locals.locale}"`),
|
|
30
31
|
});
|
|
32
|
+
return applySearchIndexingHeaders(response, event.url.pathname);
|
|
31
33
|
};
|
|
@@ -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}
|