@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,9 @@
1
+ import type { Page } from "@playwright/test";
2
+
3
+ // SvelteKit dev hydration can attach event handlers slightly after load;
4
+ // wait for the page to settle before driving client-only interactions.
5
+ export async function ready(page: Page, path: string): Promise<void> {
6
+ await page.goto(path);
7
+ await page.waitForLoadState("load");
8
+ await page.waitForTimeout(400);
9
+ }
@@ -0,0 +1,49 @@
1
+ // Helpers for the local Mailpit catcher (docker-compose / CI service). Tests that
2
+ // exercise email flows skip when it isn't reachable (see MAILPIT_URL).
3
+ const MAILPIT = process.env.MAILPIT_URL ?? "http://localhost:8025";
4
+
5
+ export async function mailpitReachable(): Promise<boolean> {
6
+ try {
7
+ return (await fetch(`${MAILPIT}/readyz`)).ok;
8
+ } catch {
9
+ return false;
10
+ }
11
+ }
12
+
13
+ export async function clearMailpit(): Promise<void> {
14
+ try {
15
+ await fetch(`${MAILPIT}/api/v1/messages`, { method: "DELETE" });
16
+ } catch {
17
+ /* ignore */
18
+ }
19
+ }
20
+
21
+ type MailpitList = { messages: Array<{ ID: string; To: Array<{ Address: string }> }> };
22
+ type MailpitMessage = { Text?: string; HTML?: string };
23
+
24
+ // Poll for the newest message addressed to `to` and return the first match of
25
+ // `pattern` in its text/HTML body.
26
+ async function waitForMatch(to: string, pattern: RegExp, label: string, timeoutMs: number): Promise<string> {
27
+ const deadline = Date.now() + timeoutMs;
28
+ while (Date.now() < deadline) {
29
+ const list = (await (await fetch(`${MAILPIT}/api/v1/messages`)).json()) as MailpitList;
30
+ const msg = list.messages?.find((m) => m.To?.some((t) => t.Address === to));
31
+ if (msg) {
32
+ const full = (await (await fetch(`${MAILPIT}/api/v1/message/${msg.ID}`)).json()) as MailpitMessage;
33
+ const match = `${full.Text ?? ""} ${full.HTML ?? ""}`.match(pattern);
34
+ if (match) return match[0];
35
+ }
36
+ await new Promise((r) => setTimeout(r, 300));
37
+ }
38
+ throw new Error(`no email with ${label} to ${to} within ${timeoutMs}ms`);
39
+ }
40
+
41
+ /** The first link in the newest email addressed to `to`. */
42
+ export function waitForLink(to: string, timeoutMs = 10000): Promise<string> {
43
+ return waitForMatch(to, /https?:\/\/[^\s"<>]+/, "a link", timeoutMs);
44
+ }
45
+
46
+ /** The one-time numeric code in the newest email addressed to `to`. */
47
+ export function waitForOtp(to: string, timeoutMs = 10000): Promise<string> {
48
+ return waitForMatch(to, /\b\d{4,8}\b/, "a one-time code", timeoutMs);
49
+ }
@@ -0,0 +1,35 @@
1
+ // Read-back helper for the dev SMS sink (the "Mailpit for SMS"). The app posts
2
+ // outgoing SMS to the sink via SMS_WEBHOOK_URL; tests poll it here to obtain the
3
+ // delivered code, so the phone-number flow is verified for real (not just "sent").
4
+ const SINK = process.env.SMS_SINK_URL ?? "http://localhost:8095";
5
+
6
+ type SinkMessage = { to: string; body: string; receivedAt: string };
7
+
8
+ export async function smsSinkReachable(): Promise<boolean> {
9
+ try {
10
+ const res = await fetch(`${SINK}/readyz`);
11
+ return res.ok;
12
+ } catch {
13
+ return false;
14
+ }
15
+ }
16
+
17
+ export async function clearSms(): Promise<void> {
18
+ await fetch(`${SINK}/messages`, { method: "DELETE" }).catch(() => undefined);
19
+ }
20
+
21
+ /** Poll the sink until an SMS to `to` arrives, then return the first 4–8 digit code. */
22
+ export async function waitForSmsOtp(to: string, timeoutMs = 10_000): Promise<string> {
23
+ const deadline = Date.now() + timeoutMs;
24
+ while (Date.now() < deadline) {
25
+ const res = await fetch(`${SINK}/messages`).catch(() => null);
26
+ if (res?.ok) {
27
+ const messages = (await res.json()) as SinkMessage[];
28
+ const match = messages.find((m) => m.to === to);
29
+ const code = match?.body.match(/\b\d{4,8}\b/)?.[0];
30
+ if (code) return code;
31
+ }
32
+ await new Promise((r) => setTimeout(r, 250));
33
+ }
34
+ throw new Error(`No SMS OTP for ${to} within ${timeoutMs}ms`);
35
+ }
@@ -0,0 +1,9 @@
1
+ import { URI, type TOTP } from "otpauth";
2
+
3
+ /** Compute the current 6-digit code from an otpauth:// URI (the `totpURI` that
4
+ * two-factor/enable returns), the same way an authenticator app would. Lets
5
+ * tests complete the real 2FA verification flow. */
6
+ export function totpCode(otpauthUri: string): string {
7
+ const totp = URI.parse(otpauthUri) as TOTP;
8
+ return totp.generate();
9
+ }
@@ -0,0 +1,26 @@
1
+ import { defineConfig, devices } from "@playwright/test";
2
+
3
+ // admin-dashboard overlay: adds a `setup` project that seeds admin/user sessions
4
+ // (storageState) which the `ui` project reuses. Serial + single worker because
5
+ // tests share one backend/DB. Runs against a live stack on E2E_BASE_URL.
6
+ const baseURL = process.env.E2E_BASE_URL ?? "http://localhost:5001";
7
+
8
+ export default defineConfig({
9
+ testDir: ".",
10
+ fullyParallel: false,
11
+ forbidOnly: !!process.env.CI,
12
+ retries: process.env.CI ? 2 : 0,
13
+ workers: 1,
14
+ reporter: [["list"], ["html", { open: "never" }]],
15
+ use: { baseURL, trace: "on-first-retry", screenshot: "only-on-failure" },
16
+ projects: [
17
+ { name: "api", testMatch: /.*\.api\.spec\.ts/, dependencies: ["setup"] },
18
+ { name: "setup", testMatch: /.*\.setup\.ts/ },
19
+ {
20
+ name: "ui",
21
+ testMatch: /.*\.ui\.spec\.ts/,
22
+ dependencies: ["setup"],
23
+ use: { ...devices["Desktop Chrome"], storageState: "playwright/.auth/admin.json" },
24
+ },
25
+ ],
26
+ });
@@ -0,0 +1,40 @@
1
+ import { test as setup, expect } from "@playwright/test";
2
+ import { ADMIN, USER, adminState, userState, type Account } from "./helpers/accounts";
3
+
4
+ const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
5
+
6
+ // Seed a session via the API (reliable, no UI hydration races) and save its
7
+ // cookies as storageState for the browser `ui` project to reuse.
8
+ async function seedSession(
9
+ playwright: import("@playwright/test").PlaywrightWorkerArgs["playwright"],
10
+ account: Account,
11
+ path: string,
12
+ ): Promise<void> {
13
+ const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: { origin: base } });
14
+ await ctx.post("/api/auth/sign-up/email", { data: account }).catch(() => undefined); // idempotent
15
+ const res = await ctx.post("/api/auth/sign-in/email", {
16
+ data: { email: account.email, password: account.password },
17
+ });
18
+ expect(res.ok(), `sign-in ${account.email}`).toBeTruthy();
19
+ await ctx.storageState({ path });
20
+ await ctx.dispose();
21
+ }
22
+
23
+ setup("seed admin session", async ({ playwright }) => {
24
+ await seedSession(playwright, ADMIN, adminState);
25
+ });
26
+
27
+ // Feature flags are DB-backed (seeded by the app_setting migration). phoneNumber
28
+ // ships off (needs an SMS provider); turn it on here so its specs run — this also
29
+ // exercises the admin settings API on every suite run.
30
+ setup("enable optional features", async ({ playwright }) => {
31
+ const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: { origin: base } });
32
+ await ctx.post("/api/auth/sign-in/email", { data: { email: ADMIN.email, password: ADMIN.password } });
33
+ const res = await ctx.put("/api/account/settings", { data: { phoneNumber: true } });
34
+ expect(res.ok(), "enable optional features via settings").toBeTruthy();
35
+ await ctx.dispose();
36
+ });
37
+
38
+ setup("seed user session", async ({ playwright }) => {
39
+ await seedSession(playwright, USER, userState);
40
+ });
@@ -0,0 +1,126 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ready } from "../helpers/hydration";
3
+
4
+ // admin storageState (project default)
5
+ test("account opens on the profile section @smoke", async ({ page }) => {
6
+ await ready(page, "/admin/account");
7
+ await expect(page.getByRole("heading", { name: "Account" })).toBeVisible();
8
+ await expect(page.getByLabel("Name", { exact: true })).toBeVisible();
9
+ await expect(page.getByLabel("Email")).toHaveValue("admin@example.com");
10
+ // save is enabled only once the name changes
11
+ await expect(page.getByRole("button", { name: "Save changes" })).toBeDisabled();
12
+ await page.getByLabel("Name", { exact: true }).fill("Admin Renamed");
13
+ await expect(page.getByRole("button", { name: "Save changes" })).toBeEnabled();
14
+ });
15
+
16
+ test("account exposes a change-email action when the address is edited @smoke", async ({ page }) => {
17
+ await ready(page, "/admin/account");
18
+ const email = page.getByLabel("Email");
19
+ await expect(email).toHaveValue("admin@example.com");
20
+ // no action until the address actually changes
21
+ await expect(page.getByRole("button", { name: "Change email" })).toHaveCount(0);
22
+ await email.fill("admin+changed@example.com");
23
+ await expect(page.getByRole("button", { name: "Change email" })).toBeVisible();
24
+ // revert without submitting — don't mutate the shared admin session
25
+ await email.fill("admin@example.com");
26
+ await expect(page.getByRole("button", { name: "Change email" })).toHaveCount(0);
27
+ });
28
+
29
+ test("account shows a username field when enabled @smoke", async ({ page }) => {
30
+ await ready(page, "/admin/account");
31
+ const username = page.getByLabel("Username", { exact: true });
32
+ test.skip((await username.count()) === 0, "username not enabled");
33
+ await expect(username).toBeVisible();
34
+ });
35
+
36
+ test("account can create and revoke an API key when enabled @smoke", async ({ page }) => {
37
+ await ready(page, "/admin/account");
38
+ const nav = page.getByRole("button", { name: "API keys" });
39
+ test.skip((await nav.count()) === 0, "api keys not enabled");
40
+ await nav.click();
41
+ const name = `ui-${Date.now()}`;
42
+ await page.getByLabel("Name", { exact: true }).fill(name);
43
+ await page.getByRole("button", { name: "Create key" }).click();
44
+ // the full key is shown once in a dialog
45
+ await expect(page.getByText("API key created")).toBeVisible();
46
+ await page.getByRole("button", { name: "Done" }).click();
47
+ // it now appears in the list; revoke it to clean up
48
+ const row = page.getByRole("row", { name: new RegExp(name) });
49
+ await expect(row).toBeVisible();
50
+ await row.getByRole("button", { name: "Revoke" }).click();
51
+ await expect(page.getByText("Key revoked")).toBeVisible();
52
+ });
53
+
54
+ test("account can register and remove a passkey when enabled @smoke", async ({ page }) => {
55
+ await ready(page, "/admin/account");
56
+ await page.getByRole("button", { name: "Security" }).click();
57
+ const addBtn = page.getByRole("button", { name: "Add passkey" });
58
+ test.skip((await addBtn.count()) === 0, "passkeys not enabled");
59
+ // A virtual authenticator lets the WebAuthn ceremony resolve without hardware.
60
+ const client = await page.context().newCDPSession(page);
61
+ await client.send("WebAuthn.enable");
62
+ await client.send("WebAuthn.addVirtualAuthenticator", {
63
+ options: { protocol: "ctap2", transport: "internal", hasResidentKey: true, hasUserVerification: true, isUserVerified: true, automaticPresenceSimulation: true },
64
+ });
65
+ const pkName = `pk-${Date.now()}`;
66
+ await page.getByLabel("Name", { exact: true }).fill(pkName);
67
+ await addBtn.click();
68
+ await expect(page.getByText("Passkey added")).toBeVisible();
69
+ const row = page.getByRole("row", { name: new RegExp(pkName) });
70
+ await expect(row).toBeVisible();
71
+ // remove it so the shared admin account keeps no leftover credential
72
+ await row.getByRole("button", { name: "Remove" }).click();
73
+ await expect(page.getByText("Passkey removed")).toBeVisible();
74
+ });
75
+
76
+ test("account shows a phone field when enabled @smoke", async ({ page }) => {
77
+ await ready(page, "/admin/account");
78
+ const phone = page.getByLabel("Phone number", { exact: true });
79
+ test.skip((await phone.count()) === 0, "phone number not enabled");
80
+ await expect(phone).toBeVisible();
81
+ });
82
+
83
+ test("admin can update their profile name", async ({ page }) => {
84
+ await ready(page, "/admin/account");
85
+ await page.getByLabel("Name", { exact: true }).fill("Admin Renamed");
86
+ await page.getByRole("button", { name: "Save changes" }).click();
87
+ await expect(page.getByText("Profile updated")).toBeVisible();
88
+ });
89
+
90
+ test("account security and sessions sub-navigation", async ({ page }) => {
91
+ await ready(page, "/admin/account");
92
+ await page.getByRole("button", { name: "Security" }).click();
93
+ await expect(page.getByLabel("Current password")).toBeVisible();
94
+ await page.getByRole("button", { name: "Sessions" }).click();
95
+ await expect(page.getByText("Active sessions")).toBeVisible();
96
+ await expect(page.getByText("Current", { exact: true })).toBeVisible();
97
+ await expect(page.getByRole("button", { name: "Sign out other sessions" })).toBeVisible();
98
+ });
99
+
100
+ test("account nav shows the core sections", async ({ page }) => {
101
+ await ready(page, "/admin/account");
102
+ await expect(page.getByRole("button", { name: "Profile" })).toBeVisible();
103
+ await expect(page.getByRole("button", { name: "Security" })).toBeVisible();
104
+ await expect(page.getByRole("button", { name: "Sessions" })).toBeVisible();
105
+ });
106
+
107
+ test("two-factor setup shows a scannable QR code when enabled @smoke", async ({ page }) => {
108
+ await ready(page, "/admin/account");
109
+ await page.getByRole("button", { name: "Security" }).click();
110
+ const heading = page.getByText("Two-factor authentication");
111
+ test.skip((await heading.count()) === 0, "two-factor not enabled");
112
+ await expect(page.getByRole("button", { name: "Enable", exact: true })).toBeVisible();
113
+ // Start setup (password + Enable) and confirm the QR renders. Stop before
114
+ // verifying so 2FA stays inactive and the shared admin session keeps working.
115
+ await page.locator("#tf-on-pw").fill("Podokit3e-Str0ng!pw");
116
+ await page.getByRole("button", { name: "Enable", exact: true }).click();
117
+ await expect(page.getByRole("img", { name: "TOTP QR code" })).toBeVisible();
118
+ });
119
+
120
+ test("danger zone offers account deletion when enabled", async ({ page }) => {
121
+ await ready(page, "/admin/account");
122
+ const danger = page.getByRole("button", { name: "Danger zone" });
123
+ test.skip((await danger.count()) === 0, "account deletion not enabled");
124
+ await danger.click();
125
+ await expect(page.getByRole("button", { name: "Delete account" })).toBeVisible();
126
+ });
@@ -0,0 +1,16 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ready } from "../helpers/hydration";
3
+
4
+ // admin storageState (project default). The Audit log page is gated by the
5
+ // audit-log module; skip when it isn't installed in the generated app.
6
+ test("admin can view audit entries when the module is installed", async ({ page }) => {
7
+ await ready(page, "/admin");
8
+ const auditNav = page.getByRole("link", { name: "Audit log" });
9
+ test.skip((await auditNav.count()) === 0, "audit-log module not installed");
10
+
11
+ await auditNav.click();
12
+ await expect(page).toHaveURL(/\/admin\/audit/);
13
+ await expect(page.getByRole("heading", { name: "Audit log" })).toBeVisible();
14
+ // seed sign-ins are audited via the better-auth hook, so an action row is present
15
+ await expect(page.getByText("auth.login").first()).toBeVisible();
16
+ });
@@ -0,0 +1,45 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { anonState, ADMIN } from "../helpers/accounts";
3
+ import { ready } from "../helpers/hydration";
4
+
5
+ test.use({ storageState: anonState });
6
+
7
+ test("login page renders @smoke", async ({ page }) => {
8
+ await page.goto("/login");
9
+ await expect(page.getByLabel("Email")).toBeVisible();
10
+ await expect(page.getByLabel("Password")).toBeVisible();
11
+ await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible();
12
+ await expect(page.getByRole("link", { name: "Sign up" })).toBeVisible();
13
+ await expect(page.getByRole("link", { name: "Forgot?" })).toBeVisible();
14
+ });
15
+
16
+ test("invalid credentials show an error", async ({ page }) => {
17
+ await ready(page, "/login");
18
+ await page.getByLabel("Email").fill("nobody@example.com");
19
+ await page.getByLabel("Password").fill("wrong-password");
20
+ await page.getByRole("button", { name: "Sign in", exact: true }).click();
21
+ await expect(page.getByRole("alert")).toBeVisible();
22
+ await expect(page).toHaveURL(/\/login/);
23
+ });
24
+
25
+ test("signup page renders", async ({ page }) => {
26
+ await page.goto("/signup");
27
+ await expect(page.getByLabel("Name")).toBeVisible();
28
+ await expect(page.getByRole("button", { name: "Create account" })).toBeVisible();
29
+ });
30
+
31
+ test("forgot-password shows a sent confirmation", async ({ page }) => {
32
+ await ready(page, "/forgot-password");
33
+ // Retry the submit to absorb any first-load hydration timing.
34
+ await expect(async () => {
35
+ await page.getByLabel("Email").fill(ADMIN.email);
36
+ await page.getByRole("button", { name: "Send reset link" }).click();
37
+ await expect(page.getByRole("alert")).toBeVisible({ timeout: 2000 });
38
+ }).toPass({ timeout: 15000 });
39
+ });
40
+
41
+ test("reset-password without a token is disabled", async ({ page }) => {
42
+ await page.goto("/reset-password");
43
+ await expect(page.getByRole("alert")).toBeVisible();
44
+ await expect(page.getByRole("button", { name: "Update password" })).toBeDisabled();
45
+ });
@@ -0,0 +1,25 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ADMIN } from "../helpers/accounts";
3
+ import { ready } from "../helpers/hydration";
4
+
5
+ // admin storageState (project default)
6
+ test("overview shows the signed-in admin @smoke", async ({ page }) => {
7
+ await page.goto("/admin");
8
+ await expect(page.getByRole("heading", { name: "Dashboard" })).toBeVisible();
9
+ // email appears in the sidebar user menu too — scope to the main content.
10
+ await expect(page.getByRole("main").getByText(ADMIN.email)).toBeVisible();
11
+ });
12
+
13
+ test("sidebar navigates to sessions and account", async ({ page }) => {
14
+ await ready(page, "/admin");
15
+ await page.getByRole("link", { name: "Sessions" }).click();
16
+ await expect(page).toHaveURL(/\/admin\/sessions/);
17
+ await expect(page.getByRole("heading", { name: "User sessions" })).toBeVisible();
18
+ // Account is reached from the user menu at the bottom of the sidebar, not the nav
19
+ await expect(page.getByRole("link", { name: "Account" })).toHaveCount(0);
20
+ await page.getByRole("button", { name: /admin@example.com/ }).click();
21
+ await page.getByRole("menuitem", { name: "Account" }).click();
22
+ await expect(page).toHaveURL(/\/admin\/account/);
23
+ await expect(page.getByRole("heading", { name: "Account" })).toBeVisible();
24
+ await expect(page.getByLabel("Name", { exact: true })).toBeVisible(); // profile section (exact: "Username" also contains "Name")
25
+ });
@@ -0,0 +1,32 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ready } from "../helpers/hydration";
3
+
4
+ // Sorting via the shared DataTable. Users is server-sorted (manual), audit is
5
+ // client-sorted — both drive the same clickable headers + aria-sort.
6
+ test("users table sorts by a column header (server) @smoke", async ({ page }) => {
7
+ await ready(page, "/admin/users");
8
+ const header = page.getByRole("columnheader", { name: /Name/ });
9
+ // Click the sort button *inside* the header — a bare name match collides with
10
+ // the sidebar user button (a "Renamed" account contains "name").
11
+ const sortByName = header.getByRole("button");
12
+ const firstName = () => page.getByRole("row").nth(1).getByRole("cell").first().innerText();
13
+
14
+ await sortByName.click();
15
+ await expect(header).toHaveAttribute("aria-sort", "ascending");
16
+ await page.waitForTimeout(400); // server refetch
17
+ const asc = await firstName();
18
+
19
+ await sortByName.click();
20
+ await expect(header).toHaveAttribute("aria-sort", "descending");
21
+ await page.waitForTimeout(400);
22
+ const desc = await firstName();
23
+
24
+ expect(asc).not.toBe(desc); // order flipped
25
+ });
26
+
27
+ test("audit table sorts client-side", async ({ page }) => {
28
+ await ready(page, "/admin/audit");
29
+ test.skip((await page.getByRole("row").count()) < 3, "not enough audit entries");
30
+ await page.getByRole("button", { name: "When" }).click(); // default desc -> asc
31
+ await expect(page.getByRole("columnheader", { name: /When/ })).toHaveAttribute("aria-sort", "ascending");
32
+ });
@@ -0,0 +1,60 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { ready } from "../helpers/hydration";
3
+ import { clearMailpit, mailpitReachable, waitForLink } from "../helpers/mailpit";
4
+
5
+ const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
6
+ const origin = { origin: base };
7
+
8
+ // Verification badges/actions are gated by the emailVerification capability. The
9
+ // seeded suite runs with verification off, so assert the gating here (nothing
10
+ // leaks in); the enabled state is covered manually / under a verification run.
11
+ test("verification badges stay hidden when the feature is off", async ({ page }) => {
12
+ await ready(page, "/admin/users");
13
+ const caps = await (await page.request.get("/api/account/capabilities")).json();
14
+ test.skip(caps.emailVerification === true, "email verification enabled");
15
+ await expect(page.getByText("Verified", { exact: true })).toHaveCount(0);
16
+ await expect(page.getByText("Unverified", { exact: true })).toHaveCount(0);
17
+ });
18
+
19
+ // Admin can trigger a password-reset email for a user (not verification-gated).
20
+ test("admin sends a password reset email to a user", async ({ page }) => {
21
+ test.skip(!(await mailpitReachable()), "Mailpit not available");
22
+ await ready(page, "/admin/users");
23
+ const email = `admin-reset-${Date.now()}@example.com`;
24
+ await page.request.post("/api/auth/admin/create-user", {
25
+ headers: { origin: base },
26
+ data: { email, password: "Podokit3e-Str0ng!pw", name: "AR", role: "user" },
27
+ });
28
+ await clearMailpit();
29
+ await page.locator("#toolbar-search").fill(email);
30
+ await page.getByRole("button", { name: "Search", exact: true }).click();
31
+ await page.getByRole("row", { name: new RegExp(email) }).getByRole("button").click();
32
+ await page.getByRole("menuitem", { name: "Manage" }).click();
33
+ const dialog = page.getByRole("dialog");
34
+ await dialog.getByRole("button", { name: "Security" }).click();
35
+ await dialog.getByRole("button", { name: "Send password reset email" }).click();
36
+ await expect(page.getByText("Password reset email sent")).toBeVisible();
37
+ await waitForLink(email); // a reset email actually arrived for this user
38
+ });
39
+
40
+ test("login shows a verification hint for unverified users", async ({ page, browser }) => {
41
+ const caps = await (await page.request.get("/api/account/capabilities")).json();
42
+ test.skip(caps.emailVerification !== true, "email verification disabled");
43
+ const email = `login-unv-${Date.now()}@example.com`;
44
+ await page.request.post("/api/auth/sign-up/email", { headers: { origin: base }, data: { email, password: "Podokit3e-Str0ng!pw", name: "LU" } });
45
+ const anon = await browser.newContext();
46
+ const p2 = await anon.newPage();
47
+ await p2.goto(`${base}/login`);
48
+ await p2.getByLabel("Email").fill(email);
49
+ await p2.getByLabel("Password").fill("Podokit3e-Str0ng!pw");
50
+ await p2.getByRole("button", { name: "Sign in" }).click();
51
+ await expect(p2.getByRole("link", { name: /resend verification/i })).toBeVisible();
52
+ await anon.close();
53
+ });
54
+
55
+ test("overview omits the unverified-users card when the feature is off", async ({ page }) => {
56
+ await ready(page, "/admin");
57
+ const caps = await (await page.request.get("/api/account/capabilities")).json();
58
+ test.skip(caps.emailVerification === true, "email verification enabled");
59
+ await expect(page.getByText("Unverified users")).toHaveCount(0);
60
+ });
@@ -0,0 +1,42 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { anonState } from "../helpers/accounts";
3
+ import { ready } from "../helpers/hydration";
4
+ import { clearMailpit, mailpitReachable, waitForLink } from "../helpers/mailpit";
5
+
6
+ const base = process.env.E2E_BASE_URL ?? "http://localhost:5001";
7
+ const origin = { origin: base };
8
+
9
+ // The reset flow is for signed-out users — run without the seeded session.
10
+ test.use({ storageState: anonState });
11
+
12
+ test("forgot password: request, open the emailed link, set a new password, sign in @smoke", async ({ page, playwright }) => {
13
+ test.skip(!(await mailpitReachable()), "Mailpit not available");
14
+ const email = `ui-reset-${Date.now()}@example.com`;
15
+ const ctx = await playwright.request.newContext({ baseURL: base, extraHTTPHeaders: origin });
16
+ const signup = await ctx.post("/api/auth/sign-up/email", { data: { email, password: "Podokit3e-Str0ng!pw", name: "UIReset" } });
17
+ expect(signup.ok()).toBeTruthy();
18
+ const verificationOn = !(await signup.json())?.token;
19
+ await ctx.dispose();
20
+ // The happy path ends by signing in; when verification is required that would be
21
+ // blocked, so this UI walkthrough only applies with verification off.
22
+ test.skip(verificationOn, "email verification enabled");
23
+ await clearMailpit();
24
+
25
+ await ready(page, "/forgot-password");
26
+ await page.getByLabel("Email").fill(email);
27
+ await page.getByRole("button", { name: "Send reset link" }).click();
28
+ await expect(page.getByText(/reset link is on its way/i)).toBeVisible();
29
+
30
+ // Open the emailed link: the API validates it and redirects to the reset page.
31
+ await page.goto(await waitForLink(email));
32
+ await page.waitForLoadState("load");
33
+ await page.waitForTimeout(400);
34
+ await page.getByLabel("New password").fill("Podokit3e-N3wStr0ng!pw");
35
+ await page.getByRole("button", { name: "Update password" }).click();
36
+ await expect(page).toHaveURL(/\/login/);
37
+
38
+ await page.getByLabel("Email").fill(email);
39
+ await page.getByLabel("Password").fill("Podokit3e-N3wStr0ng!pw");
40
+ await page.getByRole("button", { name: "Sign in" }).click();
41
+ await expect(page).toHaveURL(/\/admin/);
42
+ });
@@ -0,0 +1,22 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { anonState, userState } from "../helpers/accounts";
3
+
4
+ test.describe("unauthenticated", () => {
5
+ test.use({ storageState: anonState });
6
+ test("protected route redirects to login @smoke", async ({ page }) => {
7
+ await page.goto("/admin");
8
+ await expect(page).toHaveURL(/\/login\?redirect=/);
9
+ });
10
+ });
11
+
12
+ test.describe("normal user (non-admin)", () => {
13
+ test.use({ storageState: userState });
14
+ test("admin-only nav is hidden and admin routes are forbidden", async ({ page }) => {
15
+ await page.goto("/admin");
16
+ await expect(page.getByRole("link", { name: "Users" })).toHaveCount(0);
17
+ await expect(page.getByRole("link", { name: "Sessions" })).toHaveCount(0);
18
+ expect((await page.goto("/admin/users"))?.status()).toBe(403);
19
+ expect((await page.goto("/admin/sessions"))?.status()).toBe(403);
20
+ expect((await page.goto("/admin/settings"))?.status()).toBe(403);
21
+ });
22
+ });
@@ -0,0 +1,13 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { anonState } from "../helpers/accounts";
3
+ import { ready } from "../helpers/hydration";
4
+
5
+ test.use({ storageState: anonState });
6
+
7
+ test("language switch toggles Korean and sets <html lang>", async ({ page }) => {
8
+ await ready(page, "/login");
9
+ await page.getByRole("button", { name: "Language" }).click();
10
+ await page.getByRole("menuitem", { name: /한국어/ }).click();
11
+ await expect(page.locator("html")).toHaveAttribute("lang", "ko");
12
+ await expect(page.getByRole("button", { name: "로그인", exact: true })).toBeVisible();
13
+ });
@@ -0,0 +1,57 @@
1
+ import { expect, test } from "@playwright/test";
2
+ import { createHash, randomBytes } from "node:crypto";
3
+
4
+ // admin storageState (project default) — the admin is the resource owner.
5
+ const b64url = (b: Buffer): string => b.toString("base64url");
6
+ const REDIRECT = "http://localhost:9999/callback";
7
+
8
+ // Verifies the provider is correctly configured end to end on the front channel:
9
+ // signed discovery + JWKS, dynamic client registration, and an authorize request
10
+ // (with PKCE) that validates the client and hands off to the consent page. The
11
+ // back-channel token exchange is driven by an OIDC client SDK / real RP — see
12
+ // docs/modules.md for the manual round-trip checklist.
13
+ test("OIDC provider: discovery, JWKS, client registration, authorize handoff @smoke", async ({ page, baseURL }) => {
14
+ const headers = { origin: baseURL ?? "" };
15
+ await page.request.put("/api/account/settings", { data: { oidcProvider: true }, headers });
16
+ await expect
17
+ .poll(async () => (await page.request.get("/api/auth/.well-known/openid-configuration")).status(), { timeout: 8000 })
18
+ .toBe(200);
19
+
20
+ try {
21
+ const disc = await (await page.request.get("/api/auth/.well-known/openid-configuration")).json();
22
+ for (const field of ["issuer", "authorization_endpoint", "token_endpoint", "userinfo_endpoint", "jwks_uri"]) {
23
+ expect(typeof disc[field]).toBe("string");
24
+ }
25
+ // JWKS exposes at least one signing key (the OAuth provider needs it for id_tokens).
26
+ const jwks = await (await page.request.get(disc.jwks_uri)).json();
27
+ expect(Array.isArray(jwks.keys) && jwks.keys.length).toBeTruthy();
28
+
29
+ // Dynamic client registration returns usable credentials.
30
+ const reg = await (
31
+ await page.request.post("/api/auth/oauth2/create-client", {
32
+ data: { client_name: "E2E RP", redirect_uris: [REDIRECT] },
33
+ headers,
34
+ })
35
+ ).json();
36
+ expect(reg.client_id).toBeTruthy();
37
+ expect(reg.client_secret).toBeTruthy();
38
+
39
+ // Authorize validates the client + PKCE and hands off to the consent page with
40
+ // the signed authorization request.
41
+ const verifier = b64url(randomBytes(40));
42
+ const challenge = b64url(createHash("sha256").update(verifier).digest());
43
+ const authorizeUrl =
44
+ `/api/auth/oauth2/authorize?client_id=${reg.client_id}&redirect_uri=${encodeURIComponent(REDIRECT)}` +
45
+ `&response_type=code&scope=${encodeURIComponent("openid profile email")}&state=st` +
46
+ `&code_challenge=${challenge}&code_challenge_method=S256`;
47
+ const authz = await (await page.request.get(authorizeUrl, { headers })).json();
48
+ expect(String(authz.url)).toContain("/oauth2/consent");
49
+ expect(String(authz.url)).toContain("client_id=");
50
+
51
+ // The consent page renders (reached in a real browser navigation).
52
+ await page.goto(authz.url);
53
+ await expect(page.getByRole("button", { name: "Allow" })).toBeVisible();
54
+ } finally {
55
+ await page.request.put("/api/account/settings", { data: { oidcProvider: false }, headers });
56
+ }
57
+ });