@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
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import { Button } from "$lib/components/ui/button";
|
|
5
|
+
import { Input } from "$lib/components/ui/input";
|
|
6
|
+
import { Label } from "$lib/components/ui/label";
|
|
7
|
+
import * as Card from "$lib/components/ui/card";
|
|
8
|
+
import * as Alert from "$lib/components/ui/alert";
|
|
9
|
+
import { api } from "$lib/api";
|
|
10
|
+
import { getI18n } from "$lib/i18n";
|
|
11
|
+
|
|
12
|
+
import type { PageData } from "./$types";
|
|
13
|
+
|
|
14
|
+
let { data }: { data: PageData } = $props();
|
|
15
|
+
const i18n = getI18n();
|
|
16
|
+
let email = $state("");
|
|
17
|
+
let password = $state("");
|
|
18
|
+
let error = $state<string | null>(null);
|
|
19
|
+
let unverified = $state(false);
|
|
20
|
+
let loading = $state(false);
|
|
21
|
+
|
|
22
|
+
// Passwordless sign-in, shown only when the server enabled the magic-link plugin.
|
|
23
|
+
const magicLinkEnabled = $derived(data.capabilities?.magicLink ?? false);
|
|
24
|
+
let magicLoading = $state(false);
|
|
25
|
+
let magicSent = $state(false);
|
|
26
|
+
async function sendMagicLink(): Promise<void> {
|
|
27
|
+
if (!email) {
|
|
28
|
+
error = i18n.t.auth.magicLinkNeedsEmail;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
magicLoading = true;
|
|
32
|
+
error = null;
|
|
33
|
+
const { error: authError } = await api.auth.signIn.magicLink({
|
|
34
|
+
email,
|
|
35
|
+
callbackURL: page.url.searchParams.get("redirect") ?? "/admin",
|
|
36
|
+
});
|
|
37
|
+
magicLoading = false;
|
|
38
|
+
if (authError) {
|
|
39
|
+
error = authError.message ?? i18n.t.auth.signInFailed;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
magicSent = true;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Passwordless sign-in with an emailed one-time code (two steps: send, verify).
|
|
46
|
+
const usernameEnabled = $derived(data.capabilities?.username ?? false);
|
|
47
|
+
const emailOtpEnabled = $derived(data.capabilities?.emailOtp ?? false);
|
|
48
|
+
let otpSent = $state(false);
|
|
49
|
+
let otpCode = $state("");
|
|
50
|
+
let otpLoading = $state(false);
|
|
51
|
+
async function sendOtp(): Promise<void> {
|
|
52
|
+
if (!email) {
|
|
53
|
+
error = i18n.t.auth.magicLinkNeedsEmail;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
otpLoading = true;
|
|
57
|
+
error = null;
|
|
58
|
+
const { error: authError } = await api.auth.emailOtp.sendVerificationOtp({ email, type: "sign-in" });
|
|
59
|
+
otpLoading = false;
|
|
60
|
+
if (authError) {
|
|
61
|
+
error = authError.message ?? i18n.t.auth.signInFailed;
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
otpSent = true;
|
|
65
|
+
}
|
|
66
|
+
async function verifyOtp(): Promise<void> {
|
|
67
|
+
otpLoading = true;
|
|
68
|
+
error = null;
|
|
69
|
+
const { error: authError } = await api.auth.signIn.emailOtp({ email, otp: otpCode });
|
|
70
|
+
otpLoading = false;
|
|
71
|
+
if (authError) {
|
|
72
|
+
error = authError.message ?? i18n.t.auth.signInFailed;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await goto(page.url.searchParams.get("redirect") ?? "/admin", { invalidateAll: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Passwordless sign-in with a registered passkey (WebAuthn), shown when enabled.
|
|
79
|
+
const passkeyEnabled = $derived(data.capabilities?.passkey ?? false);
|
|
80
|
+
let passkeyLoading = $state(false);
|
|
81
|
+
async function signInWithPasskey(): Promise<void> {
|
|
82
|
+
passkeyLoading = true;
|
|
83
|
+
error = null;
|
|
84
|
+
const res = await api.auth.signIn.passkey();
|
|
85
|
+
passkeyLoading = false;
|
|
86
|
+
if (res?.error) {
|
|
87
|
+
error = res.error.message ?? i18n.t.auth.signInFailed;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await goto(page.url.searchParams.get("redirect") ?? "/admin", { invalidateAll: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function submit(event: SubmitEvent): Promise<void> {
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
loading = true;
|
|
96
|
+
error = null;
|
|
97
|
+
unverified = false;
|
|
98
|
+
// With the username plugin on, an identifier without "@" is treated as a username.
|
|
99
|
+
const asUsername = usernameEnabled && !email.includes("@");
|
|
100
|
+
const { error: authError } = asUsername
|
|
101
|
+
? await api.auth.signIn.username({ username: email, password })
|
|
102
|
+
: await api.auth.signIn.email({ email, password });
|
|
103
|
+
loading = false;
|
|
104
|
+
if (authError) {
|
|
105
|
+
// Surface an unverified address with a path to a fresh verification link.
|
|
106
|
+
unverified = authError.code === "EMAIL_NOT_VERIFIED";
|
|
107
|
+
error = unverified ? i18n.t.auth.emailNotVerified : (authError.message ?? i18n.t.auth.signInFailed);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
await goto(page.url.searchParams.get("redirect") ?? "/admin", { invalidateAll: true });
|
|
111
|
+
}
|
|
112
|
+
</script>
|
|
113
|
+
|
|
114
|
+
<Card.Root>
|
|
115
|
+
<Card.Header>
|
|
116
|
+
<Card.Title>{i18n.t.auth.signInTitle}</Card.Title>
|
|
117
|
+
<Card.Description>{i18n.t.auth.signInDesc}</Card.Description>
|
|
118
|
+
</Card.Header>
|
|
119
|
+
<Card.Content>
|
|
120
|
+
<form class="flex flex-col gap-4" onsubmit={submit}>
|
|
121
|
+
{#if error}
|
|
122
|
+
<Alert.Root variant="destructive">
|
|
123
|
+
<Alert.Description>
|
|
124
|
+
{error}
|
|
125
|
+
{#if unverified}
|
|
126
|
+
<a href="/verify-email?email={encodeURIComponent(email)}" class="font-medium underline">{i18n.t.auth.resendVerification}</a>
|
|
127
|
+
{/if}
|
|
128
|
+
</Alert.Description>
|
|
129
|
+
</Alert.Root>
|
|
130
|
+
{/if}
|
|
131
|
+
<div class="flex flex-col gap-2">
|
|
132
|
+
<Label for="email">{usernameEnabled ? i18n.t.auth.emailOrUsername : i18n.t.auth.email}</Label>
|
|
133
|
+
<Input id="email" type={usernameEnabled ? "text" : "email"} bind:value={email} required autocomplete="username" />
|
|
134
|
+
</div>
|
|
135
|
+
<div class="flex flex-col gap-2">
|
|
136
|
+
<div class="flex items-center justify-between">
|
|
137
|
+
<Label for="password">{i18n.t.auth.password}</Label>
|
|
138
|
+
<a href="/forgot-password" class="text-muted-foreground text-xs hover:underline">{i18n.t.auth.forgot}</a>
|
|
139
|
+
</div>
|
|
140
|
+
<Input id="password" type="password" bind:value={password} required autocomplete="current-password" />
|
|
141
|
+
</div>
|
|
142
|
+
<Button type="submit" disabled={loading}>{loading ? i18n.t.auth.signingIn : i18n.t.auth.signIn}</Button>
|
|
143
|
+
</form>
|
|
144
|
+
{#if magicLinkEnabled || emailOtpEnabled || passkeyEnabled}
|
|
145
|
+
<div class="mt-4 flex flex-col gap-3 border-t pt-4">
|
|
146
|
+
{#if passkeyEnabled}
|
|
147
|
+
<Button type="button" variant="outline" class="w-full" disabled={passkeyLoading} onclick={signInWithPasskey}>
|
|
148
|
+
{passkeyLoading ? i18n.t.auth.signingIn : i18n.t.auth.passkeyButton}
|
|
149
|
+
</Button>
|
|
150
|
+
{/if}
|
|
151
|
+
{#if magicLinkEnabled}
|
|
152
|
+
{#if magicSent}
|
|
153
|
+
<p class="text-muted-foreground text-sm" data-testid="magic-link-sent">{i18n.t.auth.magicLinkSent}</p>
|
|
154
|
+
{:else}
|
|
155
|
+
<Button type="button" variant="outline" class="w-full" disabled={magicLoading} onclick={sendMagicLink}>
|
|
156
|
+
{magicLoading ? i18n.t.auth.sending : i18n.t.auth.magicLinkButton}
|
|
157
|
+
</Button>
|
|
158
|
+
{/if}
|
|
159
|
+
{/if}
|
|
160
|
+
{#if emailOtpEnabled}
|
|
161
|
+
{#if otpSent}
|
|
162
|
+
<div class="flex flex-col gap-2">
|
|
163
|
+
<Label for="otp">{i18n.t.auth.otpCode}</Label>
|
|
164
|
+
<Input id="otp" bind:value={otpCode} inputmode="numeric" autocomplete="one-time-code" />
|
|
165
|
+
<Button type="button" class="w-full" disabled={otpLoading || !otpCode} onclick={verifyOtp}>
|
|
166
|
+
{otpLoading ? i18n.t.auth.signingIn : i18n.t.auth.otpVerify}
|
|
167
|
+
</Button>
|
|
168
|
+
</div>
|
|
169
|
+
{:else}
|
|
170
|
+
<Button type="button" variant="outline" class="w-full" disabled={otpLoading} onclick={sendOtp}>
|
|
171
|
+
{otpLoading ? i18n.t.auth.sending : i18n.t.auth.otpButton}
|
|
172
|
+
</Button>
|
|
173
|
+
{/if}
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
</Card.Content>
|
|
178
|
+
<Card.Footer class="justify-center">
|
|
179
|
+
<p class="text-muted-foreground text-sm">{i18n.t.auth.noAccount} <a href="/signup" class="text-foreground hover:underline">{i18n.t.auth.signUp}</a></p>
|
|
180
|
+
</Card.Footer>
|
|
181
|
+
</Card.Root>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import { Button } from "$lib/components/ui/button";
|
|
5
|
+
import { Input } from "$lib/components/ui/input";
|
|
6
|
+
import { Label } from "$lib/components/ui/label";
|
|
7
|
+
import * as Card from "$lib/components/ui/card";
|
|
8
|
+
import * as Alert from "$lib/components/ui/alert";
|
|
9
|
+
import { api } from "$lib/api";
|
|
10
|
+
import { getI18n } from "$lib/i18n";
|
|
11
|
+
|
|
12
|
+
const i18n = getI18n();
|
|
13
|
+
let password = $state("");
|
|
14
|
+
let error = $state<string | null>(null);
|
|
15
|
+
let loading = $state(false);
|
|
16
|
+
const token = $derived(page.url.searchParams.get("token") ?? "");
|
|
17
|
+
|
|
18
|
+
async function submit(event: SubmitEvent): Promise<void> {
|
|
19
|
+
event.preventDefault();
|
|
20
|
+
loading = true;
|
|
21
|
+
error = null;
|
|
22
|
+
const { error: authError } = await api.auth.resetPassword({ newPassword: password, token });
|
|
23
|
+
loading = false;
|
|
24
|
+
if (authError) {
|
|
25
|
+
error = authError.message ?? i18n.t.auth.resetFailed;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
await goto("/login", { invalidateAll: true });
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<Card.Root>
|
|
33
|
+
<Card.Header>
|
|
34
|
+
<Card.Title>{i18n.t.auth.newTitle}</Card.Title>
|
|
35
|
+
</Card.Header>
|
|
36
|
+
<Card.Content>
|
|
37
|
+
<form class="flex flex-col gap-4" onsubmit={submit}>
|
|
38
|
+
{#if error}
|
|
39
|
+
<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>
|
|
40
|
+
{/if}
|
|
41
|
+
{#if !token}
|
|
42
|
+
<Alert.Root variant="destructive"><Alert.Description>{i18n.t.auth.missingToken}</Alert.Description></Alert.Root>
|
|
43
|
+
{/if}
|
|
44
|
+
<div class="flex flex-col gap-2">
|
|
45
|
+
<Label for="password">{i18n.t.auth.newPassword}</Label>
|
|
46
|
+
<Input id="password" type="password" bind:value={password} required autocomplete="new-password" />
|
|
47
|
+
</div>
|
|
48
|
+
<Button type="submit" disabled={loading || !token}>{loading ? i18n.t.auth.updating : i18n.t.auth.updatePassword}</Button>
|
|
49
|
+
</form>
|
|
50
|
+
</Card.Content>
|
|
51
|
+
</Card.Root>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/signup/+page.svelte
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
3
|
+
import { page } from "$app/state";
|
|
4
|
+
import { Button } from "$lib/components/ui/button";
|
|
5
|
+
import { Input } from "$lib/components/ui/input";
|
|
6
|
+
import { Label } from "$lib/components/ui/label";
|
|
7
|
+
import * as Card from "$lib/components/ui/card";
|
|
8
|
+
import * as Alert from "$lib/components/ui/alert";
|
|
9
|
+
import { api } from "$lib/api";
|
|
10
|
+
import { getI18n } from "$lib/i18n";
|
|
11
|
+
|
|
12
|
+
const i18n = getI18n();
|
|
13
|
+
let name = $state("");
|
|
14
|
+
let email = $state("");
|
|
15
|
+
let password = $state("");
|
|
16
|
+
let error = $state<string | null>(null);
|
|
17
|
+
let loading = $state(false);
|
|
18
|
+
|
|
19
|
+
async function submit(event: SubmitEvent): Promise<void> {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
loading = true;
|
|
22
|
+
error = null;
|
|
23
|
+
// callbackURL: where the verification link lands once confirmed (absolute so
|
|
24
|
+
// it returns to the web app, not the API origin). Ignored when verification is off.
|
|
25
|
+
const { error: authError } = await api.auth.signUp.email({
|
|
26
|
+
name,
|
|
27
|
+
email,
|
|
28
|
+
password,
|
|
29
|
+
callbackURL: `${page.url.origin}/admin`,
|
|
30
|
+
});
|
|
31
|
+
if (authError) {
|
|
32
|
+
loading = false;
|
|
33
|
+
error = authError.message ?? i18n.t.auth.signUpFailed;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
// When email verification is required, sign-up creates no session — send the
|
|
37
|
+
// user to confirm their address instead of into the (guarded) app.
|
|
38
|
+
const { data: session } = await api.auth.getSession();
|
|
39
|
+
loading = false;
|
|
40
|
+
if (!session?.session) {
|
|
41
|
+
await goto(`/verify-email?email=${encodeURIComponent(email)}`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
await goto("/admin", { invalidateAll: true });
|
|
45
|
+
}
|
|
46
|
+
</script>
|
|
47
|
+
|
|
48
|
+
<Card.Root>
|
|
49
|
+
<Card.Header>
|
|
50
|
+
<Card.Title>{i18n.t.auth.createTitle}</Card.Title>
|
|
51
|
+
<Card.Description>{i18n.t.auth.createDesc}</Card.Description>
|
|
52
|
+
</Card.Header>
|
|
53
|
+
<Card.Content>
|
|
54
|
+
<form class="flex flex-col gap-4" onsubmit={submit}>
|
|
55
|
+
{#if error}
|
|
56
|
+
<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>
|
|
57
|
+
{/if}
|
|
58
|
+
<div class="flex flex-col gap-2">
|
|
59
|
+
<Label for="name">{i18n.t.auth.name}</Label>
|
|
60
|
+
<Input id="name" bind:value={name} required autocomplete="name" />
|
|
61
|
+
</div>
|
|
62
|
+
<div class="flex flex-col gap-2">
|
|
63
|
+
<Label for="email">{i18n.t.auth.email}</Label>
|
|
64
|
+
<Input id="email" type="email" bind:value={email} required autocomplete="email" />
|
|
65
|
+
</div>
|
|
66
|
+
<div class="flex flex-col gap-2">
|
|
67
|
+
<Label for="password">{i18n.t.auth.password}</Label>
|
|
68
|
+
<Input id="password" type="password" bind:value={password} required autocomplete="new-password" />
|
|
69
|
+
</div>
|
|
70
|
+
<Button type="submit" disabled={loading}>{loading ? i18n.t.auth.creating : i18n.t.auth.create}</Button>
|
|
71
|
+
</form>
|
|
72
|
+
</Card.Content>
|
|
73
|
+
<Card.Footer class="justify-center">
|
|
74
|
+
<p class="text-muted-foreground text-sm">{i18n.t.auth.haveAccount} <a href="/login" class="text-foreground hover:underline">{i18n.t.auth.signIn}</a></p>
|
|
75
|
+
</Card.Footer>
|
|
76
|
+
</Card.Root>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import { Button } from "$lib/components/ui/button";
|
|
4
|
+
import { Input } from "$lib/components/ui/input";
|
|
5
|
+
import { Label } from "$lib/components/ui/label";
|
|
6
|
+
import * as Card from "$lib/components/ui/card";
|
|
7
|
+
import * as Alert from "$lib/components/ui/alert";
|
|
8
|
+
import { api } from "$lib/api";
|
|
9
|
+
import { fmt, getI18n } from "$lib/i18n";
|
|
10
|
+
|
|
11
|
+
const i18n = getI18n();
|
|
12
|
+
// ?email from the sign-up redirect; ?error when the API bounces a bad/expired link here.
|
|
13
|
+
let email = $state(page.url.searchParams.get("email") ?? "");
|
|
14
|
+
const linkError = $derived(page.url.searchParams.get("error"));
|
|
15
|
+
let sent = $state(false);
|
|
16
|
+
let error = $state<string | null>(null);
|
|
17
|
+
let loading = $state(false);
|
|
18
|
+
|
|
19
|
+
async function resend(event: SubmitEvent): Promise<void> {
|
|
20
|
+
event.preventDefault();
|
|
21
|
+
loading = true;
|
|
22
|
+
error = null;
|
|
23
|
+
const { error: authError } = await api.auth.sendVerificationEmail({
|
|
24
|
+
email,
|
|
25
|
+
callbackURL: `${page.url.origin}/admin`,
|
|
26
|
+
});
|
|
27
|
+
loading = false;
|
|
28
|
+
if (authError) {
|
|
29
|
+
error = authError.message ?? i18n.t.auth.requestFailed;
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
sent = true;
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<Card.Root>
|
|
37
|
+
<Card.Header>
|
|
38
|
+
<Card.Title>{i18n.t.auth.verifyTitle}</Card.Title>
|
|
39
|
+
<Card.Description>{email ? fmt(i18n.t.auth.verifyDesc, { email }) : i18n.t.auth.verifyDescNoEmail}</Card.Description>
|
|
40
|
+
</Card.Header>
|
|
41
|
+
<Card.Content>
|
|
42
|
+
{#if linkError}
|
|
43
|
+
<Alert.Root variant="destructive" class="mb-4"><Alert.Description>{i18n.t.auth.verifyLinkError}</Alert.Description></Alert.Root>
|
|
44
|
+
{/if}
|
|
45
|
+
{#if sent}
|
|
46
|
+
<Alert.Root><Alert.Description>{i18n.t.auth.verifySent}</Alert.Description></Alert.Root>
|
|
47
|
+
{:else}
|
|
48
|
+
<form class="flex flex-col gap-4" onsubmit={resend}>
|
|
49
|
+
{#if error}
|
|
50
|
+
<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>
|
|
51
|
+
{/if}
|
|
52
|
+
<div class="flex flex-col gap-2">
|
|
53
|
+
<Label for="email">{i18n.t.auth.email}</Label>
|
|
54
|
+
<Input id="email" type="email" bind:value={email} required autocomplete="email" />
|
|
55
|
+
</div>
|
|
56
|
+
<Button type="submit" disabled={loading || !email}>{loading ? i18n.t.auth.sending : i18n.t.auth.resendVerification}</Button>
|
|
57
|
+
</form>
|
|
58
|
+
{/if}
|
|
59
|
+
</Card.Content>
|
|
60
|
+
<Card.Footer class="justify-center">
|
|
61
|
+
<a href="/login" class="text-muted-foreground text-sm hover:underline">{i18n.t.auth.backToSignIn}</a>
|
|
62
|
+
</Card.Footer>
|
|
63
|
+
</Card.Root>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { LayoutServerLoad } from "./$types";
|
|
2
|
+
import { redirect } from "@sveltejs/kit";
|
|
3
|
+
|
|
4
|
+
// "/" is a public landing page. Auth pages send signed-in users to /admin, and
|
|
5
|
+
// the admin area requires a session.
|
|
6
|
+
const PUBLIC_PATHS = ["/", "/login", "/signup", "/forgot-password", "/reset-password"];
|
|
7
|
+
const AUTH_PATHS = ["/login", "/signup", "/forgot-password", "/reset-password"];
|
|
8
|
+
|
|
9
|
+
function isPublic(pathname: string): boolean {
|
|
10
|
+
return PUBLIC_PATHS.some((path) => pathname === path || (path !== "/" && pathname.startsWith(`${path}/`)));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const load: LayoutServerLoad = ({ locals, url }) => {
|
|
14
|
+
const { pathname } = url;
|
|
15
|
+
if (locals.user && AUTH_PATHS.includes(pathname)) {
|
|
16
|
+
redirect(303, "/admin");
|
|
17
|
+
}
|
|
18
|
+
if (!locals.user && !isPublic(pathname)) {
|
|
19
|
+
redirect(303, `/login?redirect=${encodeURIComponent(pathname)}`);
|
|
20
|
+
}
|
|
21
|
+
return { user: locals.user, locale: locals.locale };
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import "../app.css";
|
|
3
|
+
import { ModeWatcher } from "mode-watcher";
|
|
4
|
+
import { setI18nContext } from "$lib/i18n";
|
|
5
|
+
import { messages, resolveLocale } from "$lib/i18n/messages";
|
|
6
|
+
|
|
7
|
+
let { children, data } = $props();
|
|
8
|
+
const locale = $derived(resolveLocale(data.locale));
|
|
9
|
+
setI18nContext({
|
|
10
|
+
get t() {
|
|
11
|
+
return messages[locale];
|
|
12
|
+
},
|
|
13
|
+
get locale() {
|
|
14
|
+
return locale;
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
<ModeWatcher />
|
|
20
|
+
{@render children()}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "$lib/components/ui/button";
|
|
3
|
+
import LanguageSwitch from "$lib/components/language-switch.svelte";
|
|
4
|
+
import ThemeToggle from "$lib/components/theme-toggle.svelte";
|
|
5
|
+
import { getI18n } from "$lib/i18n";
|
|
6
|
+
|
|
7
|
+
const i18n = getI18n();
|
|
8
|
+
</script>
|
|
9
|
+
|
|
10
|
+
<div class="flex min-h-svh flex-col">
|
|
11
|
+
<header class="flex items-center justify-end gap-2 p-4">
|
|
12
|
+
<LanguageSwitch />
|
|
13
|
+
<ThemeToggle />
|
|
14
|
+
</header>
|
|
15
|
+
<main class="flex flex-1 flex-col items-center justify-center gap-6 px-4 text-center">
|
|
16
|
+
<div class="flex flex-col gap-3">
|
|
17
|
+
<h1 class="text-3xl font-semibold">{i18n.t.landing.title}</h1>
|
|
18
|
+
<p class="text-muted-foreground max-w-md">{i18n.t.landing.subtitle}</p>
|
|
19
|
+
</div>
|
|
20
|
+
<Button href="/admin">{i18n.t.landing.openAdmin}</Button>
|
|
21
|
+
</main>
|
|
22
|
+
</div>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/oauth2/consent/+page.svelte
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { page } from "$app/state";
|
|
3
|
+
import { Button } from "$lib/components/ui/button";
|
|
4
|
+
import * as Card from "$lib/components/ui/card";
|
|
5
|
+
import { getI18n } from "$lib/i18n";
|
|
6
|
+
|
|
7
|
+
const i18n = getI18n();
|
|
8
|
+
// The OAuth provider redirects here with the authorization request in the query
|
|
9
|
+
// (and a pending-request cookie). Show what's being requested, then approve/deny.
|
|
10
|
+
const clientId = $derived(page.url.searchParams.get("client_id") ?? "");
|
|
11
|
+
const scopes = $derived((page.url.searchParams.get("scope") ?? "").split(/[+\s]/).filter(Boolean));
|
|
12
|
+
let busy = $state(false);
|
|
13
|
+
|
|
14
|
+
async function decide(accept: boolean): Promise<void> {
|
|
15
|
+
busy = true;
|
|
16
|
+
// The authorization request is carried in the signed query params we were
|
|
17
|
+
// redirected here with; forward them verbatim so the server can validate and
|
|
18
|
+
// restore it. Credentials carry the user's session. The response says where next.
|
|
19
|
+
const res = await fetch(`/api/auth/oauth2/consent${page.url.search}`, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
credentials: "include",
|
|
22
|
+
headers: { "content-type": "application/json" },
|
|
23
|
+
body: JSON.stringify({ accept }),
|
|
24
|
+
});
|
|
25
|
+
const data = (await res.json().catch(() => ({}))) as { url?: string; redirectURI?: string };
|
|
26
|
+
const next = data.url ?? data.redirectURI;
|
|
27
|
+
if (next) window.location.href = next;
|
|
28
|
+
else busy = false;
|
|
29
|
+
}
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div class="mx-auto flex min-h-[70vh] max-w-md items-center">
|
|
33
|
+
<Card.Root class="w-full">
|
|
34
|
+
<Card.Header>
|
|
35
|
+
<Card.Title>{i18n.t.consent.title}</Card.Title>
|
|
36
|
+
<Card.Description>{i18n.t.consent.subtitle}</Card.Description>
|
|
37
|
+
</Card.Header>
|
|
38
|
+
<Card.Content class="flex flex-col gap-4">
|
|
39
|
+
<div class="text-sm">
|
|
40
|
+
<p class="font-medium">{clientId}</p>
|
|
41
|
+
{#if scopes.length}
|
|
42
|
+
<p class="text-muted-foreground mt-1">{i18n.t.consent.requests}</p>
|
|
43
|
+
<ul class="text-muted-foreground mt-1 list-inside list-disc">
|
|
44
|
+
{#each scopes as s (s)}<li>{s}</li>{/each}
|
|
45
|
+
</ul>
|
|
46
|
+
{/if}
|
|
47
|
+
</div>
|
|
48
|
+
<div class="flex justify-end gap-2">
|
|
49
|
+
<Button variant="outline" disabled={busy} onclick={() => decide(false)}>{i18n.t.consent.deny}</Button>
|
|
50
|
+
<Button disabled={busy} onclick={() => decide(true)}>{i18n.t.consent.allow}</Button>
|
|
51
|
+
</div>
|
|
52
|
+
</Card.Content>
|
|
53
|
+
</Card.Root>
|
|
54
|
+
</div>
|
|
@@ -0,0 +1,97 @@
|
|
|
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("audit log records auth/admin actions", async ({ playwright }) => {
|
|
17
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
18
|
+
const probe = await admin.get("/api/audit-logs");
|
|
19
|
+
test.skip(probe.status() === 404, "audit-log module not installed");
|
|
20
|
+
|
|
21
|
+
// an admin action that flows through better-auth (bypasses the interceptor)
|
|
22
|
+
const email = `audit-${Date.now()}@example.com`;
|
|
23
|
+
const created = await admin.post("/api/auth/admin/create-user", {
|
|
24
|
+
data: { email, password: "Podokit3e-Str0ng!pw", name: "Audit", role: "user" },
|
|
25
|
+
});
|
|
26
|
+
expect(created.ok()).toBeTruthy();
|
|
27
|
+
const userId = (await created.json())?.user?.id as string;
|
|
28
|
+
|
|
29
|
+
const res = await admin.get("/api/audit-logs");
|
|
30
|
+
expect(res.ok()).toBeTruthy();
|
|
31
|
+
const entries = (await res.json()) as Array<{ action: string; actorEmail: string | null; targetLabel: string | null }>;
|
|
32
|
+
const entry = entries.find((e) => e.action === "user.create" && e.targetLabel === email);
|
|
33
|
+
expect(entry, "user.create should be audited with the target").toBeTruthy();
|
|
34
|
+
expect(entry?.actorEmail, "audit entry records the acting admin").toBe(ADMIN.email);
|
|
35
|
+
|
|
36
|
+
await admin.post("/api/auth/admin/remove-user", { data: { userId } });
|
|
37
|
+
await admin.dispose();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("the audit log is admin-only", async ({ playwright }) => {
|
|
41
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
42
|
+
test.skip((await admin.get("/api/audit-logs")).status() === 404, "audit-log module not installed");
|
|
43
|
+
await admin.dispose();
|
|
44
|
+
|
|
45
|
+
const user = await signedIn(playwright, USER);
|
|
46
|
+
expect((await user.get("/api/audit-logs")).status()).toBe(403);
|
|
47
|
+
await user.dispose();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("sending a verification email is audited", async ({ playwright }) => {
|
|
51
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
52
|
+
test.skip((await admin.get("/api/audit-logs")).status() === 404, "audit-log module not installed");
|
|
53
|
+
const email = `verif-audit-${Date.now()}@example.com`;
|
|
54
|
+
await admin.post("/api/auth/admin/create-user", { data: { email, password: "Podokit3e-Str0ng!pw", name: "VA", role: "user" } });
|
|
55
|
+
await admin.post("/api/auth/send-verification-email", { data: { email, callbackURL: `${base}/admin` } });
|
|
56
|
+
const entries = (await (await admin.get("/api/audit-logs")).json()) as Array<{ action: string; targetLabel: string | null }>;
|
|
57
|
+
expect(entries.some((e) => e.action === "auth.verification_sent"), "verification send should be audited").toBeTruthy();
|
|
58
|
+
await admin.dispose();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Audit logging is a DB-backed on/off toggle (auth_config `server`, applied live).
|
|
62
|
+
// Runs after the recording tests and restores the flag so nothing else is affected.
|
|
63
|
+
test("audit logging honours the DB on/off toggle @smoke", async ({ playwright }) => {
|
|
64
|
+
const admin = await signedIn(playwright, ADMIN);
|
|
65
|
+
test.skip((await admin.get("/api/audit-logs")).status() === 404, "audit-log module not installed");
|
|
66
|
+
const auditLog = async () => (await (await admin.get("/api/account/capabilities")).json()).auditLog as boolean;
|
|
67
|
+
const recorded = async (label: string) =>
|
|
68
|
+
((await (await admin.get("/api/audit-logs")).json()) as Array<{ action: string; targetLabel: string | null }>).some(
|
|
69
|
+
(e) => e.action === "user.create" && e.targetLabel === label,
|
|
70
|
+
);
|
|
71
|
+
const createUser = async (email: string) =>
|
|
72
|
+
(await (await admin.post("/api/auth/admin/create-user", { data: { email, password: "Podokit3e-Str0ng!pw", name: "T", role: "user" } })).json())?.user?.id as string;
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
// OFF → the action is not recorded. Wait past the config-store cache TTL so the
|
|
76
|
+
// audit gate sees the new value (its cache is independent of the capabilities one).
|
|
77
|
+
await admin.put("/api/account/auth-config", { data: { server: { auditLog: false } } });
|
|
78
|
+
await expect.poll(auditLog, { timeout: 8000 }).toBe(false);
|
|
79
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
80
|
+
const offEmail = `audit-off-${Date.now()}@example.com`;
|
|
81
|
+
const offId = await createUser(offEmail);
|
|
82
|
+
expect(await recorded(offEmail), "no audit entry while disabled").toBe(false);
|
|
83
|
+
await admin.post("/api/auth/admin/remove-user", { data: { userId: offId } });
|
|
84
|
+
|
|
85
|
+
// ON → recording resumes.
|
|
86
|
+
await admin.put("/api/account/auth-config", { data: { server: { auditLog: true } } });
|
|
87
|
+
await expect.poll(auditLog, { timeout: 8000 }).toBe(true);
|
|
88
|
+
await new Promise((r) => setTimeout(r, 4000));
|
|
89
|
+
const onEmail = `audit-on-${Date.now()}@example.com`;
|
|
90
|
+
const onId = await createUser(onEmail);
|
|
91
|
+
expect(await recorded(onEmail), "audit entry recorded once re-enabled").toBe(true);
|
|
92
|
+
await admin.post("/api/auth/admin/remove-user", { data: { userId: onId } });
|
|
93
|
+
} finally {
|
|
94
|
+
await admin.put("/api/account/auth-config", { data: { server: { auditLog: true } } });
|
|
95
|
+
await admin.dispose();
|
|
96
|
+
}
|
|
97
|
+
});
|