@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,123 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Badge } from "$lib/components/ui/badge";
|
|
3
|
+
import * as Table from "$lib/components/ui/table";
|
|
4
|
+
import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "$lib/components/data-table.svelte";
|
|
5
|
+
import TableToolbar, { type ToolbarFilter, type ToolbarSearchField } from "$lib/components/table-toolbar.svelte";
|
|
6
|
+
import { toast } from "svelte-sonner";
|
|
7
|
+
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
8
|
+
|
|
9
|
+
const i18n = getI18n();
|
|
10
|
+
type Entry = {
|
|
11
|
+
id: string;
|
|
12
|
+
action: string;
|
|
13
|
+
actorId: string | null;
|
|
14
|
+
actorName: string | null;
|
|
15
|
+
actorEmail: string | null;
|
|
16
|
+
targetLabel: string | null;
|
|
17
|
+
ip: string | null;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
let entries = $state<Entry[]>([]);
|
|
22
|
+
let page = $state(1);
|
|
23
|
+
let sort = $state<SortState | null>({ key: "createdAt", dir: "desc" });
|
|
24
|
+
let search = $state("");
|
|
25
|
+
let appliedSearch = $state("");
|
|
26
|
+
let filterValues = $state<Record<string, string>>({ action: "" });
|
|
27
|
+
let appliedFilters = $state<Record<string, string>>({ action: "" });
|
|
28
|
+
let searchField = $state("actor");
|
|
29
|
+
let appliedSearchField = $state("actor");
|
|
30
|
+
|
|
31
|
+
const columns: DataTableColumn<Entry>[] = [
|
|
32
|
+
{ key: "createdAt", label: i18n.t.audit.when, sortable: true, class: "whitespace-nowrap" },
|
|
33
|
+
{ key: "actor", label: i18n.t.audit.actor, sortable: true, value: (e) => e.actorName ?? e.actorEmail },
|
|
34
|
+
{ key: "action", label: i18n.t.audit.action, sortable: true },
|
|
35
|
+
{ key: "target", label: i18n.t.audit.target, sortable: true, value: (e) => e.targetLabel },
|
|
36
|
+
{ key: "ip", label: i18n.t.audit.ip, sortable: true },
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const actions = $derived([...new Set(entries.map((e) => e.action))].sort());
|
|
40
|
+
const filters = $derived<ToolbarFilter[]>([
|
|
41
|
+
{
|
|
42
|
+
key: "action",
|
|
43
|
+
label: i18n.t.audit.action,
|
|
44
|
+
options: [{ value: "", label: i18n.t.toolbar.all }, ...actions.map((a) => ({ value: a, label: a }))],
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
const searchFields: ToolbarSearchField[] = [
|
|
49
|
+
{ value: "actor", label: i18n.t.audit.actor },
|
|
50
|
+
{ value: "target", label: i18n.t.audit.target },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const filtered = $derived(
|
|
54
|
+
entries.filter((e) => {
|
|
55
|
+
if (appliedFilters.action && e.action !== appliedFilters.action) return false;
|
|
56
|
+
if (appliedSearch) {
|
|
57
|
+
const hay = (
|
|
58
|
+
appliedSearchField === "target"
|
|
59
|
+
? (e.targetLabel ?? "")
|
|
60
|
+
: `${e.actorName ?? ""} ${e.actorEmail ?? ""}`
|
|
61
|
+
).toLowerCase();
|
|
62
|
+
if (!hay.includes(appliedSearch.toLowerCase())) return false;
|
|
63
|
+
}
|
|
64
|
+
return true;
|
|
65
|
+
}),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
async function load(): Promise<void> {
|
|
69
|
+
try {
|
|
70
|
+
const res = await fetch("/api/audit-logs");
|
|
71
|
+
if (!res.ok) throw new Error();
|
|
72
|
+
entries = (await res.json()) as Entry[];
|
|
73
|
+
} catch {
|
|
74
|
+
toast.error(i18n.t.audit.loadFailed);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
$effect(() => {
|
|
79
|
+
void load();
|
|
80
|
+
});
|
|
81
|
+
</script>
|
|
82
|
+
|
|
83
|
+
<div class="flex flex-col gap-4">
|
|
84
|
+
<h1 class="text-2xl font-semibold">{i18n.t.audit.title}</h1>
|
|
85
|
+
|
|
86
|
+
<TableToolbar
|
|
87
|
+
{filters}
|
|
88
|
+
bind:filterValues
|
|
89
|
+
{searchFields}
|
|
90
|
+
bind:searchField
|
|
91
|
+
bind:search
|
|
92
|
+
filterHeading={i18n.t.toolbar.filter}
|
|
93
|
+
searchHeading={i18n.t.toolbar.search}
|
|
94
|
+
searchButton={i18n.t.toolbar.searchButton}
|
|
95
|
+
onSearch={() => { appliedSearch = search; appliedSearchField = searchField; appliedFilters = { ...filterValues }; page = 1; }}
|
|
96
|
+
/>
|
|
97
|
+
|
|
98
|
+
<DataTable
|
|
99
|
+
{columns}
|
|
100
|
+
rows={filtered}
|
|
101
|
+
getKey={(e) => e.id}
|
|
102
|
+
empty={i18n.t.audit.empty}
|
|
103
|
+
bind:sort
|
|
104
|
+
bind:page
|
|
105
|
+
perPage={DEFAULT_PAGE_SIZE}
|
|
106
|
+
label={fmt(i18n.t.audit.total, { count: filtered.length })}
|
|
107
|
+
>
|
|
108
|
+
{#snippet row(e)}
|
|
109
|
+
<Table.Cell class="text-muted-foreground whitespace-nowrap">{formatDateTime(e.createdAt)}</Table.Cell>
|
|
110
|
+
<Table.Cell>
|
|
111
|
+
{#if e.actorName || e.actorEmail}
|
|
112
|
+
<div class="font-medium">{e.actorName ?? e.actorEmail}</div>
|
|
113
|
+
{#if e.actorName && e.actorEmail}<div class="text-muted-foreground text-xs">{e.actorEmail}</div>{/if}
|
|
114
|
+
{:else}
|
|
115
|
+
<span class="text-muted-foreground">—</span>
|
|
116
|
+
{/if}
|
|
117
|
+
</Table.Cell>
|
|
118
|
+
<Table.Cell><Badge variant="secondary" class="font-mono text-xs">{e.action}</Badge></Table.Cell>
|
|
119
|
+
<Table.Cell class="max-w-xs truncate">{e.targetLabel ?? "—"}</Table.Cell>
|
|
120
|
+
<Table.Cell class="text-muted-foreground">{e.ip ?? "—"}</Table.Cell>
|
|
121
|
+
{/snippet}
|
|
122
|
+
</DataTable>
|
|
123
|
+
</div>
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "$lib/components/ui/button";
|
|
3
|
+
import { Input } from "$lib/components/ui/input";
|
|
4
|
+
import { Label } from "$lib/components/ui/label";
|
|
5
|
+
import { Checkbox } from "$lib/components/ui/checkbox";
|
|
6
|
+
import * as Table from "$lib/components/ui/table";
|
|
7
|
+
import * as Dialog from "$lib/components/ui/dialog";
|
|
8
|
+
import * as Select from "$lib/components/ui/select";
|
|
9
|
+
import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "$lib/components/data-table.svelte";
|
|
10
|
+
import { toast } from "svelte-sonner";
|
|
11
|
+
import { api } from "$lib/api";
|
|
12
|
+
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
13
|
+
|
|
14
|
+
const i18n = getI18n();
|
|
15
|
+
|
|
16
|
+
type Org = { id: string; name: string; slug: string; parentOrganizationId?: string | null; createdAt: string | Date };
|
|
17
|
+
let orgs = $state<Org[]>([]);
|
|
18
|
+
let sort = $state<SortState | null>({ key: "createdAt", dir: "desc" });
|
|
19
|
+
let listPage = $state(1);
|
|
20
|
+
const columns: DataTableColumn<Org>[] = [
|
|
21
|
+
{ key: "name", label: i18n.t.organizations.name, sortable: true },
|
|
22
|
+
{ key: "slug", label: i18n.t.organizations.slug, sortable: true },
|
|
23
|
+
{ key: "parent", label: i18n.t.organizations.parent, sortable: true, value: (o) => orgName(o.parentOrganizationId) },
|
|
24
|
+
{ key: "createdAt", label: i18n.t.organizations.created, sortable: true },
|
|
25
|
+
{ key: "actions", label: "", class: "w-10" },
|
|
26
|
+
];
|
|
27
|
+
let busy = $state(false);
|
|
28
|
+
let createOpen = $state(false);
|
|
29
|
+
let form = $state({ name: "", slug: "", parentOrganizationId: "" });
|
|
30
|
+
// Existing users, so managers can be picked at creation (empty = none).
|
|
31
|
+
type AppUser = { id: string; name: string; email: string };
|
|
32
|
+
let users = $state<AppUser[]>([]);
|
|
33
|
+
let managerIds = $state<string[]>([]);
|
|
34
|
+
|
|
35
|
+
const orgName = (id?: string | null): string => (id ? (orgs.find((o) => o.id === id)?.name ?? "—") : "—");
|
|
36
|
+
|
|
37
|
+
async function load(): Promise<void> {
|
|
38
|
+
const { data } = await api.auth.organization.list();
|
|
39
|
+
orgs = (data ?? []) as Org[];
|
|
40
|
+
}
|
|
41
|
+
async function loadUsers(): Promise<void> {
|
|
42
|
+
const { data } = await api.auth.admin.listUsers({ query: { limit: 100 } });
|
|
43
|
+
users = ((data?.users ?? []) as AppUser[]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Derive a slug from the name; fall back to a unique one for non-ASCII names.
|
|
47
|
+
const slugify = (s: string): string =>
|
|
48
|
+
s.toLowerCase().trim().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
49
|
+
const derivedSlug = (name: string): string => slugify(name) || `org-${Date.now().toString(36)}`;
|
|
50
|
+
|
|
51
|
+
function toggleManager(id: string): void {
|
|
52
|
+
managerIds = managerIds.includes(id) ? managerIds.filter((m) => m !== id) : [...managerIds, id];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// parentOrganizationId is a server additionalField not in the client's inferred
|
|
56
|
+
// create type; assert at the call boundary.
|
|
57
|
+
type CreateArg = Parameters<typeof api.auth.organization.create>[0];
|
|
58
|
+
|
|
59
|
+
async function createOrg(event: SubmitEvent): Promise<void> {
|
|
60
|
+
event.preventDefault();
|
|
61
|
+
busy = true;
|
|
62
|
+
const slug = form.slug.trim() || derivedSlug(form.name);
|
|
63
|
+
const { data: org, error } = await api.auth.organization.create({
|
|
64
|
+
name: form.name.trim(),
|
|
65
|
+
slug,
|
|
66
|
+
...(form.parentOrganizationId ? { parentOrganizationId: form.parentOrganizationId } : {}),
|
|
67
|
+
} as CreateArg);
|
|
68
|
+
if (error) {
|
|
69
|
+
busy = false;
|
|
70
|
+
return void toast.error(error.message ?? i18n.t.organizations.actionFailed);
|
|
71
|
+
}
|
|
72
|
+
// Add the chosen existing users as managers (role-based, so any number). Goes
|
|
73
|
+
// through our /account/org-member endpoint (better-auth's addMember is server-only).
|
|
74
|
+
const orgId = (org as { id?: string } | null)?.id;
|
|
75
|
+
if (orgId) {
|
|
76
|
+
for (const userId of managerIds) {
|
|
77
|
+
await api.post("/account/org-member", { organizationId: orgId, userId, role: "manager" }).catch(() => undefined);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
busy = false;
|
|
81
|
+
toast.success(i18n.t.organizations.createdMsg);
|
|
82
|
+
createOpen = false;
|
|
83
|
+
form = { name: "", slug: "", parentOrganizationId: "" };
|
|
84
|
+
managerIds = [];
|
|
85
|
+
await load();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function deleteOrg(organizationId: string): Promise<void> {
|
|
89
|
+
busy = true;
|
|
90
|
+
const { error } = await api.auth.organization.delete({ organizationId });
|
|
91
|
+
busy = false;
|
|
92
|
+
if (error) return void toast.error(error.message ?? i18n.t.organizations.actionFailed);
|
|
93
|
+
toast.success(i18n.t.organizations.deletedMsg);
|
|
94
|
+
await load();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Members & invitations for one organization.
|
|
98
|
+
type Member = { id: string; role: string; user: { email: string; name: string } };
|
|
99
|
+
type Invitation = { id: string; email: string; role: string; status: string };
|
|
100
|
+
let manageOpen = $state(false);
|
|
101
|
+
let manageOrg = $state<Org | null>(null);
|
|
102
|
+
let members = $state<Member[]>([]);
|
|
103
|
+
let invitations = $state<Invitation[]>([]);
|
|
104
|
+
let inviteEmail = $state("");
|
|
105
|
+
|
|
106
|
+
async function openManage(org: Org): Promise<void> {
|
|
107
|
+
manageOrg = org;
|
|
108
|
+
manageOpen = true;
|
|
109
|
+
members = [];
|
|
110
|
+
invitations = [];
|
|
111
|
+
await refreshMembers();
|
|
112
|
+
}
|
|
113
|
+
async function refreshMembers(): Promise<void> {
|
|
114
|
+
if (!manageOrg) return;
|
|
115
|
+
const { data } = await api.auth.organization.getFullOrganization({ query: { organizationId: manageOrg.id } });
|
|
116
|
+
members = ((data as { members?: Member[] } | null)?.members ?? []) as Member[];
|
|
117
|
+
invitations = (((data as { invitations?: Invitation[] } | null)?.invitations ?? []) as Invitation[]).filter((i) => i.status === "pending");
|
|
118
|
+
}
|
|
119
|
+
async function invite(event: SubmitEvent): Promise<void> {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
if (!manageOrg) return;
|
|
122
|
+
busy = true;
|
|
123
|
+
const { error } = await api.auth.organization.inviteMember({ email: inviteEmail.trim(), role: "member", organizationId: manageOrg.id });
|
|
124
|
+
busy = false;
|
|
125
|
+
if (error) return void toast.error(error.message ?? i18n.t.organizations.actionFailed);
|
|
126
|
+
toast.success(i18n.t.organizations.invited);
|
|
127
|
+
inviteEmail = "";
|
|
128
|
+
await refreshMembers();
|
|
129
|
+
}
|
|
130
|
+
async function removeMember(memberIdOrEmail: string): Promise<void> {
|
|
131
|
+
if (!manageOrg) return;
|
|
132
|
+
busy = true;
|
|
133
|
+
const { error } = await api.auth.organization.removeMember({ memberIdOrEmail, organizationId: manageOrg.id });
|
|
134
|
+
busy = false;
|
|
135
|
+
if (error) return void toast.error(error.message ?? i18n.t.organizations.actionFailed);
|
|
136
|
+
await refreshMembers();
|
|
137
|
+
}
|
|
138
|
+
async function cancelInvite(invitationId: string): Promise<void> {
|
|
139
|
+
busy = true;
|
|
140
|
+
const { error } = await api.auth.organization.cancelInvitation({ invitationId });
|
|
141
|
+
busy = false;
|
|
142
|
+
if (error) return void toast.error(error.message ?? i18n.t.organizations.actionFailed);
|
|
143
|
+
await refreshMembers();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
$effect(() => {
|
|
147
|
+
void load();
|
|
148
|
+
void loadUsers();
|
|
149
|
+
});
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<div class="flex flex-col gap-6">
|
|
153
|
+
<div class="flex items-center justify-between gap-2">
|
|
154
|
+
<div>
|
|
155
|
+
<h1 class="text-2xl font-semibold">{i18n.t.organizations.title}</h1>
|
|
156
|
+
<p class="text-muted-foreground text-sm">{i18n.t.organizations.subtitle}</p>
|
|
157
|
+
</div>
|
|
158
|
+
<Button onclick={() => (createOpen = true)}>{i18n.t.organizations.create}</Button>
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<DataTable
|
|
162
|
+
{columns}
|
|
163
|
+
rows={orgs}
|
|
164
|
+
getKey={(o) => o.id}
|
|
165
|
+
bind:sort
|
|
166
|
+
bind:page={listPage}
|
|
167
|
+
perPage={DEFAULT_PAGE_SIZE}
|
|
168
|
+
label={fmt(i18n.t.organizations.total, { count: orgs.length })}
|
|
169
|
+
empty={i18n.t.organizations.empty}
|
|
170
|
+
>
|
|
171
|
+
{#snippet row(org)}
|
|
172
|
+
<Table.Cell class="font-medium">{org.name}</Table.Cell>
|
|
173
|
+
<Table.Cell class="text-muted-foreground font-mono text-xs">{org.slug}</Table.Cell>
|
|
174
|
+
<Table.Cell class="text-muted-foreground">{orgName(org.parentOrganizationId)}</Table.Cell>
|
|
175
|
+
<Table.Cell class="text-muted-foreground">{formatDateTime(org.createdAt)}</Table.Cell>
|
|
176
|
+
<Table.Cell class="flex justify-end gap-1">
|
|
177
|
+
<Button variant="ghost" size="sm" onclick={() => openManage(org)}>{i18n.t.organizations.manage}</Button>
|
|
178
|
+
<Button variant="ghost" size="sm" disabled={busy} onclick={() => deleteOrg(org.id)}>{i18n.t.organizations.delete}</Button>
|
|
179
|
+
</Table.Cell>
|
|
180
|
+
{/snippet}
|
|
181
|
+
</DataTable>
|
|
182
|
+
</div>
|
|
183
|
+
|
|
184
|
+
<Dialog.Root bind:open={manageOpen}>
|
|
185
|
+
<Dialog.Content class="max-w-lg">
|
|
186
|
+
<Dialog.Header>
|
|
187
|
+
<Dialog.Title>{manageOrg?.name}</Dialog.Title>
|
|
188
|
+
<Dialog.Description>{i18n.t.organizations.membersDesc}</Dialog.Description>
|
|
189
|
+
</Dialog.Header>
|
|
190
|
+
<form class="flex items-end gap-2" onsubmit={invite}>
|
|
191
|
+
<div class="flex flex-1 flex-col gap-1">
|
|
192
|
+
<Label for="invite-email">{i18n.t.organizations.inviteEmail}</Label>
|
|
193
|
+
<Input id="invite-email" type="email" bind:value={inviteEmail} placeholder="teammate@example.com" required />
|
|
194
|
+
</div>
|
|
195
|
+
<Button type="submit" disabled={busy || !inviteEmail.trim()}>{i18n.t.organizations.invite}</Button>
|
|
196
|
+
</form>
|
|
197
|
+
<div class="flex flex-col gap-2">
|
|
198
|
+
<p class="text-sm font-medium">{i18n.t.organizations.members}</p>
|
|
199
|
+
{#each members as m (m.id)}
|
|
200
|
+
<div class="flex items-center justify-between gap-2 rounded-md border p-2 text-sm">
|
|
201
|
+
<div><span class="font-medium">{m.user.name || m.user.email}</span> <span class="text-muted-foreground">· {m.role}</span></div>
|
|
202
|
+
{#if m.role !== "owner"}
|
|
203
|
+
<Button variant="ghost" size="sm" disabled={busy} onclick={() => removeMember(m.id)}>{i18n.t.organizations.remove}</Button>
|
|
204
|
+
{/if}
|
|
205
|
+
</div>
|
|
206
|
+
{/each}
|
|
207
|
+
{#if invitations.length}
|
|
208
|
+
<p class="text-sm font-medium">{i18n.t.organizations.pending}</p>
|
|
209
|
+
{#each invitations as inv (inv.id)}
|
|
210
|
+
<div class="flex items-center justify-between gap-2 rounded-md border border-dashed p-2 text-sm">
|
|
211
|
+
<span class="text-muted-foreground">{inv.email} · {inv.role}</span>
|
|
212
|
+
<Button variant="ghost" size="sm" disabled={busy} onclick={() => cancelInvite(inv.id)}>{i18n.t.organizations.cancelInvite}</Button>
|
|
213
|
+
</div>
|
|
214
|
+
{/each}
|
|
215
|
+
{/if}
|
|
216
|
+
</div>
|
|
217
|
+
</Dialog.Content>
|
|
218
|
+
</Dialog.Root>
|
|
219
|
+
|
|
220
|
+
<Dialog.Root bind:open={createOpen}>
|
|
221
|
+
<Dialog.Content>
|
|
222
|
+
<Dialog.Header>
|
|
223
|
+
<Dialog.Title>{i18n.t.organizations.createTitle}</Dialog.Title>
|
|
224
|
+
</Dialog.Header>
|
|
225
|
+
<form class="flex flex-col gap-3" onsubmit={createOrg}>
|
|
226
|
+
<div class="flex flex-col gap-1">
|
|
227
|
+
<Label for="o-name">{i18n.t.organizations.name}</Label>
|
|
228
|
+
<Input id="o-name" bind:value={form.name} required />
|
|
229
|
+
</div>
|
|
230
|
+
<div class="flex flex-col gap-1">
|
|
231
|
+
<Label for="o-slug">{i18n.t.organizations.slug}</Label>
|
|
232
|
+
<Input id="o-slug" bind:value={form.slug} placeholder={form.name ? derivedSlug(form.name) : "acme"} />
|
|
233
|
+
</div>
|
|
234
|
+
<div class="flex flex-col gap-1">
|
|
235
|
+
<Label for="o-parent">{i18n.t.organizations.parent}</Label>
|
|
236
|
+
<Select.Root type="single" bind:value={form.parentOrganizationId}>
|
|
237
|
+
<Select.Trigger id="o-parent" class="w-full">{form.parentOrganizationId ? orgName(form.parentOrganizationId) : i18n.t.organizations.noParent}</Select.Trigger>
|
|
238
|
+
<Select.Content>
|
|
239
|
+
<Select.Item value="">{i18n.t.organizations.noParent}</Select.Item>
|
|
240
|
+
{#each orgs as o (o.id)}<Select.Item value={o.id}>{o.name}</Select.Item>{/each}
|
|
241
|
+
</Select.Content>
|
|
242
|
+
</Select.Root>
|
|
243
|
+
</div>
|
|
244
|
+
{#if users.length}
|
|
245
|
+
<div class="flex flex-col gap-1">
|
|
246
|
+
<Label>{i18n.t.organizations.managers}</Label>
|
|
247
|
+
<p class="text-muted-foreground text-xs">{i18n.t.organizations.managersHint}</p>
|
|
248
|
+
<div class="max-h-40 overflow-y-auto rounded-md border p-2">
|
|
249
|
+
{#each users as u (u.id)}
|
|
250
|
+
<Label class="flex items-center gap-2 py-1 text-sm font-normal">
|
|
251
|
+
<Checkbox checked={managerIds.includes(u.id)} onCheckedChange={() => toggleManager(u.id)} />
|
|
252
|
+
{u.name || u.email} <span class="text-muted-foreground">· {u.email}</span>
|
|
253
|
+
</Label>
|
|
254
|
+
{/each}
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
258
|
+
<Dialog.Footer>
|
|
259
|
+
<Button type="button" variant="outline" onclick={() => (createOpen = false)}>{i18n.t.organizations.cancel}</Button>
|
|
260
|
+
<Button type="submit" disabled={busy || !form.name.trim()}>{i18n.t.organizations.submit}</Button>
|
|
261
|
+
</Dialog.Footer>
|
|
262
|
+
</form>
|
|
263
|
+
</Dialog.Content>
|
|
264
|
+
</Dialog.Root>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Button } from "$lib/components/ui/button";
|
|
3
|
+
import { Badge } from "$lib/components/ui/badge";
|
|
4
|
+
import * as Table from "$lib/components/ui/table";
|
|
5
|
+
import * as DropdownMenu from "$lib/components/ui/dropdown-menu";
|
|
6
|
+
import DataTable, { type DataTableColumn, type SortState, DEFAULT_PAGE_SIZE } from "$lib/components/data-table.svelte";
|
|
7
|
+
import TableToolbar, { type ToolbarSearchField } from "$lib/components/table-toolbar.svelte";
|
|
8
|
+
import EllipsisIcon from "@lucide/svelte/icons/ellipsis";
|
|
9
|
+
import { toast } from "svelte-sonner";
|
|
10
|
+
import { api } from "$lib/api";
|
|
11
|
+
import { getI18n, fmt, formatDateTime } from "$lib/i18n";
|
|
12
|
+
|
|
13
|
+
const i18n = getI18n();
|
|
14
|
+
|
|
15
|
+
// better-auth has no "list all sessions" endpoint, so fan out listUserSessions
|
|
16
|
+
// over a page of users and flatten. Bounded to USER_LIMIT users — adequate for a
|
|
17
|
+
// starter admin view; paginate/scope further for large deployments.
|
|
18
|
+
const USER_LIMIT = 50;
|
|
19
|
+
type Row = {
|
|
20
|
+
id: string;
|
|
21
|
+
token: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
userName: string;
|
|
24
|
+
userEmail: string;
|
|
25
|
+
userAgent?: string | null;
|
|
26
|
+
ipAddress?: string | null;
|
|
27
|
+
createdAt: string | Date;
|
|
28
|
+
expiresAt?: string | Date | null;
|
|
29
|
+
impersonatedBy?: string | null;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let rows = $state<Row[]>([]);
|
|
33
|
+
let page = $state(1);
|
|
34
|
+
// No default sort: keep the fan-out order so the newest session isn't required
|
|
35
|
+
// to sit on page 1. Sorting is available by clicking a column header.
|
|
36
|
+
let sort = $state<SortState | null>(null);
|
|
37
|
+
let loading = $state(false);
|
|
38
|
+
let busy = $state(false);
|
|
39
|
+
let search = $state("");
|
|
40
|
+
let appliedSearch = $state("");
|
|
41
|
+
let searchField = $state("email");
|
|
42
|
+
let appliedSearchField = $state("email");
|
|
43
|
+
|
|
44
|
+
const columns: DataTableColumn<Row>[] = [
|
|
45
|
+
{ key: "user", label: i18n.t.adminSessions.user, sortable: true, value: (r) => r.userName },
|
|
46
|
+
{ key: "device", label: i18n.t.adminSessions.device, sortable: true, value: (r) => r.userAgent },
|
|
47
|
+
{ key: "ip", label: i18n.t.adminSessions.ip, sortable: true, value: (r) => r.ipAddress },
|
|
48
|
+
{ key: "createdAt", label: i18n.t.adminSessions.since, sortable: true },
|
|
49
|
+
{ key: "expiresAt", label: i18n.t.adminSessions.expires, sortable: true, value: (r) => (r.expiresAt ? new Date(r.expiresAt).getTime() : 0) },
|
|
50
|
+
{ key: "actions", label: "", class: "w-10" },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const searchFields: ToolbarSearchField[] = [
|
|
54
|
+
{ value: "email", label: i18n.t.users.email },
|
|
55
|
+
{ value: "name", label: i18n.t.users.name },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const filtered = $derived(
|
|
59
|
+
rows.filter((r) => {
|
|
60
|
+
if (!appliedSearch) return true;
|
|
61
|
+
const hay = (appliedSearchField === "name" ? r.userName : r.userEmail).toLowerCase();
|
|
62
|
+
return hay.includes(appliedSearch.toLowerCase());
|
|
63
|
+
}),
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
async function load(): Promise<void> {
|
|
67
|
+
loading = true;
|
|
68
|
+
const { data, error } = await api.auth.admin.listUsers({ query: { limit: USER_LIMIT, offset: 0 } });
|
|
69
|
+
if (error) {
|
|
70
|
+
loading = false;
|
|
71
|
+
toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const users = (data?.users ?? []) as Array<{ id: string; name: string; email: string }>;
|
|
75
|
+
const perUser = await Promise.all(
|
|
76
|
+
users.map(async (u) => {
|
|
77
|
+
const { data: res } = await api.auth.admin.listUserSessions({ userId: u.id });
|
|
78
|
+
const sessions = (res?.sessions ?? []) as Array<Omit<Row, "userId" | "userName" | "userEmail">>;
|
|
79
|
+
return sessions.map((s) => ({ ...s, userId: u.id, userName: u.name, userEmail: u.email }));
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
rows = perUser.flat();
|
|
83
|
+
loading = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function act(promise: Promise<{ error?: { message?: string } | null }>, ok: string): Promise<void> {
|
|
87
|
+
busy = true;
|
|
88
|
+
const { error } = await promise;
|
|
89
|
+
busy = false;
|
|
90
|
+
if (error) toast.error(error.message ?? i18n.t.adminSessions.loadFailed);
|
|
91
|
+
else {
|
|
92
|
+
toast.success(ok);
|
|
93
|
+
await load();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const revoke = (row: Row) =>
|
|
98
|
+
act(api.auth.admin.revokeUserSession({ sessionToken: row.token }), i18n.t.adminSessions.revoked);
|
|
99
|
+
const revokeAll = (row: Row) =>
|
|
100
|
+
act(api.auth.admin.revokeUserSessions({ userId: row.userId }), i18n.t.adminSessions.allRevoked);
|
|
101
|
+
|
|
102
|
+
$effect(() => {
|
|
103
|
+
void load();
|
|
104
|
+
});
|
|
105
|
+
</script>
|
|
106
|
+
|
|
107
|
+
<div class="flex flex-col gap-4">
|
|
108
|
+
<h1 class="text-2xl font-semibold">{i18n.t.adminSessions.title}</h1>
|
|
109
|
+
|
|
110
|
+
<TableToolbar
|
|
111
|
+
{searchFields}
|
|
112
|
+
bind:searchField
|
|
113
|
+
bind:search
|
|
114
|
+
searchHeading={i18n.t.toolbar.search}
|
|
115
|
+
searchButton={i18n.t.toolbar.searchButton}
|
|
116
|
+
onSearch={() => { appliedSearch = search; appliedSearchField = searchField; page = 1; }}
|
|
117
|
+
/>
|
|
118
|
+
|
|
119
|
+
<DataTable
|
|
120
|
+
{columns}
|
|
121
|
+
rows={filtered}
|
|
122
|
+
getKey={(r) => r.id}
|
|
123
|
+
empty={loading ? "…" : i18n.t.adminSessions.empty}
|
|
124
|
+
bind:sort
|
|
125
|
+
bind:page
|
|
126
|
+
perPage={DEFAULT_PAGE_SIZE}
|
|
127
|
+
label={fmt(i18n.t.adminSessions.total, { count: filtered.length })}
|
|
128
|
+
>
|
|
129
|
+
{#snippet row(r)}
|
|
130
|
+
<Table.Cell>
|
|
131
|
+
<div class="flex items-center gap-2">
|
|
132
|
+
<div class="font-medium">{r.userName}</div>
|
|
133
|
+
{#if r.impersonatedBy}<Badge variant="outline">{i18n.t.adminSessions.impersonated}</Badge>{/if}
|
|
134
|
+
</div>
|
|
135
|
+
<div class="text-muted-foreground text-xs">{r.userEmail}</div>
|
|
136
|
+
</Table.Cell>
|
|
137
|
+
<Table.Cell class="max-w-xs truncate">{r.userAgent ?? i18n.t.adminSessions.unknown}</Table.Cell>
|
|
138
|
+
<Table.Cell class="text-muted-foreground">{r.ipAddress ?? "—"}</Table.Cell>
|
|
139
|
+
<Table.Cell class="text-muted-foreground">{formatDateTime(r.createdAt)}</Table.Cell>
|
|
140
|
+
<Table.Cell class="text-muted-foreground">{r.expiresAt ? formatDateTime(r.expiresAt) : "—"}</Table.Cell>
|
|
141
|
+
<Table.Cell>
|
|
142
|
+
<DropdownMenu.Root>
|
|
143
|
+
<DropdownMenu.Trigger>
|
|
144
|
+
{#snippet child({ props })}
|
|
145
|
+
<Button {...props} variant="ghost" size="icon" disabled={busy}><EllipsisIcon class="size-4" /></Button>
|
|
146
|
+
{/snippet}
|
|
147
|
+
</DropdownMenu.Trigger>
|
|
148
|
+
<DropdownMenu.Content align="end">
|
|
149
|
+
<DropdownMenu.Item onSelect={() => revoke(r)}>{i18n.t.adminSessions.revoke}</DropdownMenu.Item>
|
|
150
|
+
<DropdownMenu.Item onSelect={() => revokeAll(r)}>{i18n.t.adminSessions.revokeAll}</DropdownMenu.Item>
|
|
151
|
+
</DropdownMenu.Content>
|
|
152
|
+
</DropdownMenu.Root>
|
|
153
|
+
</Table.Cell>
|
|
154
|
+
{/snippet}
|
|
155
|
+
</DataTable>
|
|
156
|
+
</div>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { PageServerLoad } from "./$types";
|
|
2
|
+
import { requireAdmin } from "$lib/server/guards";
|
|
3
|
+
|
|
4
|
+
// Non-secret view of the DB-backed auth config (OAuth providers, SMTP, server
|
|
5
|
+
// toggles). Secrets are never sent — only a `hasSecret` flag per credential.
|
|
6
|
+
// `social` is dynamic: one entry per configured provider; `catalog` lists every
|
|
7
|
+
// provider that can be added.
|
|
8
|
+
export type SocialProviderView = { id: string; enabled: boolean; clientId: string; redirectURI: string; hasSecret: boolean };
|
|
9
|
+
export type AuthConfigView = {
|
|
10
|
+
social: Record<string, SocialProviderView>;
|
|
11
|
+
catalog: ReadonlyArray<{ id: string; label: string }>;
|
|
12
|
+
smtp: { enabled: boolean; host: string; port: number; secure: boolean; user: string; from: string; hasSecret: boolean };
|
|
13
|
+
server: { requireEmailVerification: boolean; allowDelete: boolean; hibp: boolean; auditLog: boolean };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
|
17
|
+
requireAdmin(locals.user);
|
|
18
|
+
let authConfig: AuthConfigView | null = null;
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch("/api/account/auth-config");
|
|
21
|
+
if (res.ok) authConfig = (await res.json()) as AuthConfigView;
|
|
22
|
+
} catch {
|
|
23
|
+
/* leave null — the page shows the DB-config sections as unavailable */
|
|
24
|
+
}
|
|
25
|
+
return { authConfig };
|
|
26
|
+
};
|