@etus/bhono-app 0.1.1
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/cli.d.ts +13 -0
- package/dist/cli.js +46 -0
- package/dist/cli.js.map +1 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +26 -0
- package/dist/cli.test.js.map +1 -0
- package/dist/generator.d.ts +14 -0
- package/dist/generator.js +142 -0
- package/dist/generator.js.map +1 -0
- package/dist/generator.test.d.ts +1 -0
- package/dist/generator.test.js +127 -0
- package/dist/generator.test.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +97 -0
- package/dist/index.js.map +1 -0
- package/dist/prompts.d.ts +25 -0
- package/dist/prompts.js +83 -0
- package/dist/prompts.js.map +1 -0
- package/dist/prompts.test.d.ts +1 -0
- package/dist/prompts.test.js +24 -0
- package/dist/prompts.test.js.map +1 -0
- package/dist/providers/cloudflare.d.ts +37 -0
- package/dist/providers/cloudflare.js +61 -0
- package/dist/providers/cloudflare.js.map +1 -0
- package/dist/providers/cloudflare.test.d.ts +1 -0
- package/dist/providers/cloudflare.test.js +29 -0
- package/dist/providers/cloudflare.test.js.map +1 -0
- package/dist/providers/github.d.ts +16 -0
- package/dist/providers/github.js +57 -0
- package/dist/providers/github.js.map +1 -0
- package/dist/providers/github.test.d.ts +1 -0
- package/dist/providers/github.test.js +16 -0
- package/dist/providers/github.test.js.map +1 -0
- package/dist/templates.d.ts +8 -0
- package/dist/templates.js +88 -0
- package/dist/templates.js.map +1 -0
- package/dist/templates.test.d.ts +1 -0
- package/dist/templates.test.js +26 -0
- package/dist/templates.test.js.map +1 -0
- package/package.json +36 -0
- package/templates/base/.claude/agents/architect-review.md +160 -0
- package/templates/base/.claude/agents/backend-architect.md +308 -0
- package/templates/base/.claude/agents/code-reviewer.md +170 -0
- package/templates/base/.claude/agents/performance-engineer.md +166 -0
- package/templates/base/.claude/agents/test-automator.md +219 -0
- package/templates/base/.claude/commands/check-skill-rules.md +53 -0
- package/templates/base/.claude/commands/claude-md.md +250 -0
- package/templates/base/.claude/commands/code-prompt.md +212 -0
- package/templates/base/.claude/commands/explain-code.md +194 -0
- package/templates/base/.claude/commands/init-projec.md +89 -0
- package/templates/base/.claude/commands/linear/README.md +297 -0
- package/templates/base/.claude/commands/linear/create-issue.md +190 -0
- package/templates/base/.claude/commands/linear/implement-issue.md +248 -0
- package/templates/base/.claude/commands/linear/process-triage.md +399 -0
- package/templates/base/.claude/commands/linear/setup.md +180 -0
- package/templates/base/.claude/commands/prime.md +9 -0
- package/templates/base/.claude/commands/review-gap.md +10 -0
- package/templates/base/.claude/commands/setup-aa.md +311 -0
- package/templates/base/.claude/commands/ship.md +262 -0
- package/templates/base/.claude/commands/tools.md +3 -0
- package/templates/base/.claude/docs/claude-progress.txt +107 -0
- package/templates/base/.claude/hooks/package-lock.json +556 -0
- package/templates/base/.claude/hooks/package.json +16 -0
- package/templates/base/.claude/hooks/skill-activation-prompt.sh +7 -0
- package/templates/base/.claude/hooks/skill-activation-prompt.ts +142 -0
- package/templates/base/.claude/hooks/tsconfig.json +19 -0
- package/templates/base/.claude/scripts/check-updates.sh +85 -0
- package/templates/base/.claude/scripts/install_pkgs.sh +66 -0
- package/templates/base/.claude/scripts/setup-project.sh +177 -0
- package/templates/base/.claude/scripts/validate-skill-rules.sh +94 -0
- package/templates/base/.claude/settings.json +113 -0
- package/templates/base/.claude/settings.local.json +11 -0
- package/templates/base/.claude/skills/architecture-analyzer/SKILL.md +531 -0
- package/templates/base/.claude/skills/architecture-analyzer/assets/report-template.md +215 -0
- package/templates/base/.claude/skills/architecture-analyzer/references/c4-templates.md +234 -0
- package/templates/base/.claude/skills/architecture-analyzer/references/confidence-levels.md +203 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/analyze_structure.py +266 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/analyze_tech_debt.py +776 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/extract_apis.py +338 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/generate_c4.py +283 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/generate_erd.py +935 -0
- package/templates/base/.claude/skills/architecture-analyzer/scripts/map_dependencies.py +555 -0
- package/templates/base/.claude/skills/dev-browser/SKILL.md +318 -0
- package/templates/base/.claude/skills/dev-browser/bun.lock +443 -0
- package/templates/base/.claude/skills/dev-browser/package-lock.json +2927 -0
- package/templates/base/.claude/skills/dev-browser/package.json +27 -0
- package/templates/base/.claude/skills/dev-browser/scripts/start-server.ts +117 -0
- package/templates/base/.claude/skills/dev-browser/server.sh +24 -0
- package/templates/base/.claude/skills/dev-browser/src/client.ts +403 -0
- package/templates/base/.claude/skills/dev-browser/src/index.ts +281 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/templates/base/.claude/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/templates/base/.claude/skills/dev-browser/src/types.ts +27 -0
- package/templates/base/.claude/skills/dev-browser/tsconfig.json +36 -0
- package/templates/base/.claude/skills/dev-browser/vitest.config.ts +12 -0
- package/templates/base/.claude/skills/linear/SKILL.md +440 -0
- package/templates/base/.claude/skills/linear/examples.md +262 -0
- package/templates/base/.claude/skills/linear/lib/client.ts +51 -0
- package/templates/base/.claude/skills/linear/lib/config.ts +106 -0
- package/templates/base/.claude/skills/linear/lib/output.ts +34 -0
- package/templates/base/.claude/skills/linear/package-lock.json +698 -0
- package/templates/base/.claude/skills/linear/package.json +27 -0
- package/templates/base/.claude/skills/linear/reference.md +263 -0
- package/templates/base/.claude/skills/linear/scripts/comments/create.ts +47 -0
- package/templates/base/.claude/skills/linear/scripts/comments/list.ts +47 -0
- package/templates/base/.claude/skills/linear/scripts/issues/archive.ts +30 -0
- package/templates/base/.claude/skills/linear/scripts/issues/create.ts +279 -0
- package/templates/base/.claude/skills/linear/scripts/issues/get.ts +68 -0
- package/templates/base/.claude/skills/linear/scripts/issues/list.ts +67 -0
- package/templates/base/.claude/skills/linear/scripts/issues/update.ts +281 -0
- package/templates/base/.claude/skills/linear/scripts/labels/add-to-issue.ts +63 -0
- package/templates/base/.claude/skills/linear/scripts/labels/create.ts +45 -0
- package/templates/base/.claude/skills/linear/scripts/labels/list.ts +30 -0
- package/templates/base/.claude/skills/linear/scripts/list-teams.ts +52 -0
- package/templates/base/.claude/skills/linear/scripts/setup/setup-credentials.ts +96 -0
- package/templates/base/.claude/skills/linear/scripts/status/list.ts +31 -0
- package/templates/base/.claude/skills/linear/scripts/status/set-by-name.ts +78 -0
- package/templates/base/.claude/skills/linear/scripts/status/update.ts +44 -0
- package/templates/base/.claude/skills/linear/scripts/users/list.ts +59 -0
- package/templates/base/.claude/skills/linear/scripts/users/me.ts +20 -0
- package/templates/base/.claude/skills/linear/templates/README.md +203 -0
- package/templates/base/.claude/skills/linear/templates/api-reference.md +258 -0
- package/templates/base/.claude/skills/linear/templates/bug-report.md +99 -0
- package/templates/base/.claude/skills/linear/templates/feature-request.md +118 -0
- package/templates/base/.claude/skills/linear/templates/security-issue.md +162 -0
- package/templates/base/.claude/skills/linear/templates/sprint-task.md +175 -0
- package/templates/base/.claude/skills/linear/templates/tech-debt.md +137 -0
- package/templates/base/.claude/skills/linear/tsconfig.json +17 -0
- package/templates/base/.claude/skills/linear/workflows/issue-lifecycle.md +317 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/SKILL.md +113 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/global-setup.template.js +97 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/playwright.config.template.js +171 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/assets/test-template.spec.js +163 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/README.md +26 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/ads.email-deeplink.spec.ts +12 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/mobile.realism.spec.ts +16 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/examples/smoke.home.spec.ts +6 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/architecture.md +578 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/best-practices.md +260 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/ci-reporting.md +86 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/debugging.md +629 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/mobile-realism.md +50 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/optimization.md +488 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/patterns.md +513 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/resources.md +44 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/references/visual-a11y.md +66 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/auth-setup.js +202 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/performance-analyzer.js +240 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/scripts/trace-url.js +132 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/ci/github-actions.playwright.yml +78 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/fixtures.ts +44 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/global-setup.template.js +97 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/global.setup.ts +35 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/helpers/ad-gpt-observer.ts +80 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/helpers/chromium-mobile-profile.ts +93 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/playwright.config.template.js +171 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/playwright.config.ts +126 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/test-template.spec.js +163 -0
- package/templates/base/.claude/skills/playwright-e2e-testing/templates/tests/email-deeplink.ads.spec.ts +44 -0
- package/templates/base/.claude/skills/skill-rules.json +184 -0
- package/templates/base/.claude/skills/wrangler/SKILL.md +209 -0
- package/templates/base/.claude/skills/wrangler/resources/api.md +494 -0
- package/templates/base/.claude/skills/wrangler/resources/bundling.md +83 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/cert.md +64 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/check.md +66 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/containers.md +157 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/d1.md +843 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/delete.md +27 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/deploy.md +139 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/deployments.md +56 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/dev.md +157 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/dispatch-namespace.md +69 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/docs.md +61 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/how-to-run.md +62 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/hyperdrive.md +425 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/init.md +31 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-bulk.md +265 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-key.md +353 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/kv-namespace.md +265 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/login.md +23 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/logout.md +19 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/mtls-certificate.md +69 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/pages.md +175 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/pipelines.md +76 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/queues.md +132 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-bucket.md +342 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-object.md +267 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/r2-sql.md +65 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/rollback.md +40 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secret.md +308 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secrets-store-secret.md +100 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/secrets-store-store.md +60 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/setup.md +67 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/tail.md +37 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/telemetry.md +64 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/triggers.md +39 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/types.md +73 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/vectorize.md +941 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/versions.md +95 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/whoami.md +49 -0
- package/templates/base/.claude/skills/wrangler/resources/commands/workflows.md +117 -0
- package/templates/base/.claude/skills/wrangler/resources/commands.md +138 -0
- package/templates/base/.claude/skills/wrangler/resources/configuration.md +2176 -0
- package/templates/base/.claude/skills/wrangler/resources/custom-builds.md +55 -0
- package/templates/base/.claude/skills/wrangler/resources/deprecations.md +279 -0
- package/templates/base/.claude/skills/wrangler/resources/enviroments.md +416 -0
- package/templates/base/.claude/skills/wrangler/resources/extract_sections.py +119 -0
- package/templates/base/.claude/skills/wrangler/resources/process_content.py +94 -0
- package/templates/base/.claude/skills/wrangler/resources/system-enviroments-variables.md +120 -0
- package/templates/base/.dev.vars.example +15 -0
- package/templates/base/.env.example +29 -0
- package/templates/base/.github/workflows/test.yml +127 -0
- package/templates/base/.nycrc.json +16 -0
- package/templates/base/CLAUDE.md +218 -0
- package/templates/base/README.md +670 -0
- package/templates/base/auth-setup-error.png +0 -0
- package/templates/base/config/drizzle.config.ts +10 -0
- package/templates/base/config/eslint.config.js +364 -0
- package/templates/base/config/wrangler.json +76 -0
- package/templates/base/docs/app_spec.txt +879 -0
- package/templates/base/docs/app_spec_template.md +681 -0
- package/templates/base/docs/architecture/README.md +8 -0
- package/templates/base/docs/architecture/data-requirements.md +109 -0
- package/templates/base/docs/architecture/erd.md +91 -0
- package/templates/base/docs/features/feature_list.json +3128 -0
- package/templates/base/docs/hono-boilerplate-plan.md +1774 -0
- package/templates/base/docs/test-coverage-gap-analysis.md +242 -0
- package/templates/base/docs/testing.md +188 -0
- package/templates/base/index.html +16 -0
- package/templates/base/package.json +115 -0
- package/templates/base/playwright.config.ts +158 -0
- package/templates/base/pnpm-lock.yaml +8175 -0
- package/templates/base/scripts/capture-prod-session.ts +250 -0
- package/templates/base/scripts/generate-openapi.ts +23 -0
- package/templates/base/scripts/init.sh +121 -0
- package/templates/base/src/client/__tests__/button.test.tsx +30 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-dashboard-stats-cards-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-quick-action-cards-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-display-recent-activity-section-1.png +0 -0
- package/templates/base/src/client/__tests__/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__/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__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-render-dashboard-when-authenticated-1.png +0 -0
- package/templates/base/src/client/__tests__/routes/__screenshots__/dashboard.test.tsx/Dashboard-Page-when-authenticated-should-show-navigation-sidebar-1.png +0 -0
- package/templates/base/src/client/__tests__/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/__tests__/routes/__screenshots__/login.test.tsx/Login-Page-should-display-Google-OAuth-login-button-1.png +0 -0
- package/templates/base/src/client/__tests__/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__/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__/routes/__screenshots__/login.test.tsx/Login-Page-should-render-login-page-at--login-route-1.png +0 -0
- package/templates/base/src/client/__tests__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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__/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/__tests__/routes/authenticated-layout.test.tsx +252 -0
- package/templates/base/src/client/__tests__/routes/dashboard.test.tsx +136 -0
- package/templates/base/src/client/__tests__/routes/error-components.test.tsx +186 -0
- package/templates/base/src/client/__tests__/routes/login.test.tsx +112 -0
- package/templates/base/src/client/__tests__/routes/navigation.test.tsx +272 -0
- package/templates/base/src/client/__tests__/routes/root-layout.test.tsx +65 -0
- package/templates/base/src/client/__tests__/setup-browser.ts +15 -0
- package/templates/base/src/client/__tests__/setup.ts +32 -0
- package/templates/base/src/client/__tests__/test-utils.tsx +135 -0
- package/templates/base/src/client/api/.gitkeep +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-can-be-collapsed-by-default-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-expands-when-collapsed-and-expand-button-is-clicked-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-handles-logout-button-click-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-handles-navigation-clicks-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-hides-navigation-labels-when-collapsed-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-highlights-active-route-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-renders-sidebar-navigation-items-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-keyboard-shortcut-hint-when-expanded-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-info-when-authenticated-1.png +0 -0
- package/templates/base/src/client/components/__tests__/__screenshots__/sidebar.test.tsx/Sidebar-shows-user-initials-in-avatar-fallback-1.png +0 -0
- package/templates/base/src/client/components/__tests__/error-boundary.test.tsx +97 -0
- package/templates/base/src/client/components/__tests__/sidebar.test.tsx +281 -0
- package/templates/base/src/client/components/error-boundary.tsx +68 -0
- package/templates/base/src/client/components/icons.tsx +106 -0
- package/templates/base/src/client/components/layout/.gitkeep +0 -0
- package/templates/base/src/client/components/sidebar.tsx +267 -0
- package/templates/base/src/client/components/ui/.gitkeep +0 -0
- package/templates/base/src/client/components/ui/__tests__/avatar.test.tsx +308 -0
- package/templates/base/src/client/components/ui/__tests__/card.test.tsx +214 -0
- package/templates/base/src/client/components/ui/__tests__/dialog.test.tsx +297 -0
- package/templates/base/src/client/components/ui/__tests__/error-fallback.test.tsx +145 -0
- package/templates/base/src/client/components/ui/__tests__/input.test.tsx +98 -0
- package/templates/base/src/client/components/ui/__tests__/loading-skeleton.test.tsx +139 -0
- package/templates/base/src/client/components/ui/__tests__/skeleton.test.tsx +44 -0
- package/templates/base/src/client/components/ui/__tests__/sonner.test.tsx +28 -0
- package/templates/base/src/client/components/ui/__tests__/tabs.test.tsx +233 -0
- package/templates/base/src/client/components/ui/avatar.tsx +101 -0
- package/templates/base/src/client/components/ui/badge.tsx +46 -0
- package/templates/base/src/client/components/ui/button.tsx +72 -0
- package/templates/base/src/client/components/ui/card.tsx +86 -0
- package/templates/base/src/client/components/ui/dialog.tsx +140 -0
- package/templates/base/src/client/components/ui/error-fallback.tsx +179 -0
- package/templates/base/src/client/components/ui/form.tsx +172 -0
- package/templates/base/src/client/components/ui/input.tsx +24 -0
- package/templates/base/src/client/components/ui/label.tsx +22 -0
- package/templates/base/src/client/components/ui/loading-skeleton.tsx +154 -0
- package/templates/base/src/client/components/ui/separator.tsx +33 -0
- package/templates/base/src/client/components/ui/skeleton.tsx +16 -0
- package/templates/base/src/client/components/ui/sonner.tsx +29 -0
- package/templates/base/src/client/components/ui/tabs.tsx +121 -0
- package/templates/base/src/client/hooks/.gitkeep +0 -0
- package/templates/base/src/client/hooks/__tests__/use-auth.test.tsx +306 -0
- package/templates/base/src/client/hooks/__tests__/use-theme.test.tsx +172 -0
- package/templates/base/src/client/hooks/use-auth.ts +53 -0
- package/templates/base/src/client/hooks/use-theme.tsx +78 -0
- package/templates/base/src/client/index.css +881 -0
- package/templates/base/src/client/lib/query-client.ts +11 -0
- package/templates/base/src/client/lib/utils.ts +7 -0
- package/templates/base/src/client/main.tsx +26 -0
- package/templates/base/src/client/routeTree.gen.ts +258 -0
- package/templates/base/src/client/router.ts +15 -0
- package/templates/base/src/client/routes/$.tsx +77 -0
- package/templates/base/src/client/routes/.gitkeep +0 -0
- package/templates/base/src/client/routes/__root.tsx +34 -0
- package/templates/base/src/client/routes/__tests__/__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__/__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__/__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__/__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__/__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__/__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/routes/__tests__/invite.test.tsx +138 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__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__/__screenshots__/account.test.tsx/Account-Page-should-show-API-Access-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-Webhooks-section-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-connected-count-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-page-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-page-rendering-should-display-search-input-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__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__/__screenshots__/integrations.test.tsx/Integrations-Page-search-functionality-should-search-by-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-existing-webhook-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__screenshots__/integrations.test.tsx/Integrations-Page-webhooks-section-should-display-webhook-events-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__screenshots__/settings.test.tsx/Settings-Page-page-rendering-should-display-page-description-1.png +0 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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__/__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/routes/_authenticated/__tests__/account.test.tsx +324 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/integrations.test.tsx +520 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/settings.test.tsx +414 -0
- package/templates/base/src/client/routes/_authenticated/__tests__/team.test.tsx +374 -0
- package/templates/base/src/client/routes/_authenticated/account.tsx +416 -0
- package/templates/base/src/client/routes/_authenticated/dashboard.tsx +151 -0
- package/templates/base/src/client/routes/_authenticated/integrations.tsx +553 -0
- package/templates/base/src/client/routes/_authenticated/settings.tsx +310 -0
- package/templates/base/src/client/routes/_authenticated/team.tsx +395 -0
- package/templates/base/src/client/routes/_authenticated.tsx +69 -0
- package/templates/base/src/client/routes/index.tsx +155 -0
- package/templates/base/src/client/routes/invite.$token.tsx +191 -0
- package/templates/base/src/client/routes/login.tsx +92 -0
- package/templates/base/src/server/__tests__/fixtures.ts +461 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/db.test.ts +239 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/kv.test.ts +293 -0
- package/templates/base/src/server/__tests__/mocks/__tests__/r2.test.ts +363 -0
- package/templates/base/src/server/__tests__/mocks/db.ts +186 -0
- package/templates/base/src/server/__tests__/mocks/index.ts +33 -0
- package/templates/base/src/server/__tests__/mocks/kv.ts +286 -0
- package/templates/base/src/server/__tests__/mocks/r2.ts +397 -0
- package/templates/base/src/server/__tests__/setup.ts +281 -0
- package/templates/base/src/server/auth/__tests__/guards.test.ts +162 -0
- package/templates/base/src/server/auth/guards.ts +92 -0
- package/templates/base/src/server/auth/permissions.test.ts +45 -0
- package/templates/base/src/server/auth/permissions.ts +139 -0
- package/templates/base/src/server/auth/roles.test.ts +169 -0
- package/templates/base/src/server/auth/roles.ts +141 -0
- package/templates/base/src/server/db/client.ts +12 -0
- package/templates/base/src/server/db/schema/accounts.ts +20 -0
- package/templates/base/src/server/db/schema/audit-logs.ts +26 -0
- package/templates/base/src/server/db/schema/index.ts +7 -0
- package/templates/base/src/server/db/schema/invitations.ts +30 -0
- package/templates/base/src/server/db/schema/refresh-tokens.ts +22 -0
- package/templates/base/src/server/db/schema/user-accounts.ts +25 -0
- package/templates/base/src/server/db/schema/users.ts +33 -0
- package/templates/base/src/server/db/seed.ts +267 -0
- package/templates/base/src/server/env.test.ts +84 -0
- package/templates/base/src/server/env.ts +78 -0
- package/templates/base/src/server/index.ts +82 -0
- package/templates/base/src/server/lib/audit.ts +73 -0
- package/templates/base/src/server/lib/audited-db.test.ts +107 -0
- package/templates/base/src/server/lib/audited-db.ts +154 -0
- package/templates/base/src/server/lib/email.test.ts +116 -0
- package/templates/base/src/server/lib/email.ts +82 -0
- package/templates/base/src/server/lib/errors.test.ts +49 -0
- package/templates/base/src/server/lib/errors.ts +64 -0
- package/templates/base/src/server/lib/oauth.test.ts +238 -0
- package/templates/base/src/server/lib/oauth.ts +113 -0
- package/templates/base/src/server/lib/pagination.test.ts +52 -0
- package/templates/base/src/server/lib/pagination.ts +32 -0
- package/templates/base/src/server/lib/password.test.ts +151 -0
- package/templates/base/src/server/lib/password.ts +151 -0
- package/templates/base/src/server/lib/providers.test.ts +105 -0
- package/templates/base/src/server/lib/providers.ts +62 -0
- package/templates/base/src/server/lib/r2-storage.test.ts +202 -0
- package/templates/base/src/server/lib/r2-storage.ts +107 -0
- package/templates/base/src/server/lib/schema-helpers.ts +16 -0
- package/templates/base/src/server/lib/session.test.ts +758 -0
- package/templates/base/src/server/lib/session.ts +267 -0
- package/templates/base/src/server/lib/tokens.test.ts +208 -0
- package/templates/base/src/server/lib/tokens.ts +65 -0
- package/templates/base/src/server/lib/transaction.test.ts +45 -0
- package/templates/base/src/server/lib/transaction.ts +24 -0
- package/templates/base/src/server/middleware/account.test.ts +201 -0
- package/templates/base/src/server/middleware/account.ts +66 -0
- package/templates/base/src/server/middleware/auth.test.ts +345 -0
- package/templates/base/src/server/middleware/auth.ts +146 -0
- package/templates/base/src/server/middleware/cors.test.ts +88 -0
- package/templates/base/src/server/middleware/cors.ts +26 -0
- package/templates/base/src/server/middleware/error-handler.test.ts +69 -0
- package/templates/base/src/server/middleware/error-handler.ts +43 -0
- package/templates/base/src/server/middleware/index.ts +8 -0
- package/templates/base/src/server/middleware/rate-limit.test.ts +472 -0
- package/templates/base/src/server/middleware/rate-limit.ts +294 -0
- package/templates/base/src/server/middleware/request-context.test.ts +175 -0
- package/templates/base/src/server/middleware/request-context.ts +28 -0
- package/templates/base/src/server/middleware/request-logger.test.ts +92 -0
- package/templates/base/src/server/middleware/request-logger.ts +50 -0
- package/templates/base/src/server/routes/accounts/__tests__/handlers.test.ts +460 -0
- package/templates/base/src/server/routes/accounts/handlers.ts +179 -0
- package/templates/base/src/server/routes/accounts/index.ts +55 -0
- package/templates/base/src/server/routes/accounts/routes.ts +205 -0
- package/templates/base/src/server/routes/accounts/schemas.ts +31 -0
- package/templates/base/src/server/routes/api.ts +37 -0
- package/templates/base/src/server/routes/audits/__tests__/handlers.test.ts +349 -0
- package/templates/base/src/server/routes/audits/handlers.ts +37 -0
- package/templates/base/src/server/routes/audits/index.ts +14 -0
- package/templates/base/src/server/routes/audits/routes.ts +29 -0
- package/templates/base/src/server/routes/audits/schemas.ts +43 -0
- package/templates/base/src/server/routes/auth/__tests__/handlers.test.ts +381 -0
- package/templates/base/src/server/routes/auth/handlers.ts +222 -0
- package/templates/base/src/server/routes/auth/index.ts +37 -0
- package/templates/base/src/server/routes/auth/routes.ts +136 -0
- package/templates/base/src/server/routes/auth/schemas.ts +48 -0
- package/templates/base/src/server/routes/auth/test-login.ts +156 -0
- package/templates/base/src/server/routes/health/__tests__/handlers.test.ts +237 -0
- package/templates/base/src/server/routes/health/handlers.ts +83 -0
- package/templates/base/src/server/routes/health/index.ts +13 -0
- package/templates/base/src/server/routes/health/routes.ts +90 -0
- package/templates/base/src/server/routes/index.ts +53 -0
- package/templates/base/src/server/routes/invitations/__tests__/handlers.test.ts +473 -0
- package/templates/base/src/server/routes/invitations/handlers.ts +71 -0
- package/templates/base/src/server/routes/invitations/index.ts +25 -0
- package/templates/base/src/server/routes/invitations/routes.ts +69 -0
- package/templates/base/src/server/routes/invitations/schemas.ts +39 -0
- package/templates/base/src/server/routes/openapi.ts +14 -0
- package/templates/base/src/server/routes/schemas.ts +53 -0
- package/templates/base/src/server/routes/storage/__tests__/handlers.test.ts +408 -0
- package/templates/base/src/server/routes/storage/handlers.ts +100 -0
- package/templates/base/src/server/routes/storage/index.ts +42 -0
- package/templates/base/src/server/routes/storage/routes.ts +91 -0
- package/templates/base/src/server/routes/storage/schemas.ts +56 -0
- package/templates/base/src/server/routes/users/__tests__/handlers.test.ts +526 -0
- package/templates/base/src/server/routes/users/handlers.ts +228 -0
- package/templates/base/src/server/routes/users/index.ts +67 -0
- package/templates/base/src/server/routes/users/routes.ts +265 -0
- package/templates/base/src/server/routes/users/schemas.ts +67 -0
- package/templates/base/src/server/services/__tests__/accounts.test.ts +764 -0
- package/templates/base/src/server/services/__tests__/audits.test.ts +235 -0
- package/templates/base/src/server/services/__tests__/auth.test.ts +765 -0
- package/templates/base/src/server/services/__tests__/invitations.test.ts +704 -0
- package/templates/base/src/server/services/__tests__/users.test.ts +755 -0
- package/templates/base/src/server/services/accounts.ts +269 -0
- package/templates/base/src/server/services/audits.ts +82 -0
- package/templates/base/src/server/services/auth.ts +225 -0
- package/templates/base/src/server/services/index.ts +6 -0
- package/templates/base/src/server/services/invitations.ts +306 -0
- package/templates/base/src/server/services/users.ts +350 -0
- package/templates/base/src/server/types/auth.ts +36 -0
- package/templates/base/src/server/types/index.ts +117 -0
- package/templates/base/src/shared/schemas/.gitkeep +0 -0
- package/templates/base/src/shared/schemas/__tests__/schemas.test.ts +547 -0
- package/templates/base/src/shared/schemas/account.ts +15 -0
- package/templates/base/src/shared/schemas/index.ts +6 -0
- package/templates/base/src/shared/schemas/invitation.ts +9 -0
- package/templates/base/src/shared/schemas/profile.ts +10 -0
- package/templates/base/src/shared/schemas/user.ts +16 -0
- package/templates/base/src/shared/schemas/webhook.ts +12 -0
- package/templates/base/src/shared/types/.gitkeep +0 -0
- package/templates/base/src/shared/types/account.ts +12 -0
- package/templates/base/src/shared/types/api.ts +1399 -0
- package/templates/base/src/shared/types/auth.ts +24 -0
- package/templates/base/src/shared/types/index.ts +5 -0
- package/templates/base/src/shared/types/user.ts +31 -0
- package/templates/base/src/test/vitest-zod-matcher.ts +37 -0
- package/templates/base/src/test/vitest.d.ts +19 -0
- package/templates/base/tests/e2e/README.md +141 -0
- package/templates/base/tests/e2e/a11y/accessibility.spec.ts +925 -0
- package/templates/base/tests/e2e/a11y/keyboard-navigation.spec.ts +610 -0
- package/templates/base/tests/e2e/api/accounts.spec.ts +148 -0
- package/templates/base/tests/e2e/api/audit-logs.spec.ts +130 -0
- package/templates/base/tests/e2e/api/authenticated-api.spec.ts +311 -0
- package/templates/base/tests/e2e/api/storage.spec.ts +109 -0
- package/templates/base/tests/e2e/auth-flows.unauth.spec.ts +117 -0
- package/templates/base/tests/e2e/auth-logout.unauth.spec.ts +103 -0
- package/templates/base/tests/e2e/auth.setup.ts +115 -0
- package/templates/base/tests/e2e/auth.spec.ts +146 -0
- package/templates/base/tests/e2e/compatibility/cross-browser.spec.ts +152 -0
- package/templates/base/tests/e2e/compatibility/cross-browser.spec.ts-snapshots/login-chromium-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/crud/account.spec.ts +356 -0
- package/templates/base/tests/e2e/crud/integrations.spec.ts +419 -0
- package/templates/base/tests/e2e/crud/team.spec.ts +287 -0
- package/templates/base/tests/e2e/crud/users.spec.ts +239 -0
- package/templates/base/tests/e2e/errors/error-boundary.spec.ts +428 -0
- package/templates/base/tests/e2e/errors/error-handling.spec.ts +47 -0
- package/templates/base/tests/e2e/errors/error-handling.unauth.spec.ts +205 -0
- package/templates/base/tests/e2e/fixtures.ts +266 -0
- package/templates/base/tests/e2e/forms/validation.spec.ts +569 -0
- package/templates/base/tests/e2e/invitations/invite-flow.unauth.spec.ts +204 -0
- package/templates/base/tests/e2e/journeys/account-lifecycle.spec.ts +314 -0
- package/templates/base/tests/e2e/journeys/audit-investigation.spec.ts +299 -0
- package/templates/base/tests/e2e/journeys/auth-onboarding.spec.ts +232 -0
- package/templates/base/tests/e2e/journeys/critical-flows.spec.ts +281 -0
- package/templates/base/tests/e2e/journeys/error-recovery.spec.ts +354 -0
- package/templates/base/tests/e2e/journeys/file-management.spec.ts +307 -0
- package/templates/base/tests/e2e/journeys/integrations.spec.ts +372 -0
- package/templates/base/tests/e2e/journeys/multi-account.spec.ts +317 -0
- package/templates/base/tests/e2e/journeys/rbac-enforcement.spec.ts +389 -0
- package/templates/base/tests/e2e/journeys/settings-profile.spec.ts +400 -0
- package/templates/base/tests/e2e/journeys/team-collaboration.spec.ts +410 -0
- package/templates/base/tests/e2e/mobile/responsive.spec.ts +178 -0
- package/templates/base/tests/e2e/navigation/routing.spec.ts +371 -0
- package/templates/base/tests/e2e/navigation/sidebar.spec.ts +425 -0
- package/templates/base/tests/e2e/pages/ui-features.spec.ts +393 -0
- package/templates/base/tests/e2e/performance/baselines.spec.ts +162 -0
- package/templates/base/tests/e2e/performance/benchmarks.spec.ts +371 -0
- package/templates/base/tests/e2e/smoke.unauth.spec.ts +196 -0
- package/templates/base/tests/e2e/visual/components.spec.ts +650 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/confirmation-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-background-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-card-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dark-mode-sidebar-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dashboard-card-single-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dialog-content-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/dialog-with-backdrop-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/empty-search-results-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/input-default-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/input-focus-ring-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-focus-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-hover-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/primary-button-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-active-nav-item-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-collapsed-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/components.spec.ts-snapshots/sidebar-navigation-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts +192 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/account-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/create-webhook-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/dashboard-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/delete-account-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/integrations-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/invite-member-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/login-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/settings-account-tab-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/settings-profile-tab-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/sidebar-navigation-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/core-components.spec.ts-snapshots/team-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts +230 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/404-page-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/404-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/account-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/login-page-chromium-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/login-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/settings-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/team-invite-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/team-page-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/screenshots.spec.ts-snapshots/webhook-create-dialog-visual-darwin.png +0 -0
- package/templates/base/tests/e2e/visual/theme-colors.spec.ts +293 -0
- package/templates/base/tests/e2e/visual/ui-components.spec.ts +502 -0
- package/templates/base/tests/integration/accounts/crud.test.ts +1402 -0
- package/templates/base/tests/integration/audits/list.test.ts +1133 -0
- package/templates/base/tests/integration/auth/auth-service.test.ts +415 -0
- package/templates/base/tests/integration/auth/invitation-token.test.ts +529 -0
- package/templates/base/tests/integration/auth/logout.test.ts +524 -0
- package/templates/base/tests/integration/auth/oauth.test.ts +768 -0
- package/templates/base/tests/integration/auth/refresh-token.test.ts +364 -0
- package/templates/base/tests/integration/auth/session-expiry.test.ts +569 -0
- package/templates/base/tests/integration/auth/session.test.ts +520 -0
- package/templates/base/tests/integration/auth/super-admin.test.ts +451 -0
- package/templates/base/tests/integration/authorization/analytics-role.test.ts +1026 -0
- package/templates/base/tests/integration/authorization/billing-role.test.ts +776 -0
- package/templates/base/tests/integration/authorization/guards-roles.test.ts +539 -0
- package/templates/base/tests/integration/authorization/multi-tenancy.test.ts +1112 -0
- package/templates/base/tests/integration/authorization/role-hierarchy.test.ts +931 -0
- package/templates/base/tests/integration/authorization/roles.test.ts +1347 -0
- package/templates/base/tests/integration/config/production-behavior.test.ts +536 -0
- package/templates/base/tests/integration/db/constraints.test.ts +695 -0
- package/templates/base/tests/integration/fixtures/accounts.ts +136 -0
- package/templates/base/tests/integration/fixtures/index.ts +232 -0
- package/templates/base/tests/integration/fixtures/invitations.ts +195 -0
- package/templates/base/tests/integration/fixtures/users.ts +144 -0
- package/templates/base/tests/integration/health/health.test.ts +351 -0
- package/templates/base/tests/integration/invitations/crud.test.ts +1457 -0
- package/templates/base/tests/integration/invitations/email.test.ts +506 -0
- package/templates/base/tests/integration/lib/email.test.ts +174 -0
- package/templates/base/tests/integration/lib/oauth.test.ts +368 -0
- package/templates/base/tests/integration/lib/password.test.ts +192 -0
- package/templates/base/tests/integration/lib/schema-helpers.test.ts +129 -0
- package/templates/base/tests/integration/lib/tokens.test.ts +304 -0
- package/templates/base/tests/integration/middleware/auth.test.ts +499 -0
- package/templates/base/tests/integration/middleware/cors.test.ts +334 -0
- package/templates/base/tests/integration/middleware/request-context.test.ts +156 -0
- package/templates/base/tests/integration/middleware/request-logger.test.ts +313 -0
- package/templates/base/tests/integration/performance/response-times.test.ts +509 -0
- package/templates/base/tests/integration/security/cookie-security.test.ts +567 -0
- package/templates/base/tests/integration/security/csrf-protection.test.ts +542 -0
- package/templates/base/tests/integration/security/jwt-validation.test.ts +209 -0
- package/templates/base/tests/integration/security/log-sanitization.test.ts +658 -0
- package/templates/base/tests/integration/security/rate-limiting.test.ts +1251 -0
- package/templates/base/tests/integration/security/sql-injection.test.ts +663 -0
- package/templates/base/tests/integration/security/token-hashing.test.ts +371 -0
- package/templates/base/tests/integration/security/xss-prevention.test.ts +541 -0
- package/templates/base/tests/integration/setup.ts +834 -0
- package/templates/base/tests/integration/smoke.test.ts +288 -0
- package/templates/base/tests/integration/storage/upload.test.ts +1162 -0
- package/templates/base/tests/integration/storage/validation.test.ts +746 -0
- package/templates/base/tests/integration/users/crud.test.ts +1297 -0
- package/templates/base/tests/integration/users/list.test.ts +698 -0
- package/templates/base/tests/integration/vitest.config.ts +80 -0
- package/templates/base/tsconfig.app.json +18 -0
- package/templates/base/tsconfig.json +39 -0
- package/templates/base/tsconfig.node.json +16 -0
- package/templates/base/tsconfig.server.json +26 -0
- package/templates/base/tsconfig.tsbuildinfo +1 -0
- package/templates/base/vite.config.ts +46 -0
- package/templates/base/vitest.config.browser.ts +47 -0
- package/templates/base/vitest.config.frontend.ts +47 -0
- package/templates/base/vitest.config.ts +82 -0
- package/templates/base/vitest.workspace.ts +22 -0
- package/templates/modules/audit-logs/.gitkeep +0 -0
- package/templates/modules/billing/.gitkeep +0 -0
- package/templates/modules/invitations/.gitkeep +0 -0
- package/templates/modules/storage/.gitkeep +0 -0
- package/templates/modules/webhooks/.gitkeep +0 -0
- package/templates/providers/auth-email/.gitkeep +0 -0
- package/templates/providers/auth-github/.gitkeep +0 -0
- package/templates/providers/auth-google/.gitkeep +0 -0
- package/templates/providers/email-resend/.gitkeep +0 -0
- package/templates/providers/email-sendgrid/.gitkeep +0 -0
|
@@ -0,0 +1,1774 @@
|
|
|
1
|
+
# Hono Boilerplate Plan
|
|
2
|
+
|
|
3
|
+
This document outlines a plan to create a new backend boilerplate using Hono.js, preserving the core concepts from the NestJS boilerplate while leveraging Hono's lightweight, type-safe approach.
|
|
4
|
+
|
|
5
|
+
## Key Hono Features to Leverage
|
|
6
|
+
|
|
7
|
+
| Feature | Benefit |
|
|
8
|
+
|---------|---------|
|
|
9
|
+
| `@hono/zod-openapi` | Schema validation + auto-generated OpenAPI docs in one |
|
|
10
|
+
| `OpenAPIHono` | Extended Hono class with built-in OpenAPI support |
|
|
11
|
+
| `createRoute()` | Type-safe route definitions with request/response schemas |
|
|
12
|
+
| RPC mode | End-to-end type safety if frontend consumes the API |
|
|
13
|
+
| `createFactory()` | Centralized type definitions, reduces boilerplate |
|
|
14
|
+
| `c.set()`/`c.get()` | Built-in request-scoped context (replaces nestjs-cls) |
|
|
15
|
+
| JWT helper | Native JWT verify/sign/decode with multiple algorithms |
|
|
16
|
+
|
|
17
|
+
## Project Structure
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
src/
|
|
21
|
+
├── index.ts # Entry point with serve()
|
|
22
|
+
├── app.ts # OpenAPIHono app factory
|
|
23
|
+
├── env.ts # Zod schema for env validation
|
|
24
|
+
│
|
|
25
|
+
├── middleware/
|
|
26
|
+
│ ├── auth.ts # JWT validation middleware
|
|
27
|
+
│ ├── account.ts # account-id header + membership check
|
|
28
|
+
│ ├── request-context.ts # Sets user, requestId, ip in context
|
|
29
|
+
│ └── error-handler.ts # defaultHook for validation errors
|
|
30
|
+
│
|
|
31
|
+
├── routes/
|
|
32
|
+
│ ├── index.ts # app.route() aggregation + OpenAPI doc
|
|
33
|
+
│ ├── users/
|
|
34
|
+
│ │ ├── index.ts # OpenAPIHono sub-app
|
|
35
|
+
│ │ ├── routes.ts # createRoute() definitions
|
|
36
|
+
│ │ ├── handlers.ts # Handler implementations
|
|
37
|
+
│ │ └── schemas.ts # Zod schemas with .openapi()
|
|
38
|
+
│ └── accounts/
|
|
39
|
+
│ └── ...
|
|
40
|
+
│
|
|
41
|
+
├── db/
|
|
42
|
+
│ ├── client.ts # Drizzle client
|
|
43
|
+
│ ├── schema/
|
|
44
|
+
│ │ ├── users.ts
|
|
45
|
+
│ │ ├── accounts.ts
|
|
46
|
+
│ │ ├── user-accounts.ts
|
|
47
|
+
│ │ └── audit-logs.ts
|
|
48
|
+
│ ├── migrations/
|
|
49
|
+
│ └── seed.ts
|
|
50
|
+
│
|
|
51
|
+
├── services/
|
|
52
|
+
│ ├── users.ts # Business logic
|
|
53
|
+
│ ├── accounts.ts
|
|
54
|
+
│ ├── audit.ts
|
|
55
|
+
│ └── identity-provider.ts # Auth0 abstraction
|
|
56
|
+
│
|
|
57
|
+
├── auth/
|
|
58
|
+
│ ├── permissions.ts # Permission enum + matrix
|
|
59
|
+
│ ├── roles.ts # Role enum + helpers
|
|
60
|
+
│ └── guards.ts # requireRole(), requirePermission()
|
|
61
|
+
│
|
|
62
|
+
├── lib/
|
|
63
|
+
│ ├── pagination.ts # Pagination helpers
|
|
64
|
+
│ ├── errors.ts # Custom error classes
|
|
65
|
+
│ └── result.ts # Result<T, E> type (optional)
|
|
66
|
+
│
|
|
67
|
+
└── types/
|
|
68
|
+
└── index.ts # Shared types, Env definition
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Core Dependencies
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"dependencies": {
|
|
76
|
+
"hono": "^4.x",
|
|
77
|
+
"@hono/node-server": "^1.x",
|
|
78
|
+
"@hono/zod-openapi": "^0.x",
|
|
79
|
+
"@hono/swagger-ui": "^0.x",
|
|
80
|
+
"drizzle-orm": "^0.x",
|
|
81
|
+
"better-sqlite3": "^11.x",
|
|
82
|
+
"zod": "^3.x"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"drizzle-kit": "^0.x",
|
|
86
|
+
"typescript": "^5.x",
|
|
87
|
+
"@types/node": "^22.x",
|
|
88
|
+
"@types/better-sqlite3": "^7.x"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Implementation Details
|
|
94
|
+
|
|
95
|
+
### 1. App Factory with Typed Environment
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// app.ts
|
|
99
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
100
|
+
import { swaggerUI } from '@hono/swagger-ui'
|
|
101
|
+
|
|
102
|
+
export type Env = {
|
|
103
|
+
Variables: {
|
|
104
|
+
requestId: string
|
|
105
|
+
user: User | null
|
|
106
|
+
accountId: string
|
|
107
|
+
userRole: Role | null
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const createApp = () => {
|
|
112
|
+
const app = new OpenAPIHono<Env>({
|
|
113
|
+
defaultHook: (result, c) => {
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
return c.json({
|
|
116
|
+
error: 'Validation Error',
|
|
117
|
+
details: result.error.flatten()
|
|
118
|
+
}, 400)
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
return app
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 2. Route Definitions with `createRoute()`
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// routes/users/routes.ts
|
|
131
|
+
import { createRoute, z } from '@hono/zod-openapi'
|
|
132
|
+
import { UserSchema, CreateUserSchema } from './schemas'
|
|
133
|
+
|
|
134
|
+
export const listUsers = createRoute({
|
|
135
|
+
method: 'get',
|
|
136
|
+
path: '/users',
|
|
137
|
+
tags: ['Users'],
|
|
138
|
+
security: [{ Bearer: [] }],
|
|
139
|
+
request: {
|
|
140
|
+
query: z.object({
|
|
141
|
+
page: z.coerce.number().default(1),
|
|
142
|
+
limit: z.coerce.number().default(50),
|
|
143
|
+
}),
|
|
144
|
+
},
|
|
145
|
+
responses: {
|
|
146
|
+
200: {
|
|
147
|
+
content: { 'application/json': { schema: PaginatedUsersSchema } },
|
|
148
|
+
description: 'List of users',
|
|
149
|
+
},
|
|
150
|
+
401: { description: 'Unauthorized' },
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
export const createUser = createRoute({
|
|
155
|
+
method: 'post',
|
|
156
|
+
path: '/users',
|
|
157
|
+
tags: ['Users'],
|
|
158
|
+
middleware: [requireRole('ADMIN')], // Inline guard
|
|
159
|
+
request: {
|
|
160
|
+
body: { content: { 'application/json': { schema: CreateUserSchema } } },
|
|
161
|
+
},
|
|
162
|
+
responses: {
|
|
163
|
+
201: {
|
|
164
|
+
content: { 'application/json': { schema: UserSchema } },
|
|
165
|
+
description: 'User created',
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
})
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 3. Schemas with OpenAPI Metadata
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// routes/users/schemas.ts
|
|
175
|
+
import { z } from '@hono/zod-openapi'
|
|
176
|
+
|
|
177
|
+
export const UserSchema = z.object({
|
|
178
|
+
id: z.string().uuid().openapi({ example: '550e8400-e29b-41d4-a716-446655440000' }),
|
|
179
|
+
email: z.string().email().openapi({ example: 'user@example.com' }),
|
|
180
|
+
name: z.string().openapi({ example: 'John Doe' }),
|
|
181
|
+
status: z.enum(['active', 'inactive']).openapi({ example: 'active' }),
|
|
182
|
+
isSuperAdmin: z.boolean().openapi({ example: false }),
|
|
183
|
+
createdAt: z.string().datetime(),
|
|
184
|
+
}).openapi('User')
|
|
185
|
+
|
|
186
|
+
export const CreateUserSchema = z.object({
|
|
187
|
+
email: z.string().email(),
|
|
188
|
+
name: z.string().min(1).max(255),
|
|
189
|
+
accountIds: z.array(z.string().uuid()).optional(),
|
|
190
|
+
}).openapi('CreateUserInput')
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 4. Handlers with Typed Context
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
// routes/users/handlers.ts
|
|
197
|
+
import type { Context } from 'hono'
|
|
198
|
+
import type { Env } from '../../app'
|
|
199
|
+
|
|
200
|
+
export const handleListUsers = async (c: Context<Env>) => {
|
|
201
|
+
const { page, limit } = c.req.valid('query') // Typed from route schema
|
|
202
|
+
const user = c.get('user') // From middleware
|
|
203
|
+
const accountId = c.get('accountId')
|
|
204
|
+
|
|
205
|
+
const result = await usersService.findAll({ page, limit, accountId })
|
|
206
|
+
return c.json(result, 200)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export const handleCreateUser = async (c: Context<Env>) => {
|
|
210
|
+
const body = c.req.valid('json') // Typed as CreateUserSchema
|
|
211
|
+
const actor = c.get('user')
|
|
212
|
+
|
|
213
|
+
const user = await usersService.create(body, actor)
|
|
214
|
+
return c.json(user, 201)
|
|
215
|
+
}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### 5. Route Module Assembly
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// routes/users/index.ts
|
|
222
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
223
|
+
import { listUsers, createUser } from './routes'
|
|
224
|
+
import { handleListUsers, handleCreateUser } from './handlers'
|
|
225
|
+
import type { Env } from '../../app'
|
|
226
|
+
|
|
227
|
+
const users = new OpenAPIHono<Env>()
|
|
228
|
+
|
|
229
|
+
users.openapi(listUsers, handleListUsers)
|
|
230
|
+
users.openapi(createUser, handleCreateUser)
|
|
231
|
+
|
|
232
|
+
export { users }
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### 6. Main App with Swagger UI
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// routes/index.ts
|
|
239
|
+
import { OpenAPIHono } from '@hono/zod-openapi'
|
|
240
|
+
import { swaggerUI } from '@hono/swagger-ui'
|
|
241
|
+
import { users } from './users'
|
|
242
|
+
import { accounts } from './accounts'
|
|
243
|
+
import type { Env } from '../app'
|
|
244
|
+
|
|
245
|
+
const api = new OpenAPIHono<Env>()
|
|
246
|
+
|
|
247
|
+
api.route('/users', users)
|
|
248
|
+
api.route('/accounts', accounts)
|
|
249
|
+
|
|
250
|
+
// OpenAPI JSON endpoint
|
|
251
|
+
api.doc('/doc', {
|
|
252
|
+
openapi: '3.1.0',
|
|
253
|
+
info: { title: 'Etus API', version: '1.0.0' },
|
|
254
|
+
security: [{ Bearer: [] }],
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
// Swagger UI
|
|
258
|
+
api.get('/docs', swaggerUI({ url: '/doc' }))
|
|
259
|
+
|
|
260
|
+
export { api }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### 7. Drizzle Schema (Preserving Entity Model)
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
// db/schema/users.ts
|
|
267
|
+
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
|
|
268
|
+
import { sql } from 'drizzle-orm'
|
|
269
|
+
|
|
270
|
+
export const users = sqliteTable('users', {
|
|
271
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
272
|
+
email: text('email').notNull().unique(),
|
|
273
|
+
name: text('name').notNull(),
|
|
274
|
+
status: text('status', { enum: ['active', 'inactive'] }).default('active'),
|
|
275
|
+
providerIds: text('provider_ids', { mode: 'json' }).$type<string[]>().default([]),
|
|
276
|
+
isSuperAdmin: integer('is_super_admin', { mode: 'boolean' }).default(false),
|
|
277
|
+
|
|
278
|
+
// Soft delete + audit fields
|
|
279
|
+
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
|
280
|
+
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
|
|
281
|
+
deletedAt: text('deleted_at'),
|
|
282
|
+
createdById: text('created_by_id').references(() => users.id),
|
|
283
|
+
updatedById: text('updated_by_id').references(() => users.id),
|
|
284
|
+
deletedById: text('deleted_by_id').references(() => users.id),
|
|
285
|
+
})
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
// db/schema/accounts.ts
|
|
290
|
+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
291
|
+
import { sql } from 'drizzle-orm'
|
|
292
|
+
|
|
293
|
+
export const accounts = sqliteTable('accounts', {
|
|
294
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
295
|
+
name: text('name').notNull(),
|
|
296
|
+
description: text('description'),
|
|
297
|
+
domain: text('domain').unique(),
|
|
298
|
+
|
|
299
|
+
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`),
|
|
300
|
+
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`),
|
|
301
|
+
deletedAt: text('deleted_at'),
|
|
302
|
+
})
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
// db/schema/user-accounts.ts
|
|
307
|
+
import { sqliteTable, text, primaryKey } from 'drizzle-orm/sqlite-core'
|
|
308
|
+
import { users } from './users'
|
|
309
|
+
import { accounts } from './accounts'
|
|
310
|
+
|
|
311
|
+
export const userAccounts = sqliteTable('user_accounts', {
|
|
312
|
+
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
313
|
+
accountId: text('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
|
|
314
|
+
role: text('role', {
|
|
315
|
+
enum: ['ADMIN', 'MANAGER', 'EDITOR', 'AUTHOR', 'VIEWER', 'BILLING', 'ANALYTICS']
|
|
316
|
+
}).notNull(),
|
|
317
|
+
}, (table) => ({
|
|
318
|
+
pk: primaryKey({ columns: [table.userId, table.accountId] }),
|
|
319
|
+
}))
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// db/schema/audit-logs.ts
|
|
324
|
+
import { sqliteTable, text } from 'drizzle-orm/sqlite-core'
|
|
325
|
+
import { sql } from 'drizzle-orm'
|
|
326
|
+
import { users } from './users'
|
|
327
|
+
import { accounts } from './accounts'
|
|
328
|
+
|
|
329
|
+
export const auditLogs = sqliteTable('audit_logs', {
|
|
330
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
331
|
+
transactionId: text('transaction_id').notNull(),
|
|
332
|
+
accountId: text('account_id').references(() => accounts.id),
|
|
333
|
+
userId: text('user_id').references(() => users.id),
|
|
334
|
+
entity: text('entity').notNull(),
|
|
335
|
+
entityId: text('entity_id').notNull(),
|
|
336
|
+
action: text('action', { enum: ['INSERT', 'UPDATE', 'DELETE'] }).notNull(),
|
|
337
|
+
changes: text('changes', { mode: 'json' }).$type<Record<string, unknown>>(),
|
|
338
|
+
ipAddress: text('ip_address'),
|
|
339
|
+
userAgent: text('user_agent'),
|
|
340
|
+
timestamp: text('timestamp').default(sql`CURRENT_TIMESTAMP`),
|
|
341
|
+
})
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 8. Role Guard Middleware
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
// auth/guards.ts
|
|
348
|
+
import { createMiddleware } from 'hono/factory'
|
|
349
|
+
import type { Env } from '../app'
|
|
350
|
+
import { hasMinimumRole, Role } from './roles'
|
|
351
|
+
|
|
352
|
+
export const requireRole = (minRole: Role) => {
|
|
353
|
+
return createMiddleware<Env>(async (c, next) => {
|
|
354
|
+
const user = c.get('user')
|
|
355
|
+
const userRole = c.get('userRole')
|
|
356
|
+
|
|
357
|
+
if (!user) {
|
|
358
|
+
return c.json({ error: 'Unauthorized' }, 401)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Super-admin bypass
|
|
362
|
+
if (user.isSuperAdmin) {
|
|
363
|
+
return next()
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if (!userRole || !hasMinimumRole(userRole, minRole)) {
|
|
367
|
+
return c.json({ error: 'Forbidden' }, 403)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return next()
|
|
371
|
+
})
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export const requirePermission = (permission: Permission) => {
|
|
375
|
+
return createMiddleware<Env>(async (c, next) => {
|
|
376
|
+
const user = c.get('user')
|
|
377
|
+
const userRole = c.get('userRole')
|
|
378
|
+
|
|
379
|
+
if (!user) {
|
|
380
|
+
return c.json({ error: 'Unauthorized' }, 401)
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (user.isSuperAdmin) {
|
|
384
|
+
return next()
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (!userRole || !hasPermission(userRole, permission)) {
|
|
388
|
+
return c.json({ error: 'Forbidden' }, 403)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return next()
|
|
392
|
+
})
|
|
393
|
+
}
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### 9. Roles and Permissions
|
|
397
|
+
|
|
398
|
+
```typescript
|
|
399
|
+
// auth/roles.ts
|
|
400
|
+
export const Role = {
|
|
401
|
+
ADMIN: 'ADMIN',
|
|
402
|
+
MANAGER: 'MANAGER',
|
|
403
|
+
EDITOR: 'EDITOR',
|
|
404
|
+
AUTHOR: 'AUTHOR',
|
|
405
|
+
VIEWER: 'VIEWER',
|
|
406
|
+
BILLING: 'BILLING',
|
|
407
|
+
ANALYTICS: 'ANALYTICS',
|
|
408
|
+
} as const
|
|
409
|
+
|
|
410
|
+
export type Role = typeof Role[keyof typeof Role]
|
|
411
|
+
|
|
412
|
+
const ROLE_HIERARCHY: Record<string, number> = {
|
|
413
|
+
ADMIN: 0,
|
|
414
|
+
MANAGER: 1,
|
|
415
|
+
EDITOR: 2,
|
|
416
|
+
AUTHOR: 3,
|
|
417
|
+
VIEWER: 4,
|
|
418
|
+
BILLING: -1, // Non-hierarchical
|
|
419
|
+
ANALYTICS: -1, // Non-hierarchical
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export const hasMinimumRole = (userRole: Role, requiredRole: Role): boolean => {
|
|
423
|
+
const userLevel = ROLE_HIERARCHY[userRole]
|
|
424
|
+
const requiredLevel = ROLE_HIERARCHY[requiredRole]
|
|
425
|
+
|
|
426
|
+
// Non-hierarchical roles can only match exactly
|
|
427
|
+
if (userLevel === -1 || requiredLevel === -1) {
|
|
428
|
+
return userRole === requiredRole
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return userLevel <= requiredLevel
|
|
432
|
+
}
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
```typescript
|
|
436
|
+
// auth/permissions.ts
|
|
437
|
+
export const Permission = {
|
|
438
|
+
MANAGE_SYSTEM_SETTINGS: 'MANAGE_SYSTEM_SETTINGS',
|
|
439
|
+
MANAGE_ALL_USERS: 'MANAGE_ALL_USERS',
|
|
440
|
+
MANAGE_TEAM_USERS: 'MANAGE_TEAM_USERS',
|
|
441
|
+
VIEW_ALL_USERS: 'VIEW_ALL_USERS',
|
|
442
|
+
CREATE_CONTENT: 'CREATE_CONTENT',
|
|
443
|
+
EDIT_ALL_CONTENT: 'EDIT_ALL_CONTENT',
|
|
444
|
+
EDIT_OWN_CONTENT: 'EDIT_OWN_CONTENT',
|
|
445
|
+
DELETE_CONTENT: 'DELETE_CONTENT',
|
|
446
|
+
PUBLISH_CONTENT: 'PUBLISH_CONTENT',
|
|
447
|
+
VIEW_CONTENT: 'VIEW_CONTENT',
|
|
448
|
+
VIEW_ANALYTICS: 'VIEW_ANALYTICS',
|
|
449
|
+
MANAGE_BILLING: 'MANAGE_BILLING',
|
|
450
|
+
VIEW_BILLING: 'VIEW_BILLING',
|
|
451
|
+
} as const
|
|
452
|
+
|
|
453
|
+
export type Permission = typeof Permission[keyof typeof Permission]
|
|
454
|
+
|
|
455
|
+
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
|
456
|
+
ADMIN: [
|
|
457
|
+
Permission.MANAGE_SYSTEM_SETTINGS,
|
|
458
|
+
Permission.MANAGE_ALL_USERS,
|
|
459
|
+
Permission.CREATE_CONTENT,
|
|
460
|
+
Permission.EDIT_ALL_CONTENT,
|
|
461
|
+
Permission.DELETE_CONTENT,
|
|
462
|
+
Permission.PUBLISH_CONTENT,
|
|
463
|
+
Permission.VIEW_CONTENT,
|
|
464
|
+
Permission.VIEW_ANALYTICS,
|
|
465
|
+
Permission.MANAGE_BILLING,
|
|
466
|
+
Permission.VIEW_BILLING,
|
|
467
|
+
],
|
|
468
|
+
MANAGER: [
|
|
469
|
+
Permission.MANAGE_TEAM_USERS,
|
|
470
|
+
Permission.VIEW_ALL_USERS,
|
|
471
|
+
Permission.CREATE_CONTENT,
|
|
472
|
+
Permission.EDIT_ALL_CONTENT,
|
|
473
|
+
Permission.PUBLISH_CONTENT,
|
|
474
|
+
Permission.VIEW_CONTENT,
|
|
475
|
+
Permission.VIEW_ANALYTICS,
|
|
476
|
+
],
|
|
477
|
+
EDITOR: [
|
|
478
|
+
Permission.CREATE_CONTENT,
|
|
479
|
+
Permission.EDIT_ALL_CONTENT,
|
|
480
|
+
Permission.PUBLISH_CONTENT,
|
|
481
|
+
Permission.VIEW_CONTENT,
|
|
482
|
+
],
|
|
483
|
+
AUTHOR: [
|
|
484
|
+
Permission.CREATE_CONTENT,
|
|
485
|
+
Permission.EDIT_OWN_CONTENT,
|
|
486
|
+
Permission.VIEW_CONTENT,
|
|
487
|
+
],
|
|
488
|
+
VIEWER: [
|
|
489
|
+
Permission.VIEW_CONTENT,
|
|
490
|
+
],
|
|
491
|
+
BILLING: [
|
|
492
|
+
Permission.MANAGE_BILLING,
|
|
493
|
+
Permission.VIEW_BILLING,
|
|
494
|
+
],
|
|
495
|
+
ANALYTICS: [
|
|
496
|
+
Permission.VIEW_ANALYTICS,
|
|
497
|
+
],
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export const hasPermission = (role: Role, permission: Permission): boolean => {
|
|
501
|
+
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### 10. JWT Auth Middleware
|
|
506
|
+
|
|
507
|
+
```typescript
|
|
508
|
+
// middleware/auth.ts
|
|
509
|
+
import { createMiddleware } from 'hono/factory'
|
|
510
|
+
import { verify } from 'hono/jwt'
|
|
511
|
+
import type { Env } from '../app'
|
|
512
|
+
|
|
513
|
+
export const jwtAuth = createMiddleware<Env>(async (c, next) => {
|
|
514
|
+
const authHeader = c.req.header('Authorization')
|
|
515
|
+
|
|
516
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
517
|
+
return c.json({ error: 'Missing or invalid Authorization header' }, 401)
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const token = authHeader.slice(7)
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
// For Auth0 with RS256, you'd use JWKS validation
|
|
524
|
+
// This is simplified for HS256
|
|
525
|
+
const payload = await verify(token, process.env.JWT_SECRET!)
|
|
526
|
+
|
|
527
|
+
c.set('user', {
|
|
528
|
+
id: payload.sub as string,
|
|
529
|
+
email: payload.email as string,
|
|
530
|
+
isSuperAdmin: payload.isSuperAdmin as boolean ?? false,
|
|
531
|
+
})
|
|
532
|
+
|
|
533
|
+
return next()
|
|
534
|
+
} catch {
|
|
535
|
+
return c.json({ error: 'Invalid token' }, 401)
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
### 11. Account Middleware
|
|
541
|
+
|
|
542
|
+
```typescript
|
|
543
|
+
// middleware/account.ts
|
|
544
|
+
import { createMiddleware } from 'hono/factory'
|
|
545
|
+
import type { Env } from '../app'
|
|
546
|
+
import { db } from '../db/client'
|
|
547
|
+
import { userAccounts } from '../db/schema'
|
|
548
|
+
import { eq, and } from 'drizzle-orm'
|
|
549
|
+
|
|
550
|
+
export const accountMiddleware = createMiddleware<Env>(async (c, next) => {
|
|
551
|
+
const accountId = c.req.header('account-id')
|
|
552
|
+
const user = c.get('user')
|
|
553
|
+
|
|
554
|
+
if (!accountId) {
|
|
555
|
+
return c.json({ error: 'account-id header required' }, 400)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (!user) {
|
|
559
|
+
return c.json({ error: 'Unauthorized' }, 401)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Super-admin can access any account
|
|
563
|
+
if (user.isSuperAdmin) {
|
|
564
|
+
c.set('accountId', accountId)
|
|
565
|
+
c.set('userRole', 'ADMIN') // Treat as admin for super-admin
|
|
566
|
+
return next()
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Check user belongs to account
|
|
570
|
+
const [membership] = await db
|
|
571
|
+
.select()
|
|
572
|
+
.from(userAccounts)
|
|
573
|
+
.where(and(
|
|
574
|
+
eq(userAccounts.userId, user.id),
|
|
575
|
+
eq(userAccounts.accountId, accountId)
|
|
576
|
+
))
|
|
577
|
+
.limit(1)
|
|
578
|
+
|
|
579
|
+
if (!membership) {
|
|
580
|
+
return c.json({ error: 'Forbidden: No access to this account' }, 403)
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
c.set('accountId', accountId)
|
|
584
|
+
c.set('userRole', membership.role)
|
|
585
|
+
|
|
586
|
+
return next()
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### 12. Entry Point
|
|
591
|
+
|
|
592
|
+
```typescript
|
|
593
|
+
// index.ts
|
|
594
|
+
import { serve } from '@hono/node-server'
|
|
595
|
+
import { createApp } from './app'
|
|
596
|
+
import { api } from './routes'
|
|
597
|
+
import { jwtAuth } from './middleware/auth'
|
|
598
|
+
import { accountMiddleware } from './middleware/account'
|
|
599
|
+
import { requestContext } from './middleware/request-context'
|
|
600
|
+
|
|
601
|
+
const app = createApp()
|
|
602
|
+
|
|
603
|
+
// Global middleware
|
|
604
|
+
app.use('*', requestContext)
|
|
605
|
+
|
|
606
|
+
// Protected routes
|
|
607
|
+
app.use('/users/*', jwtAuth, accountMiddleware)
|
|
608
|
+
app.use('/accounts/*', jwtAuth, accountMiddleware)
|
|
609
|
+
|
|
610
|
+
// Mount API routes
|
|
611
|
+
app.route('/', api)
|
|
612
|
+
|
|
613
|
+
// Health check (unprotected)
|
|
614
|
+
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
615
|
+
|
|
616
|
+
const port = parseInt(process.env.PORT ?? '3000')
|
|
617
|
+
|
|
618
|
+
console.log(`Server running on http://localhost:${port}`)
|
|
619
|
+
console.log(`API docs available at http://localhost:${port}/docs`)
|
|
620
|
+
|
|
621
|
+
serve({ fetch: app.fetch, port })
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
## What's Preserved from NestJS Boilerplate
|
|
625
|
+
|
|
626
|
+
| Concept | Status | Notes |
|
|
627
|
+
|---------|--------|-------|
|
|
628
|
+
| Entity model (User, Account, UserAccount, AuditLog) | Preserved | Same fields and relationships |
|
|
629
|
+
| Multi-tenant via `account-id` header | Preserved | Same pattern |
|
|
630
|
+
| Role hierarchy + permissions | Preserved | Same logic, lighter implementation |
|
|
631
|
+
| Super-admin bypass | Preserved | Same behavior |
|
|
632
|
+
| Soft deletes | Preserved | Drizzle columns instead of TypeORM |
|
|
633
|
+
| Request context | Preserved | `c.set()`/`c.get()` instead of CLS |
|
|
634
|
+
| JWT auth flow | Preserved | Hono's native JWT helper |
|
|
635
|
+
| OpenAPI docs | Preserved | `@hono/zod-openapi` + Swagger UI |
|
|
636
|
+
| Validation | Preserved | Zod instead of class-validator |
|
|
637
|
+
| Pagination contract | Preserved | Same query params and response shape |
|
|
638
|
+
|
|
639
|
+
## What's Different
|
|
640
|
+
|
|
641
|
+
| Aspect | NestJS | Hono |
|
|
642
|
+
|--------|--------|------|
|
|
643
|
+
| Framework weight | Heavy (~50+ deps) | Lightweight (zero deps core) |
|
|
644
|
+
| Route definition | Decorators | `createRoute()` function |
|
|
645
|
+
| Dependency injection | Built-in DI container | Direct imports / factory functions |
|
|
646
|
+
| Validation | class-validator | Zod schemas |
|
|
647
|
+
| ORM | TypeORM | Drizzle ORM |
|
|
648
|
+
| Context passing | CLS (implicit) | `c.set()`/`c.get()` (explicit) |
|
|
649
|
+
| Modules | NestJS modules | File-based organization |
|
|
650
|
+
| Guards | Guard classes | Middleware functions |
|
|
651
|
+
| OpenAPI | @nestjs/swagger decorators | `@hono/zod-openapi` schemas |
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
# Architecture & System Design Patterns Reference
|
|
656
|
+
|
|
657
|
+
This section documents all architectural patterns from the NestJS boilerplate that MUST be preserved in any Hono implementation. Use this as the definitive reference for system design decisions.
|
|
658
|
+
|
|
659
|
+
---
|
|
660
|
+
|
|
661
|
+
## 1. Entity Design Patterns
|
|
662
|
+
|
|
663
|
+
### 1.1 Base Entity Hierarchy
|
|
664
|
+
|
|
665
|
+
The boilerplate uses a two-tier entity inheritance pattern for consistent temporal and audit fields:
|
|
666
|
+
|
|
667
|
+
```
|
|
668
|
+
SoftDeleteEntity (base)
|
|
669
|
+
├── createdAt: Date
|
|
670
|
+
├── updatedAt: Date
|
|
671
|
+
└── deletedAt: Date | null
|
|
672
|
+
|
|
673
|
+
InteractiveEntity (extends SoftDeleteEntity)
|
|
674
|
+
├── createdById: string | null
|
|
675
|
+
├── updatedById: string | null
|
|
676
|
+
└── deletedById: string | null
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
**Hono/Drizzle Implementation:**
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
// lib/schema-helpers.ts
|
|
683
|
+
import { text } from 'drizzle-orm/sqlite-core'
|
|
684
|
+
import { sql } from 'drizzle-orm'
|
|
685
|
+
|
|
686
|
+
export const softDeleteFields = {
|
|
687
|
+
createdAt: text('created_at').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
688
|
+
updatedAt: text('updated_at').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
689
|
+
deletedAt: text('deleted_at'),
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export const interactiveFields = (usersTable: any) => ({
|
|
693
|
+
...softDeleteFields,
|
|
694
|
+
createdById: text('created_by_id').references(() => usersTable.id),
|
|
695
|
+
updatedById: text('updated_by_id').references(() => usersTable.id),
|
|
696
|
+
deletedById: text('deleted_by_id').references(() => usersTable.id),
|
|
697
|
+
})
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
**Rules:**
|
|
701
|
+
- ALL entities must include `softDeleteFields` at minimum
|
|
702
|
+
- Entities that track user actions must include `interactiveFields`
|
|
703
|
+
- NEVER use hard deletes; always set `deletedAt`
|
|
704
|
+
- `updatedAt` must be updated on every modification
|
|
705
|
+
|
|
706
|
+
### 1.2 Soft Delete Pattern
|
|
707
|
+
|
|
708
|
+
**Implementation Requirements:**
|
|
709
|
+
|
|
710
|
+
1. Every query MUST filter out soft-deleted records:
|
|
711
|
+
```typescript
|
|
712
|
+
// Always include this in WHERE clauses
|
|
713
|
+
.where(isNull(table.deletedAt))
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
2. Delete operations set `deletedAt` instead of removing:
|
|
717
|
+
```typescript
|
|
718
|
+
await db
|
|
719
|
+
.update(users)
|
|
720
|
+
.set({
|
|
721
|
+
deletedAt: new Date().toISOString(),
|
|
722
|
+
deletedById: actor.id
|
|
723
|
+
})
|
|
724
|
+
.where(eq(users.id, id))
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
3. Unique constraints must account for soft deletes:
|
|
728
|
+
```typescript
|
|
729
|
+
// Use partial unique index (PostgreSQL) or application-level check
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### 1.3 Entity Relationships
|
|
733
|
+
|
|
734
|
+
**User <-> Account (Many-to-Many via UserAccount):**
|
|
735
|
+
|
|
736
|
+
```
|
|
737
|
+
User (1) --> (M) UserAccount (M) <-- (1) Account
|
|
738
|
+
|
|
|
739
|
+
└── role: Role (per-account role)
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
**Key Characteristics:**
|
|
743
|
+
- A user can belong to multiple accounts
|
|
744
|
+
- Each user-account pair has its own role
|
|
745
|
+
- Composite primary key on (userId, accountId)
|
|
746
|
+
- CASCADE delete on both foreign keys
|
|
747
|
+
|
|
748
|
+
**Audit Log Relationships:**
|
|
749
|
+
|
|
750
|
+
```
|
|
751
|
+
AuditLog
|
|
752
|
+
├── accountId -> Account (which tenant)
|
|
753
|
+
├── userId -> User (who did it)
|
|
754
|
+
├── entity: string (what type)
|
|
755
|
+
├── entityId: string (which record)
|
|
756
|
+
└── changes: JSON (what changed)
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
---
|
|
760
|
+
|
|
761
|
+
## 2. Authentication & Authorization Architecture
|
|
762
|
+
|
|
763
|
+
### 2.1 Two-Tier Privilege System
|
|
764
|
+
|
|
765
|
+
The system uses TWO independent authorization layers:
|
|
766
|
+
|
|
767
|
+
**Tier 1: Account-Level Roles (via UserAccount table)**
|
|
768
|
+
- Stored per user-account relationship
|
|
769
|
+
- Hierarchical: ADMIN > MANAGER > EDITOR > AUTHOR > VIEWER
|
|
770
|
+
- Non-hierarchical: BILLING, ANALYTICS (special access only)
|
|
771
|
+
|
|
772
|
+
**Tier 2: System Admin (isSuperAdmin flag on User)**
|
|
773
|
+
- Boolean flag on User entity
|
|
774
|
+
- Bypasses ALL role and permission checks
|
|
775
|
+
- Bypasses account-id validation
|
|
776
|
+
- Access to all accounts without UserAccount entries
|
|
777
|
+
- Should be granted to 1-2 platform administrators only
|
|
778
|
+
|
|
779
|
+
### 2.2 Role Hierarchy
|
|
780
|
+
|
|
781
|
+
```typescript
|
|
782
|
+
const ROLE_HIERARCHY: Record<Role, number> = {
|
|
783
|
+
ADMIN: 0, // Highest privilege
|
|
784
|
+
MANAGER: 1,
|
|
785
|
+
EDITOR: 2,
|
|
786
|
+
AUTHOR: 3,
|
|
787
|
+
VIEWER: 4, // Lowest privilege
|
|
788
|
+
BILLING: -1, // Non-hierarchical (special)
|
|
789
|
+
ANALYTICS: -1, // Non-hierarchical (special)
|
|
790
|
+
}
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
**Hierarchy Rules:**
|
|
794
|
+
- Lower number = higher privilege
|
|
795
|
+
- ADMIN (0) has all permissions of MANAGER (1), EDITOR (2), etc.
|
|
796
|
+
- Non-hierarchical roles (-1) can ONLY be granted via `additionalRoles` option
|
|
797
|
+
- Non-hierarchical roles CANNOT access hierarchical endpoints
|
|
798
|
+
|
|
799
|
+
**Check Function:**
|
|
800
|
+
|
|
801
|
+
```typescript
|
|
802
|
+
function hasMinimumRole(
|
|
803
|
+
userRole: Role,
|
|
804
|
+
minimumRole: Role,
|
|
805
|
+
additionalRoles: Role[] = []
|
|
806
|
+
): boolean {
|
|
807
|
+
// Check additional roles first (for non-hierarchical access)
|
|
808
|
+
if (additionalRoles.includes(userRole)) {
|
|
809
|
+
return true
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const userLevel = ROLE_HIERARCHY[userRole]
|
|
813
|
+
const requiredLevel = ROLE_HIERARCHY[minimumRole]
|
|
814
|
+
|
|
815
|
+
// Non-hierarchical roles can't access hierarchical endpoints
|
|
816
|
+
if (userLevel === -1 || requiredLevel === -1) {
|
|
817
|
+
return false
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Lower or equal level = higher or equal privilege
|
|
821
|
+
return userLevel <= requiredLevel
|
|
822
|
+
}
|
|
823
|
+
```
|
|
824
|
+
|
|
825
|
+
### 2.3 Permission Matrix
|
|
826
|
+
|
|
827
|
+
Permissions are statically mapped to roles:
|
|
828
|
+
|
|
829
|
+
```typescript
|
|
830
|
+
const ROLE_PERMISSIONS: Record<Role, Permission[]> = {
|
|
831
|
+
ADMIN: [
|
|
832
|
+
'MANAGE_TENANT_SETTINGS',
|
|
833
|
+
'MANAGE_ALL_USERS',
|
|
834
|
+
'MANAGE_TEAM_USERS',
|
|
835
|
+
'CREATE_CONTENT',
|
|
836
|
+
'EDIT_OWN_CONTENT',
|
|
837
|
+
'EDIT_ANY_CONTENT',
|
|
838
|
+
'PUBLISH_CONTENT',
|
|
839
|
+
'UNPUBLISH_DELETE_CONTENT',
|
|
840
|
+
'MANAGE_ASSETS',
|
|
841
|
+
'MANAGE_CATEGORIES_TAGS',
|
|
842
|
+
'MANAGE_COMMENTS',
|
|
843
|
+
'VIEW_ALL_CONTENT',
|
|
844
|
+
'VIEW_OWN_CONTENT',
|
|
845
|
+
'VIEW_PUBLISHED_CONTENT',
|
|
846
|
+
'VIEW_ANALYTICS',
|
|
847
|
+
'EXPORT_REPORTS',
|
|
848
|
+
],
|
|
849
|
+
MANAGER: [
|
|
850
|
+
'MANAGE_TEAM_USERS',
|
|
851
|
+
'CREATE_CONTENT',
|
|
852
|
+
'EDIT_ANY_CONTENT',
|
|
853
|
+
'PUBLISH_CONTENT',
|
|
854
|
+
'MANAGE_ASSETS',
|
|
855
|
+
'MANAGE_CATEGORIES_TAGS',
|
|
856
|
+
'VIEW_ALL_CONTENT',
|
|
857
|
+
'VIEW_ANALYTICS',
|
|
858
|
+
],
|
|
859
|
+
EDITOR: [
|
|
860
|
+
'CREATE_CONTENT',
|
|
861
|
+
'EDIT_ANY_CONTENT',
|
|
862
|
+
'PUBLISH_CONTENT',
|
|
863
|
+
'MANAGE_ASSETS',
|
|
864
|
+
'VIEW_ALL_CONTENT',
|
|
865
|
+
],
|
|
866
|
+
AUTHOR: [
|
|
867
|
+
'CREATE_CONTENT',
|
|
868
|
+
'EDIT_OWN_CONTENT',
|
|
869
|
+
'VIEW_OWN_CONTENT',
|
|
870
|
+
'VIEW_PUBLISHED_CONTENT',
|
|
871
|
+
],
|
|
872
|
+
VIEWER: [
|
|
873
|
+
'VIEW_PUBLISHED_CONTENT',
|
|
874
|
+
],
|
|
875
|
+
BILLING: [
|
|
876
|
+
'MANAGE_BILLING',
|
|
877
|
+
'VIEW_BILLING',
|
|
878
|
+
],
|
|
879
|
+
ANALYTICS: [
|
|
880
|
+
'VIEW_ANALYTICS',
|
|
881
|
+
'EXPORT_REPORTS',
|
|
882
|
+
],
|
|
883
|
+
}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
**Helper Functions:**
|
|
887
|
+
|
|
888
|
+
```typescript
|
|
889
|
+
function hasPermission(role: Role, permission: Permission): boolean {
|
|
890
|
+
return ROLE_PERMISSIONS[role]?.includes(permission) ?? false
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function hasAnyPermission(role: Role, permissions: Permission[]): boolean {
|
|
894
|
+
return permissions.some(p => hasPermission(role, p))
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function hasAllPermissions(role: Role, permissions: Permission[]): boolean {
|
|
898
|
+
return permissions.every(p => hasPermission(role, p))
|
|
899
|
+
}
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
### 2.4 JWT Validation Flow
|
|
903
|
+
|
|
904
|
+
```
|
|
905
|
+
Request with Authorization: Bearer <token>
|
|
906
|
+
|
|
|
907
|
+
v
|
|
908
|
+
┌─────────────────────────────────────┐
|
|
909
|
+
│ 1. Extract token from header │
|
|
910
|
+
└─────────────────────────────────────┘
|
|
911
|
+
|
|
|
912
|
+
v
|
|
913
|
+
┌─────────────────────────────────────┐
|
|
914
|
+
│ 2. Validate signature via JWKS │
|
|
915
|
+
│ (RS256 with Auth0) │
|
|
916
|
+
└─────────────────────────────────────┘
|
|
917
|
+
|
|
|
918
|
+
v
|
|
919
|
+
┌─────────────────────────────────────┐
|
|
920
|
+
│ 3. Verify claims: │
|
|
921
|
+
│ - iss (issuer) │
|
|
922
|
+
│ - aud (audience) │
|
|
923
|
+
│ - exp (expiration) │
|
|
924
|
+
└─────────────────────────────────────┘
|
|
925
|
+
|
|
|
926
|
+
v
|
|
927
|
+
┌─────────────────────────────────────┐
|
|
928
|
+
│ 4. Extract user info from payload: │
|
|
929
|
+
│ - sub (provider ID) │
|
|
930
|
+
│ - email │
|
|
931
|
+
│ - custom claims (roles, perms) │
|
|
932
|
+
└─────────────────────────────────────┘
|
|
933
|
+
|
|
|
934
|
+
v
|
|
935
|
+
┌─────────────────────────────────────┐
|
|
936
|
+
│ 5. Fetch user from DB by providerID │
|
|
937
|
+
│ (includes userAccounts) │
|
|
938
|
+
└─────────────────────────────────────┘
|
|
939
|
+
|
|
|
940
|
+
v
|
|
941
|
+
┌─────────────────────────────────────┐
|
|
942
|
+
│ 6. Store user in request context │
|
|
943
|
+
└─────────────────────────────────────┘
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### 2.5 Super-Admin Bypass Mechanism
|
|
947
|
+
|
|
948
|
+
**When isSuperAdmin is true:**
|
|
949
|
+
1. Skip account-id header validation
|
|
950
|
+
2. Skip role hierarchy checks
|
|
951
|
+
3. Skip permission checks
|
|
952
|
+
4. Grant access to all accounts
|
|
953
|
+
5. Treat as ADMIN for audit purposes
|
|
954
|
+
6. Log with 'SYSTEM_ADMIN' marker
|
|
955
|
+
|
|
956
|
+
**Implementation:**
|
|
957
|
+
|
|
958
|
+
```typescript
|
|
959
|
+
// In account middleware
|
|
960
|
+
if (user.isSuperAdmin) {
|
|
961
|
+
c.set('accountId', accountId)
|
|
962
|
+
c.set('userRole', 'ADMIN') // Treat as admin
|
|
963
|
+
c.set('isSystemAdminAccess', true) // Mark for audit
|
|
964
|
+
return next()
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// In role guard
|
|
968
|
+
if (user.isSuperAdmin) {
|
|
969
|
+
return next() // Bypass all checks
|
|
970
|
+
}
|
|
971
|
+
```
|
|
972
|
+
|
|
973
|
+
### 2.6 Middleware Execution Order
|
|
974
|
+
|
|
975
|
+
```
|
|
976
|
+
Request
|
|
977
|
+
|
|
|
978
|
+
v
|
|
979
|
+
┌─────────────────────────────────────┐
|
|
980
|
+
│ 1. requestContext middleware │
|
|
981
|
+
│ - Generate transactionId (UUIDv7)│
|
|
982
|
+
│ - Capture IP, userAgent │
|
|
983
|
+
└─────────────────────────────────────┘
|
|
984
|
+
|
|
|
985
|
+
v
|
|
986
|
+
┌─────────────────────────────────────┐
|
|
987
|
+
│ 2. jwtAuth middleware │
|
|
988
|
+
│ - Validate JWT │
|
|
989
|
+
│ - Fetch user from DB │
|
|
990
|
+
│ - Set user in context │
|
|
991
|
+
└─────────────────────────────────────┘
|
|
992
|
+
|
|
|
993
|
+
v
|
|
994
|
+
┌─────────────────────────────────────┐
|
|
995
|
+
│ 3. accountMiddleware │
|
|
996
|
+
│ - Validate account-id header │
|
|
997
|
+
│ - Check user-account membership │
|
|
998
|
+
│ - Set accountId, userRole │
|
|
999
|
+
└─────────────────────────────────────┘
|
|
1000
|
+
|
|
|
1001
|
+
v
|
|
1002
|
+
┌─────────────────────────────────────┐
|
|
1003
|
+
│ 4. requireRole() middleware │
|
|
1004
|
+
│ - Check role hierarchy │
|
|
1005
|
+
│ - Return 403 if insufficient │
|
|
1006
|
+
└─────────────────────────────────────┘
|
|
1007
|
+
|
|
|
1008
|
+
v
|
|
1009
|
+
┌─────────────────────────────────────┐
|
|
1010
|
+
│ 5. Route Handler │
|
|
1011
|
+
│ - Business logic │
|
|
1012
|
+
└─────────────────────────────────────┘
|
|
1013
|
+
|
|
|
1014
|
+
v
|
|
1015
|
+
┌─────────────────────────────────────┐
|
|
1016
|
+
│ 6. Audit logging (on DB operations) │
|
|
1017
|
+
│ - Capture changes with context │
|
|
1018
|
+
└─────────────────────────────────────┘
|
|
1019
|
+
```
|
|
1020
|
+
|
|
1021
|
+
---
|
|
1022
|
+
|
|
1023
|
+
## 3. Multi-Tenancy Implementation
|
|
1024
|
+
|
|
1025
|
+
### 3.1 Account Isolation Strategy
|
|
1026
|
+
|
|
1027
|
+
**Core Principle:** Every database operation MUST be scoped to an accountId.
|
|
1028
|
+
|
|
1029
|
+
**Isolation Layers:**
|
|
1030
|
+
|
|
1031
|
+
1. **Header Level:** `account-id` header required on all protected endpoints
|
|
1032
|
+
2. **Middleware Level:** Validate user has access to requested account
|
|
1033
|
+
3. **Query Level:** All queries filtered by accountId
|
|
1034
|
+
4. **Audit Level:** All changes logged with account context
|
|
1035
|
+
|
|
1036
|
+
### 3.2 Account-ID Header Flow
|
|
1037
|
+
|
|
1038
|
+
```
|
|
1039
|
+
Client Request
|
|
1040
|
+
|
|
|
1041
|
+
├── Header: account-id: <uuid>
|
|
1042
|
+
|
|
|
1043
|
+
v
|
|
1044
|
+
┌─────────────────────────────────────┐
|
|
1045
|
+
│ accountMiddleware │
|
|
1046
|
+
│ │
|
|
1047
|
+
│ 1. Extract account-id header │
|
|
1048
|
+
│ 2. If missing -> 400 Bad Request │
|
|
1049
|
+
│ 3. If super-admin -> allow any │
|
|
1050
|
+
│ 4. Query userAccounts table │
|
|
1051
|
+
│ 5. If no membership -> 403 Forbidden│
|
|
1052
|
+
│ 6. Set accountId in context │
|
|
1053
|
+
│ 7. Set userRole from membership │
|
|
1054
|
+
└─────────────────────────────────────┘
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### 3.3 Query Filtering Pattern
|
|
1058
|
+
|
|
1059
|
+
**Every service method must:**
|
|
1060
|
+
|
|
1061
|
+
1. Get accountId from context
|
|
1062
|
+
2. Include accountId in WHERE clause
|
|
1063
|
+
3. Filter out soft-deleted records
|
|
1064
|
+
|
|
1065
|
+
**Example:**
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
async findAll(pagination: PaginationQuery) {
|
|
1069
|
+
const accountId = c.get('accountId')
|
|
1070
|
+
const user = c.get('user')
|
|
1071
|
+
|
|
1072
|
+
// Super-admin sees all
|
|
1073
|
+
if (user.isSuperAdmin) {
|
|
1074
|
+
return db
|
|
1075
|
+
.select()
|
|
1076
|
+
.from(entities)
|
|
1077
|
+
.where(isNull(entities.deletedAt))
|
|
1078
|
+
.limit(pagination.limit)
|
|
1079
|
+
.offset((pagination.page - 1) * pagination.limit)
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Regular users filtered by account
|
|
1083
|
+
return db
|
|
1084
|
+
.select()
|
|
1085
|
+
.from(entities)
|
|
1086
|
+
.where(and(
|
|
1087
|
+
eq(entities.accountId, accountId),
|
|
1088
|
+
isNull(entities.deletedAt)
|
|
1089
|
+
))
|
|
1090
|
+
.limit(pagination.limit)
|
|
1091
|
+
.offset((pagination.page - 1) * pagination.limit)
|
|
1092
|
+
}
|
|
1093
|
+
```
|
|
1094
|
+
|
|
1095
|
+
### 3.4 User-Account Relationship
|
|
1096
|
+
|
|
1097
|
+
**Schema:**
|
|
1098
|
+
|
|
1099
|
+
```typescript
|
|
1100
|
+
userAccounts = sqliteTable('user_accounts', {
|
|
1101
|
+
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
1102
|
+
accountId: text('account_id').notNull().references(() => accounts.id, { onDelete: 'cascade' }),
|
|
1103
|
+
role: text('role', { enum: ROLES }).notNull().default('VIEWER'),
|
|
1104
|
+
}, (table) => ({
|
|
1105
|
+
pk: primaryKey({ columns: [table.userId, table.accountId] }),
|
|
1106
|
+
}))
|
|
1107
|
+
```
|
|
1108
|
+
|
|
1109
|
+
**Key Points:**
|
|
1110
|
+
- Composite primary key prevents duplicate assignments
|
|
1111
|
+
- Role is stored PER relationship (user can be ADMIN in Account A, VIEWER in Account B)
|
|
1112
|
+
- CASCADE delete on both sides
|
|
1113
|
+
- No soft delete on junction table (delete means removal of access)
|
|
1114
|
+
|
|
1115
|
+
---
|
|
1116
|
+
|
|
1117
|
+
## 4. Audit System Design
|
|
1118
|
+
|
|
1119
|
+
### 4.1 Audit Log Structure
|
|
1120
|
+
|
|
1121
|
+
```typescript
|
|
1122
|
+
auditLogs = sqliteTable('audit_logs', {
|
|
1123
|
+
id: text('id').primaryKey().$defaultFn(() => crypto.randomUUID()),
|
|
1124
|
+
transactionId: text('transaction_id').notNull(), // Groups related changes
|
|
1125
|
+
accountId: text('account_id').references(() => accounts.id),
|
|
1126
|
+
userId: text('user_id').references(() => users.id),
|
|
1127
|
+
entity: text('entity').notNull(), // e.g., "User", "Account"
|
|
1128
|
+
entityId: text('entity_id').notNull(), // Primary key of affected record
|
|
1129
|
+
action: text('action', { enum: ['INSERT', 'UPDATE', 'DELETE'] }).notNull(),
|
|
1130
|
+
changes: text('changes', { mode: 'json' }).$type<AuditChanges>(),
|
|
1131
|
+
ipAddress: text('ip_address'),
|
|
1132
|
+
userAgent: text('user_agent'),
|
|
1133
|
+
timestamp: text('timestamp').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
|
1134
|
+
})
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### 4.2 What Gets Captured
|
|
1138
|
+
|
|
1139
|
+
| Field | Source | Description |
|
|
1140
|
+
|-------|--------|-------------|
|
|
1141
|
+
| transactionId | UUIDv7 from context | Groups all changes in one request |
|
|
1142
|
+
| accountId | Context | Which tenant was affected |
|
|
1143
|
+
| userId | Context | Who made the change |
|
|
1144
|
+
| entity | Table name | What type of record |
|
|
1145
|
+
| entityId | Record PK | Which specific record |
|
|
1146
|
+
| action | Operation type | INSERT, UPDATE, or DELETE |
|
|
1147
|
+
| changes | JSON diff | What changed (for UPDATE) or full record (for INSERT/DELETE) |
|
|
1148
|
+
| ipAddress | Request | Client IP for forensics |
|
|
1149
|
+
| userAgent | Request | Client info for forensics |
|
|
1150
|
+
| timestamp | Server | When it happened |
|
|
1151
|
+
|
|
1152
|
+
### 4.3 Audit Logging Implementation
|
|
1153
|
+
|
|
1154
|
+
Since Hono/Drizzle doesn't have TypeORM-style subscribers, implement audit logging via service layer:
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
// lib/audit.ts
|
|
1158
|
+
type AuditAction = 'INSERT' | 'UPDATE' | 'DELETE'
|
|
1159
|
+
|
|
1160
|
+
interface AuditContext {
|
|
1161
|
+
transactionId: string
|
|
1162
|
+
accountId: string
|
|
1163
|
+
userId: string
|
|
1164
|
+
ipAddress: string
|
|
1165
|
+
userAgent: string
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function logAudit(
|
|
1169
|
+
ctx: AuditContext,
|
|
1170
|
+
entity: string,
|
|
1171
|
+
entityId: string,
|
|
1172
|
+
action: AuditAction,
|
|
1173
|
+
changes: Record<string, unknown>
|
|
1174
|
+
) {
|
|
1175
|
+
await db.insert(auditLogs).values({
|
|
1176
|
+
transactionId: ctx.transactionId,
|
|
1177
|
+
accountId: ctx.accountId,
|
|
1178
|
+
userId: ctx.userId,
|
|
1179
|
+
entity,
|
|
1180
|
+
entityId,
|
|
1181
|
+
action,
|
|
1182
|
+
changes,
|
|
1183
|
+
ipAddress: ctx.ipAddress,
|
|
1184
|
+
userAgent: ctx.userAgent,
|
|
1185
|
+
})
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
// Usage in service
|
|
1189
|
+
async function createUser(data: CreateUserInput, c: Context<Env>) {
|
|
1190
|
+
const [user] = await db.insert(users).values(data).returning()
|
|
1191
|
+
|
|
1192
|
+
await logAudit(
|
|
1193
|
+
getAuditContext(c),
|
|
1194
|
+
'User',
|
|
1195
|
+
user.id,
|
|
1196
|
+
'INSERT',
|
|
1197
|
+
user
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
return user
|
|
1201
|
+
}
|
|
1202
|
+
```
|
|
1203
|
+
|
|
1204
|
+
### 4.4 Transaction ID Propagation
|
|
1205
|
+
|
|
1206
|
+
**UUIDv7 for sortability:**
|
|
1207
|
+
|
|
1208
|
+
```typescript
|
|
1209
|
+
// middleware/request-context.ts
|
|
1210
|
+
import { uuidv7 } from 'uuidv7'
|
|
1211
|
+
|
|
1212
|
+
export const requestContext = createMiddleware<Env>(async (c, next) => {
|
|
1213
|
+
c.set('transactionId', uuidv7())
|
|
1214
|
+
c.set('ip', c.req.header('x-forwarded-for') || 'unknown')
|
|
1215
|
+
c.set('userAgent', c.req.header('user-agent') || 'unknown')
|
|
1216
|
+
await next()
|
|
1217
|
+
})
|
|
1218
|
+
```
|
|
1219
|
+
|
|
1220
|
+
**Benefits of UUIDv7:**
|
|
1221
|
+
- Time-ordered (sortable)
|
|
1222
|
+
- Unique across requests
|
|
1223
|
+
- Can group related audit entries
|
|
1224
|
+
- Efficient for time-range queries
|
|
1225
|
+
|
|
1226
|
+
---
|
|
1227
|
+
|
|
1228
|
+
## 5. Request Context Management
|
|
1229
|
+
|
|
1230
|
+
### 5.1 Context Variables
|
|
1231
|
+
|
|
1232
|
+
The following must be available throughout the request lifecycle:
|
|
1233
|
+
|
|
1234
|
+
```typescript
|
|
1235
|
+
type Env = {
|
|
1236
|
+
Variables: {
|
|
1237
|
+
// Request tracking
|
|
1238
|
+
transactionId: string // UUIDv7 for audit correlation
|
|
1239
|
+
ip: string // Client IP
|
|
1240
|
+
userAgent: string // Client user agent
|
|
1241
|
+
|
|
1242
|
+
// Authentication
|
|
1243
|
+
user: User | null // Full user entity from DB
|
|
1244
|
+
|
|
1245
|
+
// Authorization
|
|
1246
|
+
accountId: string // Current account context
|
|
1247
|
+
userRole: Role | null // User's role in current account
|
|
1248
|
+
isSystemAdminAccess: boolean // Super-admin flag
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
### 5.2 Context Flow
|
|
1254
|
+
|
|
1255
|
+
```
|
|
1256
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1257
|
+
│ requestContext middleware │
|
|
1258
|
+
│ Sets: transactionId, ip, userAgent │
|
|
1259
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1260
|
+
|
|
|
1261
|
+
v
|
|
1262
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1263
|
+
│ jwtAuth middleware │
|
|
1264
|
+
│ Sets: user (fetched from DB with userAccounts) │
|
|
1265
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1266
|
+
|
|
|
1267
|
+
v
|
|
1268
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1269
|
+
│ accountMiddleware │
|
|
1270
|
+
│ Sets: accountId, userRole, isSystemAdminAccess │
|
|
1271
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1272
|
+
|
|
|
1273
|
+
v
|
|
1274
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1275
|
+
│ Route Handler / Services │
|
|
1276
|
+
│ Reads: All context variables via c.get() │
|
|
1277
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1278
|
+
|
|
|
1279
|
+
v
|
|
1280
|
+
┌─────────────────────────────────────────────────────────────┐
|
|
1281
|
+
│ Audit Logging │
|
|
1282
|
+
│ Uses: transactionId, accountId, user.id, ip, userAgent │
|
|
1283
|
+
└─────────────────────────────────────────────────────────────┘
|
|
1284
|
+
```
|
|
1285
|
+
|
|
1286
|
+
### 5.3 Accessing Context in Handlers
|
|
1287
|
+
|
|
1288
|
+
```typescript
|
|
1289
|
+
// In route handlers
|
|
1290
|
+
export const handleCreateUser = async (c: Context<Env>) => {
|
|
1291
|
+
const body = c.req.valid('json')
|
|
1292
|
+
|
|
1293
|
+
// Access all context
|
|
1294
|
+
const user = c.get('user') // Actor
|
|
1295
|
+
const accountId = c.get('accountId') // Tenant
|
|
1296
|
+
const transactionId = c.get('transactionId') // For audit
|
|
1297
|
+
|
|
1298
|
+
// Pass to service
|
|
1299
|
+
const result = await usersService.create(body, {
|
|
1300
|
+
actor: user,
|
|
1301
|
+
accountId,
|
|
1302
|
+
transactionId,
|
|
1303
|
+
ip: c.get('ip'),
|
|
1304
|
+
userAgent: c.get('userAgent'),
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
return c.json(result, 201)
|
|
1308
|
+
}
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
---
|
|
1312
|
+
|
|
1313
|
+
## 6. API Design Patterns
|
|
1314
|
+
|
|
1315
|
+
### 6.1 Standard Endpoint Structure
|
|
1316
|
+
|
|
1317
|
+
Every resource should follow this pattern:
|
|
1318
|
+
|
|
1319
|
+
| Method | Path | Role | Description |
|
|
1320
|
+
|--------|------|------|-------------|
|
|
1321
|
+
| GET | /{resource} | MANAGER+ | List with pagination |
|
|
1322
|
+
| GET | /{resource}/:id | MANAGER+ | Get single by ID |
|
|
1323
|
+
| POST | /{resource} | ADMIN+ | Create new |
|
|
1324
|
+
| PUT | /{resource}/:id | ADMIN+ | Update existing |
|
|
1325
|
+
| DELETE | /{resource}/:id | ADMIN+ | Soft delete |
|
|
1326
|
+
|
|
1327
|
+
### 6.2 Pagination Contract
|
|
1328
|
+
|
|
1329
|
+
**Request Query Parameters:**
|
|
1330
|
+
|
|
1331
|
+
```typescript
|
|
1332
|
+
const PaginationQuerySchema = z.object({
|
|
1333
|
+
page: z.coerce.number().min(1).default(1),
|
|
1334
|
+
limit: z.coerce.number().min(1).max(100).default(50),
|
|
1335
|
+
sortBy: z.string().optional(),
|
|
1336
|
+
sortOrder: z.enum(['ASC', 'DESC']).default('DESC'),
|
|
1337
|
+
query: z.string().optional(), // Search term
|
|
1338
|
+
})
|
|
1339
|
+
```
|
|
1340
|
+
|
|
1341
|
+
**Response Shape:**
|
|
1342
|
+
|
|
1343
|
+
```typescript
|
|
1344
|
+
const PaginatedResponseSchema = <T extends z.ZodTypeAny>(itemSchema: T) =>
|
|
1345
|
+
z.object({
|
|
1346
|
+
data: z.array(itemSchema),
|
|
1347
|
+
meta: z.object({
|
|
1348
|
+
currentPage: z.number(),
|
|
1349
|
+
limit: z.number(),
|
|
1350
|
+
totalItems: z.number(),
|
|
1351
|
+
totalPages: z.number(),
|
|
1352
|
+
hasPreviousPage: z.boolean(),
|
|
1353
|
+
hasNextPage: z.boolean(),
|
|
1354
|
+
}),
|
|
1355
|
+
})
|
|
1356
|
+
```
|
|
1357
|
+
|
|
1358
|
+
**Helper Function:**
|
|
1359
|
+
|
|
1360
|
+
```typescript
|
|
1361
|
+
function createPaginationMeta(
|
|
1362
|
+
totalItems: number,
|
|
1363
|
+
page: number,
|
|
1364
|
+
limit: number
|
|
1365
|
+
): PaginationMeta {
|
|
1366
|
+
const totalPages = Math.ceil(totalItems / limit)
|
|
1367
|
+
return {
|
|
1368
|
+
currentPage: page,
|
|
1369
|
+
limit,
|
|
1370
|
+
totalItems,
|
|
1371
|
+
totalPages,
|
|
1372
|
+
hasPreviousPage: page > 1,
|
|
1373
|
+
hasNextPage: page < totalPages,
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### 6.3 Error Response Format
|
|
1379
|
+
|
|
1380
|
+
**Standard Error Shape:**
|
|
1381
|
+
|
|
1382
|
+
```typescript
|
|
1383
|
+
const ErrorResponseSchema = z.object({
|
|
1384
|
+
error: z.string(),
|
|
1385
|
+
message: z.string().optional(),
|
|
1386
|
+
details: z.unknown().optional(),
|
|
1387
|
+
statusCode: z.number(),
|
|
1388
|
+
})
|
|
1389
|
+
```
|
|
1390
|
+
|
|
1391
|
+
**HTTP Status Codes:**
|
|
1392
|
+
|
|
1393
|
+
| Code | Usage |
|
|
1394
|
+
|------|-------|
|
|
1395
|
+
| 400 | Validation errors, malformed request |
|
|
1396
|
+
| 401 | Missing or invalid JWT |
|
|
1397
|
+
| 403 | Valid JWT but insufficient role/permission |
|
|
1398
|
+
| 404 | Resource not found |
|
|
1399
|
+
| 409 | Conflict (e.g., duplicate email) |
|
|
1400
|
+
| 500 | Unexpected server error |
|
|
1401
|
+
|
|
1402
|
+
**Error Handler:**
|
|
1403
|
+
|
|
1404
|
+
```typescript
|
|
1405
|
+
app.onError((err, c) => {
|
|
1406
|
+
if (err instanceof HTTPException) {
|
|
1407
|
+
return c.json({
|
|
1408
|
+
error: err.message,
|
|
1409
|
+
statusCode: err.status,
|
|
1410
|
+
}, err.status)
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
console.error(err)
|
|
1414
|
+
return c.json({
|
|
1415
|
+
error: 'Internal Server Error',
|
|
1416
|
+
statusCode: 500,
|
|
1417
|
+
}, 500)
|
|
1418
|
+
})
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
### 6.4 OpenAPI Documentation
|
|
1422
|
+
|
|
1423
|
+
Every route must include:
|
|
1424
|
+
|
|
1425
|
+
```typescript
|
|
1426
|
+
const createUserRoute = createRoute({
|
|
1427
|
+
method: 'post',
|
|
1428
|
+
path: '/users',
|
|
1429
|
+
tags: ['Users'],
|
|
1430
|
+
summary: 'Create a new user',
|
|
1431
|
+
description: 'Creates a new user and sends Auth0 invitation',
|
|
1432
|
+
security: [{ Bearer: [] }],
|
|
1433
|
+
request: {
|
|
1434
|
+
headers: z.object({
|
|
1435
|
+
'account-id': z.string().uuid(),
|
|
1436
|
+
}),
|
|
1437
|
+
body: {
|
|
1438
|
+
content: {
|
|
1439
|
+
'application/json': { schema: CreateUserSchema },
|
|
1440
|
+
},
|
|
1441
|
+
},
|
|
1442
|
+
},
|
|
1443
|
+
responses: {
|
|
1444
|
+
201: {
|
|
1445
|
+
description: 'User created successfully',
|
|
1446
|
+
content: { 'application/json': { schema: UserSchema } },
|
|
1447
|
+
},
|
|
1448
|
+
400: {
|
|
1449
|
+
description: 'Validation error',
|
|
1450
|
+
content: { 'application/json': { schema: ErrorResponseSchema } },
|
|
1451
|
+
},
|
|
1452
|
+
401: { description: 'Unauthorized' },
|
|
1453
|
+
403: { description: 'Forbidden - insufficient role' },
|
|
1454
|
+
409: { description: 'User already exists' },
|
|
1455
|
+
},
|
|
1456
|
+
})
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
---
|
|
1460
|
+
|
|
1461
|
+
## 7. Service Layer Patterns
|
|
1462
|
+
|
|
1463
|
+
### 7.1 Service Structure
|
|
1464
|
+
|
|
1465
|
+
Each service should:
|
|
1466
|
+
- Accept context as parameter (not read from global)
|
|
1467
|
+
- Handle business logic
|
|
1468
|
+
- Call other services when needed
|
|
1469
|
+
- Trigger audit logging
|
|
1470
|
+
|
|
1471
|
+
```typescript
|
|
1472
|
+
// services/users.ts
|
|
1473
|
+
export function createUsersService(db: Database) {
|
|
1474
|
+
return {
|
|
1475
|
+
async findAll(ctx: ServiceContext, pagination: PaginationQuery) {
|
|
1476
|
+
// Implementation
|
|
1477
|
+
},
|
|
1478
|
+
|
|
1479
|
+
async findById(ctx: ServiceContext, id: string) {
|
|
1480
|
+
// Implementation
|
|
1481
|
+
},
|
|
1482
|
+
|
|
1483
|
+
async create(ctx: ServiceContext, data: CreateUserInput) {
|
|
1484
|
+
// Implementation with audit logging
|
|
1485
|
+
},
|
|
1486
|
+
|
|
1487
|
+
async update(ctx: ServiceContext, id: string, data: UpdateUserInput) {
|
|
1488
|
+
// Implementation with audit logging
|
|
1489
|
+
},
|
|
1490
|
+
|
|
1491
|
+
async delete(ctx: ServiceContext, id: string) {
|
|
1492
|
+
// Soft delete with audit logging
|
|
1493
|
+
},
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
### 7.2 Service Context
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
interface ServiceContext {
|
|
1502
|
+
accountId: string
|
|
1503
|
+
user: User
|
|
1504
|
+
transactionId: string
|
|
1505
|
+
ip: string
|
|
1506
|
+
userAgent: string
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1509
|
+
function getServiceContext(c: Context<Env>): ServiceContext {
|
|
1510
|
+
return {
|
|
1511
|
+
accountId: c.get('accountId'),
|
|
1512
|
+
user: c.get('user')!,
|
|
1513
|
+
transactionId: c.get('transactionId'),
|
|
1514
|
+
ip: c.get('ip'),
|
|
1515
|
+
userAgent: c.get('userAgent'),
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
```
|
|
1519
|
+
|
|
1520
|
+
### 7.3 External Service Integration
|
|
1521
|
+
|
|
1522
|
+
Abstract external services behind interfaces:
|
|
1523
|
+
|
|
1524
|
+
```typescript
|
|
1525
|
+
// services/identity-provider.ts
|
|
1526
|
+
interface IdentityProvider {
|
|
1527
|
+
sendInvitation(email: string, name: string): Promise<{ userId: string }>
|
|
1528
|
+
getUserByEmail(email: string): Promise<ExternalUser | null>
|
|
1529
|
+
deleteUser(userId: string): Promise<void>
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Implementation for Auth0
|
|
1533
|
+
export function createAuth0Provider(config: Auth0Config): IdentityProvider {
|
|
1534
|
+
return {
|
|
1535
|
+
async sendInvitation(email, name) {
|
|
1536
|
+
// Auth0 Management API call
|
|
1537
|
+
},
|
|
1538
|
+
async getUserByEmail(email) {
|
|
1539
|
+
// Auth0 Management API call
|
|
1540
|
+
},
|
|
1541
|
+
async deleteUser(userId) {
|
|
1542
|
+
// Auth0 Management API call
|
|
1543
|
+
},
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
```
|
|
1547
|
+
|
|
1548
|
+
---
|
|
1549
|
+
|
|
1550
|
+
## 8. Database Patterns
|
|
1551
|
+
|
|
1552
|
+
### 8.1 Migration Strategy
|
|
1553
|
+
|
|
1554
|
+
**Rules:**
|
|
1555
|
+
- NEVER use auto-sync/synchronize in production
|
|
1556
|
+
- Every schema change requires a migration
|
|
1557
|
+
- Migrations must be reversible (up and down)
|
|
1558
|
+
- Test migrations on a copy of production data
|
|
1559
|
+
|
|
1560
|
+
**Drizzle Migration Commands:**
|
|
1561
|
+
|
|
1562
|
+
```bash
|
|
1563
|
+
# Generate migration from schema changes
|
|
1564
|
+
pnpm drizzle-kit generate
|
|
1565
|
+
|
|
1566
|
+
# Apply migrations
|
|
1567
|
+
pnpm drizzle-kit migrate
|
|
1568
|
+
|
|
1569
|
+
# Drop all and recreate (dev only!)
|
|
1570
|
+
pnpm drizzle-kit push
|
|
1571
|
+
```
|
|
1572
|
+
|
|
1573
|
+
### 8.2 Seed Data
|
|
1574
|
+
|
|
1575
|
+
**Structure:**
|
|
1576
|
+
|
|
1577
|
+
```typescript
|
|
1578
|
+
// db/seed.ts
|
|
1579
|
+
async function seed() {
|
|
1580
|
+
// 1. Create default account
|
|
1581
|
+
const [account] = await db.insert(accounts).values({
|
|
1582
|
+
name: 'Default',
|
|
1583
|
+
domain: 'default.local',
|
|
1584
|
+
}).returning()
|
|
1585
|
+
|
|
1586
|
+
// 2. Create super admin user
|
|
1587
|
+
const [superAdmin] = await db.insert(users).values({
|
|
1588
|
+
email: 'admin@example.com',
|
|
1589
|
+
name: 'Super Admin',
|
|
1590
|
+
isSuperAdmin: true,
|
|
1591
|
+
status: 'active',
|
|
1592
|
+
}).returning()
|
|
1593
|
+
|
|
1594
|
+
// 3. Create test users with different roles
|
|
1595
|
+
const testUsers = [
|
|
1596
|
+
{ email: 'manager@example.com', role: 'MANAGER' },
|
|
1597
|
+
{ email: 'editor@example.com', role: 'EDITOR' },
|
|
1598
|
+
{ email: 'viewer@example.com', role: 'VIEWER' },
|
|
1599
|
+
]
|
|
1600
|
+
|
|
1601
|
+
for (const { email, role } of testUsers) {
|
|
1602
|
+
const [user] = await db.insert(users).values({
|
|
1603
|
+
email,
|
|
1604
|
+
name: email.split('@')[0],
|
|
1605
|
+
status: 'active',
|
|
1606
|
+
}).returning()
|
|
1607
|
+
|
|
1608
|
+
await db.insert(userAccounts).values({
|
|
1609
|
+
userId: user.id,
|
|
1610
|
+
accountId: account.id,
|
|
1611
|
+
role,
|
|
1612
|
+
})
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
```
|
|
1616
|
+
|
|
1617
|
+
### 8.3 Query Patterns
|
|
1618
|
+
|
|
1619
|
+
**Always Include:**
|
|
1620
|
+
|
|
1621
|
+
```typescript
|
|
1622
|
+
// 1. Account filtering (for multi-tenant queries)
|
|
1623
|
+
.where(eq(table.accountId, ctx.accountId))
|
|
1624
|
+
|
|
1625
|
+
// 2. Soft delete filtering
|
|
1626
|
+
.where(isNull(table.deletedAt))
|
|
1627
|
+
|
|
1628
|
+
// 3. Combined
|
|
1629
|
+
.where(and(
|
|
1630
|
+
eq(table.accountId, ctx.accountId),
|
|
1631
|
+
isNull(table.deletedAt)
|
|
1632
|
+
))
|
|
1633
|
+
```
|
|
1634
|
+
|
|
1635
|
+
**Pagination:**
|
|
1636
|
+
|
|
1637
|
+
```typescript
|
|
1638
|
+
async function findWithPagination<T>(
|
|
1639
|
+
query: SelectQueryBuilder<T>,
|
|
1640
|
+
page: number,
|
|
1641
|
+
limit: number
|
|
1642
|
+
) {
|
|
1643
|
+
const offset = (page - 1) * limit
|
|
1644
|
+
|
|
1645
|
+
const [data, countResult] = await Promise.all([
|
|
1646
|
+
query.limit(limit).offset(offset),
|
|
1647
|
+
db.select({ count: sql<number>`count(*)` }).from(query.as('subquery')),
|
|
1648
|
+
])
|
|
1649
|
+
|
|
1650
|
+
return {
|
|
1651
|
+
data,
|
|
1652
|
+
meta: createPaginationMeta(countResult[0].count, page, limit),
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
```
|
|
1656
|
+
|
|
1657
|
+
---
|
|
1658
|
+
|
|
1659
|
+
## 9. Security Patterns Summary
|
|
1660
|
+
|
|
1661
|
+
### 9.1 Defense in Depth
|
|
1662
|
+
|
|
1663
|
+
| Layer | Protection | Implementation |
|
|
1664
|
+
|-------|------------|----------------|
|
|
1665
|
+
| Network | HTTPS only | Infrastructure |
|
|
1666
|
+
| Header | account-id required | accountMiddleware |
|
|
1667
|
+
| Token | JWT validation | jwtAuth middleware |
|
|
1668
|
+
| Membership | User-account check | accountMiddleware |
|
|
1669
|
+
| Role | Hierarchy check | requireRole middleware |
|
|
1670
|
+
| Permission | Granular check | requirePermission middleware |
|
|
1671
|
+
| Query | Account filtering | Service layer |
|
|
1672
|
+
| Audit | Change logging | Audit service |
|
|
1673
|
+
|
|
1674
|
+
### 9.2 Input Validation
|
|
1675
|
+
|
|
1676
|
+
- ALL input validated via Zod schemas
|
|
1677
|
+
- Schemas defined once, used for validation AND OpenAPI docs
|
|
1678
|
+
- Whitelist approach (reject unknown fields)
|
|
1679
|
+
- Type coercion handled by Zod
|
|
1680
|
+
|
|
1681
|
+
### 9.3 SQL Injection Prevention
|
|
1682
|
+
|
|
1683
|
+
- NEVER use string concatenation for queries
|
|
1684
|
+
- Always use Drizzle's parameterized queries
|
|
1685
|
+
- Use `sql` template literal for raw SQL
|
|
1686
|
+
|
|
1687
|
+
```typescript
|
|
1688
|
+
// GOOD
|
|
1689
|
+
db.select().from(users).where(eq(users.email, email))
|
|
1690
|
+
|
|
1691
|
+
// GOOD (when raw SQL needed)
|
|
1692
|
+
db.execute(sql`SELECT * FROM users WHERE email = ${email}`)
|
|
1693
|
+
|
|
1694
|
+
// BAD - Never do this
|
|
1695
|
+
db.execute(`SELECT * FROM users WHERE email = '${email}'`)
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
---
|
|
1699
|
+
|
|
1700
|
+
## 10. Configuration & Environment
|
|
1701
|
+
|
|
1702
|
+
### 10.1 Required Environment Variables
|
|
1703
|
+
|
|
1704
|
+
```typescript
|
|
1705
|
+
// env.ts
|
|
1706
|
+
import { z } from 'zod'
|
|
1707
|
+
|
|
1708
|
+
const envSchema = z.object({
|
|
1709
|
+
// Server
|
|
1710
|
+
PORT: z.coerce.number().default(3000),
|
|
1711
|
+
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
|
|
1712
|
+
|
|
1713
|
+
// Database
|
|
1714
|
+
DATABASE_URL: z.string().default('db.sqlite'),
|
|
1715
|
+
|
|
1716
|
+
// Auth0
|
|
1717
|
+
JWKS_URI: z.string().url(),
|
|
1718
|
+
IDP_ISSUER: z.string().url(),
|
|
1719
|
+
IDP_AUDIENCE: z.string(),
|
|
1720
|
+
AUTH0_DOMAIN: z.string(),
|
|
1721
|
+
AUTH0_CLIENT_ID: z.string(),
|
|
1722
|
+
AUTH0_CLIENT_SECRET: z.string(),
|
|
1723
|
+
AUTH0_ROLES_CLAIM: z.string().default('https://app.example.com/roles'),
|
|
1724
|
+
|
|
1725
|
+
// Optional
|
|
1726
|
+
CORS_ORIGINS: z.string().default('*'),
|
|
1727
|
+
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
|
|
1728
|
+
})
|
|
1729
|
+
|
|
1730
|
+
export const env = envSchema.parse(process.env)
|
|
1731
|
+
```
|
|
1732
|
+
|
|
1733
|
+
### 10.2 Startup Validation
|
|
1734
|
+
|
|
1735
|
+
```typescript
|
|
1736
|
+
// index.ts
|
|
1737
|
+
import { env } from './env' // Throws if invalid
|
|
1738
|
+
|
|
1739
|
+
// Only reached if env is valid
|
|
1740
|
+
console.log(`Starting server on port ${env.PORT}...`)
|
|
1741
|
+
```
|
|
1742
|
+
|
|
1743
|
+
---
|
|
1744
|
+
|
|
1745
|
+
## 11. Quick Reference: Pattern Mapping
|
|
1746
|
+
|
|
1747
|
+
| NestJS Pattern | Hono Equivalent |
|
|
1748
|
+
|----------------|-----------------|
|
|
1749
|
+
| `@Controller()` | `new OpenAPIHono()` sub-app |
|
|
1750
|
+
| `@Get()`, `@Post()`, etc. | `createRoute()` + `app.openapi()` |
|
|
1751
|
+
| `@UseGuards(JwtAuthGuard)` | `jwtAuth` middleware |
|
|
1752
|
+
| `@MinRole(Role.ADMIN)` | `requireRole('ADMIN')` middleware |
|
|
1753
|
+
| `@Body() dto: CreateDto` | `c.req.valid('json')` with Zod schema |
|
|
1754
|
+
| `@Param('id')` | `c.req.valid('param').id` |
|
|
1755
|
+
| `@Query() query: PaginationDto` | `c.req.valid('query')` |
|
|
1756
|
+
| `ClsService.get('user')` | `c.get('user')` |
|
|
1757
|
+
| `@InjectRepository(User)` | Direct import of `db` client |
|
|
1758
|
+
| TypeORM `Repository` | Drizzle query builder |
|
|
1759
|
+
| `class-validator` decorators | Zod schema methods |
|
|
1760
|
+
| `@ApiProperty()` | `.openapi({ example: ... })` |
|
|
1761
|
+
| `@ApiTags('users')` | `tags: ['Users']` in route |
|
|
1762
|
+
| TypeORM subscriber | Manual audit logging in service |
|
|
1763
|
+
|
|
1764
|
+
---
|
|
1765
|
+
|
|
1766
|
+
## References
|
|
1767
|
+
|
|
1768
|
+
- [Hono Documentation](https://hono.dev/docs/)
|
|
1769
|
+
- [Zod OpenAPI Example](https://hono.dev/examples/zod-openapi)
|
|
1770
|
+
- [@hono/zod-openapi](https://www.npmjs.com/package/@hono/zod-openapi)
|
|
1771
|
+
- [Hono Best Practices](https://hono.dev/docs/guides/best-practices)
|
|
1772
|
+
- [Hono JWT Helper](https://hono.dev/docs/helpers/jwt)
|
|
1773
|
+
- [Hono Node.js Guide](https://hono.dev/docs/getting-started/nodejs)
|
|
1774
|
+
- [Drizzle ORM](https://orm.drizzle.team/)
|