@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
|
@@ -14,13 +14,14 @@ import type {
|
|
|
14
14
|
export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = async (c) => {
|
|
15
15
|
const query = c.req.valid('query')
|
|
16
16
|
const db = c.get('db')
|
|
17
|
+
const usersDb = c.env.DB ?? db
|
|
17
18
|
const accountId = c.get('accountId')
|
|
18
19
|
const user = c.get('user')
|
|
19
20
|
const transactionId = c.get('transactionId')
|
|
20
21
|
const ip = c.get('ip')
|
|
21
22
|
const userAgent = c.get('userAgent')
|
|
22
23
|
|
|
23
|
-
if (!db || !accountId || !user) {
|
|
24
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
24
25
|
throw new Error('Missing required context')
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -32,7 +33,7 @@ export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = as
|
|
|
32
33
|
userAgent,
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
const result = await usersService.findAll(
|
|
36
|
+
const result = await usersService.findAll(usersDb, ctx, {
|
|
36
37
|
page: query.page,
|
|
37
38
|
limit: query.limit,
|
|
38
39
|
sortBy: query.sortBy,
|
|
@@ -46,13 +47,14 @@ export const listUsersHandler: RouteHandler<typeof listUsersRoute, HonoEnv> = as
|
|
|
46
47
|
export const getUserHandler: RouteHandler<typeof getUserRoute, HonoEnv> = async (c) => {
|
|
47
48
|
const { id } = c.req.valid('param')
|
|
48
49
|
const db = c.get('db')
|
|
50
|
+
const usersDb = c.env.DB ?? db
|
|
49
51
|
const accountId = c.get('accountId')
|
|
50
52
|
const user = c.get('user')
|
|
51
53
|
const transactionId = c.get('transactionId')
|
|
52
54
|
const ip = c.get('ip')
|
|
53
55
|
const userAgent = c.get('userAgent')
|
|
54
56
|
|
|
55
|
-
if (!db || !accountId || !user) {
|
|
57
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
56
58
|
throw new Error('Missing required context')
|
|
57
59
|
}
|
|
58
60
|
|
|
@@ -64,7 +66,7 @@ export const getUserHandler: RouteHandler<typeof getUserRoute, HonoEnv> = async
|
|
|
64
66
|
userAgent,
|
|
65
67
|
}
|
|
66
68
|
|
|
67
|
-
const foundUser = await usersService.findById(
|
|
69
|
+
const foundUser = await usersService.findById(usersDb, ctx, id)
|
|
68
70
|
return c.json({ data: foundUser }, 200)
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -100,13 +102,14 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
|
|
|
100
102
|
const { id } = c.req.valid('param')
|
|
101
103
|
const data = c.req.valid('json')
|
|
102
104
|
const db = c.get('db')
|
|
105
|
+
const usersDb = c.env.DB ?? db
|
|
103
106
|
const accountId = c.get('accountId')
|
|
104
107
|
const user = c.get('user')
|
|
105
108
|
const transactionId = c.get('transactionId')
|
|
106
109
|
const ip = c.get('ip')
|
|
107
110
|
const userAgent = c.get('userAgent')
|
|
108
111
|
|
|
109
|
-
if (!db || !accountId || !user) {
|
|
112
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
110
113
|
throw new Error('Missing required context')
|
|
111
114
|
}
|
|
112
115
|
|
|
@@ -118,7 +121,7 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
|
|
|
118
121
|
userAgent,
|
|
119
122
|
}
|
|
120
123
|
|
|
121
|
-
const updatedUser = await usersService.update(
|
|
124
|
+
const updatedUser = await usersService.update(usersDb, ctx, id, {
|
|
122
125
|
name: data.name,
|
|
123
126
|
status: data.status,
|
|
124
127
|
})
|
|
@@ -129,13 +132,14 @@ export const updateUserHandler: RouteHandler<typeof updateUserRoute, HonoEnv> =
|
|
|
129
132
|
export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> = async (c) => {
|
|
130
133
|
const { id } = c.req.valid('param')
|
|
131
134
|
const db = c.get('db')
|
|
135
|
+
const usersDb = c.env.DB ?? db
|
|
132
136
|
const accountId = c.get('accountId')
|
|
133
137
|
const user = c.get('user')
|
|
134
138
|
const transactionId = c.get('transactionId')
|
|
135
139
|
const ip = c.get('ip')
|
|
136
140
|
const userAgent = c.get('userAgent')
|
|
137
141
|
|
|
138
|
-
if (!db || !accountId || !user) {
|
|
142
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
139
143
|
throw new Error('Missing required context')
|
|
140
144
|
}
|
|
141
145
|
|
|
@@ -147,7 +151,7 @@ export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> =
|
|
|
147
151
|
userAgent,
|
|
148
152
|
}
|
|
149
153
|
|
|
150
|
-
await usersService.delete(
|
|
154
|
+
await usersService.delete(usersDb, ctx, id)
|
|
151
155
|
return c.body(null, 204)
|
|
152
156
|
}
|
|
153
157
|
|
|
@@ -155,13 +159,14 @@ export const deleteUserHandler: RouteHandler<typeof deleteUserRoute, HonoEnv> =
|
|
|
155
159
|
export const createBulkUserAccountsHandler: RouteHandler<typeof createBulkUserAccountsRoute, HonoEnv> = async (c) => {
|
|
156
160
|
const data = c.req.valid('json')
|
|
157
161
|
const db = c.get('db')
|
|
162
|
+
const usersDb = c.env.DB ?? db
|
|
158
163
|
const accountId = c.get('accountId')
|
|
159
164
|
const user = c.get('user')
|
|
160
165
|
const transactionId = c.get('transactionId')
|
|
161
166
|
const ip = c.get('ip')
|
|
162
167
|
const userAgent = c.get('userAgent')
|
|
163
168
|
|
|
164
|
-
if (!db || !accountId || !user) {
|
|
169
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
165
170
|
throw new Error('Missing required context')
|
|
166
171
|
}
|
|
167
172
|
|
|
@@ -173,20 +178,21 @@ export const createBulkUserAccountsHandler: RouteHandler<typeof createBulkUserAc
|
|
|
173
178
|
userAgent,
|
|
174
179
|
}
|
|
175
180
|
|
|
176
|
-
const result = await usersService.createUserAccounts(
|
|
181
|
+
const result = await usersService.createUserAccounts(usersDb, ctx, data)
|
|
177
182
|
return c.json(result, 201)
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
export const deleteBulkUserAccountsHandler: RouteHandler<typeof deleteBulkUserAccountsRoute, HonoEnv> = async (c) => {
|
|
181
186
|
const data = c.req.valid('json')
|
|
182
187
|
const db = c.get('db')
|
|
188
|
+
const usersDb = c.env.DB ?? db
|
|
183
189
|
const accountId = c.get('accountId')
|
|
184
190
|
const user = c.get('user')
|
|
185
191
|
const transactionId = c.get('transactionId')
|
|
186
192
|
const ip = c.get('ip')
|
|
187
193
|
const userAgent = c.get('userAgent')
|
|
188
194
|
|
|
189
|
-
if (!db || !accountId || !user) {
|
|
195
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
190
196
|
throw new Error('Missing required context')
|
|
191
197
|
}
|
|
192
198
|
|
|
@@ -198,20 +204,21 @@ export const deleteBulkUserAccountsHandler: RouteHandler<typeof deleteBulkUserAc
|
|
|
198
204
|
userAgent,
|
|
199
205
|
}
|
|
200
206
|
|
|
201
|
-
const result = await usersService.deleteUserAccounts(
|
|
207
|
+
const result = await usersService.deleteUserAccounts(usersDb, ctx, data)
|
|
202
208
|
return c.json(result, 200)
|
|
203
209
|
}
|
|
204
210
|
|
|
205
211
|
export const restoreUserHandler: RouteHandler<typeof restoreUserRoute, HonoEnv> = async (c) => {
|
|
206
212
|
const { id } = c.req.valid('param')
|
|
207
213
|
const db = c.get('db')
|
|
214
|
+
const usersDb = c.env.DB ?? db
|
|
208
215
|
const accountId = c.get('accountId')
|
|
209
216
|
const user = c.get('user')
|
|
210
217
|
const transactionId = c.get('transactionId')
|
|
211
218
|
const ip = c.get('ip')
|
|
212
219
|
const userAgent = c.get('userAgent')
|
|
213
220
|
|
|
214
|
-
if (!db || !accountId || !user) {
|
|
221
|
+
if (!db || !usersDb || !accountId || !user) {
|
|
215
222
|
throw new Error('Missing required context')
|
|
216
223
|
}
|
|
217
224
|
|
|
@@ -223,6 +230,6 @@ export const restoreUserHandler: RouteHandler<typeof restoreUserRoute, HonoEnv>
|
|
|
223
230
|
userAgent,
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
const result = await usersService.restore(
|
|
233
|
+
const result = await usersService.restore(usersDb, ctx, id)
|
|
227
234
|
return c.json({ data: result }, 200)
|
|
228
235
|
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
// src/services/accounts.ts
|
|
2
|
-
import {
|
|
3
|
-
import type { Database } from '../db/client'
|
|
4
|
-
import { accounts, userAccounts, type AccountRecord } from '../db/schema'
|
|
2
|
+
import type { AccountRecord } from '../db/records'
|
|
5
3
|
import { auditedInsert, auditedUpdate, auditedDelete } from '../lib/audited-db'
|
|
6
4
|
import { createPaginationMeta, calculateOffset } from '../lib/pagination'
|
|
7
5
|
import { NotFoundError, ConflictError, ForbiddenError } from '../lib/errors'
|
|
8
6
|
import type { ServiceContext, PaginationQuery, PaginatedResponse, Account } from '../types'
|
|
7
|
+
import { queryAll, queryOne, type SqlRow } from '../db/sql'
|
|
9
8
|
|
|
10
9
|
interface CreateAccountInput {
|
|
11
10
|
name: string
|
|
@@ -19,251 +18,277 @@ interface UpdateAccountInput {
|
|
|
19
18
|
domain?: string
|
|
20
19
|
}
|
|
21
20
|
|
|
22
|
-
export const accountsService = {
|
|
23
|
-
async findAll(
|
|
24
|
-
db: Database,
|
|
25
|
-
ctx: ServiceContext,
|
|
26
|
-
pagination: PaginationQuery
|
|
27
|
-
): Promise<PaginatedResponse<Account>> {
|
|
28
|
-
const offset = calculateOffset(pagination.page, pagination.limit)
|
|
29
|
-
const conditions = [isNull(accounts.deletedAt)]
|
|
30
|
-
|
|
31
|
-
// Non-super-admin sees only their accounts
|
|
32
|
-
if (!ctx.user.isSuperAdmin) {
|
|
33
|
-
const accountIdsForUser = db
|
|
34
|
-
.select({ accountId: userAccounts.accountId })
|
|
35
|
-
.from(userAccounts)
|
|
36
|
-
.where(eq(userAccounts.userId, ctx.user.id))
|
|
37
21
|
|
|
38
|
-
|
|
39
|
-
|
|
22
|
+
const ACCOUNT_SELECT_COLUMNS = `
|
|
23
|
+
id,
|
|
24
|
+
name,
|
|
25
|
+
description,
|
|
26
|
+
domain,
|
|
27
|
+
created_at as createdAt,
|
|
28
|
+
updated_at as updatedAt,
|
|
29
|
+
deleted_at as deletedAt
|
|
30
|
+
`
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
function mapAccountRow(row: SqlRow): AccountRecord {
|
|
34
|
+
const createdAt = row.createdAt ?? row.created_at
|
|
35
|
+
const updatedAt = row.updatedAt ?? row.updated_at
|
|
36
|
+
const deletedAt = row.deletedAt ?? row.deleted_at
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
id: String(row.id ?? ''),
|
|
40
|
+
name: String(row.name ?? ''),
|
|
41
|
+
description: row.description ? String(row.description) : null,
|
|
42
|
+
domain: row.domain ? String(row.domain) : null,
|
|
43
|
+
createdAt: String(createdAt ?? ''),
|
|
44
|
+
updatedAt: String(updatedAt ?? ''),
|
|
45
|
+
deletedAt: deletedAt ? String(deletedAt) : null,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
40
48
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
function toAccount(record: AccountRecord): Account {
|
|
50
|
+
return {
|
|
51
|
+
id: record.id,
|
|
52
|
+
name: record.name,
|
|
53
|
+
description: record.description,
|
|
54
|
+
domain: record.domain,
|
|
55
|
+
createdAt: record.createdAt,
|
|
56
|
+
updatedAt: record.updatedAt,
|
|
57
|
+
deletedAt: record.deletedAt,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
61
|
+
async function findAllSql(
|
|
62
|
+
db: D1Database,
|
|
63
|
+
ctx: ServiceContext,
|
|
64
|
+
pagination: PaginationQuery
|
|
65
|
+
): Promise<PaginatedResponse<Account>> {
|
|
66
|
+
const offset = calculateOffset(pagination.page, pagination.limit)
|
|
67
|
+
const whereClauses: string[] = ['a.deleted_at IS NULL']
|
|
68
|
+
const params: unknown[] = []
|
|
69
|
+
|
|
70
|
+
if (!ctx.user.isSuperAdmin) {
|
|
71
|
+
whereClauses.push('a.id IN (SELECT account_id FROM user_accounts WHERE user_id = ?)')
|
|
72
|
+
params.push(ctx.user.id)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (pagination.query) {
|
|
76
|
+
whereClauses.push('(a.name LIKE ? OR a.domain LIKE ?)')
|
|
77
|
+
const like = `%${pagination.query}%`
|
|
78
|
+
params.push(like, like)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const whereSql = whereClauses.length > 0 ? `WHERE ${whereClauses.join(' AND ')}` : ''
|
|
82
|
+
|
|
83
|
+
const countRow = await queryOne<{ count: number }>(
|
|
84
|
+
db,
|
|
85
|
+
`SELECT count(*) as count FROM accounts a ${whereSql}`,
|
|
86
|
+
params
|
|
87
|
+
)
|
|
88
|
+
const totalItems = countRow?.count ?? 0
|
|
89
|
+
|
|
90
|
+
const rows = await queryAll(
|
|
91
|
+
db,
|
|
92
|
+
`SELECT ${ACCOUNT_SELECT_COLUMNS}
|
|
93
|
+
FROM accounts a
|
|
94
|
+
${whereSql}
|
|
95
|
+
ORDER BY a.created_at DESC
|
|
96
|
+
LIMIT ? OFFSET ?`,
|
|
97
|
+
[...params, pagination.limit, offset]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
data: rows.map((row) => toAccount(mapAccountRow(row))),
|
|
102
|
+
meta: createPaginationMeta(totalItems, pagination.page, pagination.limit),
|
|
103
|
+
}
|
|
104
|
+
}
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
async function findByIdSql(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
|
|
107
|
+
const row = await queryOne(
|
|
108
|
+
db,
|
|
109
|
+
`SELECT ${ACCOUNT_SELECT_COLUMNS}
|
|
110
|
+
FROM accounts a
|
|
111
|
+
WHERE a.id = ? AND a.deleted_at IS NULL
|
|
112
|
+
LIMIT 1`,
|
|
113
|
+
[id]
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
if (!row) {
|
|
117
|
+
throw new NotFoundError('Account')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!ctx.user.isSuperAdmin) {
|
|
121
|
+
const membership = await queryOne(
|
|
122
|
+
db,
|
|
123
|
+
`SELECT 1 as ok FROM user_accounts WHERE user_id = ? AND account_id = ? LIMIT 1`,
|
|
124
|
+
[ctx.user.id, id]
|
|
125
|
+
)
|
|
85
126
|
|
|
86
|
-
|
|
87
|
-
if (!accountRecord) {
|
|
127
|
+
if (!membership) {
|
|
88
128
|
throw new NotFoundError('Account')
|
|
89
129
|
}
|
|
130
|
+
}
|
|
90
131
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const membershipResults = await db
|
|
94
|
-
.select()
|
|
95
|
-
.from(userAccounts)
|
|
96
|
-
.where(
|
|
97
|
-
and(
|
|
98
|
-
eq(userAccounts.userId, ctx.user.id),
|
|
99
|
-
eq(userAccounts.accountId, id)
|
|
100
|
-
)
|
|
101
|
-
)
|
|
102
|
-
.limit(1)
|
|
103
|
-
|
|
104
|
-
const membership = membershipResults.at(0)
|
|
105
|
-
if (!membership) {
|
|
106
|
-
throw new NotFoundError('Account')
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
return {
|
|
111
|
-
id: accountRecord.id,
|
|
112
|
-
name: accountRecord.name,
|
|
113
|
-
description: accountRecord.description,
|
|
114
|
-
domain: accountRecord.domain,
|
|
115
|
-
createdAt: accountRecord.createdAt,
|
|
116
|
-
updatedAt: accountRecord.updatedAt,
|
|
117
|
-
deletedAt: accountRecord.deletedAt,
|
|
118
|
-
}
|
|
119
|
-
},
|
|
120
|
-
|
|
121
|
-
async create(db: Database, ctx: ServiceContext, input: CreateAccountInput): Promise<Account> {
|
|
122
|
-
// Only super-admin can create accounts
|
|
123
|
-
if (!ctx.user.isSuperAdmin) {
|
|
124
|
-
throw new ForbiddenError('Only super-admin can create accounts')
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Check domain uniqueness if provided
|
|
128
|
-
if (input.domain) {
|
|
129
|
-
const existingResults = await db
|
|
130
|
-
.select()
|
|
131
|
-
.from(accounts)
|
|
132
|
-
.where(eq(accounts.domain, input.domain))
|
|
133
|
-
.limit(1)
|
|
134
|
-
|
|
135
|
-
if (existingResults.at(0)) {
|
|
136
|
-
throw new ConflictError('Account with this domain already exists')
|
|
137
|
-
}
|
|
138
|
-
}
|
|
132
|
+
return toAccount(mapAccountRow(row))
|
|
133
|
+
}
|
|
139
134
|
|
|
140
|
-
|
|
141
|
-
|
|
135
|
+
async function createSql(
|
|
136
|
+
db: D1Database,
|
|
137
|
+
ctx: ServiceContext,
|
|
138
|
+
input: CreateAccountInput
|
|
139
|
+
): Promise<Account> {
|
|
140
|
+
if (!ctx.user.isSuperAdmin) {
|
|
141
|
+
throw new ForbiddenError('Only super-admin can create accounts')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (input.domain) {
|
|
145
|
+
const existing = await queryOne(
|
|
142
146
|
db,
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
name: input.name,
|
|
147
|
-
description: input.description ?? null,
|
|
148
|
-
domain: input.domain ?? null,
|
|
149
|
-
}
|
|
147
|
+
`SELECT 1 as ok FROM accounts WHERE domain = ? LIMIT 1`,
|
|
148
|
+
[input.domain]
|
|
150
149
|
)
|
|
151
150
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
throw new Error('Failed to create account')
|
|
151
|
+
if (existing) {
|
|
152
|
+
throw new ConflictError('Account with this domain already exists')
|
|
155
153
|
}
|
|
154
|
+
}
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
createdAt: accountRecord.createdAt,
|
|
163
|
-
updatedAt: accountRecord.updatedAt,
|
|
164
|
-
deletedAt: accountRecord.deletedAt,
|
|
165
|
-
}
|
|
166
|
-
},
|
|
156
|
+
const insertResults = await auditedInsert<AccountRecord>(db, ctx, 'accounts', {
|
|
157
|
+
name: input.name,
|
|
158
|
+
description: input.description ?? null,
|
|
159
|
+
domain: input.domain ?? null,
|
|
160
|
+
})
|
|
167
161
|
|
|
168
|
-
|
|
169
|
-
|
|
162
|
+
const accountRecord = insertResults.at(0)
|
|
163
|
+
if (!accountRecord) {
|
|
164
|
+
throw new Error('Failed to create account')
|
|
165
|
+
}
|
|
170
166
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const existingResults = await db
|
|
174
|
-
.select()
|
|
175
|
-
.from(accounts)
|
|
176
|
-
.where(and(eq(accounts.domain, input.domain), sql`${accounts.id} != ${id}`))
|
|
177
|
-
.limit(1)
|
|
167
|
+
return toAccount(mapAccountRow(accountRecord as unknown as SqlRow))
|
|
168
|
+
}
|
|
178
169
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
170
|
+
async function updateSql(
|
|
171
|
+
db: D1Database,
|
|
172
|
+
ctx: ServiceContext,
|
|
173
|
+
id: string,
|
|
174
|
+
input: UpdateAccountInput
|
|
175
|
+
): Promise<Account> {
|
|
176
|
+
if (!ctx.user.isSuperAdmin) {
|
|
177
|
+
throw new ForbiddenError('Only super-admin can update accounts')
|
|
178
|
+
}
|
|
183
179
|
|
|
184
|
-
|
|
185
|
-
|
|
180
|
+
await findByIdSql(db, ctx, id)
|
|
181
|
+
|
|
182
|
+
if (input.domain) {
|
|
183
|
+
const existing = await queryOne(
|
|
186
184
|
db,
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{
|
|
190
|
-
...input,
|
|
191
|
-
updatedAt: new Date().toISOString(),
|
|
192
|
-
},
|
|
193
|
-
eq(accounts.id, id)
|
|
185
|
+
`SELECT 1 as ok FROM accounts WHERE domain = ? AND id != ? LIMIT 1`,
|
|
186
|
+
[input.domain, id]
|
|
194
187
|
)
|
|
195
188
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
throw new Error('Failed to update account')
|
|
189
|
+
if (existing) {
|
|
190
|
+
throw new ConflictError('Account with this domain already exists')
|
|
199
191
|
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const updates: Record<string, unknown> = {
|
|
195
|
+
updated_at: new Date().toISOString(),
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (input.name !== undefined) updates.name = input.name
|
|
199
|
+
if (input.description !== undefined) updates.description = input.description ?? null
|
|
200
|
+
if (input.domain !== undefined) updates.domain = input.domain ?? null
|
|
201
|
+
|
|
202
|
+
const updateResults = await auditedUpdate<AccountRecord>(
|
|
203
|
+
db,
|
|
204
|
+
ctx,
|
|
205
|
+
'accounts',
|
|
206
|
+
updates,
|
|
207
|
+
{ clause: 'id = ?', params: [id] }
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
const accountRecord = updateResults.at(0)
|
|
211
|
+
if (!accountRecord) {
|
|
212
|
+
throw new Error('Failed to update account')
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return toAccount(mapAccountRow(accountRecord as unknown as SqlRow))
|
|
216
|
+
}
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
domain: accountRecord.domain,
|
|
206
|
-
createdAt: accountRecord.createdAt,
|
|
207
|
-
updatedAt: accountRecord.updatedAt,
|
|
208
|
-
deletedAt: accountRecord.deletedAt,
|
|
209
|
-
}
|
|
210
|
-
},
|
|
218
|
+
async function deleteSql(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
|
|
219
|
+
if (!ctx.user.isSuperAdmin) {
|
|
220
|
+
throw new ForbiddenError('Only super-admin can delete accounts')
|
|
221
|
+
}
|
|
211
222
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
throw new ForbiddenError('Only super-admin can delete accounts')
|
|
216
|
-
}
|
|
223
|
+
await findByIdSql(db, ctx, id)
|
|
224
|
+
await auditedDelete(db, ctx, 'accounts', { clause: 'id = ?', params: [id] })
|
|
225
|
+
}
|
|
217
226
|
|
|
218
|
-
|
|
227
|
+
async function restoreSql(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
|
|
228
|
+
if (!ctx.user.isSuperAdmin) {
|
|
229
|
+
throw new ForbiddenError('Only super-admin can restore accounts')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const row = await queryOne(
|
|
233
|
+
db,
|
|
234
|
+
`SELECT ${ACCOUNT_SELECT_COLUMNS}
|
|
235
|
+
FROM accounts a
|
|
236
|
+
WHERE a.id = ? AND a.deleted_at IS NOT NULL
|
|
237
|
+
LIMIT 1`,
|
|
238
|
+
[id]
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if (!row) {
|
|
242
|
+
throw new NotFoundError('Account not found or not deleted')
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const restoreResults = await auditedUpdate<AccountRecord>(
|
|
246
|
+
db,
|
|
247
|
+
ctx,
|
|
248
|
+
'accounts',
|
|
249
|
+
{ deleted_at: null },
|
|
250
|
+
{ clause: 'id = ?', params: [id] }
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
const restored = restoreResults.at(0)
|
|
254
|
+
if (!restored) {
|
|
255
|
+
throw new NotFoundError('Failed to restore account')
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return toAccount(mapAccountRow(restored as unknown as SqlRow))
|
|
259
|
+
}
|
|
219
260
|
|
|
220
|
-
|
|
221
|
-
|
|
261
|
+
export const accountsService = {
|
|
262
|
+
async findAll(
|
|
263
|
+
db: D1Database,
|
|
264
|
+
ctx: ServiceContext,
|
|
265
|
+
pagination: PaginationQuery
|
|
266
|
+
): Promise<PaginatedResponse<Account>> {
|
|
267
|
+
return findAllSql(db, ctx, pagination)
|
|
222
268
|
},
|
|
223
269
|
|
|
224
|
-
async
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
throw new ForbiddenError('Only super-admin can restore accounts')
|
|
228
|
-
}
|
|
270
|
+
async findById(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
|
|
271
|
+
return findByIdSql(db, ctx, id)
|
|
272
|
+
},
|
|
229
273
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
.from(accounts)
|
|
234
|
-
.where(and(
|
|
235
|
-
eq(accounts.id, id),
|
|
236
|
-
isNotNull(accounts.deletedAt)
|
|
237
|
-
))
|
|
238
|
-
.limit(1)
|
|
239
|
-
|
|
240
|
-
const record = recordResults.at(0)
|
|
241
|
-
if (!record) {
|
|
242
|
-
throw new NotFoundError('Account not found or not deleted')
|
|
243
|
-
}
|
|
274
|
+
async create(db: D1Database, ctx: ServiceContext, input: CreateAccountInput): Promise<Account> {
|
|
275
|
+
return createSql(db, ctx, input)
|
|
276
|
+
},
|
|
244
277
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
278
|
+
async update(
|
|
279
|
+
db: D1Database,
|
|
280
|
+
ctx: ServiceContext,
|
|
281
|
+
id: string,
|
|
282
|
+
input: UpdateAccountInput
|
|
283
|
+
): Promise<Account> {
|
|
284
|
+
return updateSql(db, ctx, id, input)
|
|
285
|
+
},
|
|
253
286
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
287
|
+
async delete(db: D1Database, ctx: ServiceContext, id: string): Promise<void> {
|
|
288
|
+
await deleteSql(db, ctx, id)
|
|
289
|
+
},
|
|
258
290
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
name: restored.name,
|
|
262
|
-
description: restored.description,
|
|
263
|
-
domain: restored.domain,
|
|
264
|
-
createdAt: restored.createdAt,
|
|
265
|
-
updatedAt: restored.updatedAt,
|
|
266
|
-
deletedAt: restored.deletedAt,
|
|
267
|
-
}
|
|
291
|
+
async restore(db: D1Database, ctx: ServiceContext, id: string): Promise<Account> {
|
|
292
|
+
return restoreSql(db, ctx, id)
|
|
268
293
|
},
|
|
269
294
|
}
|