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