@etus/bhono-app 0.1.5 → 0.1.6

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 (269) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +5 -1
  3. package/templates/base/.husky/pre-push +26 -0
  4. package/templates/base/CLAUDE.md +5 -5
  5. package/templates/base/README.md +31 -20
  6. package/templates/base/docs/app_spec.txt +13 -10
  7. package/templates/base/docs/architecture/README.md +3 -0
  8. package/templates/base/docs/architecture/data-requirements.md +4 -3
  9. package/templates/base/docs/architecture/db-bootstrap.md +39 -0
  10. package/templates/base/docs/architecture/drizzle-migration-plan.md +125 -0
  11. package/templates/base/docs/architecture/erd.md +1 -1
  12. package/templates/base/docs/architecture/sql-standards.md +100 -0
  13. package/templates/base/docs/testing.md +36 -29
  14. package/templates/base/package.json +6 -5
  15. package/templates/base/pnpm-lock.yaml +0 -123
  16. package/templates/base/schema.sql +84 -0
  17. package/templates/base/scripts/init.sh +244 -59
  18. package/templates/base/src/client/hooks/use-auth.ts +5 -0
  19. package/templates/base/src/client/routes/_authenticated/dashboard.tsx +1 -1
  20. package/templates/base/src/client/routes/index.tsx +1 -1
  21. package/templates/base/src/server/db/client.ts +3 -5
  22. package/templates/base/src/server/db/records.ts +81 -0
  23. package/templates/base/src/server/db/seed.ts +3 -2
  24. package/templates/base/src/server/db/sql.ts +96 -0
  25. package/templates/base/src/server/index.ts +16 -2
  26. package/templates/base/src/server/lib/audit.ts +74 -26
  27. package/templates/base/src/server/lib/audited-db.ts +219 -109
  28. package/templates/base/src/server/lib/transaction.ts +10 -16
  29. package/templates/base/src/server/middleware/account.ts +8 -15
  30. package/templates/base/src/server/middleware/auth.ts +102 -38
  31. package/templates/base/src/server/middleware/rate-limit.ts +6 -1
  32. package/templates/base/src/server/routes/accounts/handlers.ts +18 -6
  33. package/templates/base/src/server/routes/audits/handlers.ts +3 -1
  34. package/templates/base/src/server/routes/auth/handlers.ts +14 -9
  35. package/templates/base/src/server/routes/auth/test-login.ts +99 -45
  36. package/templates/base/src/server/routes/health/handlers.ts +4 -4
  37. package/templates/base/src/server/routes/invitations/handlers.ts +6 -3
  38. package/templates/base/src/server/routes/users/handlers.ts +21 -14
  39. package/templates/base/src/server/services/accounts.ts +242 -217
  40. package/templates/base/src/server/services/audits.ts +114 -61
  41. package/templates/base/src/server/services/auth.ts +310 -180
  42. package/templates/base/src/server/services/invitations.ts +282 -222
  43. package/templates/base/src/server/services/users.ts +383 -293
  44. package/templates/base/src/server/types/index.ts +1 -2
  45. package/templates/base/{src/server/__tests__/fixtures.ts → tests/fixtures/server.ts} +3 -3
  46. package/templates/base/{src/client/__tests__/setup-browser.ts → tests/helpers/client-setup-browser.ts} +2 -2
  47. package/templates/base/{src/client/__tests__/setup.ts → tests/helpers/client-setup.ts} +1 -1
  48. package/templates/base/{src/client/__tests__/test-utils.tsx → tests/helpers/client-test-utils.tsx} +2 -2
  49. package/templates/base/{src/server/__tests__/setup.ts → tests/helpers/server.ts} +9 -9
  50. package/templates/base/tests/integration/accounts/crud.test.ts +2 -11
  51. package/templates/base/tests/integration/audits/list.test.ts +2 -11
  52. package/templates/base/tests/integration/auth/auth-service.test.ts +1 -10
  53. package/templates/base/tests/integration/auth/invitation-token.test.ts +2 -11
  54. package/templates/base/tests/integration/auth/logout.test.ts +2 -11
  55. package/templates/base/tests/integration/auth/oauth.test.ts +23 -42
  56. package/templates/base/tests/integration/auth/refresh-token.test.ts +1 -9
  57. package/templates/base/tests/integration/auth/session-expiry.test.ts +1 -9
  58. package/templates/base/tests/integration/auth/session.test.ts +2 -11
  59. package/templates/base/tests/integration/auth/super-admin.test.ts +1 -9
  60. package/templates/base/tests/integration/authorization/analytics-role.test.ts +2 -11
  61. package/templates/base/tests/integration/authorization/billing-role.test.ts +2 -11
  62. package/templates/base/tests/integration/authorization/guards-roles.test.ts +1 -9
  63. package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +2 -11
  64. package/templates/base/tests/integration/authorization/roles.test.ts +2 -11
  65. package/templates/base/tests/integration/config/production-behavior.test.ts +2 -11
  66. package/templates/base/tests/integration/health/health.test.ts +25 -44
  67. package/templates/base/tests/integration/invitations/crud.test.ts +2 -11
  68. package/templates/base/tests/integration/invitations/email.test.ts +1 -9
  69. package/templates/base/tests/integration/middleware/auth.test.ts +3 -12
  70. package/templates/base/tests/integration/middleware/request-logger.test.ts +1 -9
  71. package/templates/base/tests/integration/performance/response-times.test.ts +1 -9
  72. package/templates/base/tests/integration/security/cookie-security.test.ts +2 -11
  73. package/templates/base/tests/integration/security/csrf-protection.test.ts +2 -11
  74. package/templates/base/tests/integration/security/log-sanitization.test.ts +1 -9
  75. package/templates/base/tests/integration/security/rate-limiting.test.ts +1 -9
  76. package/templates/base/tests/integration/security/sql-injection.test.ts +7 -18
  77. package/templates/base/tests/integration/security/xss-prevention.test.ts +2 -11
  78. package/templates/base/tests/integration/setup.ts +13 -90
  79. package/templates/base/tests/integration/smoke.test.ts +3 -2
  80. package/templates/base/tests/integration/storage/upload.test.ts +2 -11
  81. package/templates/base/tests/integration/storage/validation.test.ts +2 -11
  82. package/templates/base/tests/integration/users/crud.test.ts +2 -11
  83. package/templates/base/tests/integration/users/list.test.ts +2 -11
  84. package/templates/base/tests/integration/vitest.config.ts +2 -9
  85. package/templates/base/{src/server/__tests__ → tests}/mocks/db.ts +1 -1
  86. package/templates/base/{src/server/__tests__ → tests}/mocks/index.ts +1 -1
  87. package/templates/base/{src/server/__tests__ → tests}/mocks/kv.ts +1 -1
  88. package/templates/base/{src/server/__tests__ → tests}/mocks/r2.ts +1 -1
  89. package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/sidebar.test.tsx +1 -1
  90. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/avatar.test.tsx +1 -1
  91. package/templates/base/{src/client/__tests__ → tests/unit/client/components/ui}/button.test.tsx +1 -1
  92. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/card.test.tsx +1 -1
  93. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/dialog.test.tsx +1 -1
  94. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/input.test.tsx +1 -1
  95. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/loading-skeleton.test.tsx +1 -1
  96. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/skeleton.test.tsx +1 -1
  97. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/sonner.test.tsx +1 -1
  98. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/tabs.test.tsx +1 -1
  99. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/account.test.tsx +1 -1
  100. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/integrations.test.tsx +1 -1
  101. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/settings.test.tsx +1 -1
  102. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/team.test.tsx +1 -1
  103. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/authenticated-layout.test.tsx +1 -1
  104. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/dashboard.test.tsx +1 -1
  105. package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/invite.test.tsx +1 -1
  106. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/login.test.tsx +1 -1
  107. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/navigation.test.tsx +1 -1
  108. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/root-layout.test.tsx +1 -1
  109. package/templates/base/{src/server/auth/__tests__ → tests/unit/server/auth}/guards.test.ts +2 -2
  110. package/templates/base/{src → tests/unit}/server/auth/permissions.test.ts +1 -1
  111. package/templates/base/{src → tests/unit}/server/auth/roles.test.ts +1 -1
  112. package/templates/base/tests/unit/server/db/sql.test.ts +68 -0
  113. package/templates/base/{src → tests/unit}/server/env.test.ts +1 -1
  114. package/templates/base/tests/unit/server/lib/audited-db.test.ts +78 -0
  115. package/templates/base/{src → tests/unit}/server/lib/email.test.ts +1 -1
  116. package/templates/base/{src → tests/unit}/server/lib/errors.test.ts +1 -1
  117. package/templates/base/{src → tests/unit}/server/lib/oauth.test.ts +1 -1
  118. package/templates/base/{src → tests/unit}/server/lib/pagination.test.ts +1 -1
  119. package/templates/base/{src → tests/unit}/server/lib/password.test.ts +1 -1
  120. package/templates/base/{src → tests/unit}/server/lib/providers.test.ts +1 -1
  121. package/templates/base/{src → tests/unit}/server/lib/r2-storage.test.ts +2 -2
  122. package/templates/base/{src → tests/unit}/server/lib/session.test.ts +2 -2
  123. package/templates/base/{src → tests/unit}/server/lib/tokens.test.ts +1 -1
  124. package/templates/base/{src → tests/unit}/server/lib/transaction.test.ts +5 -14
  125. package/templates/base/{src → tests/unit}/server/middleware/account.test.ts +16 -24
  126. package/templates/base/{src → tests/unit}/server/middleware/auth.test.ts +71 -42
  127. package/templates/base/{src → tests/unit}/server/middleware/cors.test.ts +1 -1
  128. package/templates/base/{src → tests/unit}/server/middleware/error-handler.test.ts +2 -2
  129. package/templates/base/{src → tests/unit}/server/middleware/rate-limit.test.ts +3 -2
  130. package/templates/base/{src → tests/unit}/server/middleware/request-context.test.ts +1 -1
  131. package/templates/base/{src → tests/unit}/server/middleware/request-logger.test.ts +1 -1
  132. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/db.test.ts +1 -1
  133. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/kv.test.ts +1 -1
  134. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/r2.test.ts +1 -1
  135. package/templates/base/{src/server/routes/accounts/__tests__ → tests/unit/server/routes/accounts}/handlers.test.ts +12 -12
  136. package/templates/base/{src/server/routes/audits/__tests__ → tests/unit/server/routes/audits}/handlers.test.ts +11 -11
  137. package/templates/base/{src/server/routes/auth/__tests__ → tests/unit/server/routes/auth}/handlers.test.ts +13 -13
  138. package/templates/base/{src/server/routes/health/__tests__ → tests/unit/server/routes/health}/handlers.test.ts +27 -23
  139. package/templates/base/{src/server/routes/invitations/__tests__ → tests/unit/server/routes/invitations}/handlers.test.ts +14 -17
  140. package/templates/base/{src/server/routes/storage/__tests__ → tests/unit/server/routes/storage}/handlers.test.ts +6 -6
  141. package/templates/base/{src/server/routes/users/__tests__ → tests/unit/server/routes/users}/handlers.test.ts +12 -12
  142. package/templates/base/tests/unit/server/services/accounts.test.ts +258 -0
  143. package/templates/base/tests/unit/server/services/audits.test.ts +141 -0
  144. package/templates/base/tests/unit/server/services/auth.test.ts +179 -0
  145. package/templates/base/tests/unit/server/services/invitations.test.ts +165 -0
  146. package/templates/base/tests/unit/server/services/users.test.ts +351 -0
  147. package/templates/base/tsconfig.json +2 -1
  148. package/templates/base/vitest.config.browser.ts +3 -2
  149. package/templates/base/vitest.config.frontend.ts +3 -2
  150. package/templates/base/vitest.config.ts +7 -14
  151. package/templates/base/.claude/settings.local.json +0 -11
  152. package/templates/base/config/drizzle.config.ts +0 -10
  153. package/templates/base/src/server/db/schema/accounts.ts +0 -20
  154. package/templates/base/src/server/db/schema/audit-logs.ts +0 -26
  155. package/templates/base/src/server/db/schema/index.ts +0 -7
  156. package/templates/base/src/server/db/schema/invitations.ts +0 -30
  157. package/templates/base/src/server/db/schema/refresh-tokens.ts +0 -22
  158. package/templates/base/src/server/db/schema/user-accounts.ts +0 -25
  159. package/templates/base/src/server/db/schema/users.ts +0 -33
  160. package/templates/base/src/server/lib/audited-db.test.ts +0 -107
  161. package/templates/base/src/server/lib/schema-helpers.ts +0 -16
  162. package/templates/base/src/server/services/__tests__/accounts.test.ts +0 -764
  163. package/templates/base/src/server/services/__tests__/audits.test.ts +0 -235
  164. package/templates/base/src/server/services/__tests__/auth.test.ts +0 -765
  165. package/templates/base/src/server/services/__tests__/invitations.test.ts +0 -704
  166. package/templates/base/src/server/services/__tests__/users.test.ts +0 -755
  167. package/templates/base/tests/integration/lib/schema-helpers.test.ts +0 -129
  168. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-can-be-collapsed-by-default-1.png +0 -0
  169. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-expands-when-collapsed-and-expand-button-is-clicked-1.png +0 -0
  170. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
  171. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
  172. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-hides-navigation-labels-when-collapsed-1.png +0 -0
  173. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
  174. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
  175. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-keyboard-shortcut-hint-when-expanded-1.png +0 -0
  176. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-info-when-authenticated-1.png +0 -0
  177. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-initials-in-avatar-fallback-1.png +0 -0
  178. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/error-boundary.test.tsx +0 -0
  179. /package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/error-fallback.test.tsx +0 -0
  180. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-auth.test.tsx +0 -0
  181. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-theme.test.tsx +0 -0
  182. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-dashboard-stats-cards-1.png +0 -0
  183. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-quick-action-cards-1.png +0 -0
  184. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-recent-activity-section-1.png +0 -0
  185. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-first-name-in-welcome-message-1.png +0 -0
  186. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-user-information-in-sidebar-1.png +0 -0
  187. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-render-dashboard-when-authenticated-1.png +0 -0
  188. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-show-navigation-sidebar-1.png +0 -0
  189. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-unauthenticated-should-redirect-to-login-when-not-authenticated-1.png +0 -0
  190. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-accept-invitation-button-1.png +0 -0
  191. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-decline-button-linking-to-homepage-1.png +0 -0
  192. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-display-invitation-details--email--workspace--role--1.png +0 -0
  193. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-have-a-logo-link-to-homepage-1.png +0 -0
  194. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-render-invitation-page-with-inviter-name-and-workspace-1.png +0 -0
  195. /package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/__screenshots__/invite.test.tsx/Invite-Token-Page-pending-invitation-state-should-show-terms-of-service-and-privacy-policy-links-1.png +0 -0
  196. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-display-Google-OAuth-login-button-1.png +0 -0
  197. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-have-a-link-back-to-home-page-1.png +0 -0
  198. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-content-without-waiting-for-authentication-1.png +0 -0
  199. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-page-at--login-route-1.png +0 -0
  200. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-show-Terms-of-Service-and-Privacy-Policy-links-1.png +0 -0
  201. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/login.test.tsx/Login-Page-should-trigger-OAuth-flow-when-clicking-login-button-1.png +0 -0
  202. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-display-404-text-on-not-found-page-1.png +0 -0
  203. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-have-navigation-options-on-404-page-1.png +0 -0
  204. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-404-handling-should-render-404-page-for-unknown-routes-1.png +0 -0
  205. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-account-page-1.png +0 -0
  206. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-integrations-page-1.png +0 -0
  207. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-settings-page-1.png +0 -0
  208. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-authenticated-navigation-should-navigate-from-dashboard-to-team-page-1.png +0 -0
  209. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-display-navigation-links-on-home-page-1.png +0 -0
  210. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-home-page-navigation-should-have-correct-link-destinations-on-home-page-1.png +0 -0
  211. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-home-page-without-authentication-1.png +0 -0
  212. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-allow-access-to-login-page-without-authentication-1.png +0 -0
  213. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-dashboard-to-login-1.png +0 -0
  214. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-settings-to-login-1.png +0 -0
  215. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/__screenshots__/navigation.test.tsx/Navigation-unauthenticated-navigation-should-redirect-unauthenticated-users-from-team-to-login-1.png +0 -0
  216. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-Active-Sessions-section-with-session-cards-1.png +0 -0
  217. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-render-page-with-correct-title--Account--1.png +0 -0
  218. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-API-Access-section-1.png +0 -0
  219. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Connected-Accounts-section-with-Google-connected-1.png +0 -0
  220. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Danger-Zone-section-with-delete-button-1.png +0 -0
  221. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/account.test.tsx/Account-Page-should-show-Security-section-with-Two-Factor-Authentication-1.png +0 -0
  222. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-API-documentation-section-should-display-API-documentation-link-section-1.png +0 -0
  223. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-Create-Webhook-Dialog-should-have-Add-Webhook-trigger-button-1.png +0 -0
  224. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-category-filters-should-display-all-category-buttons-1.png +0 -0
  225. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-all-integration-cards-with-names-1.png +0 -0
  226. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-display-category-badges-on-cards-1.png +0 -0
  227. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Configure-button-for-connected-integrations-1.png +0 -0
  228. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connect-button-for-not-connected-integrations-1.png +0 -0
  229. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-integration-cards-should-show-Connected-badge-for-connected-integrations-1.png +0 -0
  230. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Available-Integrations-section-1.png +0 -0
  231. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Webhooks-section-1.png +0 -0
  232. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-connected-count-1.png +0 -0
  233. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-page-description-1.png +0 -0
  234. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-search-input-1.png +0 -0
  235. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-render-with-correct-title--Integrations--1.png +0 -0
  236. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-filter-integrations-based-on-search-query-1.png +0 -0
  237. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-search-by-description-1.png +0 -0
  238. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-show-no-results-message-when-search-has-no-matches-1.png +0 -0
  239. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-existing-webhook-1.png +0 -0
  240. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-events-1.png +0 -0
  241. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-last-delivery-info-1.png +0 -0
  242. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-success-status-1.png +0 -0
  243. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-have-Add-Webhook-button-1.png +0 -0
  244. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-connected-accounts-section-with-Google-provider-1.png +0 -0
  245. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Account-tab-should-display-sessions-and-danger-zone-sections-1.png +0 -0
  246. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-all-notification-toggle-options-1.png +0 -0
  247. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-email-notifications-section-1.png +0 -0
  248. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-display-toggle-switches-for-notification-options-1.png +0 -0
  249. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Notifications-tab-should-have-toggles-checked-by-default-1.png +0 -0
  250. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display--Save-Changes--button-1.png +0 -0
  251. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-personal-information-form-1.png +0 -0
  252. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-profile-picture-section-1.png +0 -0
  253. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-email-in-disabled-email-input-1.png +0 -0
  254. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-initials-in-avatar-1.png +0 -0
  255. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-Profile-tab-should-display-user-name-in-the-name-input-1.png +0 -0
  256. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-all-three-tabs-1.png +0 -0
  257. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-page-description-1.png +0 -0
  258. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-render-with-correct-title--Settings--1.png +0 -0
  259. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-show-Profile-tab-as-default-active-tab-1.png +0 -0
  260. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Account-tab-when-clicked-1.png +0 -0
  261. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/settings.test.tsx/Settings-Page-tab-navigation-should-switch-to-Notifications-tab-when-clicked-1.png +0 -0
  262. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Active-Members-section-with-member-count-1.png +0 -0
  263. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-display-Pending-Invitations-section-with-invitation-details-1.png +0 -0
  264. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-have-invite-member-button-that-can-be-clicked-1.png +0 -0
  265. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-page-with-correct-title-and-description-1.png +0 -0
  266. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-render-search-input-that-filters-team-members-1.png +0 -0
  267. /package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/__screenshots__/team.test.tsx/Team-Page-should-show-current-user-with---you---indicator-and-role-badge-1.png +0 -0
  268. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/error-components.test.tsx +0 -0
  269. /package/templates/base/{src/shared/schemas/__tests__ → tests/unit/shared}/schemas.test.ts +0 -0
