@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
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { AppException } from "@podosoft/podokit-contracts";
|
|
2
2
|
import { APIError } from "better-auth/api";
|
|
3
|
+
import type { OwnSessionRevoker } from "../account-sessions/account-sessions.routes";
|
|
3
4
|
import type { SessionRevoker } from "./admin-sessions.routes";
|
|
4
5
|
|
|
5
6
|
export const ADMIN_SESSION_REVOKE_FAILED = "ADMIN_SESSION_REVOKE_FAILED";
|
|
7
|
+
export const ACCOUNT_SESSION_REVOKE_FAILED = "ACCOUNT_SESSION_REVOKE_FAILED";
|
|
6
8
|
|
|
7
9
|
export interface RevokeUserSessionApi {
|
|
8
10
|
revokeUserSession(input: { body: { sessionToken: string }; headers: Headers }): Promise<unknown>;
|
|
9
11
|
}
|
|
10
12
|
|
|
13
|
+
export interface RevokeSessionApi {
|
|
14
|
+
revokeSession(input: { body: { token: string }; headers: Headers }): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
/**
|
|
12
18
|
* Revokes through the Better Auth admin endpoint rather than deleting the row, so
|
|
13
19
|
* its permission check and after-hooks (including the audit log) still run.
|
|
@@ -24,3 +30,21 @@ export function createSessionRevoker(api: () => RevokeUserSessionApi): SessionRe
|
|
|
24
30
|
}
|
|
25
31
|
};
|
|
26
32
|
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Revokes one of the caller's own sessions through Better Auth's
|
|
36
|
+
* `/revoke-session`, so its ownership check and after-hooks (including the
|
|
37
|
+
* audit log's `session.revoke`) still run.
|
|
38
|
+
*/
|
|
39
|
+
export function createOwnSessionRevoker(api: () => RevokeSessionApi): OwnSessionRevoker {
|
|
40
|
+
return async (token, headers) => {
|
|
41
|
+
try {
|
|
42
|
+
await api().revokeSession({ body: { token }, headers });
|
|
43
|
+
} catch (error) {
|
|
44
|
+
if (error instanceof APIError) {
|
|
45
|
+
throw new AppException(ACCOUNT_SESSION_REVOKE_FAILED, error.message, error.statusCode);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -183,5 +183,23 @@ describe("AdminSessionsService", () => {
|
|
|
183
183
|
|
|
184
184
|
expect(await service.tokenOf("a")).toBe("token-a");
|
|
185
185
|
expect(await service.tokenOf("missing")).toBeNull();
|
|
186
|
+
// With a user, only that user's session.
|
|
187
|
+
expect(await service.tokenOf("a", "alice")).toBe("token-a");
|
|
188
|
+
expect(await service.tokenOf("a", "bob")).toBeNull();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("leaves one session out and finds an active session by id", async () => {
|
|
192
|
+
await seed(db, [
|
|
193
|
+
{ id: "a", userId: "alice", createdAt: "2026-01-01T00:00:01.000Z" },
|
|
194
|
+
{ id: "b", userId: "alice", createdAt: "2026-01-01T00:00:02.000Z" },
|
|
195
|
+
{ id: "old", userId: "alice", createdAt: "2026-01-01T00:00:03.000Z", expiresAt: "2020-01-01T00:00:00.000Z" },
|
|
196
|
+
]);
|
|
197
|
+
|
|
198
|
+
const page = await service.list({ userId: "alice", excludeId: "b" });
|
|
199
|
+
expect(page.items.map((item) => item.id)).toEqual(["a"]);
|
|
200
|
+
expect(page.total).toBe(1);
|
|
201
|
+
expect(await service.find("b")).toMatchObject({ id: "b", userId: "alice", ipAddress: "203.0.113.7" });
|
|
202
|
+
expect(await service.find("old")).toBeNull();
|
|
203
|
+
expect(await service.find("missing")).toBeNull();
|
|
186
204
|
});
|
|
187
205
|
});
|
|
@@ -30,6 +30,8 @@ export interface AdminSessionQuery {
|
|
|
30
30
|
cursor?: string;
|
|
31
31
|
search?: string;
|
|
32
32
|
userId?: string;
|
|
33
|
+
/** A session to leave out, such as the caller's own. */
|
|
34
|
+
excludeId?: string;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
interface SessionRow {
|
|
@@ -76,6 +78,20 @@ function decodeCursor(value: string): Cursor {
|
|
|
76
78
|
throw new AppException(ADMIN_SESSION_CURSOR_INVALID, "The sessions cursor is invalid.", 400);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function view(row: SessionRow): AdminSessionView {
|
|
82
|
+
return {
|
|
83
|
+
id: row.id,
|
|
84
|
+
userId: row.userId,
|
|
85
|
+
userEmail: row.userEmail,
|
|
86
|
+
userName: row.userName ?? "",
|
|
87
|
+
ipAddress: row.ipAddress,
|
|
88
|
+
userAgent: row.userAgent,
|
|
89
|
+
impersonatedBy: row.impersonatedBy,
|
|
90
|
+
createdAt: iso(row.createdAt),
|
|
91
|
+
expiresAt: iso(row.expiresAt),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
79
95
|
function likePattern(search: string): string {
|
|
80
96
|
return `%${search.replace(/[\\%_]/g, (character) => `\\${character}`)}%`;
|
|
81
97
|
}
|
|
@@ -100,6 +116,7 @@ export class AdminSessionsService {
|
|
|
100
116
|
const filters = (): SQL.Query<unknown> => this.sql`
|
|
101
117
|
WHERE s."expiresAt" > ${now}
|
|
102
118
|
${query.userId ? this.sql`AND s."userId" = ${query.userId}` : this.sql``}
|
|
119
|
+
${query.excludeId ? this.sql`AND s."id" <> ${query.excludeId}` : this.sql``}
|
|
103
120
|
${search ? this.searchFilter(likePattern(search)) : this.sql``}
|
|
104
121
|
`;
|
|
105
122
|
const [rows, totals] = await Promise.all([
|
|
@@ -127,17 +144,7 @@ export class AdminSessionsService {
|
|
|
127
144
|
const page = rows.slice(0, limit);
|
|
128
145
|
const last = page.at(-1);
|
|
129
146
|
return {
|
|
130
|
-
items: page.map(
|
|
131
|
-
id: row.id,
|
|
132
|
-
userId: row.userId,
|
|
133
|
-
userEmail: row.userEmail,
|
|
134
|
-
userName: row.userName ?? "",
|
|
135
|
-
ipAddress: row.ipAddress,
|
|
136
|
-
userAgent: row.userAgent,
|
|
137
|
-
impersonatedBy: row.impersonatedBy,
|
|
138
|
-
createdAt: iso(row.createdAt),
|
|
139
|
-
expiresAt: iso(row.expiresAt),
|
|
140
|
-
})),
|
|
147
|
+
items: page.map(view),
|
|
141
148
|
// The cursor keeps the database's own text form so sub-millisecond
|
|
142
149
|
// timestamps cannot skip or repeat rows at a page boundary.
|
|
143
150
|
nextCursor: rows.length > limit && last ? encodeCursor({ createdAt: last.cursorAt, id: last.id }) : null,
|
|
@@ -145,9 +152,25 @@ export class AdminSessionsService {
|
|
|
145
152
|
};
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
|
|
155
|
+
/** One active session, or null when it does not exist or has expired. */
|
|
156
|
+
async find(sessionId: string): Promise<AdminSessionView | null> {
|
|
157
|
+
const now = new Date().toISOString();
|
|
158
|
+
const rows = await this.sql<SessionRow[]>`
|
|
159
|
+
SELECT s."id", s."userId", u."email" AS "userEmail", u."name" AS "userName",
|
|
160
|
+
s."ipAddress", s."userAgent", s."impersonatedBy", s."createdAt", s."expiresAt",
|
|
161
|
+
CAST(s."createdAt" AS TEXT) AS "cursorAt"
|
|
162
|
+
FROM "session" s
|
|
163
|
+
JOIN "user" u ON u."id" = s."userId"
|
|
164
|
+
WHERE s."id" = ${sessionId} AND s."expiresAt" > ${now}
|
|
165
|
+
`;
|
|
166
|
+
return rows[0] ? view(rows[0]) : null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The token of a session, only when `userId` is given and owns it. */
|
|
170
|
+
async tokenOf(sessionId: string, userId?: string): Promise<string | null> {
|
|
149
171
|
const rows = await this.sql<Array<{ token: string }>>`
|
|
150
|
-
SELECT "token" FROM "session"
|
|
172
|
+
SELECT "token" FROM "session"
|
|
173
|
+
WHERE "id" = ${sessionId} ${userId === undefined ? this.sql`` : this.sql`AND "userId" = ${userId}`}
|
|
151
174
|
`;
|
|
152
175
|
return rows[0]?.token ?? null;
|
|
153
176
|
}
|
|
@@ -5,8 +5,14 @@ import {
|
|
|
5
5
|
} from "@podosoft/podokit-contracts";
|
|
6
6
|
import { OBJECT_STORAGE } from "@podosoft/podokit-runtime";
|
|
7
7
|
import { Elysia, t } from "elysia";
|
|
8
|
+
import { accountSessionRoutes } from "./account-sessions/account-sessions.routes";
|
|
8
9
|
import { adminSessionRoutes } from "./admin-sessions/admin-sessions.routes";
|
|
9
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
createOwnSessionRevoker,
|
|
12
|
+
createSessionRevoker,
|
|
13
|
+
type RevokeSessionApi,
|
|
14
|
+
type RevokeUserSessionApi,
|
|
15
|
+
} from "./admin-sessions/admin-sessions.revoker";
|
|
10
16
|
import { AdminSessionsService } from "./admin-sessions/admin-sessions.service";
|
|
11
17
|
import { adminUserRoutes } from "./admin-users/admin-users.routes";
|
|
12
18
|
import { AdminUsersService } from "./admin-users/admin-users.service";
|
|
@@ -57,6 +63,12 @@ const adminPlugin: AppPlugin = ({ services }) => {
|
|
|
57
63
|
sessions: services.resolve(ADMIN_SESSIONS),
|
|
58
64
|
revoke: createSessionRevoker(() => getAuth().api as unknown as RevokeUserSessionApi),
|
|
59
65
|
}))
|
|
66
|
+
.use(accountSessionRoutes({
|
|
67
|
+
auth,
|
|
68
|
+
sessions: services.resolve(ADMIN_SESSIONS),
|
|
69
|
+
revoke: createOwnSessionRevoker(() => getAuth().api as unknown as RevokeSessionApi),
|
|
70
|
+
freshAgeSeconds: async () => (await getAuth().$context).sessionConfig.freshAge,
|
|
71
|
+
}))
|
|
60
72
|
.use(adminUserRoutes({ auth, users: services.resolve(ADMIN_USERS) }))
|
|
61
73
|
.use(siteAssetRoutes(site, profileImages))
|
|
62
74
|
.get("/site/settings", async () => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ApiError } from "@podosoft/podokit-api-client";
|
|
2
2
|
import type { RequestEvent } from "@sveltejs/kit";
|
|
3
|
+
import type { Handle } from "@sveltejs/kit/hooks";
|
|
3
4
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
5
|
import { BackendUnavailableError } from "#lib/server/api.js";
|
|
5
6
|
import { handle } from "./hooks.server";
|
|
@@ -14,12 +15,18 @@ const client = vi.hoisted(() => ({
|
|
|
14
15
|
getSession: vi.fn<() => Promise<SessionResult>>(),
|
|
15
16
|
}));
|
|
16
17
|
|
|
18
|
+
// Stands in for the application's own hook in the app-owned app-hooks.ts.
|
|
19
|
+
const appHook = vi.hoisted(() => ({
|
|
20
|
+
handle: vi.fn<Handle>(({ event, resolve }) => resolve(event)),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("#lib/server/app-hooks.js", () => ({ appHandle: appHook.handle }));
|
|
23
|
+
|
|
17
24
|
vi.mock("#lib/server/api.js", async (importOriginal) => ({
|
|
18
25
|
...(await importOriginal<typeof import("#lib/server/api.js")>()),
|
|
19
26
|
serverApiClient: () => ({ get: client.get, auth: { getSession: client.getSession } }),
|
|
20
27
|
}));
|
|
21
28
|
|
|
22
|
-
const resolve = vi.fn(async (): Promise<Response> => new Response("ok"));
|
|
29
|
+
const resolve = vi.fn(async (_event: RequestEvent): Promise<Response> => new Response("ok"));
|
|
23
30
|
|
|
24
31
|
function requestEvent(pathname: string, cookies: Record<string, string> = {}): RequestEvent {
|
|
25
32
|
return {
|
|
@@ -40,6 +47,7 @@ describe("handle", () => {
|
|
|
40
47
|
client.get.mockReset();
|
|
41
48
|
client.getSession.mockReset();
|
|
42
49
|
resolve.mockClear();
|
|
50
|
+
appHook.handle.mockClear();
|
|
43
51
|
});
|
|
44
52
|
|
|
45
53
|
it("requests the session without waiting for site settings", async () => {
|
|
@@ -144,4 +152,32 @@ describe("handle", () => {
|
|
|
144
152
|
expect(client.getSession).not.toHaveBeenCalled();
|
|
145
153
|
expect(resolve).toHaveBeenCalledTimes(1);
|
|
146
154
|
});
|
|
155
|
+
|
|
156
|
+
it("runs the application hook with the loaded runtime context", async () => {
|
|
157
|
+
client.get.mockResolvedValue({ locale: null });
|
|
158
|
+
client.getSession.mockResolvedValue(signedIn);
|
|
159
|
+
let seenUser: unknown;
|
|
160
|
+
appHook.handle.mockImplementationOnce(({ event, resolve: next }) => {
|
|
161
|
+
seenUser = event.locals.user;
|
|
162
|
+
event.locals.user = { id: "local", email: "local@localhost", name: "Local", role: "admin" };
|
|
163
|
+
return next(event);
|
|
164
|
+
});
|
|
165
|
+
const event = requestEvent("/account");
|
|
166
|
+
|
|
167
|
+
const response = await handle({ event, resolve });
|
|
168
|
+
|
|
169
|
+
expect(seenUser).toEqual({ id: "user-1" });
|
|
170
|
+
expect(resolve).toHaveBeenCalledTimes(1);
|
|
171
|
+
expect(resolve.mock.calls[0]?.[0].locals.user).toMatchObject({ id: "local", role: "admin" });
|
|
172
|
+
expect(response.headers.get("x-robots-tag")).toBe("noindex, nofollow");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("lets the application hook answer a request itself", async () => {
|
|
176
|
+
appHook.handle.mockImplementationOnce(() => new Response("teapot", { status: 418 }));
|
|
177
|
+
|
|
178
|
+
const response = await handle({ event: requestEvent("/api/site/settings"), resolve });
|
|
179
|
+
|
|
180
|
+
expect(response.status).toBe(418);
|
|
181
|
+
expect(resolve).not.toHaveBeenCalled();
|
|
182
|
+
});
|
|
147
183
|
});
|
|
@@ -3,6 +3,7 @@ import type { Handle } from "@sveltejs/kit/hooks";
|
|
|
3
3
|
import { building } from "$app/env";
|
|
4
4
|
import { routes } from "$app/manifest";
|
|
5
5
|
import { serverApiClient } from "#lib/server/api.js";
|
|
6
|
+
import { appHandle } from "#lib/server/app-hooks.js";
|
|
6
7
|
import { resolveLocale } from "#lib/i18n/messages.js";
|
|
7
8
|
import { applySearchIndexingHeaders } from "#lib/server/search-indexing.js";
|
|
8
9
|
import { isPublicPath } from "#lib/server/guards.js";
|
|
@@ -91,12 +92,20 @@ export const handle: Handle = async ({ event, resolve }) => {
|
|
|
91
92
|
} else {
|
|
92
93
|
setEmptyRuntimeContext(event);
|
|
93
94
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
95
|
+
// The application's own hook from the app-owned app-hooks.ts runs next, with
|
|
96
|
+
// the runtime context loaded. Its page transform runs before the language one.
|
|
97
|
+
const response = await appHandle({
|
|
98
|
+
event,
|
|
99
|
+
resolve: (appEvent, options) =>
|
|
100
|
+
resolve(appEvent, {
|
|
101
|
+
...options,
|
|
102
|
+
transformPageChunk: async ({ html, done }) => {
|
|
103
|
+
const transformed = options?.transformPageChunk
|
|
104
|
+
? ((await options.transformPageChunk({ html, done })) ?? "")
|
|
105
|
+
: html;
|
|
106
|
+
return transformed.replace(/(<html\b[^>]*?\slang=)(["'])[^"']*\2/i, `$1"${event.locals.locale}"`);
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
100
109
|
});
|
|
101
110
|
return applySearchIndexingHeaders(response, event.url.pathname);
|
|
102
111
|
};
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/account-sessions.test.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { listAccountSessions, revokeAccountSession } from "./account-sessions";
|
|
3
|
+
|
|
4
|
+
describe("account sessions", () => {
|
|
5
|
+
it("asks for one page with the cursor", async () => {
|
|
6
|
+
const paths: string[] = [];
|
|
7
|
+
const client = {
|
|
8
|
+
get: <T>(path: string): Promise<T> => {
|
|
9
|
+
paths.push(path);
|
|
10
|
+
return Promise.resolve({ current: null, items: [], nextCursor: null, total: 0 } as unknown as T);
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
await listAccountSessions(client, { limit: 20, cursor: "abc" });
|
|
15
|
+
await listAccountSessions(client, { limit: 20, cursor: null });
|
|
16
|
+
|
|
17
|
+
expect(paths).toEqual(["/account/sessions?limit=20&cursor=abc", "/account/sessions?limit=20"]);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("revokes by id", async () => {
|
|
21
|
+
const paths: string[] = [];
|
|
22
|
+
const client = {
|
|
23
|
+
del: <T>(path: string): Promise<T> => {
|
|
24
|
+
paths.push(path);
|
|
25
|
+
return Promise.resolve(undefined as unknown as T);
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
await revokeAccountSession(client, "id/with slash");
|
|
30
|
+
|
|
31
|
+
expect(paths).toEqual(["/account/sessions/id%2Fwith%20slash"]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ApiClient } from "@podosoft/podokit-api-client";
|
|
2
|
+
|
|
3
|
+
/** One of the signed-in user's sessions from `GET /account/sessions`. The API never returns the token. */
|
|
4
|
+
export type AccountSession = {
|
|
5
|
+
id: string;
|
|
6
|
+
ipAddress: string | null;
|
|
7
|
+
userAgent: string | null;
|
|
8
|
+
createdAt: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type AccountSessionPage = {
|
|
13
|
+
/** The session making the request, on every page. */
|
|
14
|
+
current: AccountSession | null;
|
|
15
|
+
/** The other sessions, newest first. */
|
|
16
|
+
items: AccountSession[];
|
|
17
|
+
nextCursor: string | null;
|
|
18
|
+
/** Every active session, the current one included. */
|
|
19
|
+
total: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function listAccountSessions(
|
|
23
|
+
client: Pick<ApiClient, "get">,
|
|
24
|
+
query: { limit: number; cursor?: string | null },
|
|
25
|
+
): Promise<AccountSessionPage> {
|
|
26
|
+
const params = new URLSearchParams({ limit: String(query.limit) });
|
|
27
|
+
if (query.cursor) params.set("cursor", query.cursor);
|
|
28
|
+
return client.get<AccountSessionPage>(`/account/sessions?${params.toString()}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Revokes one of the user's own sessions by id; the server looks up the token. */
|
|
32
|
+
export function revokeAccountSession(client: Pick<ApiClient, "del">, id: string): Promise<void> {
|
|
33
|
+
return client.del<void>(`/account/sessions/${encodeURIComponent(id)}`);
|
|
34
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { Messages } from "#lib/i18n/messages.js";
|
|
3
|
+
import { appFlagCards } from "./app-flag-settings";
|
|
4
|
+
|
|
5
|
+
// Stands in for the application's own list in the app-owned app-flags.ts.
|
|
6
|
+
vi.mock("./app-flags", () => ({
|
|
7
|
+
APP_FLAG_SETTINGS: [
|
|
8
|
+
{ flag: "reportsEnabled", label: () => "Reports", description: () => "Show the reports page." },
|
|
9
|
+
{ flag: "betaSearch", label: () => "Beta search" },
|
|
10
|
+
],
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
const t = {} as Messages;
|
|
14
|
+
|
|
15
|
+
describe("application flag settings", () => {
|
|
16
|
+
it("shows each listed flag with the value the API reports", () => {
|
|
17
|
+
expect(appFlagCards(t, { appFlags: { reportsEnabled: true, betaSearch: false, hidden: true } })).toEqual([
|
|
18
|
+
{ flag: "reportsEnabled", name: "Reports", description: "Show the reports page.", available: true, on: true },
|
|
19
|
+
{ flag: "betaSearch", name: "Beta search", description: "", available: true, on: false },
|
|
20
|
+
]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("marks a flag the API does not report as unavailable", () => {
|
|
24
|
+
expect(appFlagCards(t, { appFlags: { reportsEnabled: false } })[1]).toMatchObject({
|
|
25
|
+
flag: "betaSearch",
|
|
26
|
+
available: false,
|
|
27
|
+
on: false,
|
|
28
|
+
});
|
|
29
|
+
expect(appFlagCards(t, undefined).every((card) => !card.available)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/app-flag-settings.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
2
|
+
import type { Messages } from "#lib/i18n/messages.js";
|
|
3
|
+
import { APP_FLAG_SETTINGS } from "./app-flags";
|
|
4
|
+
import type { AppFlagSetting } from "./registry.svelte.js";
|
|
5
|
+
|
|
6
|
+
export type AppFlagCard = {
|
|
7
|
+
flag: string;
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
/** False when the API does not report the flag, for example a name missing from APP_FLAG_DEFAULTS. */
|
|
11
|
+
available: boolean;
|
|
12
|
+
on: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** One card per listed flag, with the value `GET /account/capabilities` reports in `appFlags`. */
|
|
16
|
+
export function appFlagCards(
|
|
17
|
+
t: Messages,
|
|
18
|
+
capabilities: Pick<Capabilities, "appFlags"> | undefined,
|
|
19
|
+
settings: readonly AppFlagSetting[] = APP_FLAG_SETTINGS,
|
|
20
|
+
): AppFlagCard[] {
|
|
21
|
+
return settings.map(({ flag, label, description }) => {
|
|
22
|
+
const value = capabilities?.appFlags?.[flag];
|
|
23
|
+
return {
|
|
24
|
+
flag,
|
|
25
|
+
name: label(t),
|
|
26
|
+
description: description?.(t) ?? "",
|
|
27
|
+
available: typeof value === "boolean",
|
|
28
|
+
on: value === true,
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppFlagSetting } from "./registry.svelte.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application flags the admin Settings page shows, one switch each, on an
|
|
5
|
+
* Application tab after PodoKit's tabs. `flag` is a key of `APP_FLAG_DEFAULTS`
|
|
6
|
+
* in the API's owned `apps/api/src/settings/app-flags.ts`; the label and
|
|
7
|
+
* description come from the app-owned `i18n/catalogs/app/*.json`, for example
|
|
8
|
+
* `{ flag: "reportsEnabled", label: (t) => t.reports.flag, description: (t) => t.reports.flagDescription }`.
|
|
9
|
+
* The tab is hidden while the list is empty. PodoKit creates this file once and
|
|
10
|
+
* never overwrites it.
|
|
11
|
+
*/
|
|
12
|
+
export const APP_FLAG_SETTINGS: readonly AppFlagSetting[] = [];
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AdminNavEntry } from "./registry.svelte.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Application-owned admin sidebar entries. The sidebar shows them after PodoKit's
|
|
5
|
+
* own pages and before module entries and Settings, for example
|
|
6
|
+
* `{ href: "/admin/reports", label: (t) => t.reports.nav, icon: ChartColumnIcon, adminOnly: true }`
|
|
7
|
+
* with the label in the app-owned `i18n/catalogs/app/*.json` and the icon from
|
|
8
|
+
* `@lucide/svelte/icons/*`. `adminOnly` hides an entry from other roles.
|
|
9
|
+
* PodoKit creates this file once and never overwrites it.
|
|
10
|
+
*/
|
|
11
|
+
export const APP_ADMIN_NAV: readonly AdminNavEntry[] = [];
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import FileTextIcon from "@lucide/svelte/icons/file-text";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { adminNavEntries } from "./nav";
|
|
4
|
+
|
|
5
|
+
// Stands in for the application's own entries in the app-owned app-nav.ts.
|
|
6
|
+
vi.mock("./app-nav", async () => {
|
|
7
|
+
const { default: icon } = await import("@lucide/svelte/icons/file-text");
|
|
8
|
+
return {
|
|
9
|
+
APP_ADMIN_NAV: [
|
|
10
|
+
{ href: "/admin/specs", label: () => "Specs", icon, adminOnly: true },
|
|
11
|
+
{ href: "/admin/help", label: () => "Help", icon },
|
|
12
|
+
],
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("admin navigation", () => {
|
|
17
|
+
it("places the application's entries before Settings", () => {
|
|
18
|
+
const hrefs = adminNavEntries({ auditLog: true }, "admin").map((entry) => entry.href);
|
|
19
|
+
|
|
20
|
+
expect(hrefs).toEqual([
|
|
21
|
+
"/admin",
|
|
22
|
+
"/admin/users",
|
|
23
|
+
"/admin/sessions",
|
|
24
|
+
"/admin/audit",
|
|
25
|
+
"/admin/specs",
|
|
26
|
+
"/admin/help",
|
|
27
|
+
"/admin/settings",
|
|
28
|
+
]);
|
|
29
|
+
expect(adminNavEntries(undefined, "admin").find((entry) => entry.href === "/admin/specs")?.icon).toBe(
|
|
30
|
+
FileTextIcon,
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("hides admin-only application entries from other roles", () => {
|
|
35
|
+
const hrefs = adminNavEntries(undefined, "moderator").map((entry) => entry.href);
|
|
36
|
+
|
|
37
|
+
expect(hrefs).toEqual(["/admin", "/admin/help"]);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import LayoutDashboardIcon from "@lucide/svelte/icons/layout-dashboard";
|
|
2
|
+
import UsersIcon from "@lucide/svelte/icons/users";
|
|
3
|
+
import MonitorSmartphoneIcon from "@lucide/svelte/icons/monitor-smartphone";
|
|
4
|
+
import ScrollTextIcon from "@lucide/svelte/icons/scroll-text";
|
|
5
|
+
import Building2Icon from "@lucide/svelte/icons/building-2";
|
|
6
|
+
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
7
|
+
import { APP_ADMIN_NAV } from "./app-nav";
|
|
8
|
+
import { moduleNavEntries, type AdminNavEntry } from "./registry.svelte.js";
|
|
9
|
+
|
|
10
|
+
export type AdminNavCapabilities = { auditLog?: boolean; organization?: boolean };
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The admin sidebar entries a user may open: PodoKit's pages, the application's
|
|
14
|
+
* entries from the app-owned app-nav.ts, module entries, then Settings.
|
|
15
|
+
*/
|
|
16
|
+
export function adminNavEntries(
|
|
17
|
+
capabilities: AdminNavCapabilities | undefined,
|
|
18
|
+
role: string | null | undefined,
|
|
19
|
+
): AdminNavEntry[] {
|
|
20
|
+
const entries: AdminNavEntry[] = [
|
|
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 } satisfies AdminNavEntry]
|
|
26
|
+
: []),
|
|
27
|
+
...(capabilities?.auditLog
|
|
28
|
+
? [{ href: "/admin/audit", label: (t) => t.nav.audit, icon: ScrollTextIcon, adminOnly: true } satisfies AdminNavEntry]
|
|
29
|
+
: []),
|
|
30
|
+
...APP_ADMIN_NAV,
|
|
31
|
+
...moduleNavEntries,
|
|
32
|
+
{ href: "/admin/settings", label: (t) => t.nav.settings, icon: SettingsIcon, adminOnly: true },
|
|
33
|
+
];
|
|
34
|
+
return entries.filter((entry) => !entry.adminOnly || role === "admin");
|
|
35
|
+
}
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/admin/registry.svelte.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Component } from "svelte";
|
|
2
|
+
import AppFlagsSettings from "#lib/components/settings/app-flags-settings.svelte";
|
|
2
3
|
import type { Messages } from "#lib/i18n/messages.js";
|
|
4
|
+
import { APP_FLAG_SETTINGS } from "./app-flags";
|
|
3
5
|
// Modules import their nav-entry icons here.
|
|
4
6
|
// podokit:begin:admin-nav-imports
|
|
5
7
|
// podokit:end:admin-nav-imports
|
|
@@ -26,6 +28,15 @@ export type AdminSettingsSection = {
|
|
|
26
28
|
component: Component;
|
|
27
29
|
};
|
|
28
30
|
|
|
31
|
+
/** A switch on the Settings page's Application tab for one of the API's
|
|
32
|
+
* application flags; see the app-owned app-flags.ts. */
|
|
33
|
+
export type AppFlagSetting = {
|
|
34
|
+
/** The key in APP_FLAG_DEFAULTS (apps/api/src/settings/app-flags.ts). */
|
|
35
|
+
flag: string;
|
|
36
|
+
label: (t: Messages) => string;
|
|
37
|
+
description?: (t: Messages) => string;
|
|
38
|
+
};
|
|
39
|
+
|
|
29
40
|
export const moduleNavEntries: AdminNavEntry[] = [
|
|
30
41
|
// podokit:begin:admin-nav-entries
|
|
31
42
|
// podokit:end:admin-nav-entries
|
|
@@ -34,4 +45,8 @@ export const moduleNavEntries: AdminNavEntry[] = [
|
|
|
34
45
|
export const moduleSettingsSections: AdminSettingsSection[] = [
|
|
35
46
|
// podokit:begin:admin-settings
|
|
36
47
|
// podokit:end:admin-settings
|
|
48
|
+
// The application's flags, after every module tab, when app-flags.ts lists any.
|
|
49
|
+
...(APP_FLAG_SETTINGS.length > 0
|
|
50
|
+
? [{ value: "application", label: (t: Messages) => t.settings.appFlags.tab, component: AppFlagsSettings }]
|
|
51
|
+
: []),
|
|
37
52
|
];
|