@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/auth.ts
2
- import { eq, and, isNull, gt } from 'drizzle-orm'
3
- import type { Database } from '../db/client'
4
2
  import { isSuperAdminEmail, type Env } from '../env'
5
- import { users, accounts, userAccounts, refreshTokens } from '../db/schema'
3
+ import type { UserRecord } from '../db/records'
6
4
  import { createAccessToken, generateRefreshToken, hashToken, getRefreshTokenExpiry } from '../lib/tokens'
7
5
  import { UnauthorizedError } from '../lib/errors'
8
6
  import { logAuthEvent, type AuthEventContext } from '../lib/audit'
9
7
  import type { GoogleUserInfo, AuthTokens } from '../types/auth'
10
8
  import type { User } from '../types'
9
+ import { execute, queryOne, type SqlRow } from '../db/sql'
11
10
 
12
11
  interface AuthResult {
13
12
  user: User
@@ -16,210 +15,341 @@ interface AuthResult {
16
15
  isNewUser: boolean
17
16
  }
18
17
 
19
- export const authService = {
20
- async findOrCreateUser(db: Database, env: Env, googleUser: GoogleUserInfo, ctx: AuthEventContext): Promise<AuthResult> {
21
- let isNewUser = false
22
-
23
- // Try to find existing user by googleId
24
- const existingUsers = await db
25
- .select()
26
- .from(users)
27
- .where(and(eq(users.googleId, googleUser.sub), isNull(users.deletedAt)))
28
- .limit(1)
29
-
30
- let userRecord = existingUsers.at(0)
31
-
32
- if (userRecord) {
33
- // Update profile info if changed
34
- if (
35
- userRecord.email !== googleUser.email ||
36
- userRecord.name !== googleUser.name ||
37
- userRecord.avatarUrl !== googleUser.picture
38
- ) {
39
- const updated = await db
40
- .update(users)
41
- .set({
42
- email: googleUser.email,
43
- name: googleUser.name,
44
- avatarUrl: googleUser.picture ?? null,
45
- updatedAt: new Date().toISOString(),
46
- })
47
- .where(eq(users.id, userRecord.id))
48
- .returning()
49
- userRecord = updated[0]
18
+
19
+ const USER_SELECT_COLUMNS = `
20
+ id,
21
+ google_id as googleId,
22
+ email,
23
+ name,
24
+ avatar_url as avatarUrl,
25
+ status,
26
+ provider_ids as providerIds,
27
+ is_super_admin as isSuperAdmin,
28
+ created_at as createdAt,
29
+ updated_at as updatedAt,
30
+ deleted_at as deletedAt
31
+ `
32
+
33
+
34
+ function toEpochSeconds(date: Date): number {
35
+ return Math.floor(date.getTime() / 1000)
36
+ }
37
+
38
+ function toBoolean(value: unknown): boolean {
39
+ if (typeof value === 'boolean') return value
40
+ if (typeof value === 'number') return value !== 0
41
+ if (typeof value === 'string') return value === '1' || value.toLowerCase() === 'true'
42
+ return false
43
+ }
44
+
45
+ function parseProviderIds(value: unknown): string[] {
46
+ if (!value) return []
47
+ if (Array.isArray(value)) return value.map((item) => String(item))
48
+ if (typeof value === 'string') {
49
+ try {
50
+ const parsed = JSON.parse(value) as unknown
51
+ if (Array.isArray(parsed)) {
52
+ return parsed.map((item) => String(item))
50
53
  }
51
- } else {
52
- isNewUser = true
53
-
54
- // Create new user (check if email is pre-registered as super admin)
55
- const shouldBeSuperAdmin = isSuperAdminEmail(env, googleUser.email)
56
- const created = await db
57
- .insert(users)
58
- .values({
59
- googleId: googleUser.sub,
60
- email: googleUser.email,
61
- name: googleUser.name,
62
- avatarUrl: googleUser.picture ?? null,
63
- status: 'active',
64
- isSuperAdmin: shouldBeSuperAdmin,
65
- })
66
- .returning()
67
- userRecord = created[0]
68
-
69
- // Create personal account
70
- const createdAccounts = await db
71
- .insert(accounts)
72
- .values({
73
- name: `${googleUser.name}'s Account`,
74
- })
75
- .returning()
76
- const accountRecord = createdAccounts[0]
77
-
78
- // Link user to account with EDITOR role
79
- await db.insert(userAccounts).values({
80
- userId: userRecord.id,
81
- accountId: accountRecord.id,
82
- role: 'EDITOR',
83
- })
84
-
85
- // Log signup event
86
- await logAuthEvent(db, ctx, 'SIGNUP', userRecord.id, {
87
- email: userRecord.email,
88
- provider: 'google',
89
- accountId: accountRecord.id,
90
- })
54
+ } catch {
55
+ return []
91
56
  }
57
+ }
58
+ return []
59
+ }
92
60
 
93
- // Generate tokens
94
- const accessToken = await createAccessToken(env, userRecord.id, userRecord.email)
95
- const refreshToken = generateRefreshToken()
96
- const tokenHash = await hashToken(refreshToken)
61
+ function mapUserRow(row: SqlRow): UserRecord {
62
+ return {
63
+ id: String(row.id ?? ''),
64
+ googleId: String(row.googleId ?? ''),
65
+ email: String(row.email ?? ''),
66
+ name: String(row.name ?? ''),
67
+ avatarUrl: row.avatarUrl ? String(row.avatarUrl) : null,
68
+ status: (row.status === 'inactive' ? 'inactive' : 'active'),
69
+ providerIds: parseProviderIds(row.providerIds),
70
+ isSuperAdmin: toBoolean(row.isSuperAdmin),
71
+ createdAt: String(row.createdAt ?? ''),
72
+ updatedAt: String(row.updatedAt ?? ''),
73
+ deletedAt: row.deletedAt ? String(row.deletedAt) : null,
74
+ }
75
+ }
97
76
 
98
- // Store refresh token
99
- await db.insert(refreshTokens).values({
100
- userId: userRecord.id,
101
- tokenHash,
102
- expiresAt: getRefreshTokenExpiry(env),
103
- })
77
+ function toUser(record: UserRecord): User {
78
+ return {
79
+ id: record.id,
80
+ email: record.email,
81
+ name: record.name,
82
+ status: record.status,
83
+ providerIds: record.providerIds ?? [],
84
+ isSuperAdmin: record.isSuperAdmin,
85
+ createdAt: record.createdAt,
86
+ updatedAt: record.updatedAt,
87
+ deletedAt: record.deletedAt,
88
+ }
89
+ }
104
90
 
105
- // Log login event
106
- await logAuthEvent(db, ctx, 'LOGIN', userRecord.id, {
107
- email: userRecord.email,
108
- provider: 'google',
109
- isNewUser,
110
- })
91
+ async function selectUserByGoogleId(db: D1Database, googleId: string): Promise<UserRecord | null> {
92
+ const row = await queryOne(
93
+ db,
94
+ `SELECT ${USER_SELECT_COLUMNS}
95
+ FROM users
96
+ WHERE google_id = ? AND deleted_at IS NULL
97
+ LIMIT 1`,
98
+ [googleId]
99
+ )
100
+ return row ? mapUserRow(row) : null
101
+ }
111
102
 
112
- return {
113
- user: {
114
- id: userRecord.id,
115
- email: userRecord.email,
116
- name: userRecord.name,
117
- status: userRecord.status,
118
- providerIds: userRecord.providerIds ?? [],
119
- isSuperAdmin: userRecord.isSuperAdmin,
120
- createdAt: userRecord.createdAt,
121
- updatedAt: userRecord.updatedAt,
122
- deletedAt: userRecord.deletedAt,
123
- },
124
- tokens: {
125
- accessToken,
126
- expiresIn: 60 * 15, // 15 minutes in seconds
127
- },
128
- refreshToken,
129
- isNewUser,
130
- }
131
- },
103
+ async function selectUserById(db: D1Database, userId: string): Promise<UserRecord | null> {
104
+ const row = await queryOne(
105
+ db,
106
+ `SELECT ${USER_SELECT_COLUMNS}
107
+ FROM users
108
+ WHERE id = ? AND deleted_at IS NULL
109
+ LIMIT 1`,
110
+ [userId]
111
+ )
112
+ return row ? mapUserRow(row) : null
113
+ }
114
+
115
+ async function updateUserSql(
116
+ db: D1Database,
117
+ userId: string,
118
+ updates: Record<string, unknown>
119
+ ): Promise<UserRecord | null> {
120
+ const columns = Object.keys(updates)
121
+ if (columns.length === 0) return selectUserById(db, userId)
122
+
123
+ const setClause = columns.map((column) => `${column} = ?`).join(', ')
124
+ const params = columns.map((column) => updates[column])
125
+ await execute(
126
+ db,
127
+ `UPDATE users SET ${setClause} WHERE id = ?`,
128
+ [...params, userId]
129
+ )
130
+
131
+ return selectUserById(db, userId)
132
+ }
133
+
134
+ async function insertUserSql(
135
+ db: D1Database,
136
+ values: Record<string, unknown>
137
+ ): Promise<UserRecord | null> {
138
+ const columns = Object.keys(values)
139
+ const placeholders = columns.map(() => '?').join(', ')
140
+ const params = columns.map((column) => values[column])
141
+
142
+ await execute(
143
+ db,
144
+ `INSERT INTO users (${columns.join(', ')}) VALUES (${placeholders})`,
145
+ params
146
+ )
132
147
 
133
- async refreshAccessToken(db: Database, env: Env, refreshToken: string, ctx: AuthEventContext): Promise<AuthTokens> {
134
- const tokenHash = await hashToken(refreshToken)
135
-
136
- // Find valid refresh token
137
- const tokenResults = await db
138
- .select()
139
- .from(refreshTokens)
140
- .where(
141
- and(
142
- eq(refreshTokens.tokenHash, tokenHash),
143
- isNull(refreshTokens.revokedAt),
144
- gt(refreshTokens.expiresAt, new Date())
145
- )
146
- )
147
- .limit(1)
148
-
149
- const tokenRecord = tokenResults.at(0)
150
- if (!tokenRecord) {
151
- throw new UnauthorizedError('Invalid or expired refresh token')
148
+ return selectUserById(db, String(values.id ?? ''))
149
+ }
150
+
151
+ async function findOrCreateUserSql(
152
+ db: D1Database,
153
+ env: Env,
154
+ googleUser: GoogleUserInfo,
155
+ ctx: AuthEventContext
156
+ ): Promise<AuthResult> {
157
+ let isNewUser = false
158
+ let userRecord = await selectUserByGoogleId(db, googleUser.sub)
159
+
160
+ if (userRecord) {
161
+ const shouldUpdate =
162
+ userRecord.email !== googleUser.email ||
163
+ userRecord.name !== googleUser.name ||
164
+ userRecord.avatarUrl !== googleUser.picture
165
+
166
+ if (shouldUpdate) {
167
+ userRecord = await updateUserSql(db, userRecord.id, {
168
+ email: googleUser.email,
169
+ name: googleUser.name,
170
+ avatar_url: googleUser.picture ?? null,
171
+ updated_at: new Date().toISOString(),
172
+ }) ?? userRecord
152
173
  }
174
+ } else {
175
+ isNewUser = true
176
+ const shouldBeSuperAdmin = isSuperAdminEmail(env, googleUser.email)
177
+ const now = new Date().toISOString()
178
+ const userId = crypto.randomUUID()
153
179
 
154
- // Get user
155
- const userResults = await db
156
- .select()
157
- .from(users)
158
- .where(and(eq(users.id, tokenRecord.userId), isNull(users.deletedAt)))
159
- .limit(1)
180
+ userRecord = await insertUserSql(db, {
181
+ id: userId,
182
+ google_id: googleUser.sub,
183
+ email: googleUser.email,
184
+ name: googleUser.name,
185
+ avatar_url: googleUser.picture ?? null,
186
+ status: 'active',
187
+ is_super_admin: shouldBeSuperAdmin ? 1 : 0,
188
+ created_at: now,
189
+ updated_at: now,
190
+ })
160
191
 
161
- const userRecord = userResults.at(0)
162
- if (userRecord?.status !== 'active') {
163
- throw new UnauthorizedError('User not found or inactive')
192
+ if (!userRecord) {
193
+ throw new Error('Failed to create user')
164
194
  }
165
195
 
166
- // Generate new access token
167
- const accessToken = await createAccessToken(env, userRecord.id, userRecord.email)
196
+ const accountId = crypto.randomUUID()
197
+ await execute(
198
+ db,
199
+ `INSERT INTO accounts (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
200
+ [accountId, `${googleUser.name}'s Account`, now, now]
201
+ )
168
202
 
169
- // Log token refresh event
170
- await logAuthEvent(db, ctx, 'TOKEN_REFRESH', userRecord.id, {
203
+ await execute(
204
+ db,
205
+ `INSERT INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)`,
206
+ [userRecord.id, accountId, 'EDITOR']
207
+ )
208
+
209
+ await logAuthEvent(db, ctx, 'SIGNUP', userRecord.id, {
171
210
  email: userRecord.email,
211
+ provider: 'google',
212
+ accountId,
172
213
  })
214
+ }
173
215
 
174
- return {
216
+ const accessToken = await createAccessToken(env, userRecord.id, userRecord.email)
217
+ const refreshToken = generateRefreshToken()
218
+ const tokenHash = await hashToken(refreshToken)
219
+ const expiresAt = toEpochSeconds(getRefreshTokenExpiry(env))
220
+ const createdAt = Math.floor(Date.now() / 1000)
221
+
222
+ await execute(
223
+ db,
224
+ `INSERT INTO refresh_tokens (id, user_id, token_hash, expires_at, created_at)
225
+ VALUES (?, ?, ?, ?, ?)`,
226
+ [crypto.randomUUID(), userRecord.id, tokenHash, expiresAt, createdAt]
227
+ )
228
+
229
+ await logAuthEvent(db, ctx, 'LOGIN', userRecord.id, {
230
+ email: userRecord.email,
231
+ provider: 'google',
232
+ isNewUser,
233
+ })
234
+
235
+ return {
236
+ user: toUser(userRecord),
237
+ tokens: {
175
238
  accessToken,
176
239
  expiresIn: 60 * 15,
177
- }
178
- },
240
+ },
241
+ refreshToken,
242
+ isNewUser,
243
+ }
244
+ }
245
+
246
+ async function refreshAccessTokenSql(
247
+ db: D1Database,
248
+ env: Env,
249
+ refreshToken: string,
250
+ ctx: AuthEventContext
251
+ ): Promise<AuthTokens> {
252
+ const tokenHash = await hashToken(refreshToken)
253
+ const now = Math.floor(Date.now() / 1000)
254
+
255
+ const tokenRecord = await queryOne(
256
+ db,
257
+ `SELECT id, user_id as userId, token_hash as tokenHash
258
+ FROM refresh_tokens
259
+ WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?
260
+ LIMIT 1`,
261
+ [tokenHash, now]
262
+ )
263
+
264
+ if (!tokenRecord) {
265
+ throw new UnauthorizedError('Invalid or expired refresh token')
266
+ }
267
+
268
+ const userRecord = await selectUserById(db, String(tokenRecord.userId ?? ''))
269
+ if (!userRecord || userRecord.status !== 'active') {
270
+ throw new UnauthorizedError('User not found or inactive')
271
+ }
272
+
273
+ const accessToken = await createAccessToken(env, userRecord.id, userRecord.email)
274
+
275
+ await logAuthEvent(db, ctx, 'TOKEN_REFRESH', userRecord.id, {
276
+ email: userRecord.email,
277
+ })
278
+
279
+ return {
280
+ accessToken,
281
+ expiresIn: 60 * 15,
282
+ }
283
+ }
284
+
285
+ async function revokeRefreshTokenSql(
286
+ db: D1Database,
287
+ refreshToken: string
288
+ ): Promise<void> {
289
+ const tokenHash = await hashToken(refreshToken)
290
+ const revokedAt = Math.floor(Date.now() / 1000)
179
291
 
180
- async revokeRefreshToken(db: Database, refreshToken: string, ctx: AuthEventContext, userId: string | null): Promise<void> {
181
- const tokenHash = await hashToken(refreshToken)
292
+ await execute(
293
+ db,
294
+ `UPDATE refresh_tokens SET revoked_at = ? WHERE token_hash = ?`,
295
+ [revokedAt, tokenHash]
296
+ )
297
+ }
298
+
299
+ async function revokeAllUserTokensSql(db: D1Database, userId: string): Promise<void> {
300
+ const revokedAt = Math.floor(Date.now() / 1000)
301
+
302
+ await execute(
303
+ db,
304
+ `UPDATE refresh_tokens SET revoked_at = ? WHERE user_id = ? AND revoked_at IS NULL`,
305
+ [revokedAt, userId]
306
+ )
307
+ }
308
+
309
+ async function getCurrentUserSql(db: D1Database, userId: string): Promise<User> {
310
+ const userRecord = await selectUserById(db, userId)
311
+ if (!userRecord) {
312
+ throw new UnauthorizedError('User not found')
313
+ }
314
+ return toUser(userRecord)
315
+ }
316
+
317
+ export const authService = {
318
+ async findOrCreateUser(
319
+ db: D1Database,
320
+ env: Env,
321
+ googleUser: GoogleUserInfo,
322
+ ctx: AuthEventContext
323
+ ): Promise<AuthResult> {
324
+ return findOrCreateUserSql(db, env, googleUser, ctx)
325
+ },
182
326
 
183
- await db
184
- .update(refreshTokens)
185
- .set({ revokedAt: new Date() })
186
- .where(eq(refreshTokens.tokenHash, tokenHash))
327
+ async refreshAccessToken(
328
+ db: D1Database,
329
+ env: Env,
330
+ refreshToken: string,
331
+ ctx: AuthEventContext
332
+ ): Promise<AuthTokens> {
333
+ return refreshAccessTokenSql(db, env, refreshToken, ctx)
334
+ },
187
335
 
188
- // Log logout event if userId provided
336
+ async revokeRefreshToken(
337
+ db: D1Database,
338
+ refreshToken: string,
339
+ ctx: AuthEventContext,
340
+ userId: string | null
341
+ ): Promise<void> {
342
+ await revokeRefreshTokenSql(db, refreshToken)
189
343
  if (userId) {
190
344
  await logAuthEvent(db, ctx, 'LOGOUT', userId, {})
191
345
  }
192
346
  },
193
347
 
194
- async revokeAllUserTokens(db: Database, userId: string): Promise<void> {
195
- await db
196
- .update(refreshTokens)
197
- .set({ revokedAt: new Date() })
198
- .where(and(eq(refreshTokens.userId, userId), isNull(refreshTokens.revokedAt)))
348
+ async revokeAllUserTokens(db: D1Database, userId: string): Promise<void> {
349
+ await revokeAllUserTokensSql(db, userId)
199
350
  },
200
351
 
201
- async getCurrentUser(db: Database, userId: string): Promise<User> {
202
- const results = await db
203
- .select()
204
- .from(users)
205
- .where(and(eq(users.id, userId), isNull(users.deletedAt)))
206
- .limit(1)
207
-
208
- const userRecord = results.at(0)
209
- if (!userRecord) {
210
- throw new UnauthorizedError('User not found')
211
- }
212
-
213
- return {
214
- id: userRecord.id,
215
- email: userRecord.email,
216
- name: userRecord.name,
217
- status: userRecord.status,
218
- providerIds: userRecord.providerIds ?? [],
219
- isSuperAdmin: userRecord.isSuperAdmin,
220
- createdAt: userRecord.createdAt,
221
- updatedAt: userRecord.updatedAt,
222
- deletedAt: userRecord.deletedAt,
223
- }
352
+ async getCurrentUser(db: D1Database, userId: string): Promise<User> {
353
+ return getCurrentUserSql(db, userId)
224
354
  },
225
355
  }