@podosoft/podokit 1.4.2 → 1.6.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/README.md +2 -1
- package/dist/app-proxy-timeouts.d.ts +27 -0
- package/dist/app-proxy-timeouts.js +180 -0
- package/dist/assemble.d.ts +5 -2
- package/dist/assemble.js +14 -4
- package/dist/deploy-checks.d.ts +44 -0
- package/dist/deploy-checks.js +267 -0
- package/dist/deploy-compose-profile.d.ts +6 -2
- package/dist/deploy-compose-profile.js +15 -6
- package/dist/deploy-compose-render.d.ts +12 -4
- package/dist/deploy-compose-render.js +47 -17
- package/dist/deploy-compose.d.ts +2 -5
- package/dist/deploy-compose.js +7 -2
- package/dist/deploy-profile.d.ts +6 -2
- package/dist/deploy-profile.js +11 -7
- package/dist/deploy-render.d.ts +4 -1
- package/dist/deploy-render.js +28 -20
- package/dist/deploy-schema.d.ts +61 -0
- package/dist/deploy-schema.js +157 -1
- package/dist/deploy.d.ts +2 -5
- package/dist/deploy.js +5 -2
- package/dist/dev.d.ts +21 -0
- package/dist/dev.js +78 -3
- package/dist/eject.js +2 -0
- package/dist/index.js +85 -5
- package/dist/lib.d.ts +2 -0
- package/dist/lib.js +5 -2
- package/dist/lockfile.d.ts +24 -0
- package/dist/lockfile.js +77 -5
- package/dist/provider.d.ts +1 -0
- package/dist/provider.js +12 -4
- package/dist/remove.js +1 -1
- package/dist/template-files.d.ts +42 -0
- package/dist/template-files.js +102 -0
- package/dist/templates/fullstack/AGENTS.md +44 -7
- package/dist/templates/fullstack/README.md +8 -7
- package/dist/templates/fullstack/apps/api/package.json +4 -4
- package/dist/templates/fullstack/apps/api/scripts/build.extensions.mjs +6 -0
- package/dist/templates/fullstack/apps/api/scripts/build.mjs +65 -16
- package/dist/templates/fullstack/apps/api/scripts/run-migrations.mjs +7 -3
- package/dist/templates/fullstack/apps/api/scripts/verify-api-contract.ts +5 -1
- package/dist/templates/fullstack/apps/api/src/app.extensions.ts +7 -4
- package/dist/templates/fullstack/apps/api/src/app.spec.ts +22 -0
- package/dist/templates/fullstack/apps/api/src/app.ts +47 -28
- package/dist/templates/fullstack/apps/api/src/build.spec.ts +118 -0
- package/dist/templates/fullstack/apps/api/src/config/env.extensions.ts +20 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.spec.ts +76 -0
- package/dist/templates/fullstack/apps/api/src/config/env.validation.ts +46 -3
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.spec.ts +53 -0
- package/dist/templates/fullstack/apps/api/src/config/provider-selection.ts +79 -0
- package/dist/templates/fullstack/apps/api/src/config/providers.ts +8 -2
- package/dist/templates/fullstack/apps/api/src/config/server-options.spec.ts +17 -6
- package/dist/templates/fullstack/apps/api/src/config/server-options.ts +5 -8
- package/dist/templates/fullstack/apps/api/src/core/api-contract.extensions.ts +6 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.spec.ts +29 -0
- package/dist/templates/fullstack/apps/api/src/core/api-contract.ts +39 -2
- package/dist/templates/fullstack/apps/api/src/core/client-address.spec.ts +82 -0
- package/dist/templates/fullstack/apps/api/src/core/client-address.ts +68 -0
- package/dist/templates/fullstack/apps/api/src/core/services.ts +43 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.spec.ts +27 -0
- package/dist/templates/fullstack/apps/api/src/database/data-source.ts +7 -10
- package/dist/templates/fullstack/apps/api/src/database/database.spec.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/database.ts +2 -3
- package/dist/templates/fullstack/apps/api/src/database/entities.ts +12 -0
- package/dist/templates/fullstack/apps/api/src/database/migrate.ts +12 -2
- package/dist/templates/fullstack/apps/api/src/database/migration-files.spec.ts +116 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-files.ts +134 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.spec.ts +36 -0
- package/dist/templates/fullstack/apps/api/src/database/migration-readiness.ts +25 -0
- package/dist/templates/fullstack/apps/api/src/database/run-migrations.spec.ts +12 -1
- package/dist/templates/fullstack/apps/api/src/database/sqlite-migrator.ts +2 -36
- package/dist/templates/fullstack/apps/api/src/main.ts +2 -2
- package/dist/templates/fullstack/apps/api/tsconfig.json +2 -0
- package/dist/templates/fullstack/apps/web/Dockerfile +4 -0
- package/dist/templates/fullstack/apps/web/package.json +1 -1
- package/dist/templates/fullstack/apps/web/src/app.css +3 -0
- package/dist/templates/fullstack/apps/web/src/app.extensions.css +11 -0
- package/dist/templates/fullstack/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/api.test.ts +19 -0
- package/dist/templates/fullstack/apps/web/src/lib/server/api.ts +4 -1
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.test.ts +370 -36
- package/dist/templates/fullstack/apps/web/src/lib/server/backend-proxy.ts +250 -41
- package/dist/templates/fullstack/apps/web/src/lib/server/body-limits.ts +5 -2
- package/dist/templates/fullstack/apps/web/src/lib/server/proxy-timeouts.ts +23 -0
- package/dist/templates/fullstack/apps/web/tsconfig.json +1 -1
- package/dist/templates/fullstack/apps/web/vite.config.test.ts +33 -0
- package/dist/templates/fullstack/apps/web/vite.config.ts +55 -13
- package/dist/templates/fullstack/apps/web/vite.extensions.ts +23 -0
- package/dist/templates/fullstack/compose.dev.yaml +7 -4
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/SKILL.md +2 -2
- package/dist/templates/fullstack/dot-agents/skills/podokit-deploy/references/profile.md +12 -3
- package/dist/templates/fullstack/dot-claude/skills/podokit-update/SKILL.md +5 -2
- package/dist/templates/fullstack/dot-env.docker.example +7 -2
- package/dist/templates/fullstack/dot-env.example +21 -1
- package/dist/templates/fullstack/dot-podokit/dev.json +1 -1
- package/dist/templates/fullstack/infra/k3s/configmap.yaml +4 -0
- package/dist/templates/fullstack/tests/README.md +40 -1
- package/dist/templates/fullstack/tests/helpers/app-paths.ts +18 -0
- package/dist/templates/fullstack/tests/helpers/paths.ts +33 -0
- package/dist/templates/fullstack/tests/package.json +2 -2
- package/dist/templates/fullstack/tests/playwright.config.ts +7 -3
- package/dist/templates/fullstack/tests/playwright.extensions.ts +88 -1
- package/dist/templates/fullstack/tests/ui/smoke.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.spec.ts +152 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/account-sessions/account-sessions.routes.ts +112 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.revoker.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.spec.ts +18 -0
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin-sessions/admin-sessions.service.ts +36 -13
- package/dist/templates/modules/admin-dashboard/files/apps/api/src/admin.module.ts +13 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.test.ts +37 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +15 -6
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flags.ts +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-nav.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.test.ts +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/nav.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts +15 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte +73 -39
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/admin-sidebar.svelte +2 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +7 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/impersonation-banner.svelte +3 -2
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/settings/app-flags-settings.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +32 -23
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.test.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte +4 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +3 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/en.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/catalogs/admin-dashboard/ko.json +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/app-hooks.ts +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts +5 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(admin)/admin/settings/+page.svelte +1 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts +2 -1
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.svelte +1 -5
- package/dist/templates/modules/admin-dashboard/files/dot-claude/skills/podokit-data-table/SKILL.md +2 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/account-sessions.api.spec.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +159 -170
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +72 -81
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +1 -6
- package/dist/templates/modules/admin-dashboard/files/tests/api/profile-image.api.spec.ts +1 -1
- package/dist/templates/modules/admin-dashboard/files/tests/api/site-settings.api.spec.ts +43 -53
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +29 -1
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +24 -6
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/app-accounts.ts +24 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/devices.ts +55 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/disposable-users.ts +5 -3
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +3 -9
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +11 -5
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +3 -10
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-flows.ui.spec.ts +4 -5
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account-sessions.ui.spec.ts +46 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +62 -59
- package/dist/templates/modules/admin-dashboard/files/tests/ui/app-flags.ui.spec.ts +63 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/appearance.ui.spec.ts +12 -16
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +3 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +32 -35
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +4 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +1 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/general-effects.ui.spec.ts +82 -91
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +6 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +16 -20
- package/dist/templates/modules/admin-dashboard/files/tests/ui/impersonation.ui.spec.ts +6 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/login-methods.ui.spec.ts +2 -3
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +2 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +5 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +7 -2
- package/dist/templates/modules/admin-dashboard/files/tests/ui/same-origin-requests.ui.spec.ts +89 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +5 -4
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings-dialogs.ui.spec.ts +67 -73
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +84 -93
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +33 -7
- package/dist/templates/modules/admin-dashboard/files/tests/ui/theme-mode.ui.spec.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/two-factor.ui.spec.ts +51 -54
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +10 -9
- package/dist/templates/modules/admin-dashboard/module.manifest.json +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +2 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +5 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.spec.ts +33 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +20 -1
- package/dist/templates/modules/audit-log/module.manifest.json +15 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-app-name.spec.ts +21 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-config-store.ts +13 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-environment.ts +9 -2
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +19 -3
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.module.ts +6 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.runtime.spec.ts +10 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.spec.ts +54 -6
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.service.ts +26 -4
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +15 -8
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.spec.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/revoke-other-sessions.ts +45 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.spec.ts +86 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/schema-recheck.ts +44 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/migrate.ts +3 -1
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-flags.ts +9 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.spec.ts +43 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +30 -5
- package/dist/templates/modules/auth/module.manifest.json +8 -2
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.spec.ts +57 -1
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/bullmq-job.queue.ts +93 -9
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.spec.ts +40 -0
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +52 -5
- package/dist/templates/modules/bullmq/files/apps/api/src/main-worker.ts +9 -7
- package/dist/templates/modules/bullmq/files/apps/api/src/worker.extensions.ts +13 -0
- package/dist/templates/modules/bullmq/module.manifest.json +5 -1
- package/dist/templates/modules/job-progress/files/apps/api/src/progress/progress.worker.spec.ts +2 -0
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.spec.ts +87 -1
- package/dist/templates/modules/jobs-local/files/apps/api/src/jobs-local/local-job.queue.ts +93 -3
- package/dist/templates/modules/jobs-local/module.manifest.json +3 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.spec.ts +16 -0
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +15 -10
- package/dist/templates/modules/mailer/module.manifest.json +1 -1
- package/dist/templates/modules/object-storage-local/files/tests/api/storage-local.api.spec.ts +1 -1
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -4
- package/dist/templates/modules/object-storage-s3/files/tests/api/storage.api.spec.ts +1 -1
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.spec.ts +2 -59
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.config.ts +0 -65
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.spec.ts +82 -21
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.identity.ts +54 -15
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.spec.ts +43 -5
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +10 -2
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.spec.ts +61 -3
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.routes.ts +48 -15
- package/dist/templates/modules/rate-limit/files/tests/api/rate-limit.api.spec.ts +63 -27
- package/dist/templates/modules/rate-limit/module.manifest.json +6 -9
- package/dist/templates/modules/sse/files/tests/api/events.api.spec.ts +1 -1
- package/dist/templates/todo/AGENTS.md +46 -6
- package/dist/templates/todo/README.md +6 -4
- package/dist/text-diff.d.ts +12 -0
- package/dist/text-diff.js +111 -0
- package/dist/update.d.ts +61 -3
- package/dist/update.js +152 -27
- package/package.json +2 -2
- package/dist/templates/modules/admin-dashboard/files/tests/api/exclusive.api.spec.ts +0 -75
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/exclusive.ts +0 -80
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/account-page.svelte
CHANGED
|
@@ -11,9 +11,12 @@
|
|
|
11
11
|
import { deviceLabel } from "#lib/device-label.js";
|
|
12
12
|
import { toast } from "svelte-sonner";
|
|
13
13
|
import { api } from "#lib/api.js";
|
|
14
|
+
import { listAccountSessions, revokeAccountSession, type AccountSession } from "#lib/account-sessions.js";
|
|
15
|
+
import { KeysetPages } from "#lib/admin/sessions.js";
|
|
14
16
|
import { getI18n, fmt, formatDateTime } from "#lib/i18n/index.js";
|
|
15
17
|
import { untrack } from "svelte";
|
|
16
18
|
import type { SessionUser } from "../../app.d.ts";
|
|
19
|
+
import ImpersonationBanner from "./impersonation-banner.svelte";
|
|
17
20
|
import UserAvatar from "./user-avatar.svelte";
|
|
18
21
|
import {
|
|
19
22
|
ApiError,
|
|
@@ -31,7 +34,13 @@
|
|
|
31
34
|
accountPath = "/account",
|
|
32
35
|
returnPath = "/",
|
|
33
36
|
}: {
|
|
34
|
-
data: {
|
|
37
|
+
data: {
|
|
38
|
+
user: SessionUser;
|
|
39
|
+
currentSessionId: string | null;
|
|
40
|
+
capabilities: Capabilities;
|
|
41
|
+
/** Set by the /account load while an administrator acts as this user. */
|
|
42
|
+
showImpersonationBanner?: boolean;
|
|
43
|
+
};
|
|
35
44
|
accountPath?: string;
|
|
36
45
|
returnPath?: string;
|
|
37
46
|
} = $props();
|
|
@@ -392,28 +401,34 @@
|
|
|
392
401
|
await goto("/login", { invalidateAll: true });
|
|
393
402
|
}
|
|
394
403
|
|
|
395
|
-
// Sessions — this user's own devices
|
|
396
|
-
|
|
404
|
+
// Sessions — this user's own devices. The API pages them, because Better Auth's
|
|
405
|
+
// /list-sessions returns at most 100 and could leave out the current one.
|
|
406
|
+
/** Better Auth's code for "this session is older than `session.freshAge`". */
|
|
397
407
|
const SESSION_NOT_FRESH = "SESSION_NOT_FRESH";
|
|
398
|
-
type Session =
|
|
408
|
+
type Session = AccountSession & { current: boolean };
|
|
399
409
|
const PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
410
|
+
// The API pages newest first by keyset, so the order is fixed rather than sortable.
|
|
411
|
+
const sessionsSort: SortState = { key: "createdAt", dir: "desc" };
|
|
412
|
+
const sessionPages = new KeysetPages();
|
|
400
413
|
let sessions = $state<Session[]>([]);
|
|
414
|
+
let sessionsTotal = $state(0);
|
|
415
|
+
let sessionsPagerTotal = $state(0);
|
|
401
416
|
let sessionsPage = $state(1);
|
|
402
|
-
let sessionsSort = $state<SortState | null>(null);
|
|
403
417
|
let busy = $state(false);
|
|
418
|
+
let latestSessionsRequest = 0;
|
|
404
419
|
const sessionsColumns: DataTableColumn<Session>[] = [
|
|
405
|
-
{ key: "userAgent", label: i18n.t.sessions.device
|
|
406
|
-
{ key: "ipAddress", label: i18n.t.sessions.ip
|
|
407
|
-
{ key: "createdAt", label: i18n.t.sessions.since
|
|
420
|
+
{ key: "userAgent", label: i18n.t.sessions.device },
|
|
421
|
+
{ key: "ipAddress", label: i18n.t.sessions.ip },
|
|
422
|
+
{ key: "createdAt", label: i18n.t.sessions.since },
|
|
408
423
|
{ key: "status", label: i18n.t.sessions.status },
|
|
409
424
|
{ key: "actions", label: "", class: "w-10" },
|
|
410
425
|
];
|
|
411
426
|
/**
|
|
412
427
|
* True when the session is too old to list sessions.
|
|
413
428
|
*
|
|
414
|
-
* better-auth
|
|
415
|
-
*
|
|
416
|
-
*
|
|
429
|
+
* The API keeps better-auth's freshness rule for listing sessions, and
|
|
430
|
+
* `session.freshAge` defaults to 24 hours. So for anyone who signed in yesterday —
|
|
431
|
+
* which is most people, most of the time — the list answers 403
|
|
417
432
|
* `SESSION_NOT_FRESH`. It is not a failure to report as one: the tab has to say what
|
|
418
433
|
* happened and what to do, or it is a blank table under an untranslated toast.
|
|
419
434
|
*/
|
|
@@ -423,32 +438,45 @@
|
|
|
423
438
|
await api.auth.signOut();
|
|
424
439
|
await goto("/login", { invalidateAll: true });
|
|
425
440
|
}
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
441
|
+
/** Loads one page; the current session leads the first. A newer request supersedes any in flight. */
|
|
442
|
+
async function loadSessions(target = 1): Promise<void> {
|
|
443
|
+
if (target === 1) sessionPages.reset();
|
|
444
|
+
const cursor = sessionPages.cursorFor(target);
|
|
445
|
+
if (cursor === undefined) return;
|
|
446
|
+
const request = ++latestSessionsRequest;
|
|
447
|
+
try {
|
|
448
|
+
const result = await listAccountSessions(api, { limit: PAGE_SIZE, cursor });
|
|
449
|
+
if (request !== latestSessionsRequest) return;
|
|
450
|
+
// A revoke can empty the last page; show the one before it instead.
|
|
451
|
+
if (result.items.length === 0 && target > 1) return loadSessions(target - 1);
|
|
452
|
+
sessionsStale = false;
|
|
453
|
+
sessionPages.record(target, result.nextCursor);
|
|
454
|
+
const others = result.items.map((s) => ({ ...s, current: false }));
|
|
455
|
+
sessions = target === 1 && result.current ? [{ ...result.current, current: true }, ...others] : others;
|
|
456
|
+
sessionsTotal = result.total;
|
|
457
|
+
sessionsPagerTotal = sessionPages.pagerTotal(result.total - (result.current ? 1 : 0), PAGE_SIZE);
|
|
458
|
+
sessionsPage = target;
|
|
459
|
+
} catch (error) {
|
|
460
|
+
if (request !== latestSessionsRequest) return;
|
|
461
|
+
if (error instanceof ApiError && error.code === SESSION_NOT_FRESH) {
|
|
462
|
+
// Explained in the panel below, not thrown at the corner of the screen.
|
|
463
|
+
sessionsStale = true;
|
|
464
|
+
sessions = [];
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
toast.error(i18n.t.sessions.loadFailed);
|
|
433
468
|
}
|
|
434
|
-
sessionsStale = false;
|
|
435
|
-
if (error) return void toast.error(error.message ?? i18n.t.sessions.loadFailed);
|
|
436
|
-
// Current session first (users expect it on top; also keeps it on page 1),
|
|
437
|
-
// then newest to oldest.
|
|
438
|
-
sessions = ((res ?? []) as Session[]).sort((a, b) => {
|
|
439
|
-
if (a.id === data.currentSessionId) return -1;
|
|
440
|
-
if (b.id === data.currentSessionId) return 1;
|
|
441
|
-
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
|
|
442
|
-
});
|
|
443
469
|
}
|
|
444
|
-
async function revoke(
|
|
470
|
+
async function revoke(id: string): Promise<void> {
|
|
445
471
|
busy = true;
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
if (error) toast.error(error.message ?? i18n.t.sessions.revokeFailed);
|
|
449
|
-
else {
|
|
472
|
+
try {
|
|
473
|
+
await revokeAccountSession(api, id);
|
|
450
474
|
toast.success(i18n.t.sessions.revoked);
|
|
451
|
-
await loadSessions();
|
|
475
|
+
await loadSessions(sessionsPage);
|
|
476
|
+
} catch {
|
|
477
|
+
toast.error(i18n.t.sessions.revokeFailed);
|
|
478
|
+
} finally {
|
|
479
|
+
busy = false;
|
|
452
480
|
}
|
|
453
481
|
}
|
|
454
482
|
async function signOutOthers(): Promise<void> {
|
|
@@ -531,6 +559,9 @@
|
|
|
531
559
|
</script>
|
|
532
560
|
|
|
533
561
|
<div class="flex flex-col gap-6">
|
|
562
|
+
{#if data.showImpersonationBanner}
|
|
563
|
+
<ImpersonationBanner email={data.user.email} class="rounded-md" />
|
|
564
|
+
{/if}
|
|
534
565
|
<h1 class="text-2xl font-semibold">{i18n.t.account.title}</h1>
|
|
535
566
|
|
|
536
567
|
<div class="flex flex-col gap-6 lg:flex-row">
|
|
@@ -824,7 +855,7 @@
|
|
|
824
855
|
<Card.Root>
|
|
825
856
|
<Card.Header class="flex flex-row items-center justify-between gap-2">
|
|
826
857
|
<Card.Title>{i18n.t.sessions.title}</Card.Title>
|
|
827
|
-
<Button variant="outline" size="sm" disabled={busy || sessionsStale ||
|
|
858
|
+
<Button variant="outline" size="sm" disabled={busy || sessionsStale || sessionsTotal <= 1} onclick={signOutOthers}>{i18n.t.sessions.signOutOthers}</Button>
|
|
828
859
|
</Card.Header>
|
|
829
860
|
<Card.Content>
|
|
830
861
|
{#if sessionsStale}
|
|
@@ -843,20 +874,23 @@
|
|
|
843
874
|
columns={sessionsColumns}
|
|
844
875
|
rows={sessions}
|
|
845
876
|
getKey={(s) => s.id}
|
|
846
|
-
|
|
877
|
+
sort={sessionsSort}
|
|
878
|
+
manualSort
|
|
879
|
+
manualPagination
|
|
847
880
|
bind:page={sessionsPage}
|
|
848
881
|
perPage={PAGE_SIZE}
|
|
849
|
-
|
|
882
|
+
total={sessionsPagerTotal}
|
|
883
|
+
label={fmt(i18n.t.sessions.total, { count: sessionsTotal })}
|
|
850
884
|
empty={i18n.t.sessions.empty}
|
|
885
|
+
onChange={(state) => void loadSessions(state.page)}
|
|
851
886
|
>
|
|
852
887
|
{#snippet row(s)}
|
|
853
|
-
{@const isCurrent = s.id === data.currentSessionId}
|
|
854
888
|
<Table.Cell class="max-w-xs truncate" title={s.userAgent ?? ""}>{deviceLabel(s.userAgent) ?? s.userAgent ?? i18n.t.sessions.unknown}</Table.Cell>
|
|
855
889
|
<Table.Cell class="text-muted-foreground">{s.ipAddress ?? "—"}</Table.Cell>
|
|
856
890
|
<Table.Cell class="text-muted-foreground">{formatDateTime(s.createdAt)}</Table.Cell>
|
|
857
|
-
<Table.Cell>{#if
|
|
891
|
+
<Table.Cell>{#if s.current}<Badge>{i18n.t.sessions.current}</Badge>{/if}</Table.Cell>
|
|
858
892
|
<Table.Cell>
|
|
859
|
-
<Button variant="ghost" size="sm" disabled={busy ||
|
|
893
|
+
<Button variant="ghost" size="sm" disabled={busy || s.current} onclick={() => revoke(s.id)}>{i18n.t.sessions.revoke}</Button>
|
|
860
894
|
</Table.Cell>
|
|
861
895
|
{/snippet}
|
|
862
896
|
</DataTable>
|
|
@@ -2,36 +2,15 @@
|
|
|
2
2
|
import { page } from "$app/state";
|
|
3
3
|
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
|
4
4
|
import SidebarUserMenu from "./sidebar-user-menu.svelte";
|
|
5
|
-
import LayoutDashboardIcon from "@lucide/svelte/icons/layout-dashboard";
|
|
6
|
-
import UsersIcon from "@lucide/svelte/icons/users";
|
|
7
|
-
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
8
|
-
import ScrollTextIcon from "@lucide/svelte/icons/scroll-text";
|
|
9
|
-
import Building2Icon from "@lucide/svelte/icons/building-2";
|
|
10
|
-
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
11
5
|
import HouseIcon from "@lucide/svelte/icons/house";
|
|
12
6
|
import { getI18n } from "#lib/i18n/index.js";
|
|
13
|
-
import {
|
|
7
|
+
import { adminNavEntries } from "#lib/admin/nav.js";
|
|
14
8
|
import type { SessionUser } from "../../app.d.ts";
|
|
15
9
|
|
|
16
10
|
let { user, capabilities }: { user: SessionUser; capabilities?: { auditLog?: boolean; organization?: boolean } } = $props();
|
|
17
11
|
const i18n = getI18n();
|
|
18
12
|
|
|
19
|
-
|
|
20
|
-
const items = $derived<NavItem[]>([
|
|
21
|
-
{ href: "/admin", label: (t) => t.nav.overview, icon: LayoutDashboardIcon },
|
|
22
|
-
{ href: "/admin/users", label: (t) => t.nav.users, icon: UsersIcon, adminOnly: true },
|
|
23
|
-
{ href: "/admin/sessions", label: (t) => t.nav.sessions, icon: MonitorSmartphoneIcon, adminOnly: true },
|
|
24
|
-
...(capabilities?.organization
|
|
25
|
-
? [{ href: "/admin/organizations", label: (t) => t.nav.organizations, icon: Building2Icon, adminOnly: true } as NavItem]
|
|
26
|
-
: []),
|
|
27
|
-
...(capabilities?.auditLog
|
|
28
|
-
? [{ href: "/admin/audit", label: (t) => t.nav.audit, icon: ScrollTextIcon, adminOnly: true } as NavItem]
|
|
29
|
-
: []),
|
|
30
|
-
// Module-contributed entries (present only for installed modules).
|
|
31
|
-
...moduleNavEntries,
|
|
32
|
-
{ href: "/admin/settings", label: (t) => t.nav.settings, icon: SettingsIcon, adminOnly: true },
|
|
33
|
-
]);
|
|
34
|
-
const visible = $derived(items.filter((item) => !item.adminOnly || user.role === "admin"));
|
|
13
|
+
const visible = $derived(adminNavEntries(capabilities, user.role));
|
|
35
14
|
</script>
|
|
36
15
|
|
|
37
16
|
<Sidebar.Root collapsible="icon">
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
2
3
|
import { cn } from "#lib/utils.js";
|
|
3
4
|
|
|
4
5
|
export type DataTableBreakpoint = "sm" | "md" | "lg" | "xl";
|
|
5
6
|
export type DataTableColumn<Row> = {
|
|
6
7
|
key: string;
|
|
7
8
|
label: string;
|
|
9
|
+
// Header content in place of the label and sort button, such as a
|
|
10
|
+
// select-all checkbox. Give it its own accessible name.
|
|
11
|
+
head?: Snippet;
|
|
8
12
|
sortable?: boolean;
|
|
9
13
|
class?: string;
|
|
10
14
|
hideBelow?: DataTableBreakpoint;
|
|
@@ -37,7 +41,6 @@
|
|
|
37
41
|
</script>
|
|
38
42
|
|
|
39
43
|
<script lang="ts" generics="Row">
|
|
40
|
-
import type { Snippet } from "svelte";
|
|
41
44
|
import { Button } from "#lib/components/ui/button/index.js";
|
|
42
45
|
import * as Table from "#lib/components/ui/table/index.js";
|
|
43
46
|
import TablePagination from "#lib/components/table-pagination.svelte";
|
|
@@ -132,7 +135,9 @@
|
|
|
132
135
|
class={getDataTableColumnClass(col)}
|
|
133
136
|
aria-sort={sort?.key === col.key ? (sort.dir === "asc" ? "ascending" : "descending") : undefined}
|
|
134
137
|
>
|
|
135
|
-
{#if col.
|
|
138
|
+
{#if col.head}
|
|
139
|
+
{@render col.head()}
|
|
140
|
+
{:else if col.sortable}
|
|
136
141
|
<Button variant="ghost" size="sm" class="-mx-2 h-auto gap-1 px-2 py-1 font-medium" onclick={() => toggle(col)}>
|
|
137
142
|
{col.label}
|
|
138
143
|
{#if sort?.key === col.key}
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.test.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createRawSnippet } from "svelte";
|
|
2
|
+
import { render } from "svelte/server";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import DataTable, { type DataTableColumn, type DataTableRowContext } from "./data-table.svelte";
|
|
5
|
+
|
|
6
|
+
type Row = { name: string };
|
|
7
|
+
|
|
8
|
+
const row = createRawSnippet<[Row, DataTableRowContext]>((item) => ({
|
|
9
|
+
render: () => `<td>${item().name}</td><td></td>`,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
describe("DataTable", () => {
|
|
13
|
+
it("renders a column's header snippet in place of its label and sort button", () => {
|
|
14
|
+
const head = createRawSnippet(() => ({ render: () => '<input type="checkbox" aria-label="Select all rows">' }));
|
|
15
|
+
const columns: DataTableColumn<Row>[] = [
|
|
16
|
+
{ key: "name", label: "Name", sortable: true },
|
|
17
|
+
{ key: "selected", label: "Selected", head, sortable: true },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const { body } = render(DataTable<Row>, { props: { columns, rows: [{ name: "Ada" }], row, perPage: 0 } });
|
|
21
|
+
|
|
22
|
+
const headers = body.match(/<th\b[\s\S]*?<\/th>/g) ?? [];
|
|
23
|
+
expect(headers).toHaveLength(2);
|
|
24
|
+
expect(headers[0]).toContain("Name");
|
|
25
|
+
expect(headers[0]).toContain("<button");
|
|
26
|
+
expect(headers[1]).toContain('aria-label="Select all rows"');
|
|
27
|
+
expect(headers[1]).not.toContain("Selected");
|
|
28
|
+
expect(headers[1]).not.toContain("<button");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -3,9 +3,10 @@
|
|
|
3
3
|
import { api } from "#lib/api.js";
|
|
4
4
|
import { Button } from "#lib/components/ui/button/index.js";
|
|
5
5
|
import { getI18n } from "#lib/i18n/index.js";
|
|
6
|
+
import { cn } from "#lib/utils.js";
|
|
6
7
|
import { toast } from "svelte-sonner";
|
|
7
8
|
|
|
8
|
-
let { email }: { email: string } = $props();
|
|
9
|
+
let { email, class: className }: { email: string; class?: string } = $props();
|
|
9
10
|
const i18n = getI18n();
|
|
10
11
|
|
|
11
12
|
async function stopImpersonating(): Promise<void> {
|
|
@@ -18,7 +19,7 @@
|
|
|
18
19
|
}
|
|
19
20
|
</script>
|
|
20
21
|
|
|
21
|
-
<div class="bg-primary text-primary-foreground flex items-center justify-between gap-2 px-4 py-2 text-sm">
|
|
22
|
+
<div class={cn("bg-primary text-primary-foreground flex items-center justify-between gap-2 px-4 py-2 text-sm", className)}>
|
|
22
23
|
<span>{i18n.t.users.impersonatingAs.replace("{email}", email)}</span>
|
|
23
24
|
<Button variant="secondary" size="sm" onclick={stopImpersonating}>{i18n.t.users.stopImpersonating}</Button>
|
|
24
25
|
</div>
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { invalidateAll } from "$app/navigation";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
5
|
+
import { toast } from "svelte-sonner";
|
|
6
|
+
import { appFlagCards } from "#lib/admin/app-flag-settings.js";
|
|
7
|
+
import { api } from "#lib/api.js";
|
|
8
|
+
import { Badge } from "#lib/components/ui/badge/index.js";
|
|
9
|
+
import * as Card from "#lib/components/ui/card/index.js";
|
|
10
|
+
import { Switch } from "#lib/components/ui/switch/index.js";
|
|
11
|
+
import { getI18n } from "#lib/i18n/index.js";
|
|
12
|
+
|
|
13
|
+
// The application's flags from app-flags.ts. They share the feature-flag
|
|
14
|
+
// endpoint, so saving needs an administrator and applies at once, like the
|
|
15
|
+
// switches on the Authentication tab.
|
|
16
|
+
const i18n = getI18n();
|
|
17
|
+
const cards = $derived(appFlagCards(i18n.t, page.data.capabilities as Capabilities | undefined));
|
|
18
|
+
let saving = $state(false);
|
|
19
|
+
|
|
20
|
+
async function toggle(flag: string, next: boolean): Promise<void> {
|
|
21
|
+
saving = true;
|
|
22
|
+
try {
|
|
23
|
+
await api.put("/account/settings", { [flag]: next });
|
|
24
|
+
await invalidateAll();
|
|
25
|
+
toast.success(i18n.t.settings.saved);
|
|
26
|
+
} catch {
|
|
27
|
+
toast.error(i18n.t.settings.saveFailed);
|
|
28
|
+
}
|
|
29
|
+
saving = false;
|
|
30
|
+
}
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<section class="flex flex-col gap-3">
|
|
34
|
+
<div>
|
|
35
|
+
<h2 class="text-lg font-semibold">{i18n.t.settings.appFlags.title}</h2>
|
|
36
|
+
<p class="text-muted-foreground text-sm">{i18n.t.settings.appFlags.subtitle}</p>
|
|
37
|
+
</div>
|
|
38
|
+
<div class="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
39
|
+
{#each cards as card (card.flag)}
|
|
40
|
+
<Card.Root class="flex min-h-[8.5rem] flex-col gap-2 p-4" data-app-flag={card.flag}>
|
|
41
|
+
<div class="flex items-start justify-between gap-3">
|
|
42
|
+
<span class="font-medium break-words">{card.name}</span>
|
|
43
|
+
<Switch
|
|
44
|
+
aria-label={card.name}
|
|
45
|
+
checked={card.on}
|
|
46
|
+
disabled={saving || !card.available}
|
|
47
|
+
onCheckedChange={(value) => toggle(card.flag, value === true)}
|
|
48
|
+
class="shrink-0"
|
|
49
|
+
/>
|
|
50
|
+
</div>
|
|
51
|
+
<p class="text-muted-foreground flex-1 text-sm">{card.description}</p>
|
|
52
|
+
<div class="mt-auto flex items-center gap-2 pt-1">
|
|
53
|
+
<Badge variant={card.on ? "secondary" : "outline"} class="max-w-full gap-1.5">
|
|
54
|
+
<span class={`size-1.5 shrink-0 rounded-full ${card.on ? "bg-green-500" : "bg-muted-foreground/40"}`}></span>
|
|
55
|
+
<span class="truncate">
|
|
56
|
+
{card.available
|
|
57
|
+
? card.on ? i18n.t.settings.enabled : i18n.t.settings.disabled
|
|
58
|
+
: i18n.t.settings.appFlags.unavailable}
|
|
59
|
+
</span>
|
|
60
|
+
</Badge>
|
|
61
|
+
</div>
|
|
62
|
+
</Card.Root>
|
|
63
|
+
{/each}
|
|
64
|
+
</div>
|
|
65
|
+
</section>
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { goto } from "$app/navigation";
|
|
3
3
|
import * as DropdownMenu from "#lib/components/ui/dropdown-menu/index.js";
|
|
4
4
|
import * as Sidebar from "#lib/components/ui/sidebar/index.js";
|
|
5
|
+
import AccountMenu from "./account-menu.svelte";
|
|
5
6
|
import UserAvatar from "./user-avatar.svelte";
|
|
6
7
|
import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down";
|
|
7
8
|
import LogOutIcon from "@lucide/svelte/icons/log-out";
|
|
@@ -12,6 +13,10 @@
|
|
|
12
13
|
|
|
13
14
|
let { user }: { user: SessionUser } = $props();
|
|
14
15
|
const i18n = getI18n();
|
|
16
|
+
// Outside a Sidebar.Provider, such as in an application's own shell, the
|
|
17
|
+
// sidebar menu parts have no state to read, so the same identity block and
|
|
18
|
+
// actions come from the shell-independent AccountMenu instead.
|
|
19
|
+
const inSidebar = (Sidebar.useSidebar() as ReturnType<typeof Sidebar.useSidebar> | undefined) !== undefined;
|
|
15
20
|
|
|
16
21
|
async function signOut(): Promise<void> {
|
|
17
22
|
await api.auth.signOut();
|
|
@@ -19,26 +24,30 @@
|
|
|
19
24
|
}
|
|
20
25
|
</script>
|
|
21
26
|
|
|
22
|
-
|
|
23
|
-
<Sidebar.
|
|
24
|
-
<
|
|
25
|
-
<DropdownMenu.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
<DropdownMenu.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</Sidebar.
|
|
27
|
+
{#if inSidebar}
|
|
28
|
+
<Sidebar.Menu>
|
|
29
|
+
<Sidebar.MenuItem>
|
|
30
|
+
<DropdownMenu.Root>
|
|
31
|
+
<DropdownMenu.Trigger data-testid="sidebar-user-menu">
|
|
32
|
+
{#snippet child({ props })}
|
|
33
|
+
<Sidebar.MenuButton {...props} size="lg" class="data-[state=open]:bg-sidebar-accent">
|
|
34
|
+
<UserAvatar {user} class="size-8" />
|
|
35
|
+
<div class="flex flex-col text-left text-sm leading-tight">
|
|
36
|
+
<span class="truncate font-medium">{user.name}</span>
|
|
37
|
+
<span class="text-muted-foreground truncate text-xs">{user.email}</span>
|
|
38
|
+
</div>
|
|
39
|
+
<ChevronsUpDownIcon class="ml-auto size-4" />
|
|
40
|
+
</Sidebar.MenuButton>
|
|
41
|
+
{/snippet}
|
|
42
|
+
</DropdownMenu.Trigger>
|
|
43
|
+
<DropdownMenu.Content align="end" side="top" class="w-56">
|
|
44
|
+
<DropdownMenu.Item onSelect={() => goto("/account")}><UserIcon class="mr-2 size-4" /> {i18n.t.nav.account}</DropdownMenu.Item>
|
|
45
|
+
<DropdownMenu.Separator />
|
|
46
|
+
<DropdownMenu.Item onSelect={signOut}><LogOutIcon class="mr-2 size-4" /> {i18n.t.nav.signOut}</DropdownMenu.Item>
|
|
47
|
+
</DropdownMenu.Content>
|
|
48
|
+
</DropdownMenu.Root>
|
|
49
|
+
</Sidebar.MenuItem>
|
|
50
|
+
</Sidebar.Menu>
|
|
51
|
+
{:else}
|
|
52
|
+
<AccountMenu {user} variant="identity" side="top" />
|
|
53
|
+
{/if}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { render } from "svelte/server";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import SidebarUserMenu from "./sidebar-user-menu.svelte";
|
|
4
|
+
|
|
5
|
+
vi.mock("$app/state", () => ({
|
|
6
|
+
page: {
|
|
7
|
+
url: new URL("http://localhost/projects"),
|
|
8
|
+
data: {
|
|
9
|
+
locale: "en",
|
|
10
|
+
messages: { nav: { account: "Account", signOut: "Sign out" }, common: { appName: "Admin" } },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("$app/navigation", () => ({ goto: vi.fn(), invalidateAll: vi.fn() }));
|
|
15
|
+
|
|
16
|
+
describe("SidebarUserMenu", () => {
|
|
17
|
+
it("renders the account menu in a shell without a sidebar", () => {
|
|
18
|
+
const user = { id: "user-1", name: "Ada Lovelace", email: "ada@example.com", role: "user" };
|
|
19
|
+
|
|
20
|
+
const { body } = render(SidebarUserMenu, { props: { user } });
|
|
21
|
+
|
|
22
|
+
expect(body).toContain('data-testid="account-menu"');
|
|
23
|
+
expect(body).not.toContain('data-testid="sidebar-user-menu"');
|
|
24
|
+
expect(body).toContain("Ada Lovelace");
|
|
25
|
+
expect(body).toContain("ada@example.com");
|
|
26
|
+
});
|
|
27
|
+
});
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-footer.svelte
CHANGED
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
</script>
|
|
12
12
|
|
|
13
13
|
{#if s.footerText || hasLinks}
|
|
14
|
-
<footer
|
|
14
|
+
<footer
|
|
15
|
+
class="text-muted-foreground flex flex-col items-center gap-1 px-4 py-6 text-center text-sm"
|
|
16
|
+
data-testid="site-footer"
|
|
17
|
+
>
|
|
15
18
|
{#if s.footerText}
|
|
16
19
|
<p>{s.footerText}</p>
|
|
17
20
|
{/if}
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/site-runtime.svelte
CHANGED
|
@@ -44,7 +44,9 @@
|
|
|
44
44
|
{/if}
|
|
45
45
|
</svelte:head>
|
|
46
46
|
|
|
47
|
-
<
|
|
47
|
+
<!-- `.light` on <html> in light mode lets component libraries that follow a `.dark`
|
|
48
|
+
or `.light` class, and fall back to the OS setting otherwise, match the theme. -->
|
|
49
|
+
<ModeWatcher lightClassNames={["light"]} />
|
|
48
50
|
<SessionIdleTimeout timeoutMinutes={sessionIdleTimeoutMinutes} />
|
|
49
51
|
<!-- podokit:begin:site-runtime-components -->
|
|
50
52
|
<!-- podokit:end:site-runtime-components -->
|
|
@@ -86,7 +86,9 @@
|
|
|
86
86
|
{:else}
|
|
87
87
|
<Label class="text-sm" for="toolbar-search">{searchFields[0].label}</Label>
|
|
88
88
|
{/if}
|
|
89
|
-
|
|
89
|
+
<!-- The minimum width makes a narrow row wrap the input to its own line
|
|
90
|
+
instead of squeezing it next to the field select. -->
|
|
91
|
+
<Input id="toolbar-search" bind:value={search} placeholder={searchPlaceholder} class="min-w-48 max-w-xs flex-1" />
|
|
90
92
|
<Button type="submit">{searchButton}</Button>
|
|
91
93
|
</form>
|
|
92
94
|
{/if}
|
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"manage": "Manage",
|
|
294
294
|
"saved": "Setting updated",
|
|
295
295
|
"saveFailed": "Failed to update setting",
|
|
296
|
+
"appFlags": {
|
|
297
|
+
"tab": "Application",
|
|
298
|
+
"title": "Application settings",
|
|
299
|
+
"subtitle": "Turn this application's own features on or off. Changes apply immediately.",
|
|
300
|
+
"unavailable": "Not available"
|
|
301
|
+
},
|
|
296
302
|
"emailPassword": "Email & password",
|
|
297
303
|
"emailPasswordDesc": "Sign in with an email address and password.",
|
|
298
304
|
"emailVerification": "Email verification",
|
|
@@ -293,6 +293,12 @@
|
|
|
293
293
|
"manage": "관리",
|
|
294
294
|
"saved": "설정을 변경했습니다",
|
|
295
295
|
"saveFailed": "설정 변경에 실패했습니다",
|
|
296
|
+
"appFlags": {
|
|
297
|
+
"tab": "애플리케이션",
|
|
298
|
+
"title": "애플리케이션 설정",
|
|
299
|
+
"subtitle": "이 애플리케이션의 기능을 켜거나 끕니다. 변경 사항은 바로 적용됩니다.",
|
|
300
|
+
"unavailable": "사용할 수 없음"
|
|
301
|
+
},
|
|
296
302
|
"emailPassword": "이메일·비밀번호",
|
|
297
303
|
"emailPasswordDesc": "이메일과 비밀번호로 로그인.",
|
|
298
304
|
"emailVerification": "이메일 인증",
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Handle } from "@sveltejs/kit/hooks";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application-owned request hook. The managed hooks.server.ts runs it for every
|
|
5
|
+
* request after PodoKit has loaded the runtime context, so `event.locals` already
|
|
6
|
+
* holds the site settings, the signed-in user, the session, and the locale. Change
|
|
7
|
+
* `event.locals`, add response headers, or answer a request yourself here instead
|
|
8
|
+
* of editing hooks.server.ts. Combine several handles with `sequence` from
|
|
9
|
+
* "@sveltejs/kit/hooks". PodoKit creates this file once and never overwrites it.
|
|
10
|
+
*/
|
|
11
|
+
export const appHandle: Handle = ({ event, resolve }) => resolve(event);
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/non-indexable-paths.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-owned path prefixes whose responses carry
|
|
3
|
+
* `X-Robots-Tag: noindex, nofollow`, in addition to the admin, account, sign-in,
|
|
4
|
+
* and API paths PodoKit already lists in search-indexing.ts. A listed path also
|
|
5
|
+
* covers its subpaths ("/app" covers "/app/projects/1"); "/" covers only the
|
|
6
|
+
* landing page. PodoKit creates this file once and never overwrites it.
|
|
7
|
+
*/
|
|
8
|
+
export const APP_NON_INDEXABLE_PATHS: readonly string[] = [];
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.test.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { applySearchIndexingHeaders, shouldPreventSearchIndexing } from "./search-indexing";
|
|
3
|
+
|
|
4
|
+
// Stands in for the application's own list in the app-owned non-indexable-paths.ts.
|
|
5
|
+
vi.mock("./non-indexable-paths", () => ({ APP_NON_INDEXABLE_PATHS: ["/", "/projects"] }));
|
|
6
|
+
|
|
7
|
+
describe("search indexing", () => {
|
|
8
|
+
it("keeps PodoKit's private paths out of search results", () => {
|
|
9
|
+
expect(shouldPreventSearchIndexing("/admin/users")).toBe(true);
|
|
10
|
+
expect(shouldPreventSearchIndexing("/account")).toBe(true);
|
|
11
|
+
expect(shouldPreventSearchIndexing("/api/site/settings")).toBe(true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it("adds the application's prefixes and their subpaths", () => {
|
|
15
|
+
expect(shouldPreventSearchIndexing("/projects")).toBe(true);
|
|
16
|
+
expect(shouldPreventSearchIndexing("/projects/a/b")).toBe(true);
|
|
17
|
+
expect(shouldPreventSearchIndexing("/projects-archive")).toBe(false);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("treats a listed root as the landing page only", () => {
|
|
21
|
+
expect(shouldPreventSearchIndexing("/")).toBe(true);
|
|
22
|
+
expect(shouldPreventSearchIndexing("/pricing")).toBe(false);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("sets the header only on non-indexable responses", () => {
|
|
26
|
+
expect(applySearchIndexingHeaders(new Response("ok"), "/projects/1").headers.get("x-robots-tag")).toBe(
|
|
27
|
+
"noindex, nofollow",
|
|
28
|
+
);
|
|
29
|
+
expect(applySearchIndexingHeaders(new Response("ok"), "/pricing").headers.has("x-robots-tag")).toBe(false);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/search-indexing.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { APP_NON_INDEXABLE_PATHS } from "./non-indexable-paths";
|
|
2
|
+
|
|
1
3
|
const NON_INDEXABLE_PATH_PREFIXES = [
|
|
2
4
|
"/api",
|
|
3
5
|
"/admin",
|
|
@@ -12,7 +14,9 @@ const NON_INDEXABLE_PATH_PREFIXES = [
|
|
|
12
14
|
"/oauth2",
|
|
13
15
|
"/accept-invitation",
|
|
14
16
|
"/maintenance",
|
|
15
|
-
|
|
17
|
+
// Application prefixes from the app-owned non-indexable-paths.ts.
|
|
18
|
+
...APP_NON_INDEXABLE_PATHS,
|
|
19
|
+
];
|
|
16
20
|
|
|
17
21
|
function matchesPathPrefix(pathname: string, prefix: string): boolean {
|
|
18
22
|
return pathname === prefix || pathname.startsWith(`${prefix}/`);
|
|
@@ -294,7 +294,7 @@
|
|
|
294
294
|
</div>
|
|
295
295
|
|
|
296
296
|
<Tabs.Root value="general">
|
|
297
|
-
<Tabs.List>
|
|
297
|
+
<Tabs.List class="h-auto max-w-full flex-wrap justify-start">
|
|
298
298
|
<Tabs.Trigger value="general">{i18n.t.general.tab}</Tabs.Trigger>
|
|
299
299
|
<Tabs.Trigger value="appearance">{i18n.t.general.appearanceTitle}</Tabs.Trigger>
|
|
300
300
|
<Tabs.Trigger value="auth">{i18n.t.settings.tab}</Tabs.Trigger>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/account/+page.server.ts
CHANGED
|
@@ -6,6 +6,7 @@ export const load: PageServerLoad = async ({ locals, fetch }) => {
|
|
|
6
6
|
await loadProtectedLayout({ locals, fetch });
|
|
7
7
|
return {
|
|
8
8
|
...(await loadAccountData(locals, fetch)),
|
|
9
|
-
|
|
9
|
+
// Rendered by the managed AccountPage, so an owned page never has to change for it.
|
|
10
|
+
showImpersonationBanner: Boolean(locals.session?.impersonatedBy),
|
|
10
11
|
};
|
|
11
12
|
};
|