@podosoft/podokit 0.2.0 → 0.4.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 +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/fullstack-nest-svelte/apps/api/package.json +29 -11
- package/dist/templates/fullstack-nest-svelte/apps/api/src/app.module.ts +3 -0
- package/dist/templates/fullstack-nest-svelte/apps/api/tsconfig.json +14 -5
- package/dist/templates/fullstack-nest-svelte/apps/web/package.json +8 -6
- 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/alert/alert-action.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert-description.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert-title.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/alert.svelte +43 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/alert/index.ts +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/avatar.svelte +26 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/avatar/index.ts +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/badge/badge.svelte +49 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/badge/index.ts +2 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dialog/index.ts +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/dropdown-menu/index.ts +54 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/index.ts +37 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-content.svelte +45 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-group-heading.svelte +21 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-group.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-item.svelte +40 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-label.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-separator.svelte +18 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select-trigger.svelte +29 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/select/select.svelte +11 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/separator/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/separator/separator.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/skeleton/index.ts +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/sonner/index.ts +1 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/sonner/sonner.svelte +34 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/index.ts +28 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-body.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-caption.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-cell.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-footer.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-head.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-header.svelte +20 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table-row.svelte +15 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/table/table.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/index.ts +18 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +40 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tabs/tabs.svelte +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/index.ts +19 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/templates/fullstack-nest-svelte/apps/web/src/lib/i18n/ko.ts +5 -2
- 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 +43 -4
- 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-gitignore +5 -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 +14 -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 +23 -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 +59 -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 +40 -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 +359 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/+layout.server.ts +14 -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)/admin/+page.svelte +25 -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 +374 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.server.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/audit/+page.svelte +78 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.server.ts +8 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/sessions/+page.svelte +122 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.server.ts +9 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(app)/admin/users/+page.svelte +406 -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 +54 -0
- package/dist/templates/modules/admin-dashboard/files/apps/web/src/routes/(auth)/login/+page.svelte +59 -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 +60 -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/tests/api/audit.api.spec.ts +48 -0
- package/dist/templates/modules/admin-dashboard/files/tests/api/auth.api.spec.ts +43 -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/playwright.config.ts +26 -0
- package/dist/templates/modules/admin-dashboard/files/tests/seed.setup.ts +29 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/account.ui.spec.ts +55 -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/guards.ui.spec.ts +21 -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/sessions.ui.spec.ts +15 -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/users-crud.ui.spec.ts +129 -0
- package/dist/templates/modules/admin-dashboard/files/tests/ui/users.ui.spec.ts +19 -0
- package/dist/templates/modules/admin-dashboard/module.manifest.json +41 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.decorator.ts +9 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.guard.ts +29 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/api-key/api-key.module.ts +9 -0
- package/dist/templates/modules/api-key-auth/files/apps/api/src/machine/machine.controller.ts +14 -0
- package/dist/templates/modules/api-key-auth/module.manifest.json +19 -0
- 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-events.ts +34 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-hook.ts +70 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit-log.entity.ts +46 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.controller.ts +21 -0
- 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 +54 -0
- package/dist/templates/modules/audit-log/files/apps/api/src/audit/audit.module.ts +14 -0
- 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 +31 -0
- package/dist/templates/modules/audit-log/module.manifest.json +39 -0
- package/dist/templates/modules/auth/files/apps/api/src/account/account.controller.ts +30 -0
- package/dist/templates/modules/auth/files/apps/api/src/account/account.module.ts +5 -0
- package/dist/templates/modules/auth/files/apps/api/src/auth/auth.ts +59 -0
- package/dist/templates/modules/auth/files/apps/api/src/health/health.controller.ts +26 -0
- package/dist/templates/modules/auth/module.manifest.json +70 -0
- package/dist/templates/modules/bullmq/module.manifest.json +11 -3
- package/dist/templates/modules/logging/files/apps/api/src/logging/logging.module.ts +29 -0
- package/dist/templates/modules/logging/module.manifest.json +27 -0
- package/dist/templates/modules/rate-limit/files/apps/api/src/rate-limit/rate-limit.module.ts +26 -0
- package/dist/templates/modules/rate-limit/module.manifest.json +25 -0
- package/dist/templates/todo/apps/api/package.json +29 -11
- package/dist/templates/todo/apps/api/src/app.module.ts +3 -0
- package/dist/templates/todo/apps/api/tsconfig.json +14 -5
- package/dist/templates/todo/apps/web/package.json +8 -6
- package/dist/templates/todo/apps/web/src/lib/api.ts +5 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-action.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-description.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert-title.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/alert.svelte +43 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/alert/index.ts +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-badge.svelte +26 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-fallback.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-group-count.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-group.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar-image.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/avatar.svelte +26 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/avatar/index.ts +22 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/badge/badge.svelte +49 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/badge/index.ts +2 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-close.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-content.svelte +48 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-description.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-footer.svelte +32 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-header.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-overlay.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-title.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog-trigger.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/dialog.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dialog/index.ts +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.svelte +16 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.svelte +44 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +31 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.svelte +22 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-group.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-item.svelte +27 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-label.svelte +24 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.svelte +16 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.svelte +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-separator.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-shortcut.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-sub.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/dropdown-menu.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/dropdown-menu/index.ts +54 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/index.ts +37 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-content.svelte +45 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-group-heading.svelte +21 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-group.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-item.svelte +40 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-label.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-scroll-down-button.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-scroll-up-button.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-separator.svelte +18 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select-trigger.svelte +29 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/select/select.svelte +11 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/separator/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/separator/separator.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/skeleton/index.ts +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/skeleton/skeleton.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/sonner/index.ts +1 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/sonner/sonner.svelte +34 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/index.ts +28 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-body.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-caption.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-cell.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-footer.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-head.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-header.svelte +20 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table-row.svelte +15 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/table/table.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/index.ts +18 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-content.svelte +17 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-list.svelte +40 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs-trigger.svelte +23 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tabs/tabs.svelte +19 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/index.ts +19 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-content.svelte +52 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-portal.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-provider.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip-trigger.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/components/ui/tooltip/tooltip.svelte +7 -0
- package/dist/templates/todo/apps/web/src/lib/i18n/ko.ts +5 -2
- 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 +43 -4
- 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-gitignore +5 -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 +14 -0
- package/dist/templates/todo/tests/playwright.config.ts +25 -0
- package/dist/templates/todo/tests/ui/smoke.ui.spec.ts +8 -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/modules/auth-jwt/files/apps/api/src/auth/auth.controller.ts +0 -31
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.module.ts +0 -22
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/auth.service.ts +0 -44
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/login.dto.ts +0 -12
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/dto/register.dto.ts +0 -13
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt-auth.guard.ts +0 -5
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/jwt.strategy.ts +0 -23
- package/dist/templates/modules/auth-jwt/files/apps/api/src/auth/user.entity.ts +0 -16
- package/dist/templates/modules/auth-jwt/files/apps/api/src/migrations/1720200000000-InitUsers.ts +0 -23
- package/dist/templates/modules/auth-jwt/module.manifest.json +0 -31
- 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,129 @@
|
|
|
1
|
+
import { expect, test, type Page } from "@playwright/test";
|
|
2
|
+
import { ready } from "../helpers/hydration";
|
|
3
|
+
|
|
4
|
+
const base = process.env.E2E_BASE_URL ?? "http://localhost:5173";
|
|
5
|
+
|
|
6
|
+
// Seed a throwaway user via the API (admin cookies come from storageState) so
|
|
7
|
+
// the mutating UI tests don't touch the shared admin/user seed accounts.
|
|
8
|
+
async function seedUser(page: Page, email: string): Promise<void> {
|
|
9
|
+
const res = await page.request.post("/api/auth/admin/create-user", {
|
|
10
|
+
headers: { origin: base },
|
|
11
|
+
data: { email, password: "password123", name: "Throwaway", role: "user" },
|
|
12
|
+
});
|
|
13
|
+
expect(res.ok()).toBeTruthy();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Open the Manage two-pane modal for a seeded user.
|
|
17
|
+
async function openManage(page: Page, email: string): Promise<void> {
|
|
18
|
+
await page.getByPlaceholder("Search by email…").fill(email);
|
|
19
|
+
await page.getByRole("row", { name: new RegExp(email) }).getByRole("button").click();
|
|
20
|
+
await page.getByRole("menuitem", { name: "Manage" }).click();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
test("admin can create a user via the dialog @smoke", async ({ page }) => {
|
|
24
|
+
await ready(page, "/admin/users");
|
|
25
|
+
const email = `ui-create-${Date.now()}@example.com`;
|
|
26
|
+
await page.getByRole("button", { name: "Add user" }).click();
|
|
27
|
+
const dialog = page.getByRole("dialog");
|
|
28
|
+
await dialog.getByLabel("Name").fill("Created");
|
|
29
|
+
await dialog.getByLabel("Email").fill(email);
|
|
30
|
+
await dialog.getByLabel("Password", { exact: true }).fill("password123");
|
|
31
|
+
await dialog.getByLabel("Confirm password").fill("password123");
|
|
32
|
+
await dialog.getByRole("button", { name: "Create" }).click();
|
|
33
|
+
await page.getByPlaceholder("Search by email…").fill(email);
|
|
34
|
+
await expect(page.getByRole("cell", { name: email })).toBeVisible();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("create rejects mismatched passwords", async ({ page }) => {
|
|
38
|
+
await ready(page, "/admin/users");
|
|
39
|
+
await page.getByRole("button", { name: "Add user" }).click();
|
|
40
|
+
const dialog = page.getByRole("dialog");
|
|
41
|
+
await dialog.getByLabel("Name").fill("Mismatch");
|
|
42
|
+
await dialog.getByLabel("Email").fill(`ui-mismatch-${Date.now()}@example.com`);
|
|
43
|
+
await dialog.getByLabel("Password", { exact: true }).fill("password123");
|
|
44
|
+
await dialog.getByLabel("Confirm password").fill("different999");
|
|
45
|
+
await dialog.getByRole("button", { name: "Create" }).click();
|
|
46
|
+
await expect(dialog.getByText("Passwords do not match")).toBeVisible();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("manage: edit profile name @smoke", async ({ page }) => {
|
|
50
|
+
await ready(page, "/admin/users");
|
|
51
|
+
const email = `ui-edit-${Date.now()}@example.com`;
|
|
52
|
+
await seedUser(page, email);
|
|
53
|
+
await openManage(page, email);
|
|
54
|
+
const dialog = page.getByRole("dialog");
|
|
55
|
+
await dialog.getByLabel("Name").fill("Edited Name");
|
|
56
|
+
await dialog.getByRole("button", { name: "Save" }).click();
|
|
57
|
+
await expect(page.getByText("User updated")).toBeVisible();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("manage: set a password (and reject mismatch)", async ({ page }) => {
|
|
61
|
+
await ready(page, "/admin/users");
|
|
62
|
+
const email = `ui-setpw-${Date.now()}@example.com`;
|
|
63
|
+
await seedUser(page, email);
|
|
64
|
+
await openManage(page, email);
|
|
65
|
+
const dialog = page.getByRole("dialog");
|
|
66
|
+
await dialog.getByRole("button", { name: "Security" }).click();
|
|
67
|
+
await dialog.getByLabel("New password", { exact: true }).fill("newpass1234");
|
|
68
|
+
await dialog.getByLabel("Confirm new password").fill("different999");
|
|
69
|
+
await dialog.getByRole("button", { name: "Set password" }).click();
|
|
70
|
+
await expect(dialog.getByText("Passwords do not match")).toBeVisible();
|
|
71
|
+
await dialog.getByLabel("Confirm new password").fill("newpass1234");
|
|
72
|
+
await dialog.getByRole("button", { name: "Set password" }).click();
|
|
73
|
+
await expect(page.getByText("Password updated")).toBeVisible();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("manage: ban a user with a reason", async ({ page }) => {
|
|
77
|
+
await ready(page, "/admin/users");
|
|
78
|
+
const email = `ui-ban-${Date.now()}@example.com`;
|
|
79
|
+
await seedUser(page, email);
|
|
80
|
+
await openManage(page, email);
|
|
81
|
+
const dialog = page.getByRole("dialog");
|
|
82
|
+
await dialog.getByRole("button", { name: "Security" }).click();
|
|
83
|
+
await dialog.getByLabel("Reason (optional)").fill("policy violation");
|
|
84
|
+
await dialog.getByRole("button", { name: "Ban", exact: true }).click();
|
|
85
|
+
await expect(page.getByText("User banned", { exact: true })).toBeVisible();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("manage: revoke a user's session", async ({ page, playwright }) => {
|
|
89
|
+
await ready(page, "/admin/users");
|
|
90
|
+
const email = `ui-sess-${Date.now()}@example.com`;
|
|
91
|
+
await seedUser(page, email);
|
|
92
|
+
// create a session for the user in an isolated context (don't touch the admin cookie)
|
|
93
|
+
const uctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: { origin: base } });
|
|
94
|
+
expect((await uctx.post("/api/auth/sign-in/email", { data: { email, password: "password123" } })).ok()).toBeTruthy();
|
|
95
|
+
await uctx.dispose();
|
|
96
|
+
await openManage(page, email);
|
|
97
|
+
const dialog = page.getByRole("dialog");
|
|
98
|
+
await dialog.getByRole("button", { name: "Sessions" }).click();
|
|
99
|
+
await dialog.getByRole("button", { name: "Revoke", exact: true }).first().click();
|
|
100
|
+
await expect(page.getByText("Session revoked")).toBeVisible();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("manage: delete a user from the danger zone", async ({ page }) => {
|
|
104
|
+
await ready(page, "/admin/users");
|
|
105
|
+
const email = `ui-delete-${Date.now()}@example.com`;
|
|
106
|
+
await seedUser(page, email);
|
|
107
|
+
await openManage(page, email);
|
|
108
|
+
const dialog = page.getByRole("dialog");
|
|
109
|
+
await dialog.getByRole("button", { name: "Danger zone" }).click();
|
|
110
|
+
await dialog.getByRole("button", { name: "Delete" }).click(); // arm
|
|
111
|
+
await dialog.getByRole("button", { name: "Delete" }).click(); // confirm
|
|
112
|
+
await expect(page.getByText("User deleted")).toBeVisible();
|
|
113
|
+
await page.getByPlaceholder("Search by email…").fill(email);
|
|
114
|
+
await expect(page.getByRole("cell", { name: email })).toHaveCount(0);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("users list shows pagination beyond one page", async ({ page }) => {
|
|
118
|
+
await ready(page, "/admin/users");
|
|
119
|
+
for (let i = 0; i < 6; i += 1) {
|
|
120
|
+
await page.request.post("/api/auth/admin/create-user", {
|
|
121
|
+
headers: { origin: base },
|
|
122
|
+
data: { email: `pg-${Date.now()}-${i}@example.com`, password: "password123", name: `Pg${i}`, role: "user" },
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
await page.reload();
|
|
126
|
+
const nav = page.getByRole("navigation", { name: "pagination" });
|
|
127
|
+
await expect(nav).toBeVisible();
|
|
128
|
+
await expect(nav.getByRole("button", { name: "Page 2" })).toBeVisible();
|
|
129
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { expect, test } from "@playwright/test";
|
|
2
|
+
|
|
3
|
+
test("admin sees the user list and can search @smoke", async ({ page }) => {
|
|
4
|
+
await page.goto("/admin/users");
|
|
5
|
+
await expect(page.getByRole("heading", { name: "Users" })).toBeVisible();
|
|
6
|
+
await expect(page.getByRole("cell", { name: "admin@example.com" })).toBeVisible();
|
|
7
|
+
await page.getByPlaceholder("Search by email…").fill("user@");
|
|
8
|
+
await expect(page.getByRole("cell", { name: "user@example.com" })).toBeVisible();
|
|
9
|
+
await expect(page.getByRole("cell", { name: "admin@example.com" })).toHaveCount(0);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test("row menu exposes admin actions", async ({ page }) => {
|
|
13
|
+
await page.goto("/admin/users");
|
|
14
|
+
await page.getByPlaceholder("Search by email…").fill("user@example.com");
|
|
15
|
+
const row = page.getByRole("row", { name: /user@example.com/ });
|
|
16
|
+
await row.getByRole("button").click();
|
|
17
|
+
await expect(page.getByRole("menuitem", { name: "Manage" })).toBeVisible();
|
|
18
|
+
await expect(page.getByRole("menuitem", { name: "Impersonate" })).toBeVisible();
|
|
19
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "admin-dashboard",
|
|
3
|
+
"description": "Admin dashboard UI (SvelteKit + shadcn-svelte): login/signup/password-reset, a sidebar shell, and user + session management via the better-auth admin plugin. Requires auth.",
|
|
4
|
+
"requires": [
|
|
5
|
+
"auth"
|
|
6
|
+
],
|
|
7
|
+
"targetApp": "web",
|
|
8
|
+
"env": [
|
|
9
|
+
"# admin-dashboard: emails auto-promoted to the admin role on sign-up (comma-separated)",
|
|
10
|
+
"ADMIN_EMAILS=admin@example.com"
|
|
11
|
+
],
|
|
12
|
+
"inject": [
|
|
13
|
+
{
|
|
14
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
15
|
+
"marker": "// podokit:auth-imports",
|
|
16
|
+
"text": "import { admin } from \"better-auth/plugins\";"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
20
|
+
"marker": "// podokit:auth-plugins",
|
|
21
|
+
"text": "plugins.push(admin({ defaultRole: \"user\", adminRoles: [\"admin\"] }));"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
25
|
+
"marker": "// podokit:auth-email-password",
|
|
26
|
+
"text": "sendResetPassword: async ({ user, url }) => {\n console.log(`[auth] password reset for ${user.email}: ${url}`);\n },"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"file": "apps/api/src/auth/auth.ts",
|
|
30
|
+
"marker": "// podokit:auth-options",
|
|
31
|
+
"text": "trustedOrigins: (process.env.CORS_ORIGIN ?? \"http://localhost:5173\").split(\",\").map((origin) => origin.trim()),\n databaseHooks: {\n user: {\n create: {\n before: async (user) => ({\n data: {\n ...user,\n role: (process.env.ADMIN_EMAILS ?? \"\")\n .split(\",\")\n .map((email) => email.trim().toLowerCase())\n .includes(user.email.toLowerCase())\n ? \"admin\"\n : \"user\",\n },\n }),\n },\n },\n },"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"instructions": [
|
|
35
|
+
"Set ADMIN_EMAILS in .env (comma-separated). Those emails become admins on sign-up.",
|
|
36
|
+
"Re-run the auth migration to add the admin columns: npx @better-auth/cli migrate -y --config apps/api/src/auth/auth.ts",
|
|
37
|
+
"Start the app (npm run dev), open /signup, register an ADMIN_EMAILS address, then sign in.",
|
|
38
|
+
"Admin pages: /dashboard/users (list, ban, set role, revoke sessions). Everyone: /dashboard/sessions, /dashboard/account.",
|
|
39
|
+
"Password reset: the reset link is logged to the API console in dev; wire a real mailer via emailAndPassword.sendResetPassword for production."
|
|
40
|
+
]
|
|
41
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { applyDecorators, UseGuards } from "@nestjs/common";
|
|
2
|
+
import { Public } from "@thallesp/nestjs-better-auth";
|
|
3
|
+
import { ApiKeyGuard } from "./api-key.guard";
|
|
4
|
+
|
|
5
|
+
// Opens a route to API-key holders: skips the user-session guard and
|
|
6
|
+
// requires a valid X-API-Key instead. For machine/service clients.
|
|
7
|
+
export function ApiKeyProtected(): ReturnType<typeof applyDecorators> {
|
|
8
|
+
return applyDecorators(Public(), UseGuards(ApiKeyGuard));
|
|
9
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type CanActivate, type ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
|
|
2
|
+
import { timingSafeEqual } from "node:crypto";
|
|
3
|
+
import type { Request } from "express";
|
|
4
|
+
|
|
5
|
+
// Validates the X-API-Key header against the API_KEYS allowlist.
|
|
6
|
+
@Injectable()
|
|
7
|
+
export class ApiKeyGuard implements CanActivate {
|
|
8
|
+
private readonly keys = (process.env.API_KEYS ?? "")
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((key) => key.trim())
|
|
11
|
+
.filter(Boolean);
|
|
12
|
+
|
|
13
|
+
canActivate(context: ExecutionContext): boolean {
|
|
14
|
+
const request = context.switchToHttp().getRequest<Request>();
|
|
15
|
+
const provided = request.header("x-api-key");
|
|
16
|
+
if (!provided) {
|
|
17
|
+
throw new UnauthorizedException("Missing X-API-Key");
|
|
18
|
+
}
|
|
19
|
+
const supplied = Buffer.from(provided);
|
|
20
|
+
const valid = this.keys.some((key) => {
|
|
21
|
+
const expected = Buffer.from(key);
|
|
22
|
+
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
|
|
23
|
+
});
|
|
24
|
+
if (!valid) {
|
|
25
|
+
throw new UnauthorizedException("Invalid API key");
|
|
26
|
+
}
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { MachineController } from "../machine/machine.controller";
|
|
3
|
+
import { ApiKeyGuard } from "./api-key.guard";
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
controllers: [MachineController],
|
|
7
|
+
providers: [ApiKeyGuard],
|
|
8
|
+
})
|
|
9
|
+
export class ApiKeyModule {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Controller, Get } from "@nestjs/common";
|
|
2
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
3
|
+
import { ApiKeyProtected } from "../api-key/api-key.decorator";
|
|
4
|
+
|
|
5
|
+
@ApiTags("machine")
|
|
6
|
+
@Controller("machine")
|
|
7
|
+
export class MachineController {
|
|
8
|
+
// Reachable with a valid X-API-Key, no user session required.
|
|
9
|
+
@ApiKeyProtected()
|
|
10
|
+
@Get("ping")
|
|
11
|
+
ping(): { ok: true; via: "api-key" } {
|
|
12
|
+
return { ok: true, via: "api-key" };
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "api-key-auth",
|
|
3
|
+
"description": "API-key auth for machine/service clients (X-API-Key), separate from user sessions. Provides an ApiKeyGuard and @ApiKeyProtected() to open a route to key holders. Requires auth.",
|
|
4
|
+
"requires": ["auth"],
|
|
5
|
+
"targetApp": "api",
|
|
6
|
+
"env": [
|
|
7
|
+
"# api keys for machine clients (comma-separated)",
|
|
8
|
+
"API_KEYS=dev-key-please-change"
|
|
9
|
+
],
|
|
10
|
+
"inject": [
|
|
11
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:imports", "text": "import { ApiKeyModule } from \"./api-key/api-key.module\";" },
|
|
12
|
+
{ "file": "apps/api/src/app.module.ts", "marker": "// podokit:module-imports", "text": "ApiKeyModule," }
|
|
13
|
+
],
|
|
14
|
+
"instructions": [
|
|
15
|
+
"Set API_KEYS in .env (comma-separated).",
|
|
16
|
+
"Protect a route for machine clients with @ApiKeyProtected() (bypasses the user session, requires X-API-Key).",
|
|
17
|
+
"Call it: curl -H 'x-api-key: dev-key-please-change' localhost:3000/machine/ping"
|
|
18
|
+
]
|
|
19
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Audit log
|
|
2
|
+
|
|
3
|
+
An append-only trail of **who did what, to which resource, when** — the
|
|
4
|
+
actor / action / target model. Every entry flows through one pipeline
|
|
5
|
+
(`AuditService` → the `audit_logs` table) so they all share one shape.
|
|
6
|
+
|
|
7
|
+
## What gets recorded
|
|
8
|
+
|
|
9
|
+
Nothing is recorded automatically except the built-in **auth/admin actions**
|
|
10
|
+
(login, user create/update/delete, ban, role change, session revoke, ...),
|
|
11
|
+
which the better-auth hook (`audit-hook.ts`) maps to semantic action codes like
|
|
12
|
+
`user.create` and `auth.login`. Everything else is **opt-in** — you decide what
|
|
13
|
+
matters.
|
|
14
|
+
|
|
15
|
+
## Audit your own routes with `@Audit`
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { Audit } from "../audit/audit.decorator";
|
|
19
|
+
|
|
20
|
+
@Post()
|
|
21
|
+
@Audit("todo.create", (_req, todo) => ({ type: "todo", id: todo.id, label: todo.title }))
|
|
22
|
+
create(@Body() dto: CreateTodoDto) {
|
|
23
|
+
return this.todos.create(dto);
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The interceptor resolves the acting user (name + email), attaches the target you
|
|
28
|
+
return, and writes one entry when the handler succeeds.
|
|
29
|
+
|
|
30
|
+
## Record from anywhere in code
|
|
31
|
+
|
|
32
|
+
```ts
|
|
33
|
+
import { recordAudit } from "../audit/audit-events";
|
|
34
|
+
|
|
35
|
+
await recordAudit({
|
|
36
|
+
action: "invoice.paid",
|
|
37
|
+
actorId, actorName, actorEmail,
|
|
38
|
+
targetType: "invoice", targetId: invoice.id, targetLabel: invoice.number,
|
|
39
|
+
metadata: { amount },
|
|
40
|
+
});
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Or inject `AuditService` in a Nest provider and call `audit.record({ ... })`.
|
|
44
|
+
Both never throw — a failed audit write never breaks the request.
|
|
45
|
+
|
|
46
|
+
## Fields (`AuditEntry`)
|
|
47
|
+
|
|
48
|
+
`action` (semantic code), `actorId/actorName/actorEmail` (denormalized so the log
|
|
49
|
+
survives renames/deletes), `targetType/targetId/targetLabel`, `ip`, `metadata`
|
|
50
|
+
(jsonb, free-form), `createdAt`.
|
|
51
|
+
|
|
52
|
+
## Customize
|
|
53
|
+
|
|
54
|
+
- **Auth actions:** edit the `ACTIONS` map in `audit-hook.ts`.
|
|
55
|
+
- **Your routes:** add `@Audit("your.action")` where you want a trail.
|
|
56
|
+
- **Schema:** add columns to `audit-log.entity.ts` + a migration, and keep the
|
|
57
|
+
`AuditEntry` type in `audit-events.ts` in sync.
|
|
58
|
+
- **Reading:** `GET /audit-logs` returns the latest 50 (admin only); extend
|
|
59
|
+
`audit.controller.ts` for filtering/pagination.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// One audit pipeline for the whole app. Every source — the @Audit decorator, the
|
|
2
|
+
// better-auth hook, and your own code — records through here, so all entries
|
|
3
|
+
// share the actor/action/target shape and one write path (audit_logs).
|
|
4
|
+
|
|
5
|
+
export type AuditEntry = {
|
|
6
|
+
// Stable semantic action code, e.g. "user.create", "invoice.paid".
|
|
7
|
+
action: string;
|
|
8
|
+
actorId?: string | null;
|
|
9
|
+
actorName?: string | null;
|
|
10
|
+
actorEmail?: string | null;
|
|
11
|
+
targetType?: string | null;
|
|
12
|
+
targetId?: string | null;
|
|
13
|
+
targetLabel?: string | null;
|
|
14
|
+
ip?: string | null;
|
|
15
|
+
metadata?: Record<string, unknown> | null;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Recorder = (entry: AuditEntry) => Promise<void>;
|
|
19
|
+
let recorder: Recorder | null = null;
|
|
20
|
+
|
|
21
|
+
// AuditService registers the real recorder on startup. The registry lives outside
|
|
22
|
+
// Nest's DI so code that runs outside the container (the better-auth hook,
|
|
23
|
+
// background jobs, ...) can still write through the one pipeline.
|
|
24
|
+
export function setAuditRecorder(fn: Recorder): void {
|
|
25
|
+
recorder = fn;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Record a custom audit event from anywhere. Awaits the write; a no-op until the
|
|
29
|
+
// AuditService has started. Example:
|
|
30
|
+
// await recordAudit({ action: "invoice.paid", actorId, targetLabel: invoice.number,
|
|
31
|
+
// metadata: { amount } });
|
|
32
|
+
export async function recordAudit(entry: AuditEntry): Promise<void> {
|
|
33
|
+
if (recorder) await recorder(entry);
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createAuthMiddleware } from "better-auth/api";
|
|
2
|
+
import { recordAudit } from "./audit-events";
|
|
3
|
+
|
|
4
|
+
// Map mutating auth/admin endpoints to semantic action codes. better-auth is
|
|
5
|
+
// mounted as middleware and bypasses the NestJS interceptor, so these security
|
|
6
|
+
// events are caught here and recorded through the same pipeline.
|
|
7
|
+
const ACTIONS: Record<string, string> = {
|
|
8
|
+
"/sign-in/email": "auth.login",
|
|
9
|
+
"/sign-up/email": "auth.signup",
|
|
10
|
+
"/sign-out": "auth.logout",
|
|
11
|
+
"/change-password": "auth.password_change",
|
|
12
|
+
"/change-email": "auth.email_change",
|
|
13
|
+
"/reset-password": "auth.password_reset",
|
|
14
|
+
"/update-user": "account.update",
|
|
15
|
+
"/delete-user": "account.delete",
|
|
16
|
+
"/revoke-session": "session.revoke",
|
|
17
|
+
"/revoke-sessions": "session.revoke_all",
|
|
18
|
+
"/revoke-other-sessions": "session.revoke_others",
|
|
19
|
+
"/two-factor/enable": "2fa.enable",
|
|
20
|
+
"/two-factor/disable": "2fa.disable",
|
|
21
|
+
"/admin/create-user": "user.create",
|
|
22
|
+
"/admin/update-user": "user.update",
|
|
23
|
+
"/admin/remove-user": "user.delete",
|
|
24
|
+
"/admin/set-role": "user.role_change",
|
|
25
|
+
"/admin/set-user-password": "user.password_set",
|
|
26
|
+
"/admin/ban-user": "user.ban",
|
|
27
|
+
"/admin/unban-user": "user.unban",
|
|
28
|
+
"/admin/revoke-user-session": "user.session_revoke",
|
|
29
|
+
"/admin/revoke-user-sessions": "user.session_revoke_all",
|
|
30
|
+
"/admin/impersonate-user": "user.impersonate",
|
|
31
|
+
"/admin/stop-impersonating": "user.stop_impersonate",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// The top-level `hooks.after` is a single middleware; it runs only on success,
|
|
35
|
+
// so it records completed actions.
|
|
36
|
+
export const auditAfterHook = createAuthMiddleware(async (ctx) => {
|
|
37
|
+
const action = ACTIONS[ctx.path];
|
|
38
|
+
if (!action) return;
|
|
39
|
+
const context = ctx.context as {
|
|
40
|
+
session?: { user?: { id?: string; name?: string; email?: string } };
|
|
41
|
+
newSession?: { user?: { id?: string; name?: string; email?: string } };
|
|
42
|
+
};
|
|
43
|
+
const actor = context.session?.user ?? context.newSession?.user;
|
|
44
|
+
const body = (ctx.body ?? {}) as { email?: string; userId?: string };
|
|
45
|
+
|
|
46
|
+
// Prefer a human-readable target: the email in the body, otherwise resolve the
|
|
47
|
+
// target user's email by id (falls back to the id for already-deleted users).
|
|
48
|
+
const targetId = body.userId ?? null;
|
|
49
|
+
let targetLabel = body.email ?? null;
|
|
50
|
+
if (!targetLabel && targetId) {
|
|
51
|
+
try {
|
|
52
|
+
const adapter = (ctx.context as { internalAdapter?: { findUserById?: (id: string) => Promise<{ email?: string } | null> } })
|
|
53
|
+
.internalAdapter;
|
|
54
|
+
const target = await adapter?.findUserById?.(targetId);
|
|
55
|
+
targetLabel = target?.email ?? targetId;
|
|
56
|
+
} catch {
|
|
57
|
+
targetLabel = targetId;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await recordAudit({
|
|
62
|
+
action,
|
|
63
|
+
actorId: actor?.id ?? null,
|
|
64
|
+
actorName: actor?.name ?? null,
|
|
65
|
+
actorEmail: actor?.email ?? null,
|
|
66
|
+
targetId,
|
|
67
|
+
targetLabel,
|
|
68
|
+
ip: ctx.headers?.get("x-forwarded-for") ?? null,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from "typeorm";
|
|
2
|
+
|
|
3
|
+
// An append-only audit trail. Fields follow the actor / action / target model:
|
|
4
|
+
// who did what, to which resource, when — with human-readable labels stored
|
|
5
|
+
// inline so entries stay meaningful even after users are renamed or deleted.
|
|
6
|
+
@Entity("audit_logs")
|
|
7
|
+
export class AuditLog {
|
|
8
|
+
@PrimaryGeneratedColumn("uuid")
|
|
9
|
+
id!: string;
|
|
10
|
+
|
|
11
|
+
// A stable, semantic action code, e.g. "user.create", "auth.login".
|
|
12
|
+
@Index()
|
|
13
|
+
@Column({ type: "varchar", length: 128 })
|
|
14
|
+
action!: string;
|
|
15
|
+
|
|
16
|
+
// Who performed it (denormalized display fields + id).
|
|
17
|
+
@Column({ type: "varchar", nullable: true })
|
|
18
|
+
actorId!: string | null;
|
|
19
|
+
|
|
20
|
+
@Column({ type: "varchar", nullable: true })
|
|
21
|
+
actorName!: string | null;
|
|
22
|
+
|
|
23
|
+
@Column({ type: "varchar", nullable: true })
|
|
24
|
+
actorEmail!: string | null;
|
|
25
|
+
|
|
26
|
+
// What it acted on (optional).
|
|
27
|
+
@Column({ type: "varchar", nullable: true })
|
|
28
|
+
targetType!: string | null;
|
|
29
|
+
|
|
30
|
+
@Column({ type: "varchar", nullable: true })
|
|
31
|
+
targetId!: string | null;
|
|
32
|
+
|
|
33
|
+
@Column({ type: "varchar", nullable: true })
|
|
34
|
+
targetLabel!: string | null;
|
|
35
|
+
|
|
36
|
+
@Column({ type: "varchar", nullable: true })
|
|
37
|
+
ip!: string | null;
|
|
38
|
+
|
|
39
|
+
// Free-form context for custom events.
|
|
40
|
+
@Column({ type: "jsonb", nullable: true })
|
|
41
|
+
metadata!: Record<string, unknown> | null;
|
|
42
|
+
|
|
43
|
+
@Index()
|
|
44
|
+
@CreateDateColumn({ type: "timestamptz" })
|
|
45
|
+
createdAt!: Date;
|
|
46
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Controller, ForbiddenException, Get } from "@nestjs/common";
|
|
2
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
|
+
import { Session, type UserSession } from "@thallesp/nestjs-better-auth";
|
|
4
|
+
import { ApiTags } from "@nestjs/swagger";
|
|
5
|
+
import { Repository } from "typeorm";
|
|
6
|
+
import { AuditLog } from "./audit-log.entity";
|
|
7
|
+
|
|
8
|
+
@ApiTags("audit")
|
|
9
|
+
@Controller("audit-logs")
|
|
10
|
+
export class AuditController {
|
|
11
|
+
constructor(@InjectRepository(AuditLog) private readonly logs: Repository<AuditLog>) {}
|
|
12
|
+
|
|
13
|
+
// Audit entries are sensitive — admins only.
|
|
14
|
+
@Get()
|
|
15
|
+
recent(@Session() session: UserSession): Promise<AuditLog[]> {
|
|
16
|
+
if ((session.user as { role?: string | null }).role !== "admin") {
|
|
17
|
+
throw new ForbiddenException("Admins only");
|
|
18
|
+
}
|
|
19
|
+
return this.logs.find({ order: { createdAt: "DESC" }, take: 50 });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { SetMetadata } from "@nestjs/common";
|
|
2
|
+
import type { Request } from "express";
|
|
3
|
+
|
|
4
|
+
export const AUDIT_KEY = "podokit:audit";
|
|
5
|
+
|
|
6
|
+
// Optionally derive the target (and extra metadata) from the request + handler
|
|
7
|
+
// result, e.g. (req, result) => ({ type: "todo", id: result.id, label: result.title }).
|
|
8
|
+
export type AuditTarget = {
|
|
9
|
+
type?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
label?: string;
|
|
12
|
+
metadata?: Record<string, unknown>;
|
|
13
|
+
};
|
|
14
|
+
export type AuditMeta = {
|
|
15
|
+
action: string;
|
|
16
|
+
resolve?: (req: Request, result: unknown) => AuditTarget | undefined;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// Mark a controller handler to be audited under a semantic action code:
|
|
20
|
+
// @Audit("todo.create", (_req, todo) => ({ type: "todo", id: todo.id, label: todo.title }))
|
|
21
|
+
export function Audit(action: string, resolve?: AuditMeta["resolve"]): MethodDecorator {
|
|
22
|
+
return SetMetadata(AUDIT_KEY, { action, resolve } satisfies AuditMeta);
|
|
23
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type CallHandler, type ExecutionContext, Injectable, type NestInterceptor } from "@nestjs/common";
|
|
2
|
+
import { Reflector } from "@nestjs/core";
|
|
3
|
+
import { fromNodeHeaders } from "better-auth/node";
|
|
4
|
+
import type { Request } from "express";
|
|
5
|
+
import { type Observable, tap } from "rxjs";
|
|
6
|
+
import { auth } from "../auth/auth";
|
|
7
|
+
import { AUDIT_KEY, type AuditMeta } from "./audit.decorator";
|
|
8
|
+
import { AuditService } from "./audit.service";
|
|
9
|
+
|
|
10
|
+
// Records handlers explicitly marked with @Audit(...). Nothing is logged unless
|
|
11
|
+
// you opt a route in, so the trail stays meaningful (semantic actions, not raw
|
|
12
|
+
// HTTP). Auth/admin actions are recorded separately by the better-auth hook.
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class AuditInterceptor implements NestInterceptor {
|
|
15
|
+
constructor(
|
|
16
|
+
private readonly reflector: Reflector,
|
|
17
|
+
private readonly audit: AuditService,
|
|
18
|
+
) {}
|
|
19
|
+
|
|
20
|
+
intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
|
|
21
|
+
const meta = this.reflector.get<AuditMeta | undefined>(AUDIT_KEY, context.getHandler());
|
|
22
|
+
if (!meta) return next.handle();
|
|
23
|
+
const req = context.switchToHttp().getRequest<Request>();
|
|
24
|
+
return next.handle().pipe(tap((result) => void this.log(meta, req, result)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
private async log(meta: AuditMeta, req: Request, result: unknown): Promise<void> {
|
|
28
|
+
let actorId: string | null = null;
|
|
29
|
+
let actorName: string | null = null;
|
|
30
|
+
let actorEmail: string | null = null;
|
|
31
|
+
try {
|
|
32
|
+
const session = await auth.api.getSession({ headers: fromNodeHeaders(req.headers) });
|
|
33
|
+
if (session?.user) {
|
|
34
|
+
actorId = session.user.id;
|
|
35
|
+
actorName = session.user.name ?? null;
|
|
36
|
+
actorEmail = session.user.email ?? null;
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
/* unauthenticated */
|
|
40
|
+
}
|
|
41
|
+
const target = meta.resolve?.(req, result);
|
|
42
|
+
await this.audit.record({
|
|
43
|
+
action: meta.action,
|
|
44
|
+
actorId,
|
|
45
|
+
actorName,
|
|
46
|
+
actorEmail,
|
|
47
|
+
targetType: target?.type ?? null,
|
|
48
|
+
targetId: target?.id ?? null,
|
|
49
|
+
targetLabel: target?.label ?? null,
|
|
50
|
+
ip: req.ip ?? null,
|
|
51
|
+
metadata: target?.metadata ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
import { APP_INTERCEPTOR } from "@nestjs/core";
|
|
3
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
4
|
+
import { AuditController } from "./audit.controller";
|
|
5
|
+
import { AuditInterceptor } from "./audit.interceptor";
|
|
6
|
+
import { AuditService } from "./audit.service";
|
|
7
|
+
import { AuditLog } from "./audit-log.entity";
|
|
8
|
+
|
|
9
|
+
@Module({
|
|
10
|
+
imports: [TypeOrmModule.forFeature([AuditLog])],
|
|
11
|
+
controllers: [AuditController],
|
|
12
|
+
providers: [AuditService, { provide: APP_INTERCEPTOR, useClass: AuditInterceptor }],
|
|
13
|
+
})
|
|
14
|
+
export class AuditModule {}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Injectable, type OnModuleInit } from "@nestjs/common";
|
|
2
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
3
|
+
import { Repository } from "typeorm";
|
|
4
|
+
import { AuditLog } from "./audit-log.entity";
|
|
5
|
+
import { setAuditRecorder, type AuditEntry } from "./audit-events";
|
|
6
|
+
|
|
7
|
+
// The one place audit entries are written. Inject it to record from Nest code,
|
|
8
|
+
// or call recordAudit() from anywhere else — both end up here.
|
|
9
|
+
@Injectable()
|
|
10
|
+
export class AuditService implements OnModuleInit {
|
|
11
|
+
constructor(@InjectRepository(AuditLog) private readonly logs: Repository<AuditLog>) {}
|
|
12
|
+
|
|
13
|
+
onModuleInit(): void {
|
|
14
|
+
setAuditRecorder((entry) => this.record(entry));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async record(entry: AuditEntry): Promise<void> {
|
|
18
|
+
try {
|
|
19
|
+
await this.logs.save(
|
|
20
|
+
this.logs.create({
|
|
21
|
+
action: entry.action,
|
|
22
|
+
actorId: entry.actorId ?? null,
|
|
23
|
+
actorName: entry.actorName ?? null,
|
|
24
|
+
actorEmail: entry.actorEmail ?? null,
|
|
25
|
+
targetType: entry.targetType ?? null,
|
|
26
|
+
targetId: entry.targetId ?? null,
|
|
27
|
+
targetLabel: entry.targetLabel ?? null,
|
|
28
|
+
ip: entry.ip ?? null,
|
|
29
|
+
metadata: entry.metadata ?? null,
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
} catch {
|
|
33
|
+
// An audit write must never break the request it describes.
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|