@podosoft/podokit 0.3.0 → 0.5.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 -2
- package/dist/add.d.ts +2 -0
- package/dist/add.js +5 -1
- package/dist/assemble.d.ts +14 -0
- package/dist/assemble.js +60 -0
- package/dist/create.d.ts +2 -0
- package/dist/create.js +7 -0
- package/dist/eject.d.ts +11 -0
- package/dist/eject.js +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -1
- package/dist/inspect.d.ts +43 -0
- package/dist/inspect.js +82 -0
- package/dist/lockfile.d.ts +80 -0
- package/dist/lockfile.js +193 -0
- package/dist/templates/base/dot-env.example +11 -6
- package/dist/templates/fullstack-nest-svelte/README.md +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +21 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +6 -3
- package/dist/templates/fullstack-nest-svelte/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/fullstack-nest-svelte/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/src/main.ts +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/tsconfig.json +14 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +4 -3
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/api.ts +5 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/api.ts +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/server/backend-proxy.ts +44 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/+page.svelte +8 -3
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/[...path]/+server.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/auth/[...all]/+server.ts +12 -0
- package/dist/templates/fullstack-nest-svelte/dot-env.example +10 -5
- package/dist/templates/fullstack-nest-svelte/dot-gitignore +5 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/docker-compose.yml +37 -0
- package/dist/templates/fullstack-nest-svelte/infra/docker/sms-sink.mjs +40 -0
- package/dist/templates/fullstack-nest-svelte/package.json +11 -3
- package/dist/templates/fullstack-nest-svelte/tests/README.md +15 -0
- package/dist/templates/fullstack-nest-svelte/tests/api/health.api.spec.ts +14 -0
- package/dist/templates/fullstack-nest-svelte/tests/package.json +15 -0
- package/dist/templates/fullstack-nest-svelte/tests/playwright.config.ts +25 -0
- package/dist/templates/fullstack-nest-svelte/tests/ui/smoke.ui.spec.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/app.d.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/hooks.server.ts +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/app-sidebar.svelte +65 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/data-table.svelte +149 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/language-switch.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/sidebar-user-menu.svelte +44 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-pagination.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/table-toolbar.svelte +93 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/theme-toggle.svelte +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible-content.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible-trigger.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/collapsible.svelte +11 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/collapsible/index.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/index.ts +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-content.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-ellipsis.svelte +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-item.svelte +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-link.svelte +42 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-next-button.svelte +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-next.svelte +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-prev-button.svelte +31 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination-previous.svelte +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/pagination/pagination.svelte +28 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/index.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-close.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-content.svelte +55 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-description.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-footer.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-header.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-overlay.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-portal.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-title.svelte +17 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet-trigger.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sheet/sheet.svelte +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/constants.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/context.svelte.ts +81 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/index.ts +75 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-content.svelte +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-footer.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-action.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-content.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group-label.svelte +33 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-group.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-header.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-input.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-inset.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-action.svelte +37 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-badge.svelte +24 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-button.svelte +102 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-item.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-skeleton.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-button.svelte +39 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub-item.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu-sub.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-menu.svelte +21 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-provider.svelte +53 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-rail.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-separator.svelte +19 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar-trigger.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/ui/sidebar/sidebar.svelte +108 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/components/user-avatar.svelte +12 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/hooks/is-mobile.svelte.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/index.ts +27 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/i18n/messages.ts +575 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/lib/server/guards.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +30 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.svelte +50 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/accept-invitation/[id]/+page.svelte +36 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/+page.svelte +45 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.server.ts +5 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/account/+page.svelte +685 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +123 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.server.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/organizations/+page.svelte +264 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +156 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.server.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/settings/+page.svelte +322 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +7 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +552 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/+layout.svelte +13 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/forgot-password/+page.svelte +61 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.server.ts +14 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +181 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/reset-password/+page.svelte +51 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte +76 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/verify-email/+page.svelte +63 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.server.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+layout.svelte +20 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/+page.svelte +22 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte +54 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/audit.api.spec.ts +97 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth-config.api.spec.ts +80 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +306 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/email.api.spec.ts +120 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/users-admin.api.spec.ts +134 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/accounts.ts +6 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/hydration.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/mailpit.ts +49 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/sms.ts +35 -0
- package/dist/templates/modules/admin-dashboard/files/tests/helpers/totp.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/tests/playwright.config.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +40 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +126 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/audit.ui.spec.ts +16 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/auth.ui.spec.ts +45 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/dashboard.ui.spec.ts +25 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/data-table.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email-admin.ui.spec.ts +60 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/email.ui.spec.ts +42 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/guards.ui.spec.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/i18n.ui.spec.ts +13 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/oidc.ui.spec.ts +57 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/organizations.ui.spec.ts +52 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/passkey.ui.spec.ts +36 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sessions.ui.spec.ts +22 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/settings.ui.spec.ts +34 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/sidebar.ui.spec.ts +16 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/table-toolbar.ui.spec.ts +32 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users-crud.ui.spec.ts +145 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +51 -0
- package/dist/templates/modules/admin-dashboard/module.manifest.json +47 -0
- package/dist/templates/modules/api-key-auth/module.manifest.json +3 -3
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/README.md +59 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-enabled.ts +16 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-events.ts +34 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +74 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-log.entity.ts +29 -8
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.controller.ts +7 -2
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.decorator.ts +23 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.interceptor.ts +38 -21
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +2 -1
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.service.ts +36 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/migrations/1720300000000-InitAuditLogs.ts +10 -4
- package/dist/templates/modules/audit-log/module.manifest.json +37 -8
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +85 -3
- package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +2 -1
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth-provider.ts +73 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +199 -31
- package/dist/templates/modules/auth/files/apps/api/src/auth/db.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/feature-gate.ts +56 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/org-permissions.ts +22 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/permissions.ts +23 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.controller.ts +33 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.entity.ts +26 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.module.ts +12 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth-config/auth-config.service.ts +118 -0
- package/dist/templates/modules/auth/files/apps/api/src/mail/mailer.ts +65 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720400000000-InitAppSettings.ts +38 -0
- package/dist/templates/modules/auth/files/apps/api/src/migrations/1720500000000-InitAuthConfig.ts +27 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/app-setting.entity.ts +16 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/flag-defaults.ts +25 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.module.ts +11 -0
- package/dist/templates/modules/auth/files/apps/api/src/settings/settings.service.ts +57 -0
- package/dist/templates/modules/auth/files/apps/api/src/sms/sms.ts +23 -0
- package/dist/templates/modules/auth/module.manifest.json +54 -20
- package/dist/templates/modules/bullmq/files/apps/api/src/jobs/worker.module.ts +6 -3
- package/dist/templates/modules/bullmq/module.manifest.json +4 -4
- package/dist/templates/modules/file-upload/module.manifest.json +3 -3
- package/dist/templates/modules/job-progress/module.manifest.json +9 -9
- package/dist/templates/modules/logging/module.manifest.json +3 -3
- package/dist/templates/modules/object-storage-s3/files/infra/docker/minio.compose.yml +5 -3
- package/dist/templates/modules/object-storage-s3/module.manifest.json +5 -5
- package/dist/templates/modules/rate-limit/module.manifest.json +2 -2
- package/dist/templates/modules/redis/module.manifest.json +4 -4
- package/dist/templates/modules/sse/module.manifest.json +4 -4
- package/dist/templates/todo/README.md +2 -2
- package/dist/templates/todo/apps/api/package.json +21 -2
- package/dist/templates/todo/apps/api/src/app.module.ts +6 -3
- package/dist/templates/todo/apps/api/src/common/app-exception.ts +4 -12
- package/dist/templates/todo/apps/api/src/config/env.validation.ts +1 -1
- package/dist/templates/todo/apps/api/src/main.ts +1 -1
- package/dist/templates/todo/apps/api/tsconfig.json +14 -5
- package/dist/templates/todo/apps/web/package.json +4 -3
- package/dist/templates/todo/apps/web/src/lib/api.ts +5 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/switch/switch.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/server/api.ts +19 -0
- package/dist/templates/todo/apps/web/src/lib/server/backend-proxy.ts +44 -5
- package/dist/templates/todo/apps/web/src/routes/+page.svelte +19 -18
- package/dist/templates/todo/apps/web/src/routes/api/[...path]/+server.ts +12 -0
- package/dist/templates/todo/apps/web/src/routes/api/auth/[...all]/+server.ts +12 -0
- package/dist/templates/todo/dot-env.example +10 -5
- package/dist/templates/todo/dot-gitignore +5 -0
- package/dist/templates/todo/infra/docker/docker-compose.yml +4 -0
- package/dist/templates/todo/package.json +11 -3
- package/dist/templates/todo/tests/README.md +15 -0
- package/dist/templates/todo/tests/api/health.api.spec.ts +14 -0
- package/dist/templates/todo/tests/package.json +15 -0
- package/dist/templates/todo/tests/playwright.config.ts +25 -0
- package/dist/templates/todo/tests/ui/smoke.ui.spec.ts +8 -0
- package/dist/update.d.ts +49 -0
- package/dist/update.js +158 -0
- package/package.json +2 -2
- package/dist/templates/fullstack-nest-svelte/apps/api/test/health.e2e-spec.ts +0 -23
- package/dist/templates/fullstack-nest-svelte/apps/web/src/routes/api/health/+server.ts +0 -12
- package/dist/templates/todo/apps/api/test/health.e2e-spec.ts +0 -23
- package/dist/templates/todo/apps/web/src/routes/api/health/+server.ts +0 -12
- package/dist/templates/todo/apps/web/src/routes/api/todos/+server.ts +0 -24
- package/dist/templates/todo/apps/web/src/routes/api/todos/[id]/+server.ts +0 -24
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// admin storageState (project default) — organizations is an admin-only view
|
|
5
|
+
test("admin can create an organization @smoke", async ({ page }) => {
|
|
6
|
+
await ready(page, "/admin");
|
|
7
|
+
const nav = page.getByRole("link", { name: "Organizations" });
|
|
8
|
+
test.skip((await nav.count()) === 0, "organizations not enabled");
|
|
9
|
+
await nav.click();
|
|
10
|
+
await expect(page).toHaveURL(/\/admin\/organizations/);
|
|
11
|
+
await page.getByRole("button", { name: "New organization" }).click();
|
|
12
|
+
const name = `Acme ${Date.now()}`;
|
|
13
|
+
await page.getByLabel("Name", { exact: true }).fill(name);
|
|
14
|
+
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
15
|
+
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
16
|
+
await expect(page.getByRole("cell", { name })).toBeVisible();
|
|
17
|
+
// clean up so the shared admin account keeps no leftover org
|
|
18
|
+
await page.getByRole("row", { name: new RegExp(name) }).getByRole("button", { name: "Delete" }).click();
|
|
19
|
+
await expect(page.getByText("Organization deleted").first()).toBeVisible();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("create an org with a non-ascii name, a parent, and a manager @smoke", async ({ page }) => {
|
|
23
|
+
await ready(page, "/admin/organizations");
|
|
24
|
+
const newBtn = page.getByRole("button", { name: "New organization" });
|
|
25
|
+
test.skip((await newBtn.count()) === 0, "organizations not enabled");
|
|
26
|
+
|
|
27
|
+
// Parent org first (Korean name → slug falls back, so it still saves).
|
|
28
|
+
const parent = `본사 ${Date.now()}`;
|
|
29
|
+
await newBtn.click();
|
|
30
|
+
await page.getByLabel("Name", { exact: true }).fill(parent);
|
|
31
|
+
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
32
|
+
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
33
|
+
await expect(page.getByRole("cell", { name: parent })).toBeVisible();
|
|
34
|
+
|
|
35
|
+
// Child org: pick the parent + a manager (an existing user).
|
|
36
|
+
const child = `영업 ${Date.now()}`;
|
|
37
|
+
await page.getByRole("button", { name: "New organization" }).click();
|
|
38
|
+
await page.getByLabel("Name", { exact: true }).fill(child);
|
|
39
|
+
await page.locator("#o-parent").click();
|
|
40
|
+
await page.getByRole("option", { name: parent }).click();
|
|
41
|
+
await page.getByRole("checkbox").first().click(); // first existing user as a manager
|
|
42
|
+
await page.getByRole("dialog").getByRole("button", { name: "Create", exact: true }).click();
|
|
43
|
+
await expect(page.getByText("Organization created").first()).toBeVisible();
|
|
44
|
+
// child row shows the parent org name
|
|
45
|
+
await expect(page.getByRole("row", { name: new RegExp(child) })).toContainText(parent);
|
|
46
|
+
|
|
47
|
+
// clean up both
|
|
48
|
+
for (const n of [child, parent]) {
|
|
49
|
+
await page.getByRole("row", { name: new RegExp(n) }).getByRole("button", { name: "Delete" }).click();
|
|
50
|
+
await expect(page.getByText("Organization deleted").first()).toBeVisible();
|
|
51
|
+
}
|
|
52
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// Full passkey round trip on a throwaway account — never touches the shared admin.
|
|
5
|
+
test.use({ storageState: { cookies: [], origins: [] } });
|
|
6
|
+
|
|
7
|
+
test("register a passkey and sign in with it @smoke", async ({ page, baseURL }) => {
|
|
8
|
+
const origin = { origin: baseURL ?? "" };
|
|
9
|
+
// A virtual authenticator answers both the register (create) and sign-in (get)
|
|
10
|
+
// WebAuthn ceremonies without hardware.
|
|
11
|
+
const client = await page.context().newCDPSession(page);
|
|
12
|
+
await client.send("WebAuthn.enable");
|
|
13
|
+
await client.send("WebAuthn.addVirtualAuthenticator", {
|
|
14
|
+
options: { protocol: "ctap2", transport: "internal", hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true },
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Sign up a throwaway user (shares the page's cookie jar) and register a passkey.
|
|
18
|
+
const email = `pk-${Date.now()}@example.com`;
|
|
19
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
20
|
+
const signup = await page.context().request.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "PK" }, headers: origin });
|
|
21
|
+
expect(signup.ok()).toBeTruthy();
|
|
22
|
+
|
|
23
|
+
await ready(page, "/admin/account");
|
|
24
|
+
await page.getByRole("button", { name: "Security" }).click();
|
|
25
|
+
const addBtn = page.getByRole("button", { name: "Add passkey" });
|
|
26
|
+
test.skip((await addBtn.count()) === 0, "passkeys not enabled");
|
|
27
|
+
await page.getByLabel("Name", { exact: true }).fill("My device");
|
|
28
|
+
await addBtn.click();
|
|
29
|
+
await expect(page.getByText("Passkey added")).toBeVisible();
|
|
30
|
+
|
|
31
|
+
// Sign out, then sign back in using only the passkey.
|
|
32
|
+
await page.context().request.post("/api/auth/sign-out", { headers: origin });
|
|
33
|
+
await ready(page, "/login");
|
|
34
|
+
await page.getByRole("button", { name: "Use a passkey" }).click();
|
|
35
|
+
await expect(page).toHaveURL(/\/admin(\/|$)/);
|
|
36
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// admin storageState (project default) — sessions is an admin-only view
|
|
5
|
+
test("sessions table shows the expires column @smoke", async ({ page }) => {
|
|
6
|
+
await ready(page, "/admin/sessions");
|
|
7
|
+
await expect(page.getByRole("columnheader", { name: "Expires" })).toBeVisible();
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("admin sees user sessions across users @smoke", async ({ page }) => {
|
|
11
|
+
await ready(page, "/admin/sessions");
|
|
12
|
+
await expect(page.getByRole("heading", { name: "User sessions" })).toBeVisible();
|
|
13
|
+
// Search by exact address — robust to pagination as sessions accumulate (the
|
|
14
|
+
// admin account alone can fill the first page). This is the across-users view.
|
|
15
|
+
await page.locator("#toolbar-search").fill("user@example.com");
|
|
16
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
17
|
+
await expect(page.getByRole("main").getByText("user@example.com").first()).toBeVisible();
|
|
18
|
+
// row actions are available
|
|
19
|
+
await page.getByRole("row", { name: /user@example.com/ }).first().getByRole("button").click();
|
|
20
|
+
await expect(page.getByRole("menuitem", { name: "Revoke session" })).toBeVisible();
|
|
21
|
+
await expect(page.getByRole("menuitem", { name: "Revoke all for user" })).toBeVisible();
|
|
22
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// admin storageState (project default)
|
|
5
|
+
test("settings lists the auth features and their status @smoke", async ({ page }) => {
|
|
6
|
+
await ready(page, "/admin/settings");
|
|
7
|
+
await expect(page.getByRole("heading", { name: "Settings" })).toBeVisible();
|
|
8
|
+
await expect(page.getByText("Email & password")).toBeVisible();
|
|
9
|
+
await expect(page.getByText("Two-factor authentication")).toBeVisible();
|
|
10
|
+
await expect(page.getByText("Breached-password check")).toBeVisible();
|
|
11
|
+
await expect(page.getByText("Magic link")).toBeVisible();
|
|
12
|
+
// each feature carries an explicit status
|
|
13
|
+
await expect(page.getByText("Enabled", { exact: true }).first()).toBeVisible();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("settings is reachable from the sidebar @smoke", async ({ page }) => {
|
|
17
|
+
await ready(page, "/admin");
|
|
18
|
+
await page.getByRole("link", { name: "Settings" }).click();
|
|
19
|
+
await expect(page).toHaveURL(/\/admin\/settings/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("admin can toggle a feature flag and it applies live @smoke", async ({ page }) => {
|
|
23
|
+
await ready(page, "/admin/settings");
|
|
24
|
+
const toggle = page.getByRole("switch", { name: "Magic link" });
|
|
25
|
+
test.skip((await toggle.count()) === 0, "magic link toggle not present");
|
|
26
|
+
// Serial workers (workers:1) make toggle-then-restore safe for other specs.
|
|
27
|
+
await expect(toggle).toBeChecked();
|
|
28
|
+
await toggle.click();
|
|
29
|
+
await expect(page.getByText("Setting updated")).toBeVisible();
|
|
30
|
+
await expect(toggle).not.toBeChecked(); // capabilities refreshed live
|
|
31
|
+
// restore so the shared magic-link/2FA specs keep their expected state
|
|
32
|
+
await toggle.click();
|
|
33
|
+
await expect(toggle).toBeChecked();
|
|
34
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// admin storageState (project default)
|
|
5
|
+
test("collapsing the sidebar hides the brand instead of overflowing @smoke", async ({ page }) => {
|
|
6
|
+
await ready(page, "/admin");
|
|
7
|
+
const brand = page.getByTestId("sidebar-brand");
|
|
8
|
+
const toggle = page.getByRole("button", { name: "Toggle Sidebar" });
|
|
9
|
+
await expect(brand).toBeVisible();
|
|
10
|
+
|
|
11
|
+
await toggle.click();
|
|
12
|
+
await expect(brand).toBeHidden(); // icon-collapsed: label is hidden, not spilling out
|
|
13
|
+
|
|
14
|
+
await toggle.click();
|
|
15
|
+
await expect(brand).toBeVisible();
|
|
16
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
// The shared toolbar: filters + search both commit on the Search button; the
|
|
5
|
+
// search field itself is selectable (Email, Name, ...).
|
|
6
|
+
test("users toolbar searches only when Search is pressed @smoke", async ({ page }) => {
|
|
7
|
+
await ready(page, "/admin/users");
|
|
8
|
+
const main = page.getByRole("main");
|
|
9
|
+
await page.locator("#toolbar-search").fill("admin@example.com");
|
|
10
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
11
|
+
await expect(main.getByRole("cell", { name: "admin@example.com" })).toBeVisible();
|
|
12
|
+
await expect(main.getByRole("cell", { name: "user@example.com" })).toHaveCount(0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("users toolbar applies filter + search together on Search", async ({ page }) => {
|
|
16
|
+
await ready(page, "/admin/users");
|
|
17
|
+
const main = page.getByRole("main");
|
|
18
|
+
await page.locator('button[data-slot="select-trigger"]').first().click(); // Role select
|
|
19
|
+
await page.getByRole("option", { name: "Admin", exact: true }).click();
|
|
20
|
+
// Selecting a filter stages it; nothing applies until Search is pressed.
|
|
21
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
22
|
+
await expect(main.getByRole("cell", { name: "admin@example.com" })).toBeVisible();
|
|
23
|
+
await expect(main.getByRole("cell", { name: "user@example.com" })).toHaveCount(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("users toolbar can pick the search field", async ({ page }) => {
|
|
27
|
+
await ready(page, "/admin/users");
|
|
28
|
+
const fieldSelect = page.locator('button[data-slot="select-trigger"]').last(); // after the filters
|
|
29
|
+
await fieldSelect.click();
|
|
30
|
+
await page.getByRole("option", { name: "Name", exact: true }).click();
|
|
31
|
+
await expect(fieldSelect).toContainText("Name");
|
|
32
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { expect, test, type Page } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
|
+
|
|
6
|
+
// Seed a throwaway user via the API (admin cookies come from storageState) so
|
|
7
|
+
// the mutating UI tests don't touch the shared admin/user seed accounts.
|
|
8
|
+
async function seedUser(page: Page, email: string): Promise<void> {
|
|
9
|
+
const res = await page.request.post("/api/auth/admin/create-user", {
|
|
10
|
+
headers: { origin: base },
|
|
11
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "Throwaway", role: "user" },
|
|
12
|
+
});
|
|
13
|
+
expect(res.ok()).toBeTruthy();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Open the Manage two-pane modal for a seeded user.
|
|
17
|
+
async function openManage(page: Page, email: string): Promise<void> {
|
|
18
|
+
await page.locator("#toolbar-search").fill(email);
|
|
19
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
20
|
+
await page.getByRole("row", { name: new RegExp(email) }).getByRole("button").click();
|
|
21
|
+
await page.getByRole("menuitem", { name: "Manage" }).click();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("admin can create a user via the dialog @smoke", async ({ page }) => {
|
|
25
|
+
await ready(page, "/admin/users");
|
|
26
|
+
const email = `ui-create-${Date.now()}@example.com`;
|
|
27
|
+
await page.getByRole("button", { name: "Add user" }).click();
|
|
28
|
+
const dialog = page.getByRole("dialog");
|
|
29
|
+
await dialog.getByLabel("Name").fill("Created");
|
|
30
|
+
await dialog.getByLabel("Email").fill(email);
|
|
31
|
+
await dialog.getByLabel("Password", { exact: true }).fill("Podokit3e-Str0ng!pw");
|
|
32
|
+
await dialog.getByLabel("Confirm password").fill("Podokit3e-Str0ng!pw");
|
|
33
|
+
await dialog.getByRole("button", { name: "Create" }).click();
|
|
34
|
+
// Wait for the create to settle (dialog closes + list reloads) before searching,
|
|
35
|
+
// otherwise the search input can be refilled by the post-create reload.
|
|
36
|
+
await expect(page.getByRole("dialog")).toBeHidden();
|
|
37
|
+
await page.locator("#toolbar-search").fill(email);
|
|
38
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
39
|
+
await expect(page.getByRole("cell", { name: email })).toBeVisible();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("create rejects mismatched passwords", async ({ page }) => {
|
|
43
|
+
await ready(page, "/admin/users");
|
|
44
|
+
await page.getByRole("button", { name: "Add user" }).click();
|
|
45
|
+
const dialog = page.getByRole("dialog");
|
|
46
|
+
await dialog.getByLabel("Name").fill("Mismatch");
|
|
47
|
+
await dialog.getByLabel("Email").fill(`ui-mismatch-${Date.now()}@example.com`);
|
|
48
|
+
await dialog.getByLabel("Password", { exact: true }).fill("Podokit3e-Str0ng!pw");
|
|
49
|
+
await dialog.getByLabel("Confirm password").fill("different999");
|
|
50
|
+
await dialog.getByRole("button", { name: "Create" }).click();
|
|
51
|
+
await expect(dialog.getByText("Passwords do not match")).toBeVisible();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("manage: edit profile name @smoke", async ({ page }) => {
|
|
55
|
+
await ready(page, "/admin/users");
|
|
56
|
+
const email = `ui-edit-${Date.now()}@example.com`;
|
|
57
|
+
await seedUser(page, email);
|
|
58
|
+
await openManage(page, email);
|
|
59
|
+
const dialog = page.getByRole("dialog");
|
|
60
|
+
await dialog.getByLabel("Name").fill("Edited Name");
|
|
61
|
+
await dialog.getByRole("button", { name: "Save" }).click();
|
|
62
|
+
await expect(page.getByText("User updated")).toBeVisible();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("manage: set a password (and reject mismatch)", async ({ page }) => {
|
|
66
|
+
await ready(page, "/admin/users");
|
|
67
|
+
const email = `ui-setpw-${Date.now()}@example.com`;
|
|
68
|
+
await seedUser(page, email);
|
|
69
|
+
await openManage(page, email);
|
|
70
|
+
const dialog = page.getByRole("dialog");
|
|
71
|
+
await dialog.getByRole("button", { name: "Security" }).click();
|
|
72
|
+
await dialog.getByLabel("New password", { exact: true }).fill("Podokit3e-N3wStr0ng!pw");
|
|
73
|
+
await dialog.getByLabel("Confirm new password").fill("different999");
|
|
74
|
+
await dialog.getByRole("button", { name: "Set password" }).click();
|
|
75
|
+
await expect(dialog.getByText("Passwords do not match")).toBeVisible();
|
|
76
|
+
await dialog.getByLabel("Confirm new password").fill("Podokit3e-N3wStr0ng!pw");
|
|
77
|
+
await dialog.getByRole("button", { name: "Set password" }).click();
|
|
78
|
+
await expect(page.getByText("Password updated")).toBeVisible();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("manage: ban a user with a reason", async ({ page }) => {
|
|
82
|
+
await ready(page, "/admin/users");
|
|
83
|
+
const email = `ui-ban-${Date.now()}@example.com`;
|
|
84
|
+
await seedUser(page, email);
|
|
85
|
+
await openManage(page, email);
|
|
86
|
+
const dialog = page.getByRole("dialog");
|
|
87
|
+
await dialog.getByRole("button", { name: "Security" }).click();
|
|
88
|
+
await dialog.getByLabel("Reason (optional)").fill("policy violation");
|
|
89
|
+
await dialog.getByRole("button", { name: "Ban", exact: true }).click();
|
|
90
|
+
await expect(page.getByText("User banned", { exact: true })).toBeVisible();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("manage: revoke a user's session", async ({ page, playwright }) => {
|
|
94
|
+
await ready(page, "/admin/users");
|
|
95
|
+
const email = `ui-sess-${Date.now()}@example.com`;
|
|
96
|
+
await seedUser(page, email);
|
|
97
|
+
// create a session for the user in an isolated context (don't touch the admin cookie)
|
|
98
|
+
const uctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: { origin: base } });
|
|
99
|
+
expect((await uctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } })).ok()).toBeTruthy();
|
|
100
|
+
await uctx.dispose();
|
|
101
|
+
await openManage(page, email);
|
|
102
|
+
const dialog = page.getByRole("dialog");
|
|
103
|
+
await dialog.getByRole("button", { name: "Sessions" }).click();
|
|
104
|
+
await dialog.getByRole("button", { name: "Revoke", exact: true }).first().click();
|
|
105
|
+
await expect(page.getByText("Session revoked")).toBeVisible();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("manage: delete a user from the danger zone", async ({ page }) => {
|
|
109
|
+
await ready(page, "/admin/users");
|
|
110
|
+
const email = `ui-delete-${Date.now()}@example.com`;
|
|
111
|
+
await seedUser(page, email);
|
|
112
|
+
await openManage(page, email);
|
|
113
|
+
const dialog = page.getByRole("dialog");
|
|
114
|
+
await dialog.getByRole("button", { name: "Danger zone" }).click();
|
|
115
|
+
await dialog.getByRole("button", { name: "Delete" }).click(); // arm
|
|
116
|
+
await dialog.getByRole("button", { name: "Delete" }).click(); // confirm
|
|
117
|
+
await expect(page.getByText("User deleted")).toBeVisible();
|
|
118
|
+
await page.locator("#toolbar-search").fill(email);
|
|
119
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
120
|
+
await expect(page.getByRole("cell", { name: email })).toHaveCount(0);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("users list shows pagination beyond one page", async ({ page }) => {
|
|
124
|
+
await ready(page, "/admin/users");
|
|
125
|
+
for (let i = 0; i < 6; i += 1) {
|
|
126
|
+
await page.request.post("/api/auth/admin/create-user", {
|
|
127
|
+
headers: { origin: base },
|
|
128
|
+
data: { email: `pg-${Date.now()}-${i}@example.com`, password: "Podokit3e-Str0ng!pw", name: `Pg${i}`, role: "user" },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
await page.reload();
|
|
132
|
+
const nav = page.getByRole("navigation", { name: "pagination" });
|
|
133
|
+
await expect(nav).toBeVisible();
|
|
134
|
+
await expect(nav.getByRole("button", { name: "Page 2" })).toBeVisible();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("manage modal closes from its footer", async ({ page }) => {
|
|
138
|
+
await ready(page, "/admin/users");
|
|
139
|
+
const email = `ui-close-${Date.now()}@example.com`;
|
|
140
|
+
await seedUser(page, email);
|
|
141
|
+
await openManage(page, email);
|
|
142
|
+
// footer Close (a visible button, distinct from the header X) dismisses the modal
|
|
143
|
+
await page.getByRole("dialog").getByRole("button", { name: "Close", exact: true }).last().click();
|
|
144
|
+
await expect(page.getByRole("dialog")).toBeHidden();
|
|
145
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
test("admin can create a user with a custom role @smoke", async ({ page }) => {
|
|
5
|
+
await ready(page, "/admin/users");
|
|
6
|
+
await page.getByRole("button", { name: "Add user" }).click();
|
|
7
|
+
const role = page.locator("#c-role");
|
|
8
|
+
test.skip((await role.count()) === 0, "role picker not present");
|
|
9
|
+
const email = `mod-${Date.now()}@example.com`;
|
|
10
|
+
await page.getByLabel("Name", { exact: true }).fill("Mod");
|
|
11
|
+
await page.getByLabel("Email", { exact: true }).fill(email);
|
|
12
|
+
await page.getByLabel("Password", { exact: true }).fill("Podokit3e-Str0ng!pw");
|
|
13
|
+
await page.getByLabel("Confirm password").fill("Podokit3e-Str0ng!pw");
|
|
14
|
+
// pick the Moderator role from the select
|
|
15
|
+
await role.click();
|
|
16
|
+
await page.getByRole("option", { name: "Moderator" }).click();
|
|
17
|
+
await page.getByRole("button", { name: "Create", exact: true }).click();
|
|
18
|
+
await expect(page.getByText("User created")).toBeVisible();
|
|
19
|
+
// it appears in the list, filterable by the Moderator role
|
|
20
|
+
await page.locator("#toolbar-search").fill(email);
|
|
21
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
22
|
+
await expect(page.getByRole("cell", { name: email })).toBeVisible();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("admin sees the user list and can search @smoke", async ({ page }) => {
|
|
26
|
+
await ready(page, "/admin/users");
|
|
27
|
+
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
|
|
28
|
+
// Search by exact address — robust to pagination as the user count grows.
|
|
29
|
+
await page.locator("#toolbar-search").fill("admin@example.com");
|
|
30
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
31
|
+
await expect(page.getByRole("cell", { name: "admin@example.com" })).toBeVisible();
|
|
32
|
+
await page.locator("#toolbar-search").fill("user@example.com");
|
|
33
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
34
|
+
await expect(page.getByRole("cell", { name: "user@example.com" })).toBeVisible();
|
|
35
|
+
await expect(page.getByRole("cell", { name: "admin@example.com" })).toHaveCount(0);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("user list shows the joined column @smoke", async ({ page }) => {
|
|
39
|
+
await ready(page, "/admin/users");
|
|
40
|
+
await expect(page.getByRole("columnheader", { name: "Joined" })).toBeVisible();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("row menu exposes admin actions", async ({ page }) => {
|
|
44
|
+
await page.goto("/admin/users");
|
|
45
|
+
await page.locator("#toolbar-search").fill("user@example.com");
|
|
46
|
+
await page.getByRole("button", { name: "Search", exact: true }).click();
|
|
47
|
+
const row = page.getByRole("row", { name: /user@example.com/ });
|
|
48
|
+
await row.getByRole("button").click();
|
|
49
|
+
await expect(page.getByRole("menuitem", { name: "Manage" })).toBeVisible();
|
|
50
|
+
await expect(page.getByRole("menuitem", { name: "Impersonate" })).toBeVisible();
|
|
51
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "admin-dashboard",
|
|
3
|
+
"description": "Admin dashboard UI (SvelteKit + shadcn-svelte): login/signup/password-reset, a sidebar shell, and user + session management via the better-auth admin plugin. Requires auth.",
|
|
4
|
+
"requires": [
|
|
5
|
+
"auth"
|
|
6
|
+
],
|
|
7
|
+
"targetApp": "web",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"qrcode": "^1.5.4"
|
|
10
|
+
},
|
|
11
|
+
"devDependencies": {
|
|
12
|
+
"@types/qrcode": "^1.5.5"
|
|
13
|
+
},
|
|
14
|
+
"env": [
|
|
15
|
+
"# admin-dashboard: emails auto-promoted to the admin role on sign-up (comma-separated)",
|
|
16
|
+
"ADMIN_EMAILS=admin@example.com"
|
|
17
|
+
],
|
|
18
|
+
"inject": [
|
|
19
|
+
{
|
|
20
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
21
|
+
"marker": "// podokit:end:auth-imports",
|
|
22
|
+
"text": "import { admin } from \"better-auth/plugins\";"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
26
|
+
"marker": "// podokit:end:auth-imports",
|
|
27
|
+
"text": "import { ac, roles } from \"./permissions\";"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
31
|
+
"marker": "// podokit:end:auth-plugins",
|
|
32
|
+
"text": "plugins.push(admin({ defaultRole: \"user\", adminRoles: [\"admin\"], ac, roles }));"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
36
|
+
"marker": "// podokit:end:auth-options",
|
|
37
|
+
"text": "trustedOrigins: (process.env.CORS_ORIGIN ?? \"http://localhost:5001\").split(\",\").map((origin) => origin.trim()),\n databaseHooks: {\n user: {\n create: {\n before: async (user) => ({\n data: {\n ...user,\n role: (process.env.ADMIN_EMAILS ?? \"\")\n .split(\",\")\n .map((email) => email.trim().toLowerCase())\n .includes(user.email.toLowerCase())\n ? \"admin\"\n : \"user\",\n },\n }),\n },\n },\n },"
|
|
38
|
+
}
|
|
39
|
+
],
|
|
40
|
+
"instructions": [
|
|
41
|
+
"Set ADMIN_EMAILS in .env (comma-separated). Those emails become admins on sign-up.",
|
|
42
|
+
"Re-run the auth migration to add the admin columns: npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts",
|
|
43
|
+
"Start the app (npm run dev), open /signup, register an ADMIN_EMAILS address, then sign in.",
|
|
44
|
+
"Admin pages: /dashboard/users (list, ban, set role, revoke sessions). Everyone: /dashboard/sessions, /dashboard/account.",
|
|
45
|
+
"Password reset: the reset link is logged to the API console in dev; wire a real mailer via emailAndPassword.sendResetPassword for production."
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"API_KEYS=dev-key-please-change"
|
|
9
9
|
],
|
|
10
10
|
"inject": [
|
|
11
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { ApiKeyModule } from \"./api-key/api-key.module\";" },
|
|
12
|
-
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "ApiKeyModule," }
|
|
11
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:imports", "text": "import { ApiKeyModule } from \"./api-key/api-key.module\";" },
|
|
12
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:end:module-imports", "text": "ApiKeyModule," }
|
|
13
13
|
],
|
|
14
14
|
"instructions": [
|
|
15
15
|
"Set API_KEYS in .env (comma-separated).",
|
|
16
16
|
"Protect a route for machine clients with @ApiKeyProtected() (bypasses the user session, requires X-API-Key).",
|
|
17
|
-
"Call it: curl -H 'x-api-key: dev-key-please-change' localhost:
|
|
17
|
+
"Call it: curl -H 'x-api-key: dev-key-please-change' localhost:5002/machine/ping"
|
|
18
18
|
]
|
|
19
19
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Audit log
|
|
2
|
+
|
|
3
|
+
An append-only trail of **who did what, to which resource, when** — the
|
|
4
|
+
actor / action / target model. Every entry flows through one pipeline
|
|
5
|
+
(`AuditService` → the `audit_logs` table) so they all share one shape.
|
|
6
|
+
|
|
7
|
+
## What gets recorded
|
|
8
|
+
|
|
9
|
+
Nothing is recorded automatically except the built-in **auth/admin actions**
|
|
10
|
+
(login, user create/update/delete, ban, role change, session revoke, ...),
|
|
11
|
+
which the better-auth hook (`audit-hook.ts`) maps to semantic action codes like
|
|
12
|
+
`user.create` and `auth.login`. Everything else is **opt-in** — you decide what
|
|
13
|
+
matters.
|
|
14
|
+
|
|
15
|
+
## Audit your own routes with `@Audit`
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Audit } from "../audit/audit.decorator";
|
|
19
|
+
|
|
20
|
+
@Post()
|
|
21
|
+
@Audit("todo.create", (_req, todo) => ({ type: "todo", id: todo.id, label: todo.title }))
|
|
22
|
+
create(@Body() dto: CreateTodoDto) {
|
|
23
|
+
return this.todos.create(dto);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The interceptor resolves the acting user (name + email), attaches the target you
|
|
28
|
+
return, and writes one entry when the handler succeeds.
|
|
29
|
+
|
|
30
|
+
## Record from anywhere in code
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { recordAudit } from "../audit/audit-events";
|
|
34
|
+
|
|
35
|
+
await recordAudit({
|
|
36
|
+
action: "invoice.paid",
|
|
37
|
+
actorId, actorName, actorEmail,
|
|
38
|
+
targetType: "invoice", targetId: invoice.id, targetLabel: invoice.number,
|
|
39
|
+
metadata: { amount },
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or inject `AuditService` in a Nest provider and call `audit.record({ ... })`.
|
|
44
|
+
Both never throw — a failed audit write never breaks the request.
|
|
45
|
+
|
|
46
|
+
## Fields (`AuditEntry`)
|
|
47
|
+
|
|
48
|
+
`action` (semantic code), `actorId/actorName/actorEmail` (denormalized so the log
|
|
49
|
+
survives renames/deletes), `targetType/targetId/targetLabel`, `ip`, `metadata`
|
|
50
|
+
(jsonb, free-form), `createdAt`.
|
|
51
|
+
|
|
52
|
+
## Customize
|
|
53
|
+
|
|
54
|
+
- **Auth actions:** edit the `ACTIONS` map in `audit-hook.ts`.
|
|
55
|
+
- **Your routes:** add `@Audit("your.action")` where you want a trail.
|
|
56
|
+
- **Schema:** add columns to `audit-log.entity.ts` + a migration, and keep the
|
|
57
|
+
`AuditEntry` type in `audit-events.ts` in sync.
|
|
58
|
+
- **Reading:** `GET /audit-logs` returns the latest 50 (admin only); extend
|
|
59
|
+
`audit.controller.ts` for filtering/pagination.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { pool } from "../auth/db";
|
|
2
|
+
import { createConfigStore } from "@podosoft/podokit-auth";
|
|
3
|
+
|
|
4
|
+
// Audit logging is an admin-managed, DB-backed server toggle (auth_config `server`
|
|
5
|
+
// row, env fallback AUDIT_LOG_ENABLED), read through the shared config store's
|
|
6
|
+
// short TTL cache — the same mechanism as the other server-enforced toggles.
|
|
7
|
+
// Toggling it on the Settings page takes effect within the cache TTL, no restart.
|
|
8
|
+
const store = createConfigStore(pool);
|
|
9
|
+
|
|
10
|
+
export async function auditEnabled(): Promise<boolean> {
|
|
11
|
+
try {
|
|
12
|
+
return (await store.capabilitiesSnapshot()).auditLog;
|
|
13
|
+
} catch {
|
|
14
|
+
return true; // never let a config read failure silently drop the audit trail
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// One audit pipeline for the whole app. Every source — the @Audit decorator, the
|
|
2
|
+
// better-auth hook, and your own code — records through here, so all entries
|
|
3
|
+
// share the actor/action/target shape and one write path (audit_logs).
|
|
4
|
+
|
|
5
|
+
export type AuditEntry = {
|
|
6
|
+
// Stable semantic action code, e.g. "user.create", "invoice.paid".
|
|
7
|
+
action: string;
|
|
8
|
+
actorId?: string | null;
|
|
9
|
+
actorName?: string | null;
|
|
10
|
+
actorEmail?: string | null;
|
|
11
|
+
targetType?: string | null;
|
|
12
|
+
targetId?: string | null;
|
|
13
|
+
targetLabel?: string | null;
|
|
14
|
+
ip?: string | null;
|
|
15
|
+
metadata?: Record<string, unknown> | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Recorder = (entry: AuditEntry) => Promise<void>;
|
|
19
|
+
let recorder: Recorder | null = null;
|
|
20
|
+
|
|
21
|
+
// AuditService registers the real recorder on startup. The registry lives outside
|
|
22
|
+
// Nest's DI so code that runs outside the container (the better-auth hook,
|
|
23
|
+
// background jobs, ...) can still write through the one pipeline.
|
|
24
|
+
export function setAuditRecorder(fn: Recorder): void {
|
|
25
|
+
recorder = fn;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Record a custom audit event from anywhere. Awaits the write; a no-op until the
|
|
29
|
+
// AuditService has started. Example:
|
|
30
|
+
// await recordAudit({ action: "invoice.paid", actorId, targetLabel: invoice.number,
|
|
31
|
+
// metadata: { amount } });
|
|
32
|
+
export async function recordAudit(entry: AuditEntry): Promise<void> {
|
|
33
|
+
if (recorder) await recorder(entry);
|
|
34
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
2
|
+
import { recordAudit } from "./audit-events";
|
|
3
|
+
import { auditEnabled } from "./audit-enabled";
|
|
4
|
+
|
|
5
|
+
// Map mutating auth/admin endpoints to semantic action codes. better-auth is
|
|
6
|
+
// mounted as middleware and bypasses the NestJS interceptor, so these security
|
|
7
|
+
// events are caught here and recorded through the same pipeline.
|
|
8
|
+
const ACTIONS: Record<string, string> = {
|
|
9
|
+
"/sign-in/email": "auth.login",
|
|
10
|
+
"/sign-up/email": "auth.signup",
|
|
11
|
+
"/sign-out": "auth.logout",
|
|
12
|
+
"/change-password": "auth.password_change",
|
|
13
|
+
"/change-email": "auth.email_change",
|
|
14
|
+
"/reset-password": "auth.password_reset",
|
|
15
|
+
"/send-verification-email": "auth.verification_sent",
|
|
16
|
+
"/verify-email": "auth.email_verified",
|
|
17
|
+
"/update-user": "account.update",
|
|
18
|
+
"/delete-user": "account.delete",
|
|
19
|
+
"/revoke-session": "session.revoke",
|
|
20
|
+
"/revoke-sessions": "session.revoke_all",
|
|
21
|
+
"/revoke-other-sessions": "session.revoke_others",
|
|
22
|
+
"/two-factor/enable": "2fa.enable",
|
|
23
|
+
"/two-factor/disable": "2fa.disable",
|
|
24
|
+
"/admin/create-user": "user.create",
|
|
25
|
+
"/admin/update-user": "user.update",
|
|
26
|
+
"/admin/remove-user": "user.delete",
|
|
27
|
+
"/admin/set-role": "user.role_change",
|
|
28
|
+
"/admin/set-user-password": "user.password_set",
|
|
29
|
+
"/admin/ban-user": "user.ban",
|
|
30
|
+
"/admin/unban-user": "user.unban",
|
|
31
|
+
"/admin/revoke-user-session": "user.session_revoke",
|
|
32
|
+
"/admin/revoke-user-sessions": "user.session_revoke_all",
|
|
33
|
+
"/admin/impersonate-user": "user.impersonate",
|
|
34
|
+
"/admin/stop-impersonating": "user.stop_impersonate",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// The top-level `hooks.after` is a single middleware; it runs only on success,
|
|
38
|
+
// so it records completed actions.
|
|
39
|
+
export const auditAfterHook = createAuthMiddleware(async (ctx) => {
|
|
40
|
+
const action = ACTIONS[ctx.path];
|
|
41
|
+
if (!action) return;
|
|
42
|
+
if (!(await auditEnabled())) return;
|
|
43
|
+
const context = ctx.context as {
|
|
44
|
+
session?: { user?: { id?: string; name?: string; email?: string } };
|
|
45
|
+
newSession?: { user?: { id?: string; name?: string; email?: string } };
|
|
46
|
+
};
|
|
47
|
+
const actor = context.session?.user ?? context.newSession?.user;
|
|
48
|
+
const body = (ctx.body ?? {}) as { email?: string; userId?: string };
|
|
49
|
+
|
|
50
|
+
// Prefer a human-readable target: the email in the body, otherwise resolve the
|
|
51
|
+
// target user's email by id (falls back to the id for already-deleted users).
|
|
52
|
+
const targetId = body.userId ?? null;
|
|
53
|
+
let targetLabel = body.email ?? null;
|
|
54
|
+
if (!targetLabel && targetId) {
|
|
55
|
+
try {
|
|
56
|
+
const adapter = (ctx.context as { internalAdapter?: { findUserById?: (id: string) => Promise<{ email?: string } | null> } })
|
|
57
|
+
.internalAdapter;
|
|
58
|
+
const target = await adapter?.findUserById?.(targetId);
|
|
59
|
+
targetLabel = target?.email ?? targetId;
|
|
60
|
+
} catch {
|
|
61
|
+
targetLabel = targetId;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await recordAudit({
|
|
66
|
+
action,
|
|
67
|
+
actorId: actor?.id ?? null,
|
|
68
|
+
actorName: actor?.name ?? null,
|
|
69
|
+
actorEmail: actor?.email ?? null,
|
|
70
|
+
targetId,
|
|
71
|
+
targetLabel,
|
|
72
|
+
ip: ctx.headers?.get("x-forwarded-for") ?? null,
|
|
73
|
+
});
|
|
74
|
+
});
|