@etus/bhono-app 0.1.5 → 0.1.7

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 (300) hide show
  1. package/dist/index.js +0 -0
  2. package/package.json +7 -2
  3. package/templates/base/.claude/commands/check-skill-rules.md +112 -29
  4. package/templates/base/.claude/commands/linear/implement-issue.md +383 -55
  5. package/templates/base/.claude/commands/ship.md +77 -13
  6. package/templates/base/.claude/hooks/package-lock.json +0 -419
  7. package/templates/base/.claude/hooks/skill-activation-prompt.ts +185 -113
  8. package/templates/base/.claude/hooks/skill-tool-guard.sh +6 -0
  9. package/templates/base/.claude/hooks/skill-tool-guard.ts +198 -0
  10. package/templates/base/.claude/scripts/validate-skill-rules.sh +55 -32
  11. package/templates/base/.claude/settings.json +18 -11
  12. package/templates/base/.claude/skills/skill-rules.json +326 -173
  13. package/templates/base/.env.example +3 -0
  14. package/templates/base/CLAUDE.md +5 -5
  15. package/templates/base/README.md +40 -27
  16. package/templates/base/config/eslint.config.js +1 -0
  17. package/templates/base/config/wrangler.json +16 -17
  18. package/templates/base/docs/SETUP-GUIDE.md +566 -0
  19. package/templates/base/docs/app_spec.txt +13 -10
  20. package/templates/base/docs/architecture/README.md +162 -5
  21. package/templates/base/docs/architecture/api-catalog.md +575 -0
  22. package/templates/base/docs/architecture/c4-component.md +309 -0
  23. package/templates/base/docs/architecture/c4-container.md +183 -0
  24. package/templates/base/docs/architecture/c4-context.md +106 -0
  25. package/templates/base/docs/architecture/data-requirements.md +4 -3
  26. package/templates/base/docs/architecture/db-bootstrap.md +39 -0
  27. package/templates/base/docs/architecture/dependencies.md +327 -0
  28. package/templates/base/docs/architecture/drizzle-migration-plan.md +125 -0
  29. package/templates/base/docs/architecture/erd.md +1 -1
  30. package/templates/base/docs/architecture/sql-standards.md +100 -0
  31. package/templates/base/docs/architecture/tech-debt.md +184 -0
  32. package/templates/base/docs/testing.md +36 -29
  33. package/templates/base/package.json +26 -20
  34. package/templates/base/schema.sql +84 -0
  35. package/templates/base/scripts/capture-prod-session.ts +2 -2
  36. package/templates/base/scripts/init.sh +244 -59
  37. package/templates/base/scripts/sync-template.sh +104 -0
  38. package/templates/base/src/client/hooks/use-auth.ts +5 -0
  39. package/templates/base/src/client/routes/_authenticated/dashboard.tsx +1 -1
  40. package/templates/base/src/client/routes/index.tsx +1 -1
  41. package/templates/base/src/server/db/client.ts +3 -5
  42. package/templates/base/src/server/db/records.ts +81 -0
  43. package/templates/base/src/server/db/seed.ts +3 -2
  44. package/templates/base/src/server/db/sql.ts +116 -0
  45. package/templates/base/src/server/index.ts +17 -2
  46. package/templates/base/src/server/lib/audit.ts +74 -26
  47. package/templates/base/src/server/lib/audited-db.ts +219 -109
  48. package/templates/base/src/server/lib/transaction.ts +10 -16
  49. package/templates/base/src/server/middleware/account.ts +9 -16
  50. package/templates/base/src/server/middleware/auth.ts +102 -38
  51. package/templates/base/src/server/middleware/rate-limit.ts +8 -6
  52. package/templates/base/src/server/routes/accounts/handlers.ts +18 -6
  53. package/templates/base/src/server/routes/audits/handlers.ts +3 -1
  54. package/templates/base/src/server/routes/auth/handlers.ts +15 -10
  55. package/templates/base/src/server/routes/auth/test-login.ts +99 -45
  56. package/templates/base/src/server/routes/health/handlers.ts +4 -4
  57. package/templates/base/src/server/routes/index.ts +9 -0
  58. package/templates/base/src/server/routes/invitations/handlers.ts +9 -6
  59. package/templates/base/src/server/routes/openapi.ts +1 -1
  60. package/templates/base/src/server/routes/users/handlers.ts +21 -14
  61. package/templates/base/src/server/services/accounts.ts +242 -217
  62. package/templates/base/src/server/services/audits.ts +114 -61
  63. package/templates/base/src/server/services/auth.ts +310 -180
  64. package/templates/base/src/server/services/invitations.ts +282 -222
  65. package/templates/base/src/server/services/users.ts +383 -293
  66. package/templates/base/src/server/types/index.ts +1 -2
  67. package/templates/base/src/shared/types/api.ts +66 -198
  68. package/templates/base/tests/e2e/auth.setup.ts +1 -1
  69. package/templates/base/{src/server/__tests__/fixtures.ts → tests/fixtures/server.ts} +3 -3
  70. package/templates/base/{src/client/__tests__/setup-browser.ts → tests/helpers/client-setup-browser.ts} +2 -2
  71. package/templates/base/{src/client/__tests__/setup.ts → tests/helpers/client-setup.ts} +1 -1
  72. package/templates/base/{src/client/__tests__/test-utils.tsx → tests/helpers/client-test-utils.tsx} +2 -2
  73. package/templates/base/{src/server/__tests__/setup.ts → tests/helpers/server.ts} +9 -9
  74. package/templates/base/tests/integration/accounts/crud.test.ts +2 -11
  75. package/templates/base/tests/integration/audits/list.test.ts +2 -11
  76. package/templates/base/tests/integration/auth/auth-service.test.ts +1 -10
  77. package/templates/base/tests/integration/auth/invitation-token.test.ts +2 -11
  78. package/templates/base/tests/integration/auth/logout.test.ts +2 -11
  79. package/templates/base/tests/integration/auth/oauth.test.ts +23 -42
  80. package/templates/base/tests/integration/auth/refresh-token.test.ts +1 -9
  81. package/templates/base/tests/integration/auth/session-expiry.test.ts +1 -9
  82. package/templates/base/tests/integration/auth/session.test.ts +2 -11
  83. package/templates/base/tests/integration/auth/super-admin.test.ts +1 -9
  84. package/templates/base/tests/integration/authorization/analytics-role.test.ts +2 -11
  85. package/templates/base/tests/integration/authorization/billing-role.test.ts +2 -11
  86. package/templates/base/tests/integration/authorization/guards-roles.test.ts +1 -9
  87. package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +2 -11
  88. package/templates/base/tests/integration/authorization/roles.test.ts +2 -11
  89. package/templates/base/tests/integration/config/production-behavior.test.ts +2 -11
  90. package/templates/base/tests/integration/health/health.test.ts +25 -44
  91. package/templates/base/tests/integration/invitations/crud.test.ts +2 -11
  92. package/templates/base/tests/integration/invitations/email.test.ts +1 -9
  93. package/templates/base/tests/integration/middleware/auth.test.ts +3 -12
  94. package/templates/base/tests/integration/middleware/request-logger.test.ts +1 -9
  95. package/templates/base/tests/integration/performance/response-times.test.ts +1 -9
  96. package/templates/base/tests/integration/security/cookie-security.test.ts +2 -11
  97. package/templates/base/tests/integration/security/csrf-protection.test.ts +2 -11
  98. package/templates/base/tests/integration/security/log-sanitization.test.ts +1 -9
  99. package/templates/base/tests/integration/security/rate-limiting.test.ts +1 -9
  100. package/templates/base/tests/integration/security/sql-injection.test.ts +7 -18
  101. package/templates/base/tests/integration/security/xss-prevention.test.ts +2 -11
  102. package/templates/base/tests/integration/setup.ts +13 -90
  103. package/templates/base/tests/integration/smoke.test.ts +3 -2
  104. package/templates/base/tests/integration/storage/upload.test.ts +2 -11
  105. package/templates/base/tests/integration/storage/validation.test.ts +2 -11
  106. package/templates/base/tests/integration/users/crud.test.ts +2 -11
  107. package/templates/base/tests/integration/users/list.test.ts +2 -11
  108. package/templates/base/tests/integration/vitest.config.ts +2 -9
  109. package/templates/base/{src/server/__tests__ → tests}/mocks/db.ts +1 -1
  110. package/templates/base/{src/server/__tests__ → tests}/mocks/index.ts +1 -1
  111. package/templates/base/{src/server/__tests__ → tests}/mocks/kv.ts +1 -1
  112. package/templates/base/{src/server/__tests__ → tests}/mocks/r2.ts +1 -1
  113. package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/sidebar.test.tsx +1 -1
  114. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/avatar.test.tsx +1 -1
  115. package/templates/base/{src/client/__tests__ → tests/unit/client/components/ui}/button.test.tsx +1 -1
  116. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/card.test.tsx +1 -1
  117. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/dialog.test.tsx +1 -1
  118. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/input.test.tsx +1 -1
  119. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/loading-skeleton.test.tsx +1 -1
  120. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/skeleton.test.tsx +1 -1
  121. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/sonner.test.tsx +1 -1
  122. package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/tabs.test.tsx +1 -1
  123. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/account.test.tsx +1 -1
  124. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/integrations.test.tsx +1 -1
  125. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/settings.test.tsx +1 -1
  126. package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/team.test.tsx +1 -1
  127. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/authenticated-layout.test.tsx +1 -1
  128. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/dashboard.test.tsx +1 -1
  129. package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/invite.test.tsx +1 -1
  130. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/login.test.tsx +1 -1
  131. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/navigation.test.tsx +1 -1
  132. package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/root-layout.test.tsx +1 -1
  133. package/templates/base/{src/server/auth/__tests__ → tests/unit/server/auth}/guards.test.ts +2 -2
  134. package/templates/base/{src → tests/unit}/server/auth/permissions.test.ts +1 -1
  135. package/templates/base/{src → tests/unit}/server/auth/roles.test.ts +1 -1
  136. package/templates/base/tests/unit/server/db/sql.test.ts +68 -0
  137. package/templates/base/{src → tests/unit}/server/env.test.ts +1 -1
  138. package/templates/base/tests/unit/server/lib/audited-db.test.ts +78 -0
  139. package/templates/base/{src → tests/unit}/server/lib/email.test.ts +1 -1
  140. package/templates/base/{src → tests/unit}/server/lib/errors.test.ts +1 -1
  141. package/templates/base/{src → tests/unit}/server/lib/oauth.test.ts +1 -1
  142. package/templates/base/{src → tests/unit}/server/lib/pagination.test.ts +1 -1
  143. package/templates/base/{src → tests/unit}/server/lib/password.test.ts +1 -1
  144. package/templates/base/{src → tests/unit}/server/lib/providers.test.ts +1 -1
  145. package/templates/base/{src → tests/unit}/server/lib/r2-storage.test.ts +2 -2
  146. package/templates/base/{src → tests/unit}/server/lib/session.test.ts +2 -2
  147. package/templates/base/{src → tests/unit}/server/lib/tokens.test.ts +1 -1
  148. package/templates/base/{src → tests/unit}/server/lib/transaction.test.ts +5 -14
  149. package/templates/base/{src → tests/unit}/server/middleware/account.test.ts +16 -24
  150. package/templates/base/tests/unit/server/middleware/auth.test.ts +647 -0
  151. package/templates/base/{src → tests/unit}/server/middleware/cors.test.ts +1 -1
  152. package/templates/base/{src → tests/unit}/server/middleware/error-handler.test.ts +2 -2
  153. package/templates/base/{src → tests/unit}/server/middleware/rate-limit.test.ts +3 -2
  154. package/templates/base/{src → tests/unit}/server/middleware/request-context.test.ts +1 -1
  155. package/templates/base/{src → tests/unit}/server/middleware/request-logger.test.ts +1 -1
  156. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/db.test.ts +1 -1
  157. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/kv.test.ts +1 -1
  158. package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/r2.test.ts +1 -1
  159. package/templates/base/{src/server/routes/accounts/__tests__ → tests/unit/server/routes/accounts}/handlers.test.ts +12 -12
  160. package/templates/base/{src/server/routes/audits/__tests__ → tests/unit/server/routes/audits}/handlers.test.ts +11 -11
  161. package/templates/base/{src/server/routes/auth/__tests__ → tests/unit/server/routes/auth}/handlers.test.ts +124 -13
  162. package/templates/base/{src/server/routes/health/__tests__ → tests/unit/server/routes/health}/handlers.test.ts +27 -23
  163. package/templates/base/{src/server/routes/invitations/__tests__ → tests/unit/server/routes/invitations}/handlers.test.ts +14 -17
  164. package/templates/base/{src/server/routes/storage/__tests__ → tests/unit/server/routes/storage}/handlers.test.ts +6 -6
  165. package/templates/base/{src/server/routes/users/__tests__ → tests/unit/server/routes/users}/handlers.test.ts +81 -17
  166. package/templates/base/tests/unit/server/services/accounts.test.ts +406 -0
  167. package/templates/base/tests/unit/server/services/audits.test.ts +360 -0
  168. package/templates/base/tests/unit/server/services/auth.test.ts +656 -0
  169. package/templates/base/tests/unit/server/services/invitations.test.ts +343 -0
  170. package/templates/base/tests/unit/server/services/users.test.ts +706 -0
  171. package/templates/base/{src/shared/schemas/__tests__ → tests/unit/shared}/schemas.test.ts +1 -1
  172. package/templates/base/tsconfig.json +2 -1
  173. package/templates/base/vite.config.ts +3 -1
  174. package/templates/base/vitest.config.browser.ts +3 -2
  175. package/templates/base/vitest.config.frontend.ts +3 -2
  176. package/templates/base/vitest.config.ts +7 -14
  177. package/templates/base/.claude/settings.local.json +0 -11
  178. package/templates/base/.github/workflows/test.yml +0 -127
  179. package/templates/base/auth-setup-error.png +0 -0
  180. package/templates/base/config/drizzle.config.ts +0 -10
  181. package/templates/base/pnpm-lock.yaml +0 -8175
  182. package/templates/base/src/server/db/schema/accounts.ts +0 -20
  183. package/templates/base/src/server/db/schema/audit-logs.ts +0 -26
  184. package/templates/base/src/server/db/schema/index.ts +0 -7
  185. package/templates/base/src/server/db/schema/invitations.ts +0 -30
  186. package/templates/base/src/server/db/schema/refresh-tokens.ts +0 -22
  187. package/templates/base/src/server/db/schema/user-accounts.ts +0 -25
  188. package/templates/base/src/server/db/schema/users.ts +0 -33
  189. package/templates/base/src/server/lib/audited-db.test.ts +0 -107
  190. package/templates/base/src/server/lib/schema-helpers.ts +0 -16
  191. package/templates/base/src/server/middleware/auth.test.ts +0 -345
  192. package/templates/base/src/server/services/__tests__/accounts.test.ts +0 -764
  193. package/templates/base/src/server/services/__tests__/audits.test.ts +0 -235
  194. package/templates/base/src/server/services/__tests__/auth.test.ts +0 -765
  195. package/templates/base/src/server/services/__tests__/invitations.test.ts +0 -704
  196. package/templates/base/src/server/services/__tests__/users.test.ts +0 -755
  197. package/templates/base/tests/e2e/_auth/.gitkeep +0 -0
  198. package/templates/base/tests/integration/lib/schema-helpers.test.ts +0 -129
  199. package/templates/base/tsconfig.tsbuildinfo +0 -1
  200. /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
  201. /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
  202. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
  203. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
  204. /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
  205. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
  206. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
  207. /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
  208. /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
  209. /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
  210. /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/error-boundary.test.tsx +0 -0
  211. /package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/error-fallback.test.tsx +0 -0
  212. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-auth.test.tsx +0 -0
  213. /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-theme.test.tsx +0 -0
  214. /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
  215. /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
  216. /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
  217. /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
  218. /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
  219. /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
  220. /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
  221. /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
  222. /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
  223. /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
  224. /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
  225. /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
  226. /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
  227. /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
  228. /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
  229. /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
  230. /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
  231. /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
  232. /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
  233. /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
  234. /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
  235. /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
  236. /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
  237. /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
  238. /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
  239. /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
  240. /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
  241. /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
  242. /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
  243. /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
  244. /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
  245. /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
  246. /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
  247. /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
  248. /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
  249. /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
  250. /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
  251. /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
  252. /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
  253. /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
  254. /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
  255. /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
  256. /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
  257. /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
  258. /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
  259. /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
  260. /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
  261. /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
  262. /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
  263. /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
  264. /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
  265. /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
  266. /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
  267. /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
  268. /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
  269. /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
  270. /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
  271. /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
  272. /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
  273. /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
  274. /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
  275. /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
  276. /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
  277. /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
  278. /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
  279. /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
  280. /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
  281. /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
  282. /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
  283. /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
  284. /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
  285. /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
  286. /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
  287. /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
  288. /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
  289. /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
  290. /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
  291. /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
  292. /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
  293. /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
  294. /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
  295. /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
  296. /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
  297. /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
  298. /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
  299. /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
  300. /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/error-components.test.tsx +0 -0
