@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,80 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ADMIN } from "../helpers/accounts";
|
|
3
|
+
|
|
4
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
|
+
const origin = { origin: base };
|
|
6
|
+
|
|
7
|
+
// Fresh admin context (not the shared storageState) so these never rotate the
|
|
8
|
+
// seeded admin session.
|
|
9
|
+
async function adminCtx(playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"]) {
|
|
10
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
11
|
+
await ctx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
12
|
+
return ctx;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test("admin can enable Google OAuth from the DB and it applies without a restart @smoke", async ({ playwright }) => {
|
|
16
|
+
const admin = await adminCtx(playwright);
|
|
17
|
+
await admin.put("/api/account/auth-config", { data: { social: { google: { enabled: false } } } }); // clean start
|
|
18
|
+
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
19
|
+
const social = () => anon.post("/api/auth/sign-in/social", { data: { provider: "google", callbackURL: `${base}/admin` } });
|
|
20
|
+
|
|
21
|
+
// Before: provider not configured.
|
|
22
|
+
await expect.poll(async () => (await social()).status(), { timeout: 8000 }).toBe(404);
|
|
23
|
+
|
|
24
|
+
// Paste credentials via the admin API.
|
|
25
|
+
const put = await admin.put("/api/account/auth-config", {
|
|
26
|
+
data: { social: { google: { enabled: true, clientId: "dummy-google-client-id", clientSecret: "dummy-google-client-secret" } } },
|
|
27
|
+
});
|
|
28
|
+
expect(put.ok()).toBeTruthy();
|
|
29
|
+
const view = await put.json();
|
|
30
|
+
expect(view.social.google.hasSecret).toBe(true);
|
|
31
|
+
expect(view.social.google.clientId).toBe("dummy-google-client-id");
|
|
32
|
+
expect("clientSecret" in view.social.google).toBe(false); // secret is never returned
|
|
33
|
+
|
|
34
|
+
// Applied live (no restart): social sign-in now redirects to Google.
|
|
35
|
+
await expect
|
|
36
|
+
.poll(async () => {
|
|
37
|
+
const r = await social();
|
|
38
|
+
return r.ok() ? (((await r.json())?.url as string) ?? "") : "";
|
|
39
|
+
}, { timeout: 8000 })
|
|
40
|
+
.toContain("accounts.google.com");
|
|
41
|
+
const caps = await (await anon.get("/api/account/capabilities")).json();
|
|
42
|
+
expect(caps.providers).toContain("google");
|
|
43
|
+
|
|
44
|
+
await admin.put("/api/account/auth-config", { data: { social: { google: { enabled: false } } } }); // restore
|
|
45
|
+
await anon.dispose();
|
|
46
|
+
await admin.dispose();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("admin can add and remove a social provider dynamically @smoke", async ({ playwright }) => {
|
|
50
|
+
const admin = await adminCtx(playwright);
|
|
51
|
+
// The catalog offers providers beyond the two legacy env-backed ones.
|
|
52
|
+
const catalog = (await (await admin.get("/api/account/auth-config")).json()).catalog as Array<{ id: string }>;
|
|
53
|
+
expect(catalog.map((c) => c.id)).toEqual(expect.arrayContaining(["google", "github", "apple"]));
|
|
54
|
+
|
|
55
|
+
const anon = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
56
|
+
const github = () => anon.post("/api/auth/sign-in/social", { data: { provider: "github", callbackURL: `${base}/admin` } });
|
|
57
|
+
|
|
58
|
+
// Add GitHub (a provider with no env fallback) → applies live, no restart.
|
|
59
|
+
await admin.put("/api/account/auth-config", { data: { social: { github: { enabled: true, clientId: "dummy-github-client-id", clientSecret: "dummy-github-client-secret" } } } });
|
|
60
|
+
await expect
|
|
61
|
+
.poll(async () => { const r = await github(); return r.ok() ? (((await r.json())?.url as string) ?? "") : ""; }, { timeout: 8000 })
|
|
62
|
+
.toContain("github.com");
|
|
63
|
+
|
|
64
|
+
// Remove it → the row is deleted and the provider stops working (again live).
|
|
65
|
+
await admin.put("/api/account/auth-config", { data: { social: { github: { delete: true } } } });
|
|
66
|
+
expect("github" in (await (await admin.get("/api/account/auth-config")).json()).social).toBe(false);
|
|
67
|
+
await expect.poll(async () => (await github()).status(), { timeout: 8000 }).toBe(404);
|
|
68
|
+
|
|
69
|
+
await anon.dispose();
|
|
70
|
+
await admin.dispose();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("auth-config is admin-only and never leaks secrets @smoke", async ({ playwright }) => {
|
|
74
|
+
const user = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
75
|
+
const email = `acfg-${Date.now()}@example.com`;
|
|
76
|
+
await user.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "U" } });
|
|
77
|
+
expect((await user.get("/api/account/auth-config")).status()).toBe(403);
|
|
78
|
+
expect((await user.put("/api/account/auth-config", { data: { server: { hibp: true } } })).status()).toBe(403);
|
|
79
|
+
await user.dispose();
|
|
80
|
+
});
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ADMIN, USER } from "../helpers/accounts";
|
|
3
|
+
import { clearSms, smsSinkReachable, waitForSmsOtp } from "../helpers/sms";
|
|
4
|
+
import { totpCode } from "../helpers/totp";
|
|
5
|
+
|
|
6
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
7
|
+
const origin = { origin: base };
|
|
8
|
+
|
|
9
|
+
// Accounts are created by the `setup` project (this `api` project depends on it).
|
|
10
|
+
test("account/me is 401 when unauthenticated @smoke", async ({ request }) => {
|
|
11
|
+
const res = await request.get("/api/account/me", { headers: origin });
|
|
12
|
+
expect(res.status()).toBe(401);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("rejects a breached password on sign-up @smoke", async ({ playwright }) => {
|
|
16
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
17
|
+
await ctx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
18
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
19
|
+
test.skip(!caps?.passwordBreachCheck, "breach check not enabled");
|
|
20
|
+
// "password" is one of the most-breached passwords; Have I Been Pwned rejects it.
|
|
21
|
+
const res = await ctx.post("/api/auth/sign-up/email", {
|
|
22
|
+
data: { name: "Pwned", email: `pwned-${Date.now()}@example.com`, password: "password" },
|
|
23
|
+
});
|
|
24
|
+
expect(res.ok()).toBeFalsy();
|
|
25
|
+
await ctx.dispose();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("phone number: verify with the code delivered to the SMS sink @smoke", async ({ playwright }) => {
|
|
29
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
30
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
31
|
+
test.skip(!caps?.phoneNumber, "phone number not enabled");
|
|
32
|
+
test.skip(!(await smsSinkReachable()), "SMS sink not reachable");
|
|
33
|
+
await clearSms();
|
|
34
|
+
const email = `phone-${Date.now()}@example.com`;
|
|
35
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Phone" } });
|
|
36
|
+
// Unique number per run so the "phone already exists" guard never trips.
|
|
37
|
+
const phone = `+1555${Date.now().toString().slice(-7)}`;
|
|
38
|
+
|
|
39
|
+
const sent = await ctx.post("/api/auth/phone-number/send-otp", { data: { phoneNumber: phone } });
|
|
40
|
+
expect(sent.ok()).toBeTruthy();
|
|
41
|
+
const code = await waitForSmsOtp(phone); // read the real code back from the sink
|
|
42
|
+
|
|
43
|
+
const verified = await ctx.post("/api/auth/phone-number/verify", {
|
|
44
|
+
data: { phoneNumber: phone, code, updatePhoneNumber: true },
|
|
45
|
+
});
|
|
46
|
+
expect(verified.ok()).toBeTruthy();
|
|
47
|
+
const session = await (await ctx.get("/api/auth/get-session")).json();
|
|
48
|
+
expect(session?.user?.phoneNumber).toBe(phone);
|
|
49
|
+
expect(session?.user?.phoneNumberVerified).toBe(true);
|
|
50
|
+
await ctx.dispose();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("two-factor: enable with a real TOTP code and require it at sign-in @smoke", async ({ playwright }) => {
|
|
54
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
55
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
56
|
+
test.skip(!caps?.twoFactor, "two-factor not enabled");
|
|
57
|
+
const email = `tf-${Date.now()}@example.com`;
|
|
58
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
59
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "TF" } });
|
|
60
|
+
|
|
61
|
+
// Enable 2FA and confirm it with a code computed from the returned otpauth URI.
|
|
62
|
+
const enable = await (await ctx.post("/api/auth/two-factor/enable", { data: { password: pw } })).json();
|
|
63
|
+
expect(typeof enable.totpURI).toBe("string");
|
|
64
|
+
const verified = await ctx.post("/api/auth/two-factor/verify-totp", { data: { code: totpCode(enable.totpURI) } });
|
|
65
|
+
expect(verified.ok()).toBeTruthy();
|
|
66
|
+
|
|
67
|
+
// A fresh sign-in must now be challenged for the second factor (no full session).
|
|
68
|
+
const ctx2 = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
69
|
+
const signin = await (await ctx2.post("/api/auth/sign-in/email", { data: { email, password: pw } })).json();
|
|
70
|
+
expect(signin.twoFactorRedirect).toBe(true);
|
|
71
|
+
const session = await (await ctx2.get("/api/auth/get-session")).json();
|
|
72
|
+
expect(session?.user).toBeFalsy();
|
|
73
|
+
await ctx.dispose();
|
|
74
|
+
await ctx2.dispose();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("multi-session holds several accounts and switches between them @smoke", async ({ playwright }) => {
|
|
78
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
79
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
80
|
+
test.skip(!caps?.multiSession, "multi-session not enabled");
|
|
81
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
82
|
+
const a = `ms-a-${Date.now()}@example.com`;
|
|
83
|
+
const b = `ms-b-${Date.now()}@example.com`;
|
|
84
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email: a, password: pw, name: "A" } });
|
|
85
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email: b, password: pw, name: "B" } });
|
|
86
|
+
type Device = { session: { token: string }; user: { email: string } };
|
|
87
|
+
const devices = (await (await ctx.get("/api/auth/multi-session/list-device-sessions")).json()) as Device[];
|
|
88
|
+
expect(devices.length).toBeGreaterThanOrEqual(2);
|
|
89
|
+
// most recent sign-up is active; switch back to the first account
|
|
90
|
+
const first = devices.find((d) => d.user?.email === a);
|
|
91
|
+
expect(first?.session?.token).toBeTruthy();
|
|
92
|
+
await ctx.post("/api/auth/multi-session/set-active", { data: { sessionToken: first!.session.token } });
|
|
93
|
+
const session = await (await ctx.get("/api/auth/get-session")).json();
|
|
94
|
+
expect(session?.user?.email).toBe(a);
|
|
95
|
+
await ctx.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("a user can sign in with a username @smoke", async ({ playwright }) => {
|
|
99
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
100
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
101
|
+
test.skip(!caps?.username, "username not enabled");
|
|
102
|
+
const email = `uname-${Date.now()}@example.com`;
|
|
103
|
+
const uname = `user${Date.now()}`;
|
|
104
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
105
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "Uname" } });
|
|
106
|
+
expect((await ctx.post("/api/auth/update-user", { data: { username: uname } })).ok()).toBeTruthy();
|
|
107
|
+
await ctx.dispose();
|
|
108
|
+
const ctx2 = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
109
|
+
const signedIn = await ctx2.post("/api/auth/sign-in/username", { data: { username: uname, password: pw } });
|
|
110
|
+
expect(signedIn.ok()).toBeTruthy();
|
|
111
|
+
const session = await (await ctx2.get("/api/auth/get-session")).json();
|
|
112
|
+
expect(session?.user?.username).toBe(uname);
|
|
113
|
+
await ctx2.dispose();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("bearer token authenticates API requests without a cookie @smoke", async ({ playwright }) => {
|
|
117
|
+
const email = `bearer-${Date.now()}@example.com`;
|
|
118
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
119
|
+
const signup = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
120
|
+
await signup.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "Bearer" } });
|
|
121
|
+
const signIn = await signup.post("/api/auth/sign-in/email", { data: { email, password: pw } });
|
|
122
|
+
expect(signIn.ok()).toBeTruthy();
|
|
123
|
+
// The bearer plugin accepts the session token as `Authorization: Bearer`. Take it
|
|
124
|
+
// from the session cookie the sign-in set (equivalent to the set-auth-token header).
|
|
125
|
+
const state = await signup.storageState();
|
|
126
|
+
const token = state.cookies.find((c) => c.name.endsWith("session_token"))?.value;
|
|
127
|
+
expect(token, "sign-in set a session token").toBeTruthy();
|
|
128
|
+
await signup.dispose();
|
|
129
|
+
// Fresh context = no session cookie; authenticate purely via the bearer header.
|
|
130
|
+
const api = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
131
|
+
expect((await api.get("/api/account/me")).status()).toBe(401);
|
|
132
|
+
const authed = await api.get("/api/account/me", { headers: { Authorization: `Bearer ${token}` } });
|
|
133
|
+
expect(authed.ok()).toBeTruthy();
|
|
134
|
+
expect((await authed.json())?.email).toBe(email);
|
|
135
|
+
await api.dispose();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("a user can create, list and revoke a personal API key @smoke", async ({ playwright }) => {
|
|
139
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
140
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
141
|
+
test.skip(!caps?.apiKey, "api keys not enabled");
|
|
142
|
+
const email = `apikey-${Date.now()}@example.com`;
|
|
143
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Key" } });
|
|
144
|
+
|
|
145
|
+
const created = await ctx.post("/api/auth/api-key/create", { data: { name: "ci" } });
|
|
146
|
+
expect(created.ok()).toBeTruthy();
|
|
147
|
+
const key = (await created.json())?.key as string;
|
|
148
|
+
expect(typeof key).toBe("string");
|
|
149
|
+
expect(key.length).toBeGreaterThan(0);
|
|
150
|
+
|
|
151
|
+
const body = (await (await ctx.get("/api/auth/api-key/list")).json()) as { apiKeys: Array<{ id: string; name: string | null }> };
|
|
152
|
+
expect(body.apiKeys.some((k) => k.name === "ci")).toBeTruthy();
|
|
153
|
+
|
|
154
|
+
const del = await ctx.post("/api/auth/api-key/delete", { data: { keyId: body.apiKeys[0].id } });
|
|
155
|
+
expect(del.ok()).toBeTruthy();
|
|
156
|
+
await ctx.dispose();
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("access control enforces custom role permissions @smoke", async ({ playwright }) => {
|
|
160
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
161
|
+
await admin.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
162
|
+
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
163
|
+
expect(caps.roles).toEqual(expect.arrayContaining(["admin", "moderator", "user"]));
|
|
164
|
+
|
|
165
|
+
// A default user lacks the content:create permission.
|
|
166
|
+
const email = `ac-${Date.now()}@example.com`;
|
|
167
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
168
|
+
const u = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
169
|
+
await u.post("/api/auth/sign-up/email", { data: { email, password: pw, name: "AC" } });
|
|
170
|
+
const before = await (await u.post("/api/auth/admin/has-permission", { data: { permissions: { content: ["create"] } } })).json();
|
|
171
|
+
expect(before.success).toBeFalsy();
|
|
172
|
+
const me = await (await u.get("/api/auth/get-session")).json();
|
|
173
|
+
await u.dispose();
|
|
174
|
+
|
|
175
|
+
// Promote to moderator; a fresh session now passes the same check.
|
|
176
|
+
const set = await admin.post("/api/auth/admin/set-role", { data: { userId: me.user.id, role: "moderator" } });
|
|
177
|
+
expect(set.ok()).toBeTruthy();
|
|
178
|
+
const u2 = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
179
|
+
await u2.post("/api/auth/sign-in/email", { data: { email, password: pw } });
|
|
180
|
+
const after = await (await u2.post("/api/auth/admin/has-permission", { data: { permissions: { content: ["create"] } } })).json();
|
|
181
|
+
expect(after.success).toBeTruthy();
|
|
182
|
+
await u2.dispose();
|
|
183
|
+
await admin.dispose();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("a user can create, list and delete an organization @smoke", async ({ playwright }) => {
|
|
187
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
188
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
189
|
+
test.skip(!caps?.organization, "organizations not enabled");
|
|
190
|
+
const email = `org-${Date.now()}@example.com`;
|
|
191
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Org" } });
|
|
192
|
+
|
|
193
|
+
const slug = `acme-${Date.now()}`;
|
|
194
|
+
const created = await ctx.post("/api/auth/organization/create", { data: { name: "Acme", slug } });
|
|
195
|
+
expect(created.ok()).toBeTruthy();
|
|
196
|
+
const orgId = (await created.json())?.id as string;
|
|
197
|
+
expect(typeof orgId).toBe("string");
|
|
198
|
+
|
|
199
|
+
const list = (await (await ctx.get("/api/auth/organization/list")).json()) as Array<{ id: string; slug: string }>;
|
|
200
|
+
expect(list.some((o) => o.slug === slug)).toBeTruthy();
|
|
201
|
+
|
|
202
|
+
const del = await ctx.post("/api/auth/organization/delete", { data: { organizationId: orgId } });
|
|
203
|
+
expect(del.ok()).toBeTruthy();
|
|
204
|
+
await ctx.dispose();
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("an organization can have a parent and manager members @smoke", async ({ playwright }) => {
|
|
208
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
209
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
210
|
+
test.skip(!caps?.organization, "organizations not enabled");
|
|
211
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
212
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email: `orgp-${Date.now()}@example.com`, password: pw, name: "Owner" } });
|
|
213
|
+
|
|
214
|
+
// A parent org, then a child that points at it.
|
|
215
|
+
const parent = await (await ctx.post("/api/auth/organization/create", { data: { name: "HQ", slug: `hq-${Date.now()}` } })).json();
|
|
216
|
+
const child = await ctx.post("/api/auth/organization/create", {
|
|
217
|
+
data: { name: "Sales", slug: `sales-${Date.now()}`, parentOrganizationId: parent.id },
|
|
218
|
+
});
|
|
219
|
+
expect(child.ok()).toBeTruthy();
|
|
220
|
+
const childId = (await child.json()).id as string;
|
|
221
|
+
const full = await (await ctx.get(`/api/auth/organization/get-full-organization?organizationId=${childId}`)).json();
|
|
222
|
+
expect(full.parentOrganizationId).toBe(parent.id);
|
|
223
|
+
|
|
224
|
+
// Add two existing users as managers (role-based → any number).
|
|
225
|
+
const managers: string[] = [];
|
|
226
|
+
for (let i = 0; i < 2; i++) {
|
|
227
|
+
const mCtx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
228
|
+
const email = `mgr-${Date.now()}-${i}@example.com`;
|
|
229
|
+
await mCtx.post("/api/auth/sign-up/email", { data: { email, password: pw, name: `Mgr${i}` } });
|
|
230
|
+
managers.push((await (await mCtx.get("/api/auth/get-session")).json()).user.id);
|
|
231
|
+
await mCtx.dispose();
|
|
232
|
+
}
|
|
233
|
+
for (const userId of managers) {
|
|
234
|
+
const added = await ctx.post("/api/account/org-member", { data: { userId, organizationId: childId, role: "manager" } });
|
|
235
|
+
expect(added.ok()).toBeTruthy();
|
|
236
|
+
}
|
|
237
|
+
const full2 = await (await ctx.get(`/api/auth/organization/get-full-organization?organizationId=${childId}`)).json();
|
|
238
|
+
const mgrCount = (full2.members ?? []).filter((m: { role: string }) => m.role === "manager").length;
|
|
239
|
+
expect(mgrCount).toBe(2);
|
|
240
|
+
await ctx.dispose();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// OIDC discovery + the enabled/disabled behaviour is covered by the front-channel
|
|
244
|
+
// test in tests/ui/oidc.ui.spec.ts (which owns toggling the oidcProvider flag), and
|
|
245
|
+
// the server-side gate is covered generically by the feature-toggle test above.
|
|
246
|
+
|
|
247
|
+
test("only admins can change settings @smoke", async ({ playwright }) => {
|
|
248
|
+
const userCtx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
249
|
+
await userCtx.post("/api/auth/sign-in/email", { data: { email: USER.email, password: USER.password } });
|
|
250
|
+
const asUser = await userCtx.put("/api/account/settings", { data: { magicLink: false } });
|
|
251
|
+
expect(asUser.status()).toBe(403);
|
|
252
|
+
await userCtx.dispose();
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("disabling a feature blocks its endpoint server-side and re-enabling restores it @smoke", async ({ playwright }) => {
|
|
256
|
+
// Uses a fresh admin context (not the shared storageState) so it never rotates
|
|
257
|
+
// the seeded session. Toggles a flag and confirms the feature gate enforces it.
|
|
258
|
+
const admin = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
259
|
+
await admin.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
260
|
+
const caps = await (await admin.get("/api/account/capabilities")).json();
|
|
261
|
+
test.skip(!caps?.magicLink, "magic link not enabled");
|
|
262
|
+
|
|
263
|
+
// A disabled feature's endpoint 404s; the gate reads the flag through a short cache.
|
|
264
|
+
const status = (): Promise<number> =>
|
|
265
|
+
admin.post("/api/auth/sign-in/magic-link", { data: { email: "probe@example.com" } }).then((r) => r.status());
|
|
266
|
+
|
|
267
|
+
expect((await admin.put("/api/account/settings", { data: { magicLink: false } })).ok()).toBeTruthy();
|
|
268
|
+
await expect.poll(status, { timeout: 8000 }).toBe(404);
|
|
269
|
+
|
|
270
|
+
expect((await admin.put("/api/account/settings", { data: { magicLink: true } })).ok()).toBeTruthy();
|
|
271
|
+
await expect.poll(status, { timeout: 8000 }).not.toBe(404);
|
|
272
|
+
|
|
273
|
+
await admin.dispose();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("sign-in with a wrong password is rejected", async ({ request }) => {
|
|
277
|
+
const res = await request.post("/api/auth/sign-in/email", {
|
|
278
|
+
headers: origin,
|
|
279
|
+
data: { email: ADMIN.email, password: "definitely-wrong" },
|
|
280
|
+
});
|
|
281
|
+
expect(res.ok()).toBeFalsy();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("ADMIN_EMAILS account has the admin role @smoke", async ({ playwright }) => {
|
|
285
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
286
|
+
await ctx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
287
|
+
const session = await ctx.get("/api/auth/get-session");
|
|
288
|
+
const body = await session.json();
|
|
289
|
+
expect(body?.user?.email).toBe(ADMIN.email);
|
|
290
|
+
expect(body?.user?.role).toBe("admin");
|
|
291
|
+
await ctx.dispose();
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("admin can list users, a normal user cannot (403)", async ({ playwright }) => {
|
|
295
|
+
const adminCtx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
296
|
+
await adminCtx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
297
|
+
const asAdmin = await adminCtx.get("/api/auth/admin/list-users?limit=5");
|
|
298
|
+
expect(asAdmin.ok()).toBeTruthy();
|
|
299
|
+
await adminCtx.dispose();
|
|
300
|
+
|
|
301
|
+
const userCtx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
302
|
+
await userCtx.post("/api/auth/sign-in/email", { data: { email: USER.email, password: USER.password } });
|
|
303
|
+
const asUser = await userCtx.get("/api/auth/admin/list-users?limit=5");
|
|
304
|
+
expect(asUser.status()).toBe(403);
|
|
305
|
+
await userCtx.dispose();
|
|
306
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { clearMailpit, mailpitReachable, waitForLink, waitForOtp } from "../helpers/mailpit";
|
|
3
|
+
|
|
4
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
|
+
const origin = { origin: base };
|
|
6
|
+
|
|
7
|
+
test("email one-time code signs a user in @smoke", async ({ playwright }) => {
|
|
8
|
+
test.skip(!(await mailpitReachable()), "Mailpit not available");
|
|
9
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
10
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
11
|
+
test.skip(!caps?.emailOtp, "email OTP not enabled");
|
|
12
|
+
const email = `otp-${Date.now()}@example.com`;
|
|
13
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Otp" } });
|
|
14
|
+
await clearMailpit();
|
|
15
|
+
const requested = await ctx.post("/api/auth/email-otp/send-verification-otp", { data: { email, type: "sign-in" } });
|
|
16
|
+
expect(requested.ok()).toBeTruthy();
|
|
17
|
+
const otp = await waitForOtp(email);
|
|
18
|
+
const signedIn = await ctx.post("/api/auth/sign-in/email-otp", { data: { email, otp } });
|
|
19
|
+
expect(signedIn.ok()).toBeTruthy();
|
|
20
|
+
const session = await (await ctx.get("/api/auth/get-session")).json();
|
|
21
|
+
expect(session?.user?.email).toBe(email);
|
|
22
|
+
await ctx.dispose();
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("organization invitation can be sent and accepted @smoke", async ({ playwright }) => {
|
|
26
|
+
const owner = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
27
|
+
const caps = await (await owner.get("/api/account/capabilities")).json();
|
|
28
|
+
test.skip(!caps?.organization, "organizations not enabled");
|
|
29
|
+
const pw = "Podokit3e-Str0ng!pw";
|
|
30
|
+
await owner.post("/api/auth/sign-up/email", { data: { email: `owner-${Date.now()}@example.com`, password: pw, name: "Owner" } });
|
|
31
|
+
const orgId = (await (await owner.post("/api/auth/organization/create", { data: { name: "Invco", slug: `invco-${Date.now()}` } })).json())?.id as string;
|
|
32
|
+
|
|
33
|
+
// invite a fresh user
|
|
34
|
+
const inviteeEmail = `invitee-${Date.now()}@example.com`;
|
|
35
|
+
const inv = await owner.post("/api/auth/organization/invite-member", { data: { email: inviteeEmail, role: "member", organizationId: orgId } });
|
|
36
|
+
expect(inv.ok()).toBeTruthy();
|
|
37
|
+
const invitationId = (await inv.json())?.id as string;
|
|
38
|
+
|
|
39
|
+
// the invitee signs up and accepts
|
|
40
|
+
const invitee = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
41
|
+
await invitee.post("/api/auth/sign-up/email", { data: { email: inviteeEmail, password: pw, name: "Invitee" } });
|
|
42
|
+
const accepted = await invitee.post("/api/auth/organization/accept-invitation", { data: { invitationId } });
|
|
43
|
+
expect(accepted.ok()).toBeTruthy();
|
|
44
|
+
await invitee.dispose();
|
|
45
|
+
|
|
46
|
+
// the owner now sees two members
|
|
47
|
+
const full = await (await owner.get(`/api/auth/organization/get-full-organization?organizationId=${orgId}`)).json();
|
|
48
|
+
expect((full?.members ?? []).length).toBeGreaterThanOrEqual(2);
|
|
49
|
+
await owner.dispose();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("magic link signs a user in @smoke", async ({ playwright }) => {
|
|
53
|
+
test.skip(!(await mailpitReachable()), "Mailpit not available");
|
|
54
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
55
|
+
const caps = await (await ctx.get("/api/account/capabilities")).json();
|
|
56
|
+
test.skip(!caps?.magicLink, "magic link not enabled");
|
|
57
|
+
// Use a throwaway account, not the shared ADMIN — a magic-link sign-in can
|
|
58
|
+
// rotate the account's sessions, which would invalidate the seeded storageState.
|
|
59
|
+
const email = `magic-${Date.now()}@example.com`;
|
|
60
|
+
await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Magic" } });
|
|
61
|
+
await clearMailpit();
|
|
62
|
+
const requested = await ctx.post("/api/auth/sign-in/magic-link", { data: { email, callbackURL: `${base}/admin` } });
|
|
63
|
+
expect(requested.ok()).toBeTruthy();
|
|
64
|
+
const link = await waitForLink(email);
|
|
65
|
+
// Following the emailed link validates the token and sets the session cookie.
|
|
66
|
+
await ctx.get(link, { maxRedirects: 0 });
|
|
67
|
+
const session = await (await ctx.get("/api/auth/get-session")).json();
|
|
68
|
+
expect(session?.user?.email).toBe(email);
|
|
69
|
+
await ctx.dispose();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Follow an emailed auth link (which hits the API to validate) and read the
|
|
73
|
+
// token the API hands back on its redirect.
|
|
74
|
+
function tokenFromRedirect(location: string): string | null {
|
|
75
|
+
return new URL(location, base).searchParams.get("token");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
test("password reset delivers an email and updates the password @smoke", async ({ playwright }) => {
|
|
79
|
+
test.skip(!(await mailpitReachable()), "Mailpit not available");
|
|
80
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
81
|
+
const email = `reset-${Date.now()}@example.com`;
|
|
82
|
+
const signup = await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Reset" } });
|
|
83
|
+
expect(signup.ok()).toBeTruthy();
|
|
84
|
+
// With verification on, sign-up returns no session and the new password still
|
|
85
|
+
// can't sign in until confirmed — so only assert sign-in when it's off.
|
|
86
|
+
const verificationOn = !(await signup.json())?.token;
|
|
87
|
+
|
|
88
|
+
await clearMailpit();
|
|
89
|
+
const requested = await ctx.post("/api/auth/request-password-reset", { data: { email, redirectTo: `${base}/reset-password` } });
|
|
90
|
+
expect(requested.ok()).toBeTruthy();
|
|
91
|
+
|
|
92
|
+
const link = await waitForLink(email);
|
|
93
|
+
const redirected = await ctx.get(link, { maxRedirects: 0 });
|
|
94
|
+
const token = tokenFromRedirect(redirected.headers()["location"] ?? "");
|
|
95
|
+
expect(token, "the reset link resolves to a valid token").toBeTruthy();
|
|
96
|
+
|
|
97
|
+
expect((await ctx.post("/api/auth/reset-password", { data: { newPassword: "Podokit3e-N3wStr0ng!pw", token } })).ok()).toBeTruthy();
|
|
98
|
+
if (!verificationOn) {
|
|
99
|
+
expect((await ctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-N3wStr0ng!pw" } })).ok()).toBeTruthy();
|
|
100
|
+
expect((await ctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } })).status()).toBe(401);
|
|
101
|
+
}
|
|
102
|
+
await ctx.dispose();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("email verification blocks sign-in until the address is confirmed", async ({ playwright }) => {
|
|
106
|
+
test.skip(!(await mailpitReachable()), "Mailpit not available");
|
|
107
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
108
|
+
const email = `verify-${Date.now()}@example.com`;
|
|
109
|
+
await clearMailpit();
|
|
110
|
+
const signup = await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "Verify", callbackURL: `${base}/admin` } });
|
|
111
|
+
expect(signup.ok()).toBeTruthy();
|
|
112
|
+
// When verification is off, sign-up returns a session — nothing to test here.
|
|
113
|
+
test.skip(Boolean((await signup.json())?.token), "email verification not enabled");
|
|
114
|
+
|
|
115
|
+
expect((await ctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } })).status()).toBe(403);
|
|
116
|
+
const link = await waitForLink(email);
|
|
117
|
+
await ctx.get(link, { maxRedirects: 0 });
|
|
118
|
+
expect((await ctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } })).ok()).toBeTruthy();
|
|
119
|
+
await ctx.dispose();
|
|
120
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
import { ADMIN, USER } from "../helpers/accounts";
|
|
3
|
+
|
|
4
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
|
|
5
|
+
const origin = { origin: base };
|
|
6
|
+
|
|
7
|
+
async function signedIn(
|
|
8
|
+
playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"],
|
|
9
|
+
account: { email: string; password: string },
|
|
10
|
+
) {
|
|
11
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
12
|
+
await ctx.post("/api/auth/sign-in/email", { data: account });
|
|
13
|
+
return ctx;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
test("admin can create a user, set its password, and remove it", async ({ playwright }) => {
|
|
17
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
18
|
+
const email = `api-crud-${Date.now()}@example.com`;
|
|
19
|
+
|
|
20
|
+
const created = await admin.post("/api/auth/admin/create-user", {
|
|
21
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "API CRUD", role: "user" },
|
|
22
|
+
});
|
|
23
|
+
expect(created.ok()).toBeTruthy();
|
|
24
|
+
const id = (await created.json())?.user?.id as string;
|
|
25
|
+
expect(id).toBeTruthy();
|
|
26
|
+
|
|
27
|
+
const setPw = await admin.post("/api/auth/admin/set-user-password", {
|
|
28
|
+
data: { userId: id, newPassword: "Podokit3e-N3wStr0ng!pw" },
|
|
29
|
+
});
|
|
30
|
+
expect(setPw.ok()).toBeTruthy();
|
|
31
|
+
|
|
32
|
+
// the new password works
|
|
33
|
+
const fresh = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
34
|
+
const signin = await fresh.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-N3wStr0ng!pw" } });
|
|
35
|
+
expect(signin.ok()).toBeTruthy();
|
|
36
|
+
await fresh.dispose();
|
|
37
|
+
|
|
38
|
+
const removed = await admin.post("/api/auth/admin/remove-user", { data: { userId: id } });
|
|
39
|
+
expect(removed.ok()).toBeTruthy();
|
|
40
|
+
await admin.dispose();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("a normal user cannot create users (403)", async ({ playwright }) => {
|
|
44
|
+
const user = await signedIn(playwright, USER);
|
|
45
|
+
const res = await user.post("/api/auth/admin/create-user", {
|
|
46
|
+
data: { email: `nope-${Date.now()}@example.com`, password: "Podokit3e-Str0ng!pw", name: "Nope", role: "user" },
|
|
47
|
+
});
|
|
48
|
+
expect(res.status()).toBe(403);
|
|
49
|
+
await user.dispose();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("admin can list and revoke a user's sessions", async ({ playwright }) => {
|
|
53
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
54
|
+
const email = `api-sess-${Date.now()}@example.com`;
|
|
55
|
+
const created = await admin.post("/api/auth/admin/create-user", {
|
|
56
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "API Sess", role: "user" },
|
|
57
|
+
});
|
|
58
|
+
const userId = (await created.json())?.user?.id as string;
|
|
59
|
+
|
|
60
|
+
// the user signs in -> creates a session
|
|
61
|
+
const theirs = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
62
|
+
expect((await theirs.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } })).ok()).toBeTruthy();
|
|
63
|
+
await theirs.dispose();
|
|
64
|
+
|
|
65
|
+
const listed = await admin.post("/api/auth/admin/list-user-sessions", { data: { userId } });
|
|
66
|
+
expect(listed.ok()).toBeTruthy();
|
|
67
|
+
expect(((await listed.json())?.sessions ?? []).length).toBeGreaterThan(0);
|
|
68
|
+
|
|
69
|
+
const revoked = await admin.post("/api/auth/admin/revoke-user-sessions", { data: { userId } });
|
|
70
|
+
expect(revoked.ok()).toBeTruthy();
|
|
71
|
+
await admin.post("/api/auth/admin/remove-user", { data: { userId } });
|
|
72
|
+
await admin.dispose();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("revokeOtherSessions keeps the current session valid", async ({ playwright }) => {
|
|
76
|
+
// Use a throwaway account so we don't revoke the shared USER's ui storageState.
|
|
77
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
78
|
+
const email = `api-revoke-others-${Date.now()}@example.com`;
|
|
79
|
+
await admin.post("/api/auth/admin/create-user", { data: { email, password: "Podokit3e-Str0ng!pw", name: "RO", role: "user" } });
|
|
80
|
+
await admin.dispose();
|
|
81
|
+
|
|
82
|
+
const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
83
|
+
await ctx.post("/api/auth/sign-in/email", { data: { email, password: "Podokit3e-Str0ng!pw" } });
|
|
84
|
+
expect((await ctx.post("/api/auth/revoke-other-sessions")).ok()).toBeTruthy();
|
|
85
|
+
const after = await ctx.get("/api/auth/get-session");
|
|
86
|
+
expect((await after.json())?.user?.email).toBe(email); // still signed in
|
|
87
|
+
await ctx.dispose();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("sessions record a client IP (forwarded through the proxy)", async ({ playwright }) => {
|
|
91
|
+
const ctx = await signedIn(playwright, ADMIN);
|
|
92
|
+
const res = await ctx.get("/api/auth/list-sessions");
|
|
93
|
+
const sessions = (await res.json()) as Array<{ ipAddress?: string | null }>;
|
|
94
|
+
expect(sessions.length).toBeGreaterThan(0);
|
|
95
|
+
expect(sessions[0]?.ipAddress).toBeTruthy();
|
|
96
|
+
expect(sessions[0]?.ipAddress).not.toBe("0000:0000:0000:0000:0000:0000:0000:0000");
|
|
97
|
+
await ctx.dispose();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("account capabilities reports optional-feature flags", async ({ playwright }) => {
|
|
101
|
+
const ctx = await signedIn(playwright, ADMIN);
|
|
102
|
+
const res = await ctx.get("/api/account/capabilities");
|
|
103
|
+
expect(res.ok()).toBeTruthy();
|
|
104
|
+
const caps = await res.json();
|
|
105
|
+
expect(typeof caps.twoFactor).toBe("boolean");
|
|
106
|
+
expect(Array.isArray(caps.providers)).toBe(true);
|
|
107
|
+
expect(typeof caps.deleteAccount).toBe("boolean");
|
|
108
|
+
await ctx.dispose();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("admin can update a user and impersonate them", async ({ playwright }) => {
|
|
112
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
113
|
+
const email = `api-edit-${Date.now()}@example.com`;
|
|
114
|
+
const created = await admin.post("/api/auth/admin/create-user", {
|
|
115
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "Edit Me", role: "user" },
|
|
116
|
+
});
|
|
117
|
+
const userId = (await created.json())?.user?.id as string;
|
|
118
|
+
|
|
119
|
+
const updated = await admin.post("/api/auth/admin/update-user", { data: { userId, data: { name: "Updated" } } });
|
|
120
|
+
expect(updated.ok()).toBeTruthy();
|
|
121
|
+
|
|
122
|
+
// impersonate on a fresh context so shared sessions are untouched
|
|
123
|
+
const imp = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
|
|
124
|
+
await imp.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
|
|
125
|
+
expect((await imp.post("/api/auth/admin/impersonate-user", { data: { userId } })).ok()).toBeTruthy();
|
|
126
|
+
const session = await (await imp.get("/api/auth/get-session")).json();
|
|
127
|
+
expect(session?.user?.email).toBe(email);
|
|
128
|
+
expect(session?.session?.impersonatedBy).toBeTruthy();
|
|
129
|
+
await imp.post("/api/auth/admin/stop-impersonating");
|
|
130
|
+
await imp.dispose();
|
|
131
|
+
|
|
132
|
+
await admin.post("/api/auth/admin/remove-user", { data: { userId } });
|
|
133
|
+
await admin.dispose();
|
|
134
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type Account = { name: string; email: string; password: string };
|
|
2
|
+
export const ADMIN = { name: "Admin", email: "admin@example.com", password: "Podokit3e-Str0ng!pw" };
|
|
3
|
+
export const USER = { name: "Normal User", email: "user@example.com", password: "Podokit3e-Str0ng!pw" };
|
|
4
|
+
export const adminState = "playwright/.auth/admin.json";
|
|
5
|
+
export const userState = "playwright/.auth/user.json";
|
|
6
|
+
export const anonState = { cookies: [], origins: [] } as const;
|