@@ -1,13 +1,12 @@
1
1
  // src/services/users.ts
2
- import { eq, and, isNull, isNotNull, sql } from 'drizzle-orm'
3
- import type { Database } from '../db/client'
4
- import { users, userAccounts, type UserRecord } from '../db/schema'
2
+ import type { UserRecord } from '../db/records'
5
3
  import { logAudit } from '../lib/audit'
6
4
  import { auditedUpdate, auditedDelete } from '../lib/audited-db'
7
5
  import { createPaginationMeta, calculateOffset } from '../lib/pagination'
8
6
  import { NotFoundError } from '../lib/errors'
9
7
  import type { ServiceContext, PaginationQuery, PaginatedResponse, User } from '../types'
10
8
  import type { Role } from '../auth/roles'
9
+ import { execute, queryAll, queryOne, type SqlRow } from '../db/sql'
11
10
 
12
11
  // NOTE: CreateUserInput is commented out since user creation is disabled
13
12
  // (users should only be created through Google OAuth)
@@ -17,334 +16,425 @@ interface UpdateUserInput {
17
16
  status?: 'active' | 'inactive'
18
17
  }
19
18
 
20
- export const usersService = {
21
- async findAll(
22
- db: Database,
23
- ctx: ServiceContext,
24
- pagination: PaginationQuery
25
- ): Promise<PaginatedResponse<User>> {
26
- const offset = calculateOffset(pagination.page, pagination.limit)
27
-
28
- // Build base query conditions
29
- const conditions = [isNull(users.deletedAt)]
30
19
 
31
- // Non-super-admin sees only users in their account
32
- if (!ctx.user.isSuperAdmin) {
33
- const userIdsInAccount = db
34
- .select({ userId: userAccounts.userId })
35
- .from(userAccounts)
36
- .where(eq(userAccounts.accountId, ctx.accountId))
37
-
38
- conditions.push(sql`${users.id} IN ${userIdsInAccount}`)
20
+ const USER_SELECT_COLUMNS = `
21
+ id,
22
+ google_id as googleId,
23
+ email,
24
+ name,
25
+ avatar_url as avatarUrl,
26
+ status,
27
+ provider_ids as providerIds,
28
+ is_super_admin as isSuperAdmin,
29
+ created_at as createdAt,
30
+ updated_at as updatedAt,
31
+ deleted_at as deletedAt
32
+ `
33
+
34
+
35
+ function parseProviderIds(value: unknown): string[] {
36
+ if (!value) return []
37
+ if (Array.isArray(value)) return value.map((item) => String(item))
38
+ if (typeof value === 'string') {
39
+ try {
40
+ const parsed = JSON.parse(value) as unknown
41
+ if (Array.isArray(parsed)) {
42
+ return parsed.map((item) => String(item))
43
+ }
44
+ } catch {
45
+ return []
39
46
  }
47
+ }
48
+ return []
49
+ }
40
50
 
41
- // Add search filter if provided
42
- if (pagination.query) {
43
- conditions.push(
44
- sql`(${users.email} LIKE ${'%' + pagination.query + '%'} OR ${users.name} LIKE ${'%' + pagination.query + '%'})`
45
- )
46
- }
51
+ function toBoolean(value: unknown): boolean {
52
+ if (typeof value === 'boolean') return value
53
+ if (typeof value === 'number') return value !== 0
54
+ if (typeof value === 'string') return value === '1' || value.toLowerCase() === 'true'
55
+ return false
56
+ }
47
57
 
48
- // Get total count
49
- const countResults = await db
50
- .select({ count: sql<number>`count(*)` })
51
- .from(users)
52
- .where(and(...conditions))
53
-
54
- const totalItems = countResults.at(0)?.count ?? 0
55
-
56
- // Get paginated data
57
- const data = await db
58
- .select()
59
- .from(users)
60
- .where(and(...conditions))
61
- .limit(pagination.limit)
62
- .offset(offset)
63
- .orderBy(sql`${users.createdAt} DESC`)
64
-
65
- return {
66
- data: data.map((u) => ({
67
- id: u.id,
68
- email: u.email,
69
- name: u.name,
70
- status: u.status,
71
- providerIds: u.providerIds ?? [],
72
- isSuperAdmin: u.isSuperAdmin,
73
- createdAt: u.createdAt,
74
- updatedAt: u.updatedAt,
75
- deletedAt: u.deletedAt,
76
- })),
77
- meta: createPaginationMeta(totalItems, pagination.page, pagination.limit),
78
- }
79
- },
58
+ function mapUserRow(row: SqlRow): UserRecord {
59
+ const googleId = row.googleId ?? row.google_id
60
+ const avatarUrl = row.avatarUrl ?? row.avatar_url
61
+ const providerIds = row.providerIds ?? row.provider_ids
62
+ const isSuperAdmin = row.isSuperAdmin ?? row.is_super_admin
63
+ const createdAt = row.createdAt ?? row.created_at
64
+ const updatedAt = row.updatedAt ?? row.updated_at
65
+ const deletedAt = row.deletedAt ?? row.deleted_at
66
+
67
+ return {
68
+ id: String(row.id ?? ''),
69
+ googleId: String(googleId ?? ''),
70
+ email: String(row.email ?? ''),
71
+ name: String(row.name ?? ''),
72
+ avatarUrl: avatarUrl ? String(avatarUrl) : null,
73
+ status: row.status === 'inactive' ? 'inactive' : 'active',
74
+ providerIds: parseProviderIds(providerIds),
75
+ isSuperAdmin: toBoolean(isSuperAdmin),
76
+ createdAt: String(createdAt ?? ''),
77
+ updatedAt: String(updatedAt ?? ''),
78
+ deletedAt: deletedAt ? String(deletedAt) : null,
79
+ }
80
+ }
80
81
 
81
- async findById(db: Database, ctx: ServiceContext, id: string): Promise<User> {
82
- const userResults = await db
83
- .select()
84
- .from(users)
85
- .where(and(eq(users.id, id), isNull(users.deletedAt)))
86
- .limit(1)
82
+ function toUser(record: UserRecord): User {
83
+ return {
84
+ id: record.id,
85
+ email: record.email,
86
+ name: record.name,
87
+ status: record.status,
88
+ providerIds: record.providerIds ?? [],
89
+ isSuperAdmin: record.isSuperAdmin,
90
+ createdAt: record.createdAt,
91
+ updatedAt: record.updatedAt,
92
+ deletedAt: record.deletedAt,
93
+ }
94
+ }
87
95
 
88
- const userRecord = userResults.at(0)
89
- if (!userRecord) {
96
+ async function findAllSql(
97
+ db: D1Database,
98
+ ctx: ServiceContext,
99
+ pagination: PaginationQuery
100
+ ): Promise<PaginatedResponse<User>> {
101
+ const offset = calculateOffset(pagination.page, pagination.limit)
102
+ const whereClauses: string[] = ['u.deleted_at IS NULL']
103
+ const params: unknown[] = []
104
+
105
+ if (!ctx.user.isSuperAdmin) {
106
+ whereClauses.push('u.id IN (SELECT user_id FROM user_accounts WHERE account_id = ?)')
107
+ params.push(ctx.accountId)
108
+ }
109
+
110
+ if (pagination.query) {
111
+ whereClauses.push('(u.email LIKE ? OR u.name LIKE ?)')
112
+ const like = `%${pagination.query}%`
113
+ params.push(like, like)
114
+ }
115
+
116
+ const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
117
+
118
+ const countRow = await queryOne<{ count: number }>(
119
+ db,
120
+ `SELECT count(*) as count FROM users u ${whereSql}`,
121
+ params
122
+ )
123
+ const totalItems = countRow?.count ?? 0
124
+
125
+ const rows = await queryAll(
126
+ db,
127
+ `SELECT ${USER_SELECT_COLUMNS}
128
+ FROM users u
129
+ ${whereSql}
130
+ ORDER BY u.created_at DESC
131
+ LIMIT ? OFFSET ?`,
132
+ [...params, pagination.limit, offset]
133
+ )
134
+
135
+ return {
136
+ data: rows.map((row) => toUser(mapUserRow(row))),
137
+ meta: createPaginationMeta(totalItems, pagination.page, pagination.limit),
138
+ }
139
+ }
140
+
141
+ async function findByIdSql(
142
+ db: D1Database,
143
+ ctx: ServiceContext,
144
+ id: string
145
+ ): Promise<User> {
146
+ const row = await queryOne(
147
+ db,
148
+ `SELECT ${USER_SELECT_COLUMNS}
149
+ FROM users u
150
+ WHERE u.id = ? AND u.deleted_at IS NULL
151
+ LIMIT 1`,
152
+ [id]
153
+ )
154
+
155
+ if (!row) {
156
+ throw new NotFoundError('User')
157
+ }
158
+
159
+ if (!ctx.user.isSuperAdmin) {
160
+ const membership = await queryOne(
161
+ db,
162
+ `SELECT 1 as ok FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
163
+ [id, ctx.accountId]
164
+ )
165
+ if (!membership) {
90
166
  throw new NotFoundError('User')
91
167
  }
168
+ }
92
169
 
93
- // Check user has access (super-admin or same account)
94
- if (!ctx.user.isSuperAdmin) {
95
- const membershipResults = await db
96
- .select()
97
- .from(userAccounts)
98
- .where(
99
- and(
100
- eq(userAccounts.userId, id),
101
- eq(userAccounts.accountId, ctx.accountId)
102
- )
103
- )
104
- .limit(1)
105
-
106
- const membership = membershipResults.at(0)
107
- if (!membership) {
108
- throw new NotFoundError('User')
109
- }
110
- }
170
+ return toUser(mapUserRow(row))
171
+ }
111
172
 
112
- return {
113
- id: userRecord.id,
114
- email: userRecord.email,
115
- name: userRecord.name,
116
- status: userRecord.status,
117
- providerIds: userRecord.providerIds ?? [],
118
- isSuperAdmin: userRecord.isSuperAdmin,
119
- createdAt: userRecord.createdAt,
120
- updatedAt: userRecord.updatedAt,
121
- deletedAt: userRecord.deletedAt,
122
- }
123
- },
173
+ async function updateSql(
174
+ db: D1Database,
175
+ ctx: ServiceContext,
176
+ id: string,
177
+ input: UpdateUserInput
178
+ ): Promise<User> {
179
+ await findByIdSql(db, ctx, id)
180
+
181
+ const updates: Record<string, unknown> = {
182
+ updated_at: new Date().toISOString(),
183
+ updated_by_id: ctx.user.id,
184
+ }
185
+ if (input.name !== undefined) updates.name = input.name
186
+ if (input.status !== undefined) updates.status = input.status
187
+
188
+ const results = await auditedUpdate<UserRecord>(
189
+ db,
190
+ ctx,
191
+ 'users',
192
+ updates,
193
+ { clause: 'id = ?', params: [id] }
194
+ )
195
+
196
+ const updated = results.at(0)
197
+ if (!updated) {
198
+ throw new Error('Failed to update user')
199
+ }
200
+
201
+ return toUser(mapUserRow(updated as unknown as SqlRow))
202
+ }
203
+
204
+ async function deleteSql(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
205
+ await findByIdSql(db, ctx, id)
206
+ await auditedDelete(db, ctx, 'users', { clause: 'id = ?', params: [id] })
207
+ }
124
208
 
125
- // NOTE: User creation is disabled - users should only be created through Google OAuth
126
- // If you need to manually create users, add googleId to the CreateUserInput interface
127
- // and ensure the googleId is provided when creating users.
128
- /*
129
- async create(ctx: ServiceContext, input: CreateUserInput): Promise<User> {
130
- // Check email doesn't already exist
131
- const [existing] = await db
132
- .select()
133
- .from(users)
134
- .where(eq(users.email, input.email))
135
- .limit(1)
209
+ async function restoreSql(db: D1Database, ctx: ServiceContext, id: string): Promise<User> {
210
+ const row = await queryOne(
211
+ db,
212
+ `SELECT ${USER_SELECT_COLUMNS}
213
+ FROM users u
214
+ WHERE u.id = ? AND u.deleted_at IS NOT NULL
215
+ LIMIT 1`,
216
+ [id]
217
+ )
218
+
219
+ if (!row) {
220
+ throw new NotFoundError('User not found or not deleted')
221
+ }
222
+
223
+ const restored = await auditedUpdate<UserRecord>(
224
+ db,
225
+ ctx,
226
+ 'users',
227
+ { deleted_at: null, deleted_by_id: null },
228
+ { clause: 'id = ?', params: [id] }
229
+ )
230
+
231
+ const restoredRow = restored.at(0)
232
+ if (!restoredRow) {
233
+ throw new NotFoundError('Failed to restore user')
234
+ }
235
+
236
+ return toUser(mapUserRow(restoredRow as unknown as SqlRow))
237
+ }
238
+
239
+ async function listRolesSql(
240
+ db: D1Database,
241
+ ctx: ServiceContext,
242
+ userId: string
243
+ ): Promise<{ accountId: string; role: Role }[]> {
244
+ await findByIdSql(db, ctx, userId)
245
+
246
+ const rows = await queryAll(
247
+ db,
248
+ `SELECT account_id as accountId, role
249
+ FROM user_accounts
250
+ WHERE user_id = ?`,
251
+ [userId]
252
+ )
253
+
254
+ return rows.map((row) => ({
255
+ accountId: String(row.accountId ?? row.account_id ?? ''),
256
+ role: String(row.role ?? '') as Role,
257
+ }))
258
+ }
259
+
260
+ async function updateRoleSql(
261
+ db: D1Database,
262
+ ctx: ServiceContext,
263
+ userId: string,
264
+ accountId: string,
265
+ role: Role
266
+ ): Promise<void> {
267
+ await findByIdSql(db, ctx, userId)
268
+
269
+ const membership = await queryOne(
270
+ db,
271
+ `SELECT role FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
272
+ [userId, accountId]
273
+ )
274
+
275
+ if (!membership) {
276
+ throw new NotFoundError('User not found in account')
277
+ }
278
+
279
+ await execute(
280
+ db,
281
+ `UPDATE user_accounts SET role = ? WHERE user_id = ? AND account_id = ?`,
282
+ [role, userId, accountId]
283
+ )
284
+
285
+ await logAudit(db, ctx, 'UserAccount', `${userId}-${accountId}`, 'UPDATE', { role })
286
+ }
287
+
288
+ async function removeFromAccountSql(
289
+ db: D1Database,
290
+ ctx: ServiceContext,
291
+ userId: string,
292
+ accountId: string
293
+ ): Promise<void> {
294
+ await findByIdSql(db, ctx, userId)
295
+
296
+ await execute(
297
+ db,
298
+ `DELETE FROM user_accounts WHERE user_id = ? AND account_id = ?`,
299
+ [userId, accountId]
300
+ )
301
+
302
+ await logAudit(db, ctx, 'UserAccount', `${userId}-${accountId}`, 'DELETE', {
303
+ userId,
304
+ accountId,
305
+ })
306
+ }
307
+
308
+ async function createUserAccountsSql(
309
+ db: D1Database,
310
+ ctx: ServiceContext,
311
+ items: { userId: string; accountId: string; role: Role }[]
312
+ ): Promise<{ success: boolean; count: number }> {
313
+ let count = 0
314
+
315
+ for (const item of items) {
316
+ const existing = await queryOne(
317
+ db,
318
+ `SELECT role FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
319
+ [item.userId, item.accountId]
320
+ )
136
321
 
137
322
  if (existing) {
138
- throw new ConflictError('User with this email already exists')
323
+ await execute(
324
+ db,
325
+ `UPDATE user_accounts SET role = ? WHERE user_id = ? AND account_id = ?`,
326
+ [item.role, item.userId, item.accountId]
327
+ )
328
+ } else {
329
+ await execute(
330
+ db,
331
+ `INSERT INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)`,
332
+ [item.userId, item.accountId, item.role]
333
+ )
139
334
  }
140
335
 
141
- // Create user
142
- const [userRecord] = await db
143
- .insert(users)
144
- .values({
145
- email: input.email,
146
- name: input.name,
147
- status: 'active',
148
- createdById: ctx.user.id,
149
- updatedById: ctx.user.id,
150
- })
151
- .returning()
152
-
153
- // Create user-account relationship
154
- await db.insert(userAccounts).values({
155
- userId: userRecord.id,
156
- accountId: ctx.accountId,
157
- role: input.role,
336
+ count++
337
+
338
+ await logAudit(db, ctx, 'UserAccount', `${item.userId}-${item.accountId}`, 'INSERT', {
339
+ userId: item.userId,
340
+ accountId: item.accountId,
341
+ role: item.role,
158
342
  })
343
+ }
159
344
 
160
- // Log audit
161
- await logAudit(db, ctx, 'User', userRecord.id, 'INSERT', userRecord)
162
-
163
- return {
164
- id: userRecord.id,
165
- email: userRecord.email,
166
- name: userRecord.name,
167
- status: userRecord.status,
168
- providerIds: userRecord.providerIds ?? [],
169
- isSuperAdmin: userRecord.isSuperAdmin,
170
- createdAt: userRecord.createdAt,
171
- updatedAt: userRecord.updatedAt,
172
- deletedAt: userRecord.deletedAt,
173
- }
174
- },
175
- */
345
+ return { success: true, count }
346
+ }
176
347
 
177
- async update(db: Database, ctx: ServiceContext, id: string, input: UpdateUserInput): Promise<User> {
178
- // Verify user exists and accessible
179
- await this.findById(db, ctx, id)
348
+ async function deleteUserAccountsSql(
349
+ db: D1Database,
350
+ ctx: ServiceContext,
351
+ items: { userId: string; accountId: string; role: Role }[]
352
+ ): Promise<{ success: boolean; count: number }> {
353
+ let count = 0
180
354
 
181
- // Update user with audit
182
- const updateResults = await auditedUpdate<UserRecord>(
355
+ for (const item of items) {
356
+ await execute(
183
357
  db,
184
- ctx,
185
- users,
186
- {
187
- ...input,
188
- updatedAt: new Date().toISOString(),
189
- updatedById: ctx.user.id,
190
- },
191
- eq(users.id, id)
358
+ `DELETE FROM user_accounts WHERE user_id = ? AND account_id = ?`,
359
+ [item.userId, item.accountId]
192
360
  )
193
361
 
194
- const userRecord: UserRecord | undefined = updateResults.at(0)
195
- if (!userRecord) {
196
- throw new Error('Failed to update user')
197
- }
362
+ count++
198
363
 
199
- return {
200
- id: userRecord.id,
201
- email: userRecord.email,
202
- name: userRecord.name,
203
- status: userRecord.status,
204
- providerIds: userRecord.providerIds ?? [],
205
- isSuperAdmin: userRecord.isSuperAdmin,
206
- createdAt: userRecord.createdAt,
207
- updatedAt: userRecord.updatedAt,
208
- deletedAt: userRecord.deletedAt,
209
- }
210
- },
364
+ await logAudit(db, ctx, 'UserAccount', `${item.userId}-${item.accountId}`, 'DELETE', {
365
+ userId: item.userId,
366
+ accountId: item.accountId,
367
+ })
368
+ }
211
369
 
212
- async delete(db: Database, ctx: ServiceContext, id: string): Promise<void> {
213
- // Verify user exists and accessible
214
- await this.findById(db, ctx, id)
370
+ return { success: true, count }
371
+ }
215
372
 
216
- // Soft delete with audit
217
- await auditedDelete(db, ctx, users, eq(users.id, id))
373
+ export const usersService = {
374
+ async findAll(
375
+ db: D1Database,
376
+ ctx: ServiceContext,
377
+ pagination: PaginationQuery
378
+ ): Promise<PaginatedResponse<User>> {
379
+ return findAllSql(db, ctx, pagination)
218
380
  },
219
381
 
220
- // Bulk User-Account Operations
221
- async createUserAccounts(
222
- db: Database,
223
- ctx: ServiceContext,
224
- items: { userId: string; accountId: string; role: Role }[]
225
- ): Promise<{ success: boolean; count: number }> {
226
- let count = 0
227
-
228
- for (const item of items) {
229
- // Check if relationship already exists
230
- const existingResults = await db
231
- .select()
232
- .from(userAccounts)
233
- .where(
234
- and(
235
- eq(userAccounts.userId, item.userId),
236
- eq(userAccounts.accountId, item.accountId)
237
- )
238
- )
239
- .limit(1)
240
-
241
- const existing = existingResults.at(0)
242
- if (existing) {
243
- // Update existing role
244
- await db
245
- .update(userAccounts)
246
- .set({ role: item.role })
247
- .where(
248
- and(
249
- eq(userAccounts.userId, item.userId),
250
- eq(userAccounts.accountId, item.accountId)
251
- )
252
- )
253
- } else {
254
- // Create new relationship
255
- await db.insert(userAccounts).values({
256
- userId: item.userId,
257
- accountId: item.accountId,
258
- role: item.role,
259
- })
260
- }
382
+ async findById(db: D1Database, ctx: ServiceContext, id: string): Promise<User> {
383
+ return findByIdSql(db, ctx, id)
384
+ },
261
385
 
262
- count++
386
+ async update(db: D1Database, ctx: ServiceContext, id: string, input: UpdateUserInput): Promise<User> {
387
+ return updateSql(db, ctx, id, input)
388
+ },
263
389
 
264
- // Log audit
265
- await logAudit(db, ctx, 'UserAccount', `${item.userId}-${item.accountId}`, 'INSERT', {
266
- userId: item.userId,
267
- accountId: item.accountId,
268
- role: item.role,
269
- })
270
- }
390
+ async delete(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
391
+ await deleteSql(db, ctx, id)
392
+ },
271
393
 
272
- return { success: true, count }
394
+ async restore(db: D1Database, ctx: ServiceContext, id: string): Promise<User> {
395
+ return restoreSql(db, ctx, id)
273
396
  },
274
397
 
275
- async deleteUserAccounts(
276
- db: Database,
398
+ async listUserRoles(
399
+ db: D1Database,
277
400
  ctx: ServiceContext,
278
- items: { userId: string; accountId: string; role: Role }[]
279
- ): Promise<{ success: boolean; count: number }> {
280
- let count = 0
281
-
282
- for (const item of items) {
283
- await db
284
- .delete(userAccounts)
285
- .where(
286
- and(
287
- eq(userAccounts.userId, item.userId),
288
- eq(userAccounts.accountId, item.accountId)
289
- )
290
- )
291
-
292
- count++
293
-
294
- // Log audit
295
- await logAudit(db, ctx, 'UserAccount', `${item.userId}-${item.accountId}`, 'DELETE', {
296
- userId: item.userId,
297
- accountId: item.accountId,
298
- })
299
- }
300
-
301
- return { success: true, count }
401
+ userId: string
402
+ ): Promise<{ accountId: string; role: Role }[]> {
403
+ return listRolesSql(db, ctx, userId)
302
404
  },
303
405
 
304
- async restore(
305
- db: Database,
406
+ async updateRole(
407
+ db: D1Database,
306
408
  ctx: ServiceContext,
307
- id: string
308
- ): Promise<User> {
309
- // Find deleted record
310
- const recordResults = await db
311
- .select()
312
- .from(users)
313
- .where(and(
314
- eq(users.id, id),
315
- isNotNull(users.deletedAt)
316
- ))
317
- .limit(1)
318
-
319
- const record = recordResults.at(0)
320
- if (!record) {
321
- throw new NotFoundError('User not found or not deleted')
322
- }
409
+ userId: string,
410
+ accountId: string,
411
+ role: Role
412
+ ): Promise<void> {
413
+ await updateRoleSql(db, ctx, userId, accountId, role)
414
+ },
323
415
 
324
- // Restore user
325
- const restoreResults = await auditedUpdate<UserRecord>(
326
- db,
327
- ctx,
328
- users,
329
- { deletedAt: null, deletedById: null },
330
- eq(users.id, id)
331
- )
416
+ async removeFromAccount(
417
+ db: D1Database,
418
+ ctx: ServiceContext,
419
+ userId: string,
420
+ accountId: string
421
+ ): Promise<void> {
422
+ await removeFromAccountSql(db, ctx, userId, accountId)
423
+ },
332
424
 
333
- const restored: UserRecord | undefined = restoreResults.at(0)
334
- if (!restored) {
335
- throw new NotFoundError('Failed to restore user')
336
- }
425
+ async createUserAccounts(
426
+ db: D1Database,
427
+ ctx: ServiceContext,
428
+ items: { userId: string; accountId: string; role: Role }[]
429
+ ): Promise<{ success: boolean; count: number }> {
430
+ return createUserAccountsSql(db, ctx, items)
431
+ },
337
432
 
338
- return {
339
- id: restored.id,
340
- email: restored.email,
341
- name: restored.name,
342
- status: restored.status,
343
- providerIds: restored.providerIds ?? [],
344
- isSuperAdmin: restored.isSuperAdmin,
345
- createdAt: restored.createdAt,
346
- updatedAt: restored.updatedAt,
347
- deletedAt: restored.deletedAt,
348
- }
433
+ async deleteUserAccounts(
434
+ db: D1Database,
435
+ ctx: ServiceContext,
436
+ items: { userId: string; accountId: string; role: Role }[]
437
+ ): Promise<{ success: boolean; count: number }> {
438
+ return deleteUserAccountsSql(db, ctx, items)
349
439
  },
350
440
  }