@@ -1,13 +1,11 @@
1
1
  // src/services/invitations.ts
2
- import { eq, and, isNull, gt } from 'drizzle-orm'
3
- import type { Database } from '../db/client'
4
- import { invitations, users, userAccounts, accounts } from '../db/schema'
5
2
  import { sendInvitationEmail } from '../lib/email'
6
3
  import { logAuthEvent, type AuthEventContext } from '../lib/audit'
7
4
  import { ConflictError, NotFoundError, ForbiddenError } from '../lib/errors'
8
5
  import type { Env } from '../env'
9
6
  import { hasMinimumRole, type Role } from '../auth/roles'
10
7
  import type { ServiceContext } from '../types'
8
+ import { execute, queryAll, queryOne, toStringValue, type SqlRow } from '../db/sql'
11
9
 
12
10
  function generateToken(): string {
13
11
  const array = new Uint8Array(32)
@@ -44,133 +42,295 @@ interface InvitationResult {
44
42
  }
45
43
  }
46
44
 
47
- export const invitationsService = {
48
- async create(db: Database, env: Env, ctx: ServiceContext, input: CreateInvitationInput): Promise<InvitationResult> {
49
- const { email, role } = input
50
45
 
51
- // Check inviter has a role in this account
52
- if (!ctx.userRole) {
53
- throw new ForbiddenError('User must have a role in this account to invite others')
54
- }
46
+ function mapInvitationRow(row: SqlRow): {
47
+ id: string
48
+ email: string
49
+ role: Role
50
+ invitedById: string
51
+ inviterName: string
52
+ expiresAt: string
53
+ createdAt: string
54
+ } {
55
+ const invitedById = row.invitedById ?? row.invited_by_id
56
+ const inviterName = row.inviterName ?? row.inviter_name
57
+ const expiresAt = row.expiresAt ?? row.expires_at
58
+ const createdAt = row.createdAt ?? row.created_at
59
+
60
+ return {
61
+ id: toStringValue(row.id),
62
+ email: toStringValue(row.email),
63
+ role: toStringValue(row.role) as Role,
64
+ invitedById: toStringValue(invitedById),
65
+ inviterName: toStringValue(inviterName),
66
+ expiresAt: toStringValue(expiresAt),
67
+ createdAt: toStringValue(createdAt),
68
+ }
69
+ }
55
70
 
56
- // Check inviter can assign this role (can't assign higher than own role)
57
- if (!hasMinimumRole(ctx.userRole, role)) {
58
- throw new ForbiddenError('Cannot assign a role higher than your own')
59
- }
71
+ async function createSql(
72
+ db: D1Database,
73
+ env: Env,
74
+ ctx: ServiceContext,
75
+ input: CreateInvitationInput
76
+ ): Promise<InvitationResult> {
77
+ const { email, role } = input
60
78
 
61
- // Check if user already in this account
62
- const membershipResults = await db
63
- .select()
64
- .from(userAccounts)
65
- .innerJoin(users, eq(users.id, userAccounts.userId))
66
- .where(
67
- and(
68
- eq(userAccounts.accountId, ctx.accountId),
69
- eq(users.email, email),
70
- isNull(users.deletedAt)
71
- )
72
- )
73
- .limit(1)
74
-
75
- if (membershipResults.at(0)) {
76
- throw new ConflictError('User is already a member of this account')
77
- }
79
+ if (!ctx.userRole) {
80
+ throw new ForbiddenError('User must have a role in this account to invite others')
81
+ }
78
82
 
79
- // Check if user exists in system
80
- const existingUserResults = await db
81
- .select()
82
- .from(users)
83
- .where(and(eq(users.email, email), isNull(users.deletedAt)))
84
- .limit(1)
85
-
86
- const existingUser = existingUserResults.at(0)
87
- if (existingUser) {
88
- // Link immediately
89
- await db.insert(userAccounts).values({
90
- userId: existingUser.id,
91
- accountId: ctx.accountId,
92
- role,
93
- })
94
-
95
- return {
96
- linked: true,
97
- invited: false,
98
- user: {
99
- id: existingUser.id,
100
- email: existingUser.email,
101
- name: existingUser.name,
102
- },
103
- }
104
- }
83
+ if (!hasMinimumRole(ctx.userRole, role)) {
84
+ throw new ForbiddenError('Cannot assign a role higher than your own')
85
+ }
105
86
 
106
- // Check for existing pending invitation
107
- const existingInvitationResults = await db
108
- .select()
109
- .from(invitations)
110
- .where(
111
- and(
112
- eq(invitations.accountId, ctx.accountId),
113
- eq(invitations.email, email),
114
- isNull(invitations.acceptedAt),
115
- gt(invitations.expiresAt, new Date().toISOString())
116
- )
117
- )
118
- .limit(1)
119
-
120
- if (existingInvitationResults.at(0)) {
121
- throw new ConflictError('Pending invitation already exists for this email')
122
- }
87
+ const membership = await queryOne(
88
+ db,
89
+ `SELECT 1 as ok
90
+ FROM user_accounts ua
91
+ INNER JOIN users u ON u.id = ua.user_id
92
+ WHERE ua.account_id = ? AND u.email = ? AND u.deleted_at IS NULL
93
+ LIMIT 1`,
94
+ [ctx.accountId, email]
95
+ )
96
+
97
+ if (membership) {
98
+ throw new ConflictError('User is already a member of this account')
99
+ }
123
100
 
124
- // Get account name for email
125
- const accountResults = await db
126
- .select()
127
- .from(accounts)
128
- .where(eq(accounts.id, ctx.accountId))
129
- .limit(1)
101
+ const existingUser = await queryOne<{
102
+ id: string
103
+ email: string
104
+ name: string
105
+ }>(
106
+ db,
107
+ `SELECT id, email, name FROM users WHERE email = ? AND deleted_at IS NULL LIMIT 1`,
108
+ [email]
109
+ )
110
+
111
+ if (existingUser) {
112
+ await execute(
113
+ db,
114
+ `INSERT INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)`,
115
+ [existingUser.id, ctx.accountId, role]
116
+ )
130
117
 
131
- const account = accountResults.at(0)
132
- if (!account) {
133
- throw new Error('Account not found')
118
+ return {
119
+ linked: true,
120
+ invited: false,
121
+ user: {
122
+ id: existingUser.id,
123
+ email: existingUser.email,
124
+ name: existingUser.name,
125
+ },
134
126
  }
127
+ }
135
128
 
136
- // Create invitation
137
- const token = generateToken()
138
- const expiresAt = getExpiryDate()
139
-
140
- const insertResults = await db
141
- .insert(invitations)
142
- .values({
143
- accountId: ctx.accountId,
144
- email,
145
- role,
146
- token,
147
- invitedById: ctx.user.id,
148
- expiresAt,
149
- })
150
- .returning()
151
-
152
- const invitation = insertResults.at(0)
153
- if (!invitation) {
154
- throw new Error('Failed to create invitation')
155
- }
129
+ const existingInvitation = await queryOne(
130
+ db,
131
+ `SELECT id FROM invitations
132
+ WHERE account_id = ? AND email = ? AND accepted_at IS NULL AND expires_at > ?
133
+ LIMIT 1`,
134
+ [ctx.accountId, email, new Date().toISOString()]
135
+ )
156
136
 
157
- // Send email
158
- const inviteUrl = `${env.APP_URL}/auth/invite/${token}`
159
- await sendInvitationEmail(env, email, ctx.user.name, account.name, inviteUrl)
137
+ if (existingInvitation) {
138
+ throw new ConflictError('Pending invitation already exists for this email')
139
+ }
140
+
141
+ const account = await queryOne<{ name: string }>(
142
+ db,
143
+ `SELECT name FROM accounts WHERE id = ? LIMIT 1`,
144
+ [ctx.accountId]
145
+ )
146
+
147
+ if (!account) {
148
+ throw new Error('Account not found')
149
+ }
150
+
151
+ const token = generateToken()
152
+ const expiresAt = getExpiryDate()
153
+ const invitationId = crypto.randomUUID()
154
+
155
+ await execute(
156
+ db,
157
+ `INSERT INTO invitations (
158
+ id,
159
+ account_id,
160
+ email,
161
+ role,
162
+ token,
163
+ invited_by_id,
164
+ expires_at
165
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)`,
166
+ [invitationId, ctx.accountId, email, role, token, ctx.user.id, expiresAt]
167
+ )
168
+
169
+ const inviteUrl = `${env.APP_URL}/auth/invite/${token}`
170
+ await sendInvitationEmail(env, email, ctx.user.name, account.name, inviteUrl)
171
+
172
+ return {
173
+ linked: false,
174
+ invited: true,
175
+ invitation: {
176
+ id: invitationId,
177
+ email,
178
+ role,
179
+ expiresAt,
180
+ },
181
+ }
182
+ }
160
183
 
184
+ async function listSql(
185
+ db: D1Database,
186
+ ctx: ServiceContext
187
+ ): Promise<{
188
+ id: string
189
+ email: string
190
+ role: Role
191
+ invitedBy: { id: string; name: string }
192
+ expiresAt: string
193
+ createdAt: string
194
+ }[]> {
195
+ const rows = await queryAll(
196
+ db,
197
+ `SELECT
198
+ i.id,
199
+ i.email,
200
+ i.role,
201
+ i.expires_at as expiresAt,
202
+ i.created_at as createdAt,
203
+ i.invited_by_id as invitedById,
204
+ u.name as inviterName
205
+ FROM invitations i
206
+ INNER JOIN users u ON u.id = i.invited_by_id
207
+ WHERE i.account_id = ? AND i.accepted_at IS NULL AND i.expires_at > ?
208
+ ORDER BY i.created_at`,
209
+ [ctx.accountId, new Date().toISOString()]
210
+ )
211
+
212
+ return rows.map((row) => {
213
+ const mapped = mapInvitationRow(row)
161
214
  return {
162
- linked: false,
163
- invited: true,
164
- invitation: {
165
- id: invitation.id,
166
- email: invitation.email,
167
- role: invitation.role as Role,
168
- expiresAt: invitation.expiresAt,
169
- },
215
+ id: mapped.id,
216
+ email: mapped.email,
217
+ role: mapped.role,
218
+ invitedBy: { id: mapped.invitedById, name: mapped.inviterName },
219
+ expiresAt: mapped.expiresAt,
220
+ createdAt: mapped.createdAt,
170
221
  }
222
+ })
223
+ }
224
+
225
+ async function revokeSql(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
226
+ const invitation = await queryOne(
227
+ db,
228
+ `SELECT id FROM invitations
229
+ WHERE id = ? AND account_id = ? AND accepted_at IS NULL
230
+ LIMIT 1`,
231
+ [id, ctx.accountId]
232
+ )
233
+
234
+ if (!invitation) {
235
+ throw new NotFoundError('Invitation')
236
+ }
237
+
238
+ await execute(db, `DELETE FROM invitations WHERE id = ?`, [id])
239
+ }
240
+
241
+ async function getByTokenSql(
242
+ db: D1Database,
243
+ token: string
244
+ ): Promise<{
245
+ id: string
246
+ accountId: string
247
+ email: string
248
+ role: Role
249
+ accountName: string
250
+ } | null> {
251
+ const row = await queryOne(
252
+ db,
253
+ `SELECT
254
+ i.id,
255
+ i.account_id as accountId,
256
+ i.email,
257
+ i.role,
258
+ i.expires_at as expiresAt,
259
+ i.accepted_at as acceptedAt,
260
+ a.name as accountName
261
+ FROM invitations i
262
+ INNER JOIN accounts a ON a.id = i.account_id
263
+ WHERE i.token = ?
264
+ LIMIT 1`,
265
+ [token]
266
+ )
267
+
268
+ if (!row) return null
269
+
270
+ const acceptedAt = row.acceptedAt ?? row.accepted_at
271
+ if (acceptedAt) return null
272
+
273
+ const expiresAtValue = row.expiresAt ?? row.expires_at
274
+ if (expiresAtValue && new Date(toStringValue(expiresAtValue)) < new Date()) return null
275
+
276
+ return {
277
+ id: toStringValue(row.id),
278
+ accountId: toStringValue(row.accountId ?? row.account_id),
279
+ email: toStringValue(row.email),
280
+ role: toStringValue(row.role) as Role,
281
+ accountName: toStringValue(row.accountName ?? row.account_name),
282
+ }
283
+ }
284
+
285
+ async function acceptSql(
286
+ db: D1Database,
287
+ invitationId: string,
288
+ userId: string,
289
+ ctx: AuthEventContext
290
+ ): Promise<void> {
291
+ const invitation = await queryOne(
292
+ db,
293
+ `SELECT id, account_id as accountId, role FROM invitations WHERE id = ? LIMIT 1`,
294
+ [invitationId]
295
+ )
296
+
297
+ if (!invitation) {
298
+ throw new NotFoundError('Invitation')
299
+ }
300
+
301
+ const accountId = toStringValue(invitation.accountId ?? invitation.account_id)
302
+ const role = toStringValue(invitation.role) as Role
303
+
304
+ await execute(
305
+ db,
306
+ `INSERT INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)`,
307
+ [userId, accountId, role]
308
+ )
309
+
310
+ await execute(
311
+ db,
312
+ `UPDATE invitations SET accepted_at = ? WHERE id = ?`,
313
+ [new Date().toISOString(), invitationId]
314
+ )
315
+
316
+ await logAuthEvent(db, ctx, 'LOGIN', userId, {
317
+ invitationAccepted: true,
318
+ accountId,
319
+ role,
320
+ })
321
+ }
322
+
323
+ export const invitationsService = {
324
+ async create(
325
+ db: D1Database,
326
+ env: Env,
327
+ ctx: ServiceContext,
328
+ input: CreateInvitationInput
329
+ ): Promise<InvitationResult> {
330
+ return createSql(db, env, ctx, input)
171
331
  },
172
332
 
173
- async list(db: Database, ctx: ServiceContext): Promise<{
333
+ async list(db: D1Database, ctx: ServiceContext): Promise<{
174
334
  id: string
175
335
  email: string
176
336
  role: Role
@@ -178,129 +338,29 @@ export const invitationsService = {
178
338
  expiresAt: string
179
339
  createdAt: string
180
340
  }[]> {
181
- const results = await db
182
- .select({
183
- id: invitations.id,
184
- email: invitations.email,
185
- role: invitations.role,
186
- expiresAt: invitations.expiresAt,
187
- createdAt: invitations.createdAt,
188
- invitedById: invitations.invitedById,
189
- inviterName: users.name,
190
- })
191
- .from(invitations)
192
- .innerJoin(users, eq(users.id, invitations.invitedById))
193
- .where(
194
- and(
195
- eq(invitations.accountId, ctx.accountId),
196
- isNull(invitations.acceptedAt),
197
- gt(invitations.expiresAt, new Date().toISOString())
198
- )
199
- )
200
- .orderBy(invitations.createdAt)
201
-
202
- return results.map((r) => ({
203
- id: r.id,
204
- email: r.email,
205
- role: r.role as Role,
206
- invitedBy: { id: r.invitedById, name: r.inviterName },
207
- expiresAt: r.expiresAt,
208
- createdAt: r.createdAt,
209
- }))
341
+ return listSql(db, ctx)
210
342
  },
211
343
 
212
- async revoke(db: Database, ctx: ServiceContext, id: string): Promise<void> {
213
- const invitationResults = await db
214
- .select()
215
- .from(invitations)
216
- .where(
217
- and(
218
- eq(invitations.id, id),
219
- eq(invitations.accountId, ctx.accountId),
220
- isNull(invitations.acceptedAt)
221
- )
222
- )
223
- .limit(1)
224
-
225
- const invitation = invitationResults.at(0)
226
- if (!invitation) {
227
- throw new NotFoundError('Invitation')
228
- }
229
-
230
- await db.delete(invitations).where(eq(invitations.id, id))
344
+ async revoke(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
345
+ await revokeSql(db, ctx, id)
231
346
  },
232
347
 
233
- async getByToken(db: Database, token: string): Promise<{
348
+ async getByToken(db: D1Database, token: string): Promise<{
234
349
  id: string
235
350
  accountId: string
236
351
  email: string
237
352
  role: Role
238
353
  accountName: string
239
354
  } | null> {
240
- const tokenResults = await db
241
- .select({
242
- id: invitations.id,
243
- accountId: invitations.accountId,
244
- email: invitations.email,
245
- role: invitations.role,
246
- expiresAt: invitations.expiresAt,
247
- acceptedAt: invitations.acceptedAt,
248
- accountName: accounts.name,
249
- })
250
- .from(invitations)
251
- .innerJoin(accounts, eq(accounts.id, invitations.accountId))
252
- .where(eq(invitations.token, token))
253
- .limit(1)
254
-
255
- const result = tokenResults.at(0)
256
- if (!result) return null
257
- if (result.acceptedAt) return null
258
- if (new Date(result.expiresAt) < new Date()) return null
259
-
260
- return {
261
- id: result.id,
262
- accountId: result.accountId,
263
- email: result.email,
264
- role: result.role as Role,
265
- accountName: result.accountName,
266
- }
355
+ return getByTokenSql(db, token)
267
356
  },
268
357
 
269
358
  async accept(
270
- db: Database,
359
+ db: D1Database,
271
360
  invitationId: string,
272
361
  userId: string,
273
362
  ctx: AuthEventContext
274
363
  ): Promise<void> {
275
- const invitationResults = await db
276
- .select()
277
- .from(invitations)
278
- .where(eq(invitations.id, invitationId))
279
- .limit(1)
280
-
281
- const invitation = invitationResults.at(0)
282
- if (!invitation) {
283
- throw new NotFoundError('Invitation')
284
- }
285
-
286
- // Create user-account relationship
287
- await db.insert(userAccounts).values({
288
- userId,
289
- accountId: invitation.accountId,
290
- role: invitation.role,
291
- })
292
-
293
- // Mark invitation as accepted
294
- await db
295
- .update(invitations)
296
- .set({ acceptedAt: new Date().toISOString() })
297
- .where(eq(invitations.id, invitationId))
298
-
299
- // Log event
300
- await logAuthEvent(db, ctx, 'LOGIN', userId, {
301
- invitationAccepted: true,
302
- accountId: invitation.accountId,
303
- role: invitation.role,
304
- })
364
+ await acceptSql(db, invitationId, userId, ctx)
305
365
  },
306
366
  }