@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,552 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { goto } from "$app/navigation";
|
|
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 { Badge } from "$lib/components/ui/badge";
|
|
7
|
+
import { Checkbox } from "$lib/components/ui/checkbox";
|
|
8
|
+
import * as Table from "$lib/components/ui/table";
|
|
9
|
+
import * as Dialog from "$lib/components/ui/dialog";
|
|
10
|
+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
|
11
|
+
import * as Select from "$lib/components/ui/select";
|
|
12
|
+
import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "$lib/components/data-table.svelte";
|
|
13
|
+
import TableToolbar, { type ToolbarFilter, type ToolbarSearchField } from "$lib/components/table-toolbar.svelte";
|
|
14
|
+
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
|
15
|
+
import PlusIcon from "@lucide/svelte/icons/plus";
|
|
16
|
+
import { toast } from "svelte-sonner";
|
|
17
|
+
import { api } from "$lib/api";
|
|
18
|
+
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
19
|
+
import type { PageData } from "./$types";
|
|
20
|
+
|
|
21
|
+
let { data }: { data: PageData } = $props();
|
|
22
|
+
const i18n = getI18n();
|
|
23
|
+
const emailVerificationEnabled = $derived(data.capabilities.emailVerification);
|
|
24
|
+
// Assignable roles come from the server (access-control); label the known ones.
|
|
25
|
+
const roleNames = $derived(data.capabilities.roles ?? ["admin", "user"]);
|
|
26
|
+
const roleLabel = (r: string): string =>
|
|
27
|
+
({ admin: i18n.t.users.roleAdmin, user: i18n.t.users.roleUser, moderator: i18n.t.users.roleModerator }) [r] ?? r;
|
|
28
|
+
|
|
29
|
+
type Row = {
|
|
30
|
+
id: string;
|
|
31
|
+
name: string;
|
|
32
|
+
email: string;
|
|
33
|
+
role?: string | null;
|
|
34
|
+
banned?: boolean | null;
|
|
35
|
+
emailVerified?: boolean | null;
|
|
36
|
+
createdAt?: string | Date | null;
|
|
37
|
+
banReason?: string | null;
|
|
38
|
+
banExpires?: string | Date | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const PAGE_SIZE = DEFAULT_PAGE_SIZE;
|
|
42
|
+
const LOAD_LIMIT = 500; // bounded client-side list; add server-side filtering for larger deployments
|
|
43
|
+
let users = $state<Row[]>([]);
|
|
44
|
+
let page = $state(1);
|
|
45
|
+
let sort = $state<SortState | null>(null);
|
|
46
|
+
let busy = $state(false);
|
|
47
|
+
|
|
48
|
+
// Toolbar: filters + search both commit on the Search button (nothing applies
|
|
49
|
+
// until then), so multiple filter/search conditions apply together.
|
|
50
|
+
let search = $state("");
|
|
51
|
+
let appliedSearch = $state("");
|
|
52
|
+
let searchField = $state("email");
|
|
53
|
+
let appliedSearchField = $state("email");
|
|
54
|
+
let filterValues = $state<Record<string, string>>({ role: "", status: "" });
|
|
55
|
+
let appliedFilters = $state<Record<string, string>>({ role: "", status: "" });
|
|
56
|
+
|
|
57
|
+
const columns: DataTableColumn<Row>[] = [
|
|
58
|
+
{ key: "name", label: i18n.t.users.name, sortable: true },
|
|
59
|
+
{ key: "email", label: i18n.t.users.email, sortable: true },
|
|
60
|
+
{ key: "role", label: i18n.t.users.role, sortable: true },
|
|
61
|
+
{ key: "status", label: i18n.t.users.status },
|
|
62
|
+
{ key: "createdAt", label: i18n.t.users.joined, sortable: true, value: (u) => (u.createdAt ? new Date(u.createdAt).getTime() : 0) },
|
|
63
|
+
{ key: "actions", label: "", class: "w-10" },
|
|
64
|
+
];
|
|
65
|
+
const filters = $derived<ToolbarFilter[]>([
|
|
66
|
+
{
|
|
67
|
+
key: "role",
|
|
68
|
+
label: i18n.t.users.role,
|
|
69
|
+
options: [{ value: "", label: i18n.t.toolbar.all }, ...roleNames.map((r) => ({ value: r, label: roleLabel(r) }))],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: "status",
|
|
73
|
+
label: i18n.t.users.status,
|
|
74
|
+
options: [
|
|
75
|
+
{ value: "", label: i18n.t.toolbar.all },
|
|
76
|
+
{ value: "active", label: i18n.t.users.active },
|
|
77
|
+
{ value: "banned", label: i18n.t.users.banned },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
const searchFields: ToolbarSearchField[] = [
|
|
82
|
+
{ value: "email", label: i18n.t.users.email },
|
|
83
|
+
{ value: "name", label: i18n.t.users.name },
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const filtered = $derived(
|
|
87
|
+
users.filter((u) => {
|
|
88
|
+
if (appliedSearch) {
|
|
89
|
+
const hay = (appliedSearchField === "name" ? u.name : u.email).toLowerCase();
|
|
90
|
+
if (!hay.includes(appliedSearch.toLowerCase())) return false;
|
|
91
|
+
}
|
|
92
|
+
if (appliedFilters.role && (u.role ?? "user") !== appliedFilters.role) return false;
|
|
93
|
+
if (appliedFilters.status === "banned" && !u.banned) return false;
|
|
94
|
+
if (appliedFilters.status === "active" && u.banned) return false;
|
|
95
|
+
return true;
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
async function load(): Promise<void> {
|
|
100
|
+
const { data: res, error } = await api.auth.admin.listUsers({ query: { limit: LOAD_LIMIT } });
|
|
101
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.loadFailed);
|
|
102
|
+
users = (res?.users ?? []) as Row[];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async function applySearch(): Promise<void> {
|
|
106
|
+
await load(); // refresh from the server on explicit search, then filter client-side
|
|
107
|
+
appliedSearch = search;
|
|
108
|
+
appliedSearchField = searchField;
|
|
109
|
+
appliedFilters = { ...filterValues };
|
|
110
|
+
page = 1;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Impersonate — become the user, then a banner offers "stop impersonating".
|
|
114
|
+
async function impersonate(u: Row): Promise<void> {
|
|
115
|
+
const { error } = await api.auth.admin.impersonateUser({ userId: u.id });
|
|
116
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
117
|
+
await goto("/admin", { invalidateAll: true });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create user
|
|
121
|
+
let createOpen = $state(false);
|
|
122
|
+
let form = $state({ name: "", email: "", password: "", confirm: "", role: "user", sendVerify: true });
|
|
123
|
+
let createError = $state("");
|
|
124
|
+
async function createUser(event: Event): Promise<void> {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
if (form.password !== form.confirm) {
|
|
127
|
+
createError = i18n.t.users.passwordMismatch;
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
createError = "";
|
|
131
|
+
busy = true;
|
|
132
|
+
const { error } = await api.auth.admin.createUser({
|
|
133
|
+
name: form.name,
|
|
134
|
+
email: form.email,
|
|
135
|
+
password: form.password,
|
|
136
|
+
// Roles are server-driven (capabilities.roles); the typed client only knows
|
|
137
|
+
// the built-in ones, so assert at this boundary — the server validates it.
|
|
138
|
+
role: form.role as "admin" | "user",
|
|
139
|
+
});
|
|
140
|
+
busy = false;
|
|
141
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
142
|
+
toast.success(i18n.t.users.userCreated);
|
|
143
|
+
if (form.sendVerify && emailVerificationEnabled) {
|
|
144
|
+
await api.auth.sendVerificationEmail({ email: form.email, callbackURL: `${location.origin}/admin` });
|
|
145
|
+
}
|
|
146
|
+
createOpen = false;
|
|
147
|
+
form = { name: "", email: "", password: "", confirm: "", role: "user", sendVerify: true };
|
|
148
|
+
page = 1;
|
|
149
|
+
await load();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Manage user — a two-pane modal mirroring the account settings, admin-scoped.
|
|
153
|
+
type ManageSection = "profile" | "security" | "sessions" | "danger";
|
|
154
|
+
type Session = { id: string; token: string; userAgent?: string | null; ipAddress?: string | null; createdAt: string | Date };
|
|
155
|
+
let manageOpen = $state(false);
|
|
156
|
+
let mUser = $state<Row | null>(null);
|
|
157
|
+
let mSection = $state<ManageSection>("profile");
|
|
158
|
+
const sections: ManageSection[] = ["profile", "security", "sessions", "danger"];
|
|
159
|
+
// profile
|
|
160
|
+
let mName = $state("");
|
|
161
|
+
let mEmail = $state("");
|
|
162
|
+
let mRole = $state("user");
|
|
163
|
+
// security
|
|
164
|
+
let mNewPassword = $state("");
|
|
165
|
+
let mConfirmPassword = $state("");
|
|
166
|
+
let mPwError = $state("");
|
|
167
|
+
let mBanReason = $state("");
|
|
168
|
+
let mBanDays = $state("");
|
|
169
|
+
// sessions
|
|
170
|
+
let mSessions = $state<Session[]>([]);
|
|
171
|
+
let mSessionsPage = $state(1);
|
|
172
|
+
let mSessionsSort = $state<SortState | null>(null);
|
|
173
|
+
const mSessionsColumns: DataTableColumn<Session>[] = [
|
|
174
|
+
{ key: "userAgent", label: i18n.t.adminSessions.device, sortable: true },
|
|
175
|
+
{ key: "ipAddress", label: i18n.t.adminSessions.ip, sortable: true },
|
|
176
|
+
{ key: "createdAt", label: i18n.t.adminSessions.since, sortable: true, value: (s) => new Date(s.createdAt).getTime() },
|
|
177
|
+
{ key: "actions", label: "", class: "w-10" },
|
|
178
|
+
];
|
|
179
|
+
// danger
|
|
180
|
+
let mDeleteArmed = $state(false);
|
|
181
|
+
|
|
182
|
+
async function openManage(u: Row): Promise<void> {
|
|
183
|
+
mUser = u;
|
|
184
|
+
mSection = "profile";
|
|
185
|
+
mName = u.name;
|
|
186
|
+
mEmail = u.email;
|
|
187
|
+
mRole = u.role ?? "user";
|
|
188
|
+
mNewPassword = "";
|
|
189
|
+
mConfirmPassword = "";
|
|
190
|
+
mPwError = "";
|
|
191
|
+
mBanReason = "";
|
|
192
|
+
mBanDays = "";
|
|
193
|
+
mDeleteArmed = false;
|
|
194
|
+
mSessions = [];
|
|
195
|
+
mSessionsPage = 1;
|
|
196
|
+
manageOpen = true;
|
|
197
|
+
await loadManagedSessions();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function loadManagedSessions(): Promise<void> {
|
|
201
|
+
if (!mUser) return;
|
|
202
|
+
const { data: res } = await api.auth.admin.listUserSessions({ userId: mUser.id });
|
|
203
|
+
mSessions = (res?.sessions ?? []) as Session[];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function saveProfile(event: Event): Promise<void> {
|
|
207
|
+
event.preventDefault();
|
|
208
|
+
if (!mUser) return;
|
|
209
|
+
busy = true;
|
|
210
|
+
const nextRole = mRole;
|
|
211
|
+
// Profile fields go through updateUser; role changes go through the dedicated
|
|
212
|
+
// setRole endpoint (aligns with the admin plugin's access-control model).
|
|
213
|
+
const { error } = await api.auth.admin.updateUser({
|
|
214
|
+
userId: mUser.id,
|
|
215
|
+
data: { name: mName, email: mEmail },
|
|
216
|
+
});
|
|
217
|
+
if (!error && nextRole !== (mUser.role ?? "user")) {
|
|
218
|
+
// Custom roles are valid at runtime (server-enforced); assert past the
|
|
219
|
+
// typed client, which only knows the built-in roles.
|
|
220
|
+
const { error: roleError } = await api.auth.admin.setRole({ userId: mUser.id, role: nextRole as "admin" | "user" });
|
|
221
|
+
if (roleError) {
|
|
222
|
+
busy = false;
|
|
223
|
+
return void toast.error(roleError.message ?? i18n.t.users.actionFailed);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
busy = false;
|
|
227
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
228
|
+
mUser = { ...mUser, name: mName, email: mEmail, role: nextRole };
|
|
229
|
+
toast.success(i18n.t.users.userUpdated);
|
|
230
|
+
await load();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function setPassword(event: Event): Promise<void> {
|
|
234
|
+
event.preventDefault();
|
|
235
|
+
if (!mUser) return;
|
|
236
|
+
if (mNewPassword !== mConfirmPassword) {
|
|
237
|
+
mPwError = i18n.t.users.passwordMismatch;
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
mPwError = "";
|
|
241
|
+
busy = true;
|
|
242
|
+
const { error } = await api.auth.admin.setUserPassword({ userId: mUser.id, newPassword: mNewPassword });
|
|
243
|
+
busy = false;
|
|
244
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
245
|
+
mNewPassword = "";
|
|
246
|
+
mConfirmPassword = "";
|
|
247
|
+
toast.success(i18n.t.users.passwordSet);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
async function resendVerification(): Promise<void> {
|
|
251
|
+
if (!mUser) return;
|
|
252
|
+
busy = true;
|
|
253
|
+
const { error } = await api.auth.sendVerificationEmail({ email: mUser.email, callbackURL: `${location.origin}/admin` });
|
|
254
|
+
busy = false;
|
|
255
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
256
|
+
toast.success(i18n.t.users.verificationSent);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function markVerified(): Promise<void> {
|
|
260
|
+
if (!mUser) return;
|
|
261
|
+
busy = true;
|
|
262
|
+
const { error } = await api.auth.admin.updateUser({ userId: mUser.id, data: { emailVerified: true } });
|
|
263
|
+
busy = false;
|
|
264
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
265
|
+
mUser = { ...mUser, emailVerified: true };
|
|
266
|
+
toast.success(i18n.t.users.markedVerified);
|
|
267
|
+
await load();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function sendResetEmail(): Promise<void> {
|
|
271
|
+
if (!mUser) return;
|
|
272
|
+
busy = true;
|
|
273
|
+
const { error } = await api.auth.requestPasswordReset({ email: mUser.email, redirectTo: `${location.origin}/reset-password` });
|
|
274
|
+
busy = false;
|
|
275
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
276
|
+
toast.success(i18n.t.users.resetEmailSent);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function ban(event: Event): Promise<void> {
|
|
280
|
+
event.preventDefault();
|
|
281
|
+
if (!mUser) return;
|
|
282
|
+
const days = Number(mBanDays);
|
|
283
|
+
busy = true;
|
|
284
|
+
const { error } = await api.auth.admin.banUser({
|
|
285
|
+
userId: mUser.id,
|
|
286
|
+
...(mBanReason ? { banReason: mBanReason } : {}),
|
|
287
|
+
...(days > 0 ? { banExpiresIn: days * 24 * 60 * 60 } : {}),
|
|
288
|
+
});
|
|
289
|
+
busy = false;
|
|
290
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
291
|
+
mUser = { ...mUser, banned: true };
|
|
292
|
+
toast.success(i18n.t.users.userBanned);
|
|
293
|
+
await load();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function unban(): Promise<void> {
|
|
297
|
+
if (!mUser) return;
|
|
298
|
+
busy = true;
|
|
299
|
+
const { error } = await api.auth.admin.unbanUser({ userId: mUser.id });
|
|
300
|
+
busy = false;
|
|
301
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
302
|
+
mUser = { ...mUser, banned: false };
|
|
303
|
+
toast.success(i18n.t.users.userUnbanned);
|
|
304
|
+
await load();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function revokeSession(token: string): Promise<void> {
|
|
308
|
+
busy = true;
|
|
309
|
+
const { error } = await api.auth.admin.revokeUserSession({ sessionToken: token });
|
|
310
|
+
busy = false;
|
|
311
|
+
if (error) return void toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
|
|
312
|
+
toast.success(i18n.t.adminSessions.revoked);
|
|
313
|
+
await loadManagedSessions();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
async function revokeAllSessions(): Promise<void> {
|
|
317
|
+
if (!mUser) return;
|
|
318
|
+
busy = true;
|
|
319
|
+
const { error } = await api.auth.admin.revokeUserSessions({ userId: mUser.id });
|
|
320
|
+
busy = false;
|
|
321
|
+
if (error) return void toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
|
|
322
|
+
toast.success(i18n.t.adminSessions.allRevoked);
|
|
323
|
+
await loadManagedSessions();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function deleteUser(): Promise<void> {
|
|
327
|
+
if (!mUser) return;
|
|
328
|
+
busy = true;
|
|
329
|
+
const { error } = await api.auth.admin.removeUser({ userId: mUser.id });
|
|
330
|
+
busy = false;
|
|
331
|
+
if (error) return void toast.error(error.message ?? i18n.t.users.actionFailed);
|
|
332
|
+
toast.success(i18n.t.users.userDeleted);
|
|
333
|
+
manageOpen = false;
|
|
334
|
+
await load();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
$effect(() => {
|
|
338
|
+
void load();
|
|
339
|
+
});
|
|
340
|
+
</script>
|
|
341
|
+
|
|
342
|
+
<div class="flex flex-col gap-4">
|
|
343
|
+
<div class="flex items-center justify-between gap-2">
|
|
344
|
+
<h1 class="text-2xl font-semibold">{i18n.t.users.title}</h1>
|
|
345
|
+
<Button onclick={() => { createError = ""; createOpen = true; }}><PlusIcon class="size-4" />{i18n.t.users.addUser}</Button>
|
|
346
|
+
</div>
|
|
347
|
+
<TableToolbar
|
|
348
|
+
{filters}
|
|
349
|
+
bind:filterValues
|
|
350
|
+
{searchFields}
|
|
351
|
+
bind:searchField
|
|
352
|
+
bind:search
|
|
353
|
+
filterHeading={i18n.t.toolbar.filter}
|
|
354
|
+
searchHeading={i18n.t.toolbar.search}
|
|
355
|
+
searchButton={i18n.t.toolbar.searchButton}
|
|
356
|
+
onSearch={applySearch}
|
|
357
|
+
/>
|
|
358
|
+
|
|
359
|
+
<DataTable
|
|
360
|
+
{columns}
|
|
361
|
+
rows={filtered}
|
|
362
|
+
getKey={(u) => u.id}
|
|
363
|
+
empty={i18n.t.users.empty}
|
|
364
|
+
bind:sort
|
|
365
|
+
bind:page
|
|
366
|
+
perPage={PAGE_SIZE}
|
|
367
|
+
label={fmt(i18n.t.users.total, { count: filtered.length })}
|
|
368
|
+
>
|
|
369
|
+
{#snippet row(user)}
|
|
370
|
+
<Table.Cell class="font-medium">{user.name}</Table.Cell>
|
|
371
|
+
<Table.Cell class="text-muted-foreground">{user.email}</Table.Cell>
|
|
372
|
+
<Table.Cell><Badge variant={user.role === "admin" ? "default" : "secondary"}>{user.role ?? "user"}</Badge></Table.Cell>
|
|
373
|
+
<Table.Cell>
|
|
374
|
+
<div class="flex flex-wrap gap-1">
|
|
375
|
+
{#if user.banned}<Badge variant="destructive">{i18n.t.users.banned}</Badge>{:else}<Badge variant="outline">{i18n.t.users.active}</Badge>{/if}
|
|
376
|
+
{#if emailVerificationEnabled}
|
|
377
|
+
{#if user.emailVerified}<Badge variant="outline" class="text-green-600 dark:text-green-400">{i18n.t.users.verified}</Badge>{:else}<Badge variant="secondary">{i18n.t.users.unverified}</Badge>{/if}
|
|
378
|
+
{/if}
|
|
379
|
+
</div>
|
|
380
|
+
</Table.Cell>
|
|
381
|
+
<Table.Cell class="text-muted-foreground">{user.createdAt ? formatDateTime(user.createdAt) : "—"}</Table.Cell>
|
|
382
|
+
<Table.Cell>
|
|
383
|
+
<DropdownMenu.Root>
|
|
384
|
+
<DropdownMenu.Trigger>
|
|
385
|
+
{#snippet child({ props })}
|
|
386
|
+
<Button {...props} variant="ghost" size="icon"><EllipsisIcon class="size-4" /></Button>
|
|
387
|
+
{/snippet}
|
|
388
|
+
</DropdownMenu.Trigger>
|
|
389
|
+
<DropdownMenu.Content align="end">
|
|
390
|
+
<DropdownMenu.Item onSelect={() => openManage(user)}>{i18n.t.users.manage}</DropdownMenu.Item>
|
|
391
|
+
<DropdownMenu.Item disabled={user.id === data.currentUserId} onSelect={() => impersonate(user)}>
|
|
392
|
+
{i18n.t.users.impersonate}
|
|
393
|
+
</DropdownMenu.Item>
|
|
394
|
+
</DropdownMenu.Content>
|
|
395
|
+
</DropdownMenu.Root>
|
|
396
|
+
</Table.Cell>
|
|
397
|
+
{/snippet}
|
|
398
|
+
</DataTable>
|
|
399
|
+
</div>
|
|
400
|
+
|
|
401
|
+
<!-- Create user -->
|
|
402
|
+
<Dialog.Root bind:open={createOpen}>
|
|
403
|
+
<Dialog.Content>
|
|
404
|
+
<Dialog.Header><Dialog.Title>{i18n.t.users.createTitle}</Dialog.Title></Dialog.Header>
|
|
405
|
+
<form onsubmit={createUser} class="flex flex-col gap-3">
|
|
406
|
+
<div class="flex flex-col gap-1"><Label for="c-name">{i18n.t.users.name}</Label><Input id="c-name" bind:value={form.name} required /></div>
|
|
407
|
+
<div class="flex flex-col gap-1"><Label for="c-email">{i18n.t.users.email}</Label><Input id="c-email" type="email" bind:value={form.email} required /></div>
|
|
408
|
+
<div class="flex flex-col gap-1"><Label for="c-password">{i18n.t.users.password}</Label><Input id="c-password" type="password" bind:value={form.password} required /></div>
|
|
409
|
+
<div class="flex flex-col gap-1"><Label for="c-confirm">{i18n.t.users.confirmPassword}</Label><Input id="c-confirm" type="password" bind:value={form.confirm} required /></div>
|
|
410
|
+
{#if createError}<p class="text-destructive text-sm">{createError}</p>{/if}
|
|
411
|
+
<div class="flex flex-col gap-1">
|
|
412
|
+
<Label for="c-role">{i18n.t.users.role}</Label>
|
|
413
|
+
<Select.Root type="single" bind:value={form.role}>
|
|
414
|
+
<Select.Trigger id="c-role" class="w-full">{roleLabel(form.role)}</Select.Trigger>
|
|
415
|
+
<Select.Content>
|
|
416
|
+
{#each roleNames as r (r)}<Select.Item value={r}>{roleLabel(r)}</Select.Item>{/each}
|
|
417
|
+
</Select.Content>
|
|
418
|
+
</Select.Root>
|
|
419
|
+
</div>
|
|
420
|
+
{#if emailVerificationEnabled}
|
|
421
|
+
<Label class="flex items-center gap-2"><Checkbox bind:checked={form.sendVerify} />{i18n.t.users.sendVerificationOnCreate}</Label>
|
|
422
|
+
{/if}
|
|
423
|
+
<Dialog.Footer>
|
|
424
|
+
<Button type="button" variant="outline" onclick={() => (createOpen = false)}>{i18n.t.users.cancel}</Button>
|
|
425
|
+
<Button type="submit" disabled={busy}>{i18n.t.users.create}</Button>
|
|
426
|
+
</Dialog.Footer>
|
|
427
|
+
</form>
|
|
428
|
+
</Dialog.Content>
|
|
429
|
+
</Dialog.Root>
|
|
430
|
+
|
|
431
|
+
<!-- Manage user (two-pane) -->
|
|
432
|
+
<Dialog.Root bind:open={manageOpen}>
|
|
433
|
+
<Dialog.Content class="sm:max-w-3xl">
|
|
434
|
+
<Dialog.Header><Dialog.Title class="truncate">{mUser?.email}</Dialog.Title></Dialog.Header>
|
|
435
|
+
<div class="flex flex-col gap-4 sm:flex-row">
|
|
436
|
+
<nav class="flex shrink-0 flex-wrap gap-1 sm:w-40 sm:flex-col">
|
|
437
|
+
{#each sections as key (key)}
|
|
438
|
+
<Button
|
|
439
|
+
variant="ghost"
|
|
440
|
+
onclick={() => (mSection = key)}
|
|
441
|
+
aria-current={mSection === key ? "page" : undefined}
|
|
442
|
+
class="h-auto justify-start px-3 py-2 text-sm font-medium {mSection === key ? 'bg-muted' : 'text-muted-foreground'}"
|
|
443
|
+
>
|
|
444
|
+
{i18n.t.account.nav[key]}
|
|
445
|
+
</Button>
|
|
446
|
+
{/each}
|
|
447
|
+
</nav>
|
|
448
|
+
|
|
449
|
+
<div class="h-[26rem] min-w-0 flex-1 overflow-y-auto">
|
|
450
|
+
{#if mSection === "profile"}
|
|
451
|
+
<form class="flex flex-col gap-3" onsubmit={saveProfile}>
|
|
452
|
+
<div class="flex flex-col gap-1"><Label for="m-name">{i18n.t.users.name}</Label><Input id="m-name" bind:value={mName} required /></div>
|
|
453
|
+
<div class="flex flex-col gap-1"><Label for="m-email">{i18n.t.users.email}</Label><Input id="m-email" type="email" bind:value={mEmail} required /></div>
|
|
454
|
+
<div class="flex flex-col gap-1">
|
|
455
|
+
<Label for="m-role">{i18n.t.users.role}</Label>
|
|
456
|
+
<Select.Root type="single" bind:value={mRole}>
|
|
457
|
+
<Select.Trigger id="m-role" class="w-full">{roleLabel(mRole)}</Select.Trigger>
|
|
458
|
+
<Select.Content>
|
|
459
|
+
{#each roleNames as r (r)}<Select.Item value={r}>{roleLabel(r)}</Select.Item>{/each}
|
|
460
|
+
</Select.Content>
|
|
461
|
+
</Select.Root>
|
|
462
|
+
</div>
|
|
463
|
+
{#if mUser?.banned}<Badge variant="destructive" class="w-fit">{i18n.t.users.banned}</Badge>{/if}
|
|
464
|
+
{#if emailVerificationEnabled}
|
|
465
|
+
<div class="flex items-center gap-2">
|
|
466
|
+
{#if mUser?.emailVerified}<Badge variant="outline" class="text-green-600 dark:text-green-400">{i18n.t.users.verified}</Badge>
|
|
467
|
+
{:else}
|
|
468
|
+
<Badge variant="secondary">{i18n.t.users.unverified}</Badge>
|
|
469
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onclick={resendVerification}>{i18n.t.users.sendVerification}</Button>
|
|
470
|
+
<Button type="button" variant="outline" size="sm" disabled={busy} onclick={markVerified}>{i18n.t.users.markVerified}</Button>
|
|
471
|
+
{/if}
|
|
472
|
+
</div>
|
|
473
|
+
{/if}
|
|
474
|
+
<Button type="submit" class="w-fit" disabled={busy}>{i18n.t.users.save}</Button>
|
|
475
|
+
</form>
|
|
476
|
+
{:else if mSection === "security"}
|
|
477
|
+
<div class="flex flex-col gap-6">
|
|
478
|
+
<form class="flex flex-col gap-3" onsubmit={setPassword}>
|
|
479
|
+
<div class="flex flex-col gap-1"><Label for="m-pw">{i18n.t.users.newPassword}</Label><Input id="m-pw" type="password" bind:value={mNewPassword} required /></div>
|
|
480
|
+
<div class="flex flex-col gap-1"><Label for="m-pw2">{i18n.t.users.confirmNewPassword}</Label><Input id="m-pw2" type="password" bind:value={mConfirmPassword} required /></div>
|
|
481
|
+
{#if mPwError}<p class="text-destructive text-sm">{mPwError}</p>{/if}
|
|
482
|
+
<Button type="submit" class="w-fit" disabled={busy}>{i18n.t.users.setPassword}</Button>
|
|
483
|
+
</form>
|
|
484
|
+
<div class="border-t pt-4">
|
|
485
|
+
<Button type="button" variant="outline" disabled={busy} onclick={sendResetEmail}>{i18n.t.users.sendResetEmail}</Button>
|
|
486
|
+
</div>
|
|
487
|
+
<div class="border-t pt-4">
|
|
488
|
+
{#if mUser?.banned}
|
|
489
|
+
<div class="mb-3 flex flex-col gap-1 text-sm">
|
|
490
|
+
{#if mUser.banReason}<p><span class="text-muted-foreground">{i18n.t.users.banReason}:</span> {mUser.banReason}</p>{/if}
|
|
491
|
+
{#if mUser.banExpires}<p><span class="text-muted-foreground">{i18n.t.users.banExpiry}:</span> {formatDateTime(mUser.banExpires)}</p>{:else}<p class="text-muted-foreground">{i18n.t.users.banPermanent}</p>{/if}
|
|
492
|
+
</div>
|
|
493
|
+
<Button variant="outline" disabled={busy} onclick={unban}>{i18n.t.users.unban}</Button>
|
|
494
|
+
{:else}
|
|
495
|
+
<form class="flex flex-col gap-3" onsubmit={ban}>
|
|
496
|
+
<div class="flex flex-col gap-1"><Label for="m-reason">{i18n.t.users.banReason}</Label><Input id="m-reason" bind:value={mBanReason} /></div>
|
|
497
|
+
<div class="flex flex-col gap-1"><Label for="m-days">{i18n.t.users.banExpiryDays}</Label><Input id="m-days" type="number" min="0" bind:value={mBanDays} /></div>
|
|
498
|
+
<Button type="submit" variant="destructive" class="w-fit" disabled={busy}>{i18n.t.users.ban}</Button>
|
|
499
|
+
</form>
|
|
500
|
+
{/if}
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
{:else if mSection === "sessions"}
|
|
504
|
+
<div class="flex flex-col gap-3">
|
|
505
|
+
<div class="flex justify-end">
|
|
506
|
+
<Button variant="outline" size="sm" disabled={busy || mSessions.length === 0} onclick={revokeAllSessions}>{i18n.t.adminSessions.revokeAll}</Button>
|
|
507
|
+
</div>
|
|
508
|
+
<DataTable
|
|
509
|
+
columns={mSessionsColumns}
|
|
510
|
+
rows={mSessions}
|
|
511
|
+
getKey={(s) => s.id}
|
|
512
|
+
bind:sort={mSessionsSort}
|
|
513
|
+
bind:page={mSessionsPage}
|
|
514
|
+
perPage={PAGE_SIZE}
|
|
515
|
+
label={fmt(i18n.t.adminSessions.total, { count: mSessions.length })}
|
|
516
|
+
empty={i18n.t.adminSessions.empty}
|
|
517
|
+
>
|
|
518
|
+
{#snippet row(s)}
|
|
519
|
+
<Table.Cell class="max-w-40 truncate">{s.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
520
|
+
<Table.Cell class="text-muted-foreground">{s.ipAddress ?? "—"}</Table.Cell>
|
|
521
|
+
<Table.Cell class="text-muted-foreground">{formatDateTime(s.createdAt)}</Table.Cell>
|
|
522
|
+
<Table.Cell><Button variant="ghost" size="sm" disabled={busy} onclick={() => revokeSession(s.token)}>{i18n.t.users.revokeSession}</Button></Table.Cell>
|
|
523
|
+
{/snippet}
|
|
524
|
+
</DataTable>
|
|
525
|
+
</div>
|
|
526
|
+
{:else if mSection === "danger"}
|
|
527
|
+
<div class="border-destructive/50 flex flex-col gap-3 rounded-md border p-4">
|
|
528
|
+
<p class="text-muted-foreground text-sm">{fmt(i18n.t.users.deleteConfirm, { email: mUser?.email ?? "" })}</p>
|
|
529
|
+
{#if mDeleteArmed}
|
|
530
|
+
<div class="flex gap-2">
|
|
531
|
+
<Button variant="outline" onclick={() => (mDeleteArmed = false)}>{i18n.t.users.cancel}</Button>
|
|
532
|
+
<Button variant="destructive" disabled={busy} onclick={deleteUser}>{i18n.t.users.delete}</Button>
|
|
533
|
+
</div>
|
|
534
|
+
{:else}
|
|
535
|
+
<Button
|
|
536
|
+
variant="destructive"
|
|
537
|
+
class="w-fit"
|
|
538
|
+
disabled={mUser?.id === data.currentUserId}
|
|
539
|
+
onclick={() => (mDeleteArmed = true)}
|
|
540
|
+
>
|
|
541
|
+
{i18n.t.users.delete}
|
|
542
|
+
</Button>
|
|
543
|
+
{/if}
|
|
544
|
+
</div>
|
|
545
|
+
{/if}
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
<Dialog.Footer>
|
|
549
|
+
<Button variant="outline" onclick={() => (manageOpen = false)}>{i18n.t.users.close}</Button>
|
|
550
|
+
</Dialog.Footer>
|
|
551
|
+
</Dialog.Content>
|
|
552
|
+
</Dialog.Root>
|
package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/+layout.svelte
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import LanguageSwitch from "$lib/components/language-switch.svelte";
|
|
3
|
+
let { children } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<div class="bg-muted/30 relative flex min-h-svh items-center justify-center p-6">
|
|
7
|
+
<div class="absolute right-4 top-4">
|
|
8
|
+
<LanguageSwitch />
|
|
9
|
+
</div>
|
|
10
|
+
<div class="w-full max-w-sm">
|
|
11
|
+
{@render children()}
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
@@ -0,0 +1,61 @@
|
|
|
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 { getI18n } from "$lib/i18n";
|
|
10
|
+
|
|
11
|
+
const i18n = getI18n();
|
|
12
|
+
let email = $state("");
|
|
13
|
+
let sent = $state(false);
|
|
14
|
+
let error = $state<string | null>(null);
|
|
15
|
+
let loading = $state(false);
|
|
16
|
+
|
|
17
|
+
async function submit(event: SubmitEvent): Promise<void> {
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
loading = true;
|
|
20
|
+
error = null;
|
|
21
|
+
// Absolute URL: the emailed link hits the API to validate, then redirects
|
|
22
|
+
// here (the web app) with a valid token — a relative path would resolve
|
|
23
|
+
// against the API origin.
|
|
24
|
+
const { error: authError } = await api.auth.requestPasswordReset({
|
|
25
|
+
email,
|
|
26
|
+
redirectTo: `${page.url.origin}/reset-password`,
|
|
27
|
+
});
|
|
28
|
+
loading = false;
|
|
29
|
+
if (authError) {
|
|
30
|
+
error = authError.message ?? i18n.t.auth.requestFailed;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
sent = true;
|
|
34
|
+
}
|
|
35
|
+
</script>
|
|
36
|
+
|
|
37
|
+
<Card.Root>
|
|
38
|
+
<Card.Header>
|
|
39
|
+
<Card.Title>{i18n.t.auth.resetTitle}</Card.Title>
|
|
40
|
+
<Card.Description>{i18n.t.auth.resetDesc}</Card.Description>
|
|
41
|
+
</Card.Header>
|
|
42
|
+
<Card.Content>
|
|
43
|
+
{#if sent}
|
|
44
|
+
<Alert.Root><Alert.Description>{i18n.t.auth.resetSent}</Alert.Description></Alert.Root>
|
|
45
|
+
{:else}
|
|
46
|
+
<form class="flex flex-col gap-4" onsubmit={submit}>
|
|
47
|
+
{#if error}
|
|
48
|
+
<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>
|
|
49
|
+
{/if}
|
|
50
|
+
<div class="flex flex-col gap-2">
|
|
51
|
+
<Label for="email">{i18n.t.auth.email}</Label>
|
|
52
|
+
<Input id="email" type="email" bind:value={email} required autocomplete="email" />
|
|
53
|
+
</div>
|
|
54
|
+
<Button type="submit" disabled={loading}>{loading ? i18n.t.auth.sending : i18n.t.auth.sendReset}</Button>
|
|
55
|
+
</form>
|
|
56
|
+
{/if}
|
|
57
|
+
</Card.Content>
|
|
58
|
+
<Card.Footer class="justify-center">
|
|
59
|
+
<a href="/login" class="text-muted-foreground text-sm hover:underline">{i18n.t.auth.backToSignIn}</a>
|
|
60
|
+
</Card.Footer>
|
|
61
|
+
</Card.Root>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PageServerLoad } from "./$types";
|
|
2
|
+
import type { Capabilities } from "@podosoft/podokit-api-client";
|
|
3
|
+
|
|
4
|
+
// Capabilities is public so the login page can offer whichever sign-in methods
|
|
5
|
+
// (magic link, ...) the server actually enabled.
|
|
6
|
+
export const load: PageServerLoad = async ({ fetch }) => {
|
|
7
|
+
try {
|
|
8
|
+
const res = await fetch("/api/account/capabilities");
|
|
9
|
+
if (res.ok) return { capabilities: (await res.json()) as Capabilities };
|
|
10
|
+
} catch {
|
|
11
|
+
/* fall through to defaults */
|
|
12
|
+
}
|
|
13
|
+
return { capabilities: null };
|
|
14
|
+
};
|