@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.
- package/dist/index.js +0 -0
- package/package.json +5 -1
- package/templates/base/.husky/pre-push +26 -0
- package/templates/base/CLAUDE.md +5 -5
- package/templates/base/README.md +31 -20
- package/templates/base/docs/app_spec.txt +13 -10
- package/templates/base/docs/architecture/README.md +3 -0
- package/templates/base/docs/architecture/data-requirements.md +4 -3
- package/templates/base/docs/architecture/db-bootstrap.md +39 -0
- package/templates/base/docs/architecture/drizzle-migration-plan.md +125 -0
- package/templates/base/docs/architecture/erd.md +1 -1
- package/templates/base/docs/architecture/sql-standards.md +100 -0
- package/templates/base/docs/testing.md +36 -29
- package/templates/base/package.json +6 -5
- package/templates/base/pnpm-lock.yaml +0 -123
- package/templates/base/schema.sql +84 -0
- package/templates/base/scripts/init.sh +244 -59
- package/templates/base/src/client/hooks/use-auth.ts +5 -0
- package/templates/base/src/client/routes/_authenticated/dashboard.tsx +1 -1
- package/templates/base/src/client/routes/index.tsx +1 -1
- package/templates/base/src/server/db/client.ts +3 -5
- package/templates/base/src/server/db/records.ts +81 -0
- package/templates/base/src/server/db/seed.ts +3 -2
- package/templates/base/src/server/db/sql.ts +96 -0
- package/templates/base/src/server/index.ts +16 -2
- package/templates/base/src/server/lib/audit.ts +74 -26
- package/templates/base/src/server/lib/audited-db.ts +219 -109
- package/templates/base/src/server/lib/transaction.ts +10 -16
- package/templates/base/src/server/middleware/account.ts +8 -15
- package/templates/base/src/server/middleware/auth.ts +102 -38
- package/templates/base/src/server/middleware/rate-limit.ts +6 -1
- package/templates/base/src/server/routes/accounts/handlers.ts +18 -6
- package/templates/base/src/server/routes/audits/handlers.ts +3 -1
- package/templates/base/src/server/routes/auth/handlers.ts +14 -9
- package/templates/base/src/server/routes/auth/test-login.ts +99 -45
- package/templates/base/src/server/routes/health/handlers.ts +4 -4
- package/templates/base/src/server/routes/invitations/handlers.ts +6 -3
- package/templates/base/src/server/routes/users/handlers.ts +21 -14
- package/templates/base/src/server/services/accounts.ts +242 -217
- package/templates/base/src/server/services/audits.ts +114 -61
- package/templates/base/src/server/services/auth.ts +310 -180
- package/templates/base/src/server/services/invitations.ts +282 -222
- package/templates/base/src/server/services/users.ts +383 -293
- package/templates/base/src/server/types/index.ts +1 -2
- package/templates/base/{src/server/__tests__/fixtures.ts → tests/fixtures/server.ts} +3 -3
- package/templates/base/{src/client/__tests__/setup-browser.ts → tests/helpers/client-setup-browser.ts} +2 -2
- package/templates/base/{src/client/__tests__/setup.ts → tests/helpers/client-setup.ts} +1 -1
- package/templates/base/{src/client/__tests__/test-utils.tsx → tests/helpers/client-test-utils.tsx} +2 -2
- package/templates/base/{src/server/__tests__/setup.ts → tests/helpers/server.ts} +9 -9
- package/templates/base/tests/integration/accounts/crud.test.ts +2 -11
- package/templates/base/tests/integration/audits/list.test.ts +2 -11
- package/templates/base/tests/integration/auth/auth-service.test.ts +1 -10
- package/templates/base/tests/integration/auth/invitation-token.test.ts +2 -11
- package/templates/base/tests/integration/auth/logout.test.ts +2 -11
- package/templates/base/tests/integration/auth/oauth.test.ts +23 -42
- package/templates/base/tests/integration/auth/refresh-token.test.ts +1 -9
- package/templates/base/tests/integration/auth/session-expiry.test.ts +1 -9
- package/templates/base/tests/integration/auth/session.test.ts +2 -11
- package/templates/base/tests/integration/auth/super-admin.test.ts +1 -9
- package/templates/base/tests/integration/authorization/analytics-role.test.ts +2 -11
- package/templates/base/tests/integration/authorization/billing-role.test.ts +2 -11
- package/templates/base/tests/integration/authorization/guards-roles.test.ts +1 -9
- package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +2 -11
- package/templates/base/tests/integration/authorization/roles.test.ts +2 -11
- package/templates/base/tests/integration/config/production-behavior.test.ts +2 -11
- package/templates/base/tests/integration/health/health.test.ts +25 -44
- package/templates/base/tests/integration/invitations/crud.test.ts +2 -11
- package/templates/base/tests/integration/invitations/email.test.ts +1 -9
- package/templates/base/tests/integration/middleware/auth.test.ts +3 -12
- package/templates/base/tests/integration/middleware/request-logger.test.ts +1 -9
- package/templates/base/tests/integration/performance/response-times.test.ts +1 -9
- package/templates/base/tests/integration/security/cookie-security.test.ts +2 -11
- package/templates/base/tests/integration/security/csrf-protection.test.ts +2 -11
- package/templates/base/tests/integration/security/log-sanitization.test.ts +1 -9
- package/templates/base/tests/integration/security/rate-limiting.test.ts +1 -9
- package/templates/base/tests/integration/security/sql-injection.test.ts +7 -18
- package/templates/base/tests/integration/security/xss-prevention.test.ts +2 -11
- package/templates/base/tests/integration/setup.ts +13 -90
- package/templates/base/tests/integration/smoke.test.ts +3 -2
- package/templates/base/tests/integration/storage/upload.test.ts +2 -11
- package/templates/base/tests/integration/storage/validation.test.ts +2 -11
- package/templates/base/tests/integration/users/crud.test.ts +2 -11
- package/templates/base/tests/integration/users/list.test.ts +2 -11
- package/templates/base/tests/integration/vitest.config.ts +2 -9
- package/templates/base/{src/server/__tests__ → tests}/mocks/db.ts +1 -1
- package/templates/base/{src/server/__tests__ → tests}/mocks/index.ts +1 -1
- package/templates/base/{src/server/__tests__ → tests}/mocks/kv.ts +1 -1
- package/templates/base/{src/server/__tests__ → tests}/mocks/r2.ts +1 -1
- package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/sidebar.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/avatar.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client/components/ui}/button.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/card.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/dialog.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/input.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/loading-skeleton.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/skeleton.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/sonner.test.tsx +1 -1
- package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/tabs.test.tsx +1 -1
- package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/account.test.tsx +1 -1
- package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/integrations.test.tsx +1 -1
- package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/settings.test.tsx +1 -1
- package/templates/base/{src/client/routes/_authenticated/__tests__ → tests/unit/client/routes/_authenticated}/team.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/authenticated-layout.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/dashboard.test.tsx +1 -1
- package/templates/base/{src/client/routes/__tests__ → tests/unit/client/routes}/invite.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/login.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/navigation.test.tsx +1 -1
- package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/root-layout.test.tsx +1 -1
- package/templates/base/{src/server/auth/__tests__ → tests/unit/server/auth}/guards.test.ts +2 -2
- package/templates/base/{src → tests/unit}/server/auth/permissions.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/auth/roles.test.ts +1 -1
- package/templates/base/tests/unit/server/db/sql.test.ts +68 -0
- package/templates/base/{src → tests/unit}/server/env.test.ts +1 -1
- package/templates/base/tests/unit/server/lib/audited-db.test.ts +78 -0
- package/templates/base/{src → tests/unit}/server/lib/email.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/errors.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/oauth.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/pagination.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/password.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/providers.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/r2-storage.test.ts +2 -2
- package/templates/base/{src → tests/unit}/server/lib/session.test.ts +2 -2
- package/templates/base/{src → tests/unit}/server/lib/tokens.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/lib/transaction.test.ts +5 -14
- package/templates/base/{src → tests/unit}/server/middleware/account.test.ts +16 -24
- package/templates/base/{src → tests/unit}/server/middleware/auth.test.ts +71 -42
- package/templates/base/{src → tests/unit}/server/middleware/cors.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/middleware/error-handler.test.ts +2 -2
- package/templates/base/{src → tests/unit}/server/middleware/rate-limit.test.ts +3 -2
- package/templates/base/{src → tests/unit}/server/middleware/request-context.test.ts +1 -1
- package/templates/base/{src → tests/unit}/server/middleware/request-logger.test.ts +1 -1
- package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/db.test.ts +1 -1
- package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/kv.test.ts +1 -1
- package/templates/base/{src/server/__tests__/mocks/__tests__ → tests/unit/server/mocks}/r2.test.ts +1 -1
- package/templates/base/{src/server/routes/accounts/__tests__ → tests/unit/server/routes/accounts}/handlers.test.ts +12 -12
- package/templates/base/{src/server/routes/audits/__tests__ → tests/unit/server/routes/audits}/handlers.test.ts +11 -11
- package/templates/base/{src/server/routes/auth/__tests__ → tests/unit/server/routes/auth}/handlers.test.ts +13 -13
- package/templates/base/{src/server/routes/health/__tests__ → tests/unit/server/routes/health}/handlers.test.ts +27 -23
- package/templates/base/{src/server/routes/invitations/__tests__ → tests/unit/server/routes/invitations}/handlers.test.ts +14 -17
- package/templates/base/{src/server/routes/storage/__tests__ → tests/unit/server/routes/storage}/handlers.test.ts +6 -6
- package/templates/base/{src/server/routes/users/__tests__ → tests/unit/server/routes/users}/handlers.test.ts +12 -12
- package/templates/base/tests/unit/server/services/accounts.test.ts +258 -0
- package/templates/base/tests/unit/server/services/audits.test.ts +141 -0
- package/templates/base/tests/unit/server/services/auth.test.ts +179 -0
- package/templates/base/tests/unit/server/services/invitations.test.ts +165 -0
- package/templates/base/tests/unit/server/services/users.test.ts +351 -0
- package/templates/base/tsconfig.json +2 -1
- package/templates/base/vitest.config.browser.ts +3 -2
- package/templates/base/vitest.config.frontend.ts +3 -2
- package/templates/base/vitest.config.ts +7 -14
- package/templates/base/.claude/settings.local.json +0 -11
- package/templates/base/config/drizzle.config.ts +0 -10
- package/templates/base/src/server/db/schema/accounts.ts +0 -20
- package/templates/base/src/server/db/schema/audit-logs.ts +0 -26
- package/templates/base/src/server/db/schema/index.ts +0 -7
- package/templates/base/src/server/db/schema/invitations.ts +0 -30
- package/templates/base/src/server/db/schema/refresh-tokens.ts +0 -22
- package/templates/base/src/server/db/schema/user-accounts.ts +0 -25
- package/templates/base/src/server/db/schema/users.ts +0 -33
- package/templates/base/src/server/lib/audited-db.test.ts +0 -107
- package/templates/base/src/server/lib/schema-helpers.ts +0 -16
- package/templates/base/src/server/services/__tests__/accounts.test.ts +0 -764
- package/templates/base/src/server/services/__tests__/audits.test.ts +0 -235
- package/templates/base/src/server/services/__tests__/auth.test.ts +0 -765
- package/templates/base/src/server/services/__tests__/invitations.test.ts +0 -704
- package/templates/base/src/server/services/__tests__/users.test.ts +0 -755
- package/templates/base/tests/integration/lib/schema-helpers.test.ts +0 -129
- /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
- /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
- /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
- /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
- /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
- /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
- /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
- /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
- /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
- /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
- /package/templates/base/{src/client/components/__tests__ → tests/unit/client/components}/error-boundary.test.tsx +0 -0
- /package/templates/base/{src/client/components/ui/__tests__ → tests/unit/client/components/ui}/error-fallback.test.tsx +0 -0
- /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-auth.test.tsx +0 -0
- /package/templates/base/{src/client/hooks/__tests__ → tests/unit/client/hooks}/use-theme.test.tsx +0 -0
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /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
- /package/templates/base/{src/client/__tests__ → tests/unit/client}/routes/error-components.test.tsx +0 -0
- /package/templates/base/{src/shared/schemas/__tests__ → tests/unit/shared}/schemas.test.ts +0 -0
|
@@ -2,10 +2,9 @@
|
|
|
2
2
|
import { createMiddleware } from 'hono/factory'
|
|
3
3
|
import { verify } from 'hono/jwt'
|
|
4
4
|
import { HTTPException } from 'hono/http-exception'
|
|
5
|
-
import { users } from '../db/schema'
|
|
6
|
-
import { eq, and, isNull } from 'drizzle-orm'
|
|
7
5
|
import type { HonoEnv } from '../types'
|
|
8
6
|
import { getSession } from '../lib/session'
|
|
7
|
+
import { queryOne, type SqlRow } from '../db/sql'
|
|
9
8
|
|
|
10
9
|
interface JWTPayload {
|
|
11
10
|
sub: string
|
|
@@ -14,6 +13,63 @@ interface JWTPayload {
|
|
|
14
13
|
exp: number
|
|
15
14
|
}
|
|
16
15
|
|
|
16
|
+
|
|
17
|
+
const USER_SELECT_COLUMNS = `
|
|
18
|
+
id,
|
|
19
|
+
email,
|
|
20
|
+
name,
|
|
21
|
+
status,
|
|
22
|
+
provider_ids as providerIds,
|
|
23
|
+
is_super_admin as isSuperAdmin,
|
|
24
|
+
created_at as createdAt,
|
|
25
|
+
updated_at as updatedAt,
|
|
26
|
+
deleted_at as deletedAt
|
|
27
|
+
`
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
function parseProviderIds(value: unknown): string[] {
|
|
31
|
+
if (!value) return []
|
|
32
|
+
if (Array.isArray(value)) return value.map((item) => String(item))
|
|
33
|
+
if (typeof value === 'string') {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(value) as unknown
|
|
36
|
+
if (Array.isArray(parsed)) {
|
|
37
|
+
return parsed.map((item) => String(item))
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return []
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return []
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function toBoolean(value: unknown): boolean {
|
|
47
|
+
if (typeof value === 'boolean') return value
|
|
48
|
+
if (typeof value === 'number') return value !== 0
|
|
49
|
+
if (typeof value === 'string') return value === '1' || value.toLowerCase() === 'true'
|
|
50
|
+
return false
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function mapUserRow(row: SqlRow) {
|
|
54
|
+
const providerIds = row.providerIds ?? row.provider_ids
|
|
55
|
+
const isSuperAdmin = row.isSuperAdmin ?? row.is_super_admin
|
|
56
|
+
const createdAt = row.createdAt ?? row.created_at
|
|
57
|
+
const updatedAt = row.updatedAt ?? row.updated_at
|
|
58
|
+
const deletedAt = row.deletedAt ?? row.deleted_at
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
id: String(row.id ?? ''),
|
|
62
|
+
email: String(row.email ?? ''),
|
|
63
|
+
name: String(row.name ?? ''),
|
|
64
|
+
status: row.status === 'inactive' ? 'inactive' : 'active',
|
|
65
|
+
providerIds: parseProviderIds(providerIds),
|
|
66
|
+
isSuperAdmin: toBoolean(isSuperAdmin),
|
|
67
|
+
createdAt: String(createdAt ?? ''),
|
|
68
|
+
updatedAt: String(updatedAt ?? ''),
|
|
69
|
+
deletedAt: deletedAt ? String(deletedAt) : null,
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
17
73
|
/**
|
|
18
74
|
* Session-based authentication middleware
|
|
19
75
|
* Validates session from cookie and loads user from database
|
|
@@ -29,23 +85,27 @@ export const sessionAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
29
85
|
|
|
30
86
|
// Look up user in database to ensure they still exist and are active
|
|
31
87
|
const db = c.get('db')
|
|
32
|
-
|
|
88
|
+
const authDb = c.env.DB ?? db
|
|
89
|
+
if (!authDb) {
|
|
33
90
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
34
91
|
}
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
92
|
+
const user = await queryOne(
|
|
93
|
+
authDb,
|
|
94
|
+
`SELECT ${USER_SELECT_COLUMNS}
|
|
95
|
+
FROM users
|
|
96
|
+
WHERE id = ? AND deleted_at IS NULL
|
|
97
|
+
LIMIT 1`,
|
|
98
|
+
[session.userId]
|
|
99
|
+
)
|
|
42
100
|
if (!user) {
|
|
43
101
|
throw new HTTPException(401, {
|
|
44
102
|
message: 'User not found',
|
|
45
103
|
})
|
|
46
104
|
}
|
|
47
105
|
|
|
48
|
-
|
|
106
|
+
const mappedUser = mapUserRow(user as SqlRow)
|
|
107
|
+
|
|
108
|
+
if (mappedUser.status !== 'active') {
|
|
49
109
|
throw new HTTPException(401, {
|
|
50
110
|
message: 'User account is not active',
|
|
51
111
|
})
|
|
@@ -53,15 +113,15 @@ export const sessionAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
53
113
|
|
|
54
114
|
// Set user in context
|
|
55
115
|
c.set('user', {
|
|
56
|
-
id:
|
|
57
|
-
email:
|
|
58
|
-
name:
|
|
59
|
-
status:
|
|
60
|
-
providerIds:
|
|
61
|
-
isSuperAdmin:
|
|
62
|
-
createdAt:
|
|
63
|
-
updatedAt:
|
|
64
|
-
deletedAt:
|
|
116
|
+
id: mappedUser.id,
|
|
117
|
+
email: mappedUser.email,
|
|
118
|
+
name: mappedUser.name,
|
|
119
|
+
status: mappedUser.status,
|
|
120
|
+
providerIds: mappedUser.providerIds ?? [],
|
|
121
|
+
isSuperAdmin: mappedUser.isSuperAdmin,
|
|
122
|
+
createdAt: mappedUser.createdAt,
|
|
123
|
+
updatedAt: mappedUser.updatedAt,
|
|
124
|
+
deletedAt: mappedUser.deletedAt,
|
|
65
125
|
})
|
|
66
126
|
|
|
67
127
|
await next()
|
|
@@ -107,23 +167,27 @@ export const jwtAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
107
167
|
|
|
108
168
|
// Look up user in database by ID (from sub claim)
|
|
109
169
|
const db = c.get('db')
|
|
110
|
-
|
|
170
|
+
const authDb = c.env.DB ?? db
|
|
171
|
+
if (!authDb) {
|
|
111
172
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
112
173
|
}
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
174
|
+
const user = await queryOne(
|
|
175
|
+
authDb,
|
|
176
|
+
`SELECT ${USER_SELECT_COLUMNS}
|
|
177
|
+
FROM users
|
|
178
|
+
WHERE id = ? AND deleted_at IS NULL
|
|
179
|
+
LIMIT 1`,
|
|
180
|
+
[payload.sub]
|
|
181
|
+
)
|
|
120
182
|
if (!user) {
|
|
121
183
|
throw new HTTPException(401, {
|
|
122
184
|
message: 'User not found',
|
|
123
185
|
})
|
|
124
186
|
}
|
|
125
187
|
|
|
126
|
-
|
|
188
|
+
const mappedUser = mapUserRow(user as SqlRow)
|
|
189
|
+
|
|
190
|
+
if (mappedUser.status !== 'active') {
|
|
127
191
|
throw new HTTPException(401, {
|
|
128
192
|
message: 'User account is not active',
|
|
129
193
|
})
|
|
@@ -131,15 +195,15 @@ export const jwtAuth = createMiddleware<HonoEnv>(async (c, next) => {
|
|
|
131
195
|
|
|
132
196
|
// Set user in context
|
|
133
197
|
c.set('user', {
|
|
134
|
-
id:
|
|
135
|
-
email:
|
|
136
|
-
name:
|
|
137
|
-
status:
|
|
138
|
-
providerIds:
|
|
139
|
-
isSuperAdmin:
|
|
140
|
-
createdAt:
|
|
141
|
-
updatedAt:
|
|
142
|
-
deletedAt:
|
|
198
|
+
id: mappedUser.id,
|
|
199
|
+
email: mappedUser.email,
|
|
200
|
+
name: mappedUser.name,
|
|
201
|
+
status: mappedUser.status,
|
|
202
|
+
providerIds: mappedUser.providerIds ?? [],
|
|
203
|
+
isSuperAdmin: mappedUser.isSuperAdmin,
|
|
204
|
+
createdAt: mappedUser.createdAt,
|
|
205
|
+
updatedAt: mappedUser.updatedAt,
|
|
206
|
+
deletedAt: mappedUser.deletedAt,
|
|
143
207
|
})
|
|
144
208
|
|
|
145
209
|
await next()
|
|
@@ -240,8 +240,13 @@ export function rateLimit(options: RateLimitOptions = {}) {
|
|
|
240
240
|
export function authRateLimit() {
|
|
241
241
|
return rateLimit({
|
|
242
242
|
windowMs: 60000, // 1 minute
|
|
243
|
-
max: 10, // 10 requests per minute for
|
|
243
|
+
max: 10, // 10 requests per minute for login endpoints (brute force protection)
|
|
244
244
|
message: 'Too many authentication attempts, please try again later',
|
|
245
|
+
// Use separate key prefix to not conflict with global rate limit
|
|
246
|
+
keyGenerator: (c) => {
|
|
247
|
+
const ip = c.get('ip') || c.req.header('x-forwarded-for')?.split(',')[0].trim() || 'unknown'
|
|
248
|
+
return `auth:${ip}` // Different prefix than global rate limit
|
|
249
|
+
},
|
|
245
250
|
})
|
|
246
251
|
}
|
|
247
252
|
|
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
export const listAccountsHandler: RouteHandler<typeof listAccountsRoute, HonoEnv> = async (c) => {
|
|
14
14
|
const query = c.req.valid('query')
|
|
15
15
|
const db = c.get('db')
|
|
16
|
+
const envDb = c.env.DB
|
|
16
17
|
const accountId = c.get('accountId')
|
|
17
18
|
const user = c.get('user')
|
|
18
19
|
const transactionId = c.get('transactionId')
|
|
@@ -31,7 +32,8 @@ export const listAccountsHandler: RouteHandler<typeof listAccountsRoute, HonoEnv
|
|
|
31
32
|
userAgent,
|
|
32
33
|
}
|
|
33
34
|
|
|
34
|
-
const
|
|
35
|
+
const accountsDb = envDb ?? db
|
|
36
|
+
const result = await accountsService.findAll(accountsDb, ctx, {
|
|
35
37
|
page: query.page,
|
|
36
38
|
limit: query.limit,
|
|
37
39
|
sortBy: query.sortBy,
|
|
@@ -45,6 +47,7 @@ export const listAccountsHandler: RouteHandler<typeof listAccountsRoute, HonoEnv
|
|
|
45
47
|
export const getAccountHandler: RouteHandler<typeof getAccountRoute, HonoEnv> = async (c) => {
|
|
46
48
|
const { id } = c.req.valid('param')
|
|
47
49
|
const db = c.get('db')
|
|
50
|
+
const envDb = c.env.DB
|
|
48
51
|
const accountId = c.get('accountId')
|
|
49
52
|
const user = c.get('user')
|
|
50
53
|
const transactionId = c.get('transactionId')
|
|
@@ -63,13 +66,15 @@ export const getAccountHandler: RouteHandler<typeof getAccountRoute, HonoEnv> =
|
|
|
63
66
|
userAgent,
|
|
64
67
|
}
|
|
65
68
|
|
|
66
|
-
const
|
|
69
|
+
const accountsDb = envDb ?? db
|
|
70
|
+
const account = await accountsService.findById(accountsDb, ctx, id)
|
|
67
71
|
return c.json({ data: account }, 200)
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
export const createAccountHandler: RouteHandler<typeof createAccountRoute, HonoEnv> = async (c) => {
|
|
71
75
|
const data = c.req.valid('json')
|
|
72
76
|
const db = c.get('db')
|
|
77
|
+
const envDb = c.env.DB
|
|
73
78
|
const accountId = c.get('accountId')
|
|
74
79
|
const user = c.get('user')
|
|
75
80
|
const transactionId = c.get('transactionId')
|
|
@@ -88,7 +93,8 @@ export const createAccountHandler: RouteHandler<typeof createAccountRoute, HonoE
|
|
|
88
93
|
userAgent,
|
|
89
94
|
}
|
|
90
95
|
|
|
91
|
-
const
|
|
96
|
+
const accountsDb = envDb ?? db
|
|
97
|
+
const newAccount = await accountsService.create(accountsDb, ctx, {
|
|
92
98
|
name: data.name,
|
|
93
99
|
description: data.description,
|
|
94
100
|
domain: data.domain,
|
|
@@ -101,6 +107,7 @@ export const updateAccountHandler: RouteHandler<typeof updateAccountRoute, HonoE
|
|
|
101
107
|
const { id } = c.req.valid('param')
|
|
102
108
|
const data = c.req.valid('json')
|
|
103
109
|
const db = c.get('db')
|
|
110
|
+
const envDb = c.env.DB
|
|
104
111
|
const accountId = c.get('accountId')
|
|
105
112
|
const user = c.get('user')
|
|
106
113
|
const transactionId = c.get('transactionId')
|
|
@@ -119,7 +126,8 @@ export const updateAccountHandler: RouteHandler<typeof updateAccountRoute, HonoE
|
|
|
119
126
|
userAgent,
|
|
120
127
|
}
|
|
121
128
|
|
|
122
|
-
const
|
|
129
|
+
const accountsDb = envDb ?? db
|
|
130
|
+
const updatedAccount = await accountsService.update(accountsDb, ctx, id, {
|
|
123
131
|
name: data.name,
|
|
124
132
|
description: data.description,
|
|
125
133
|
domain: data.domain,
|
|
@@ -131,6 +139,7 @@ export const updateAccountHandler: RouteHandler<typeof updateAccountRoute, HonoE
|
|
|
131
139
|
export const deleteAccountHandler: RouteHandler<typeof deleteAccountRoute, HonoEnv> = async (c) => {
|
|
132
140
|
const { id } = c.req.valid('param')
|
|
133
141
|
const db = c.get('db')
|
|
142
|
+
const envDb = c.env.DB
|
|
134
143
|
const accountId = c.get('accountId')
|
|
135
144
|
const user = c.get('user')
|
|
136
145
|
const transactionId = c.get('transactionId')
|
|
@@ -149,13 +158,15 @@ export const deleteAccountHandler: RouteHandler<typeof deleteAccountRoute, HonoE
|
|
|
149
158
|
userAgent,
|
|
150
159
|
}
|
|
151
160
|
|
|
152
|
-
|
|
161
|
+
const accountsDb = envDb ?? db
|
|
162
|
+
await accountsService.delete(accountsDb, ctx, id)
|
|
153
163
|
return c.body(null, 204)
|
|
154
164
|
}
|
|
155
165
|
|
|
156
166
|
export const restoreAccountHandler: RouteHandler<typeof restoreAccountRoute, HonoEnv> = async (c) => {
|
|
157
167
|
const { id } = c.req.valid('param')
|
|
158
168
|
const db = c.get('db')
|
|
169
|
+
const envDb = c.env.DB
|
|
159
170
|
const accountId = c.get('accountId')
|
|
160
171
|
const user = c.get('user')
|
|
161
172
|
const transactionId = c.get('transactionId')
|
|
@@ -174,6 +185,7 @@ export const restoreAccountHandler: RouteHandler<typeof restoreAccountRoute, Hon
|
|
|
174
185
|
userAgent,
|
|
175
186
|
}
|
|
176
187
|
|
|
177
|
-
const
|
|
188
|
+
const accountsDb = envDb ?? db
|
|
189
|
+
const result = await accountsService.restore(accountsDb, ctx, id)
|
|
178
190
|
return c.json({ data: result }, 200)
|
|
179
191
|
}
|
|
@@ -7,6 +7,7 @@ import type { listAuditLogsRoute } from './routes'
|
|
|
7
7
|
export const listAuditLogsHandler: RouteHandler<typeof listAuditLogsRoute, HonoEnv> = async (c) => {
|
|
8
8
|
const query = c.req.valid('query')
|
|
9
9
|
const db = c.get('db')
|
|
10
|
+
const envDb = c.env.DB
|
|
10
11
|
const accountId = c.get('accountId')
|
|
11
12
|
const user = c.get('user')
|
|
12
13
|
const transactionId = c.get('transactionId')
|
|
@@ -25,7 +26,8 @@ export const listAuditLogsHandler: RouteHandler<typeof listAuditLogsRoute, HonoE
|
|
|
25
26
|
userAgent,
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
const
|
|
29
|
+
const auditsDb = envDb ?? db
|
|
30
|
+
const result = await auditsService.findAll(auditsDb, ctx, {
|
|
29
31
|
page: query.page,
|
|
30
32
|
limit: query.limit,
|
|
31
33
|
entity: query.entity,
|
|
@@ -61,10 +61,12 @@ export const loginHandler: RouteHandler<typeof loginRoute, HonoEnv> = async (c)
|
|
|
61
61
|
export const callbackHandler: RouteHandler<typeof callbackRoute, HonoEnv> = async (c) => {
|
|
62
62
|
const db = c.get('db')
|
|
63
63
|
const env = c.env
|
|
64
|
+
const authDb = env.DB ?? db
|
|
65
|
+
const inviteDb = env.DB ?? db
|
|
64
66
|
const { code, state } = c.req.valid('query')
|
|
65
67
|
const ctx = getAuthContext(c)
|
|
66
68
|
|
|
67
|
-
if (!db) {
|
|
69
|
+
if (!db || !authDb) {
|
|
68
70
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -99,16 +101,16 @@ export const callbackHandler: RouteHandler<typeof callbackRoute, HonoEnv> = asyn
|
|
|
99
101
|
const googleUser = decodeIdToken(tokens.id_token)
|
|
100
102
|
|
|
101
103
|
// Find or create user
|
|
102
|
-
const result = await authService.findOrCreateUser(
|
|
104
|
+
const result = await authService.findOrCreateUser(authDb, env, googleUser, ctx)
|
|
103
105
|
|
|
104
106
|
// Check for pending invitation
|
|
105
107
|
const pendingInvitation = getCookie(c, 'pending_invitation')
|
|
106
108
|
if (pendingInvitation) {
|
|
107
109
|
deleteCookie(c, 'pending_invitation')
|
|
108
110
|
|
|
109
|
-
const invitation = await invitationsService.getByToken(
|
|
111
|
+
const invitation = await invitationsService.getByToken(inviteDb, pendingInvitation)
|
|
110
112
|
if (invitation) {
|
|
111
|
-
await invitationsService.accept(
|
|
113
|
+
await invitationsService.accept(inviteDb, invitation.id, result.user.id, ctx)
|
|
112
114
|
}
|
|
113
115
|
}
|
|
114
116
|
|
|
@@ -131,9 +133,10 @@ export const callbackHandler: RouteHandler<typeof callbackRoute, HonoEnv> = asyn
|
|
|
131
133
|
export const refreshHandler: RouteHandler<typeof refreshRoute, HonoEnv> = async (c) => {
|
|
132
134
|
const db = c.get('db')
|
|
133
135
|
const env = c.env
|
|
136
|
+
const authDb = env.DB ?? db
|
|
134
137
|
const refreshToken = getCookie(c, 'refresh_token')
|
|
135
138
|
|
|
136
|
-
if (!db) {
|
|
139
|
+
if (!db || !authDb) {
|
|
137
140
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
138
141
|
}
|
|
139
142
|
|
|
@@ -142,24 +145,25 @@ export const refreshHandler: RouteHandler<typeof refreshRoute, HonoEnv> = async
|
|
|
142
145
|
}
|
|
143
146
|
|
|
144
147
|
const ctx = getAuthContext(c)
|
|
145
|
-
const tokens = await authService.refreshAccessToken(
|
|
148
|
+
const tokens = await authService.refreshAccessToken(authDb, env, refreshToken, ctx)
|
|
146
149
|
|
|
147
150
|
return c.json({ tokens }, 200)
|
|
148
151
|
}
|
|
149
152
|
|
|
150
153
|
export const logoutHandler: RouteHandler<typeof logoutRoute, HonoEnv> = async (c) => {
|
|
151
154
|
const db = c.get('db')
|
|
155
|
+
const authDb = c.env.DB ?? db
|
|
152
156
|
const ctx = getAuthContext(c)
|
|
153
157
|
const session = getSession(c)
|
|
154
158
|
|
|
155
|
-
if (!db) {
|
|
159
|
+
if (!db || !authDb) {
|
|
156
160
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
// Log logout event if we have a session
|
|
160
164
|
if (session) {
|
|
161
165
|
const { logAuthEvent } = await import('../../lib/audit')
|
|
162
|
-
await logAuthEvent(
|
|
166
|
+
await logAuthEvent(authDb, ctx, 'LOGOUT', session.userId, {})
|
|
163
167
|
}
|
|
164
168
|
|
|
165
169
|
// Destroy session (removes from KV and clears cookie)
|
|
@@ -193,6 +197,7 @@ export const meHandler: RouteHandler<typeof meRoute, HonoEnv> = (c) => {
|
|
|
193
197
|
export const inviteHandler: RouteHandler<typeof inviteRoute, HonoEnv> = async (c) => {
|
|
194
198
|
const db = c.get('db')
|
|
195
199
|
const env = c.env
|
|
200
|
+
const inviteDb = env.DB ?? db
|
|
196
201
|
const { token } = c.req.valid('param')
|
|
197
202
|
|
|
198
203
|
if (!db) {
|
|
@@ -202,7 +207,7 @@ export const inviteHandler: RouteHandler<typeof inviteRoute, HonoEnv> = async (c
|
|
|
202
207
|
const isProduction = env.ENVIRONMENT === 'production'
|
|
203
208
|
|
|
204
209
|
// Validate invitation
|
|
205
|
-
const invitation = await invitationsService.getByToken(
|
|
210
|
+
const invitation = await invitationsService.getByToken(inviteDb, token)
|
|
206
211
|
|
|
207
212
|
if (!invitation) {
|
|
208
213
|
throw new HTTPException(400, { message: 'Invalid or expired invitation' })
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// src/server/routes/auth/test-login.ts
|
|
2
2
|
import type { RouteHandler } from '@hono/zod-openapi'
|
|
3
3
|
import { createRoute, z } from '@hono/zod-openapi'
|
|
4
|
-
import { eq } from 'drizzle-orm'
|
|
5
4
|
import { HTTPException } from 'hono/http-exception'
|
|
6
|
-
import {
|
|
5
|
+
import { execute, queryOne, type SqlRow } from '../../db/sql'
|
|
6
|
+
import type { UserRecord } from '../../db/records'
|
|
7
7
|
import { createSession } from '../../lib/session'
|
|
8
8
|
import type { HonoEnv } from '../../types'
|
|
9
9
|
|
|
@@ -12,6 +12,42 @@ const TestLoginSchema = z.object({
|
|
|
12
12
|
name: z.string().optional(),
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
+
const USER_SELECT_COLUMNS = `
|
|
16
|
+
id,
|
|
17
|
+
google_id as googleId,
|
|
18
|
+
email,
|
|
19
|
+
name,
|
|
20
|
+
avatar_url as avatarUrl,
|
|
21
|
+
status,
|
|
22
|
+
is_super_admin as isSuperAdmin,
|
|
23
|
+
created_at as createdAt,
|
|
24
|
+
updated_at as updatedAt,
|
|
25
|
+
deleted_at as deletedAt
|
|
26
|
+
`
|
|
27
|
+
|
|
28
|
+
function toBoolean(value: unknown): boolean {
|
|
29
|
+
if (typeof value === 'boolean') return value
|
|
30
|
+
if (typeof value === 'number') return value !== 0
|
|
31
|
+
if (typeof value === 'string') return value === '1' || value.toLowerCase() === 'true'
|
|
32
|
+
return false
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mapUserRow(row: SqlRow): UserRecord {
|
|
36
|
+
return {
|
|
37
|
+
id: String(row.id ?? ''),
|
|
38
|
+
googleId: String(row.googleId ?? row.google_id ?? ''),
|
|
39
|
+
email: String(row.email ?? ''),
|
|
40
|
+
name: String(row.name ?? ''),
|
|
41
|
+
avatarUrl: row.avatarUrl ? String(row.avatarUrl) : null,
|
|
42
|
+
status: row.status === 'inactive' ? 'inactive' : 'active',
|
|
43
|
+
providerIds: [],
|
|
44
|
+
isSuperAdmin: toBoolean(row.isSuperAdmin ?? row.is_super_admin),
|
|
45
|
+
createdAt: String(row.createdAt ?? row.created_at ?? ''),
|
|
46
|
+
updatedAt: String(row.updatedAt ?? row.updated_at ?? ''),
|
|
47
|
+
deletedAt: row.deletedAt ? String(row.deletedAt) : null,
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
15
51
|
export const testLoginRoute = createRoute({
|
|
16
52
|
method: 'post',
|
|
17
53
|
path: '/test-login',
|
|
@@ -61,20 +97,23 @@ export const testLoginHandler: RouteHandler<typeof testLoginRoute, HonoEnv> = as
|
|
|
61
97
|
}
|
|
62
98
|
|
|
63
99
|
const { email, name } = c.req.valid('json')
|
|
64
|
-
const db = c.get('db')
|
|
100
|
+
const db = c.env.DB ?? c.get('db')
|
|
65
101
|
|
|
66
102
|
if (!db) {
|
|
67
103
|
throw new Error('Database not initialized')
|
|
68
104
|
}
|
|
69
105
|
|
|
70
106
|
// Find or create user
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
107
|
+
const existingUser = await queryOne(
|
|
108
|
+
db,
|
|
109
|
+
`SELECT ${USER_SELECT_COLUMNS}
|
|
110
|
+
FROM users
|
|
111
|
+
WHERE email = ? AND deleted_at IS NULL
|
|
112
|
+
LIMIT 1`,
|
|
113
|
+
[email]
|
|
114
|
+
)
|
|
76
115
|
|
|
77
|
-
let user: UserRecord | undefined =
|
|
116
|
+
let user: UserRecord | undefined = existingUser ? mapUserRow(existingUser) : undefined
|
|
78
117
|
let defaultAccountId: string | null = null
|
|
79
118
|
|
|
80
119
|
if (user === undefined) {
|
|
@@ -82,49 +121,64 @@ export const testLoginHandler: RouteHandler<typeof testLoginRoute, HonoEnv> = as
|
|
|
82
121
|
const userId = crypto.randomUUID()
|
|
83
122
|
const now = new Date().toISOString()
|
|
84
123
|
|
|
85
|
-
await
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
124
|
+
await execute(
|
|
125
|
+
db,
|
|
126
|
+
`INSERT INTO users (
|
|
127
|
+
id,
|
|
128
|
+
email,
|
|
129
|
+
name,
|
|
130
|
+
google_id,
|
|
131
|
+
status,
|
|
132
|
+
is_super_admin,
|
|
133
|
+
created_at,
|
|
134
|
+
updated_at
|
|
135
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
136
|
+
[
|
|
137
|
+
userId,
|
|
138
|
+
email,
|
|
139
|
+
name ?? 'E2E Test User',
|
|
140
|
+
`test-${userId}`,
|
|
141
|
+
'active',
|
|
142
|
+
0,
|
|
143
|
+
now,
|
|
144
|
+
now,
|
|
145
|
+
]
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
const createdUser = await queryOne(
|
|
149
|
+
db,
|
|
150
|
+
`SELECT ${USER_SELECT_COLUMNS}
|
|
151
|
+
FROM users
|
|
152
|
+
WHERE id = ?
|
|
153
|
+
LIMIT 1`,
|
|
154
|
+
[userId]
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
user = createdUser ? mapUserRow(createdUser) : undefined
|
|
102
158
|
|
|
103
159
|
// Create a default account for the user
|
|
104
160
|
defaultAccountId = crypto.randomUUID()
|
|
105
|
-
await
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
})
|
|
161
|
+
await execute(
|
|
162
|
+
db,
|
|
163
|
+
`INSERT INTO accounts (id, name, created_at, updated_at) VALUES (?, ?, ?, ?)`,
|
|
164
|
+
[defaultAccountId, `${name ?? 'Test'}'s Workspace`, now, now]
|
|
165
|
+
)
|
|
111
166
|
|
|
112
167
|
// Link user to account as ADMIN
|
|
113
|
-
await
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
168
|
+
await execute(
|
|
169
|
+
db,
|
|
170
|
+
`INSERT INTO user_accounts (user_id, account_id, role) VALUES (?, ?, ?)`,
|
|
171
|
+
[userId, defaultAccountId, 'ADMIN']
|
|
172
|
+
)
|
|
118
173
|
} else {
|
|
119
174
|
// Get the user's first account
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (userAccount !== undefined) {
|
|
175
|
+
const userAccount = await queryOne<{ accountId: string }>(
|
|
176
|
+
db,
|
|
177
|
+
`SELECT account_id as accountId FROM user_accounts WHERE user_id = ? LIMIT 1`,
|
|
178
|
+
[user.id]
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if (userAccount?.accountId) {
|
|
128
182
|
defaultAccountId = userAccount.accountId
|
|
129
183
|
}
|
|
130
184
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import type { Context } from 'hono'
|
|
2
2
|
import type { HonoEnv } from '../../types'
|
|
3
|
-
import {
|
|
3
|
+
import { queryOne } from '../../db/sql'
|
|
4
4
|
|
|
5
5
|
async function checkDatabase(c: Context<HonoEnv>): Promise<'up' | 'down'> {
|
|
6
6
|
try {
|
|
7
|
-
const db = c.
|
|
7
|
+
const db = c.env.DB
|
|
8
8
|
if (!db) {
|
|
9
9
|
return 'down'
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
// Simple connectivity check using
|
|
13
|
-
await db
|
|
12
|
+
// Simple connectivity check using D1
|
|
13
|
+
await queryOne(db, 'SELECT 1 AS ok')
|
|
14
14
|
return 'up'
|
|
15
15
|
} catch {
|
|
16
16
|
return 'down'
|
|
@@ -32,26 +32,28 @@ export const createInvitationHandler: RouteHandler<typeof createInvitationRoute,
|
|
|
32
32
|
const body = c.req.valid('json')
|
|
33
33
|
const db = c.get('db')
|
|
34
34
|
const env = c.env
|
|
35
|
+
const invitationsDb = env.DB ?? db
|
|
35
36
|
const ctx = getServiceContext(c)
|
|
36
37
|
|
|
37
38
|
if (!db) {
|
|
38
39
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
const result = await invitationsService.create(
|
|
42
|
+
const result = await invitationsService.create(invitationsDb, env, ctx, body)
|
|
42
43
|
|
|
43
44
|
return c.json(result, 200)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export const listInvitationsHandler: RouteHandler<typeof listInvitationsRoute, HonoEnv> = async (c) => {
|
|
47
48
|
const db = c.get('db')
|
|
49
|
+
const invitationsDb = c.env.DB ?? db
|
|
48
50
|
const ctx = getServiceContext(c)
|
|
49
51
|
|
|
50
52
|
if (!db) {
|
|
51
53
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
const invitations = await invitationsService.list(
|
|
56
|
+
const invitations = await invitationsService.list(invitationsDb, ctx)
|
|
55
57
|
|
|
56
58
|
return c.json({ data: invitations }, 200)
|
|
57
59
|
}
|
|
@@ -59,13 +61,14 @@ export const listInvitationsHandler: RouteHandler<typeof listInvitationsRoute, H
|
|
|
59
61
|
export const revokeInvitationHandler: RouteHandler<typeof revokeInvitationRoute, HonoEnv> = async (c) => {
|
|
60
62
|
const { id } = c.req.valid('param')
|
|
61
63
|
const db = c.get('db')
|
|
64
|
+
const invitationsDb = c.env.DB ?? db
|
|
62
65
|
const ctx = getServiceContext(c)
|
|
63
66
|
|
|
64
67
|
if (!db) {
|
|
65
68
|
throw new HTTPException(500, { message: 'Database not initialized' })
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
await invitationsService.revoke(
|
|
71
|
+
await invitationsService.revoke(invitationsDb, ctx, id)
|
|
69
72
|
|
|
70
73
|
return c.body(null, 204)
|
|
71
74
|
}
|