@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,1112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-Tenancy Isolation Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive tests for tenant data isolation across all API endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Test categories:
|
|
7
|
+
* 1. User access isolation - Users can only access users in their accounts
|
|
8
|
+
* 2. Account access isolation - Users can only access accounts they belong to
|
|
9
|
+
* 3. Invitation isolation - Users can only manage invitations in their accounts
|
|
10
|
+
* 4. Audit log isolation - Users can only view audit logs in their accounts
|
|
11
|
+
* 5. Account switching - Users with multiple accounts get correct role enforcement
|
|
12
|
+
* 6. Super admin override - Super admins can access any account
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, beforeAll } from 'vitest'
|
|
16
|
+
import { Hono } from 'hono'
|
|
17
|
+
import { getEnv, getDb, getSqlite, type TestEnv } from '../setup'
|
|
18
|
+
import {
|
|
19
|
+
createUser,
|
|
20
|
+
createSuperAdmin,
|
|
21
|
+
createUserSession,
|
|
22
|
+
createAccount,
|
|
23
|
+
addUserToAccount,
|
|
24
|
+
createMultiTenantScenario,
|
|
25
|
+
createInvitation,
|
|
26
|
+
type Role,
|
|
27
|
+
} from '../fixtures'
|
|
28
|
+
import type { HonoEnv } from '../../../src/server/types'
|
|
29
|
+
import { api } from '../../../src/server/routes'
|
|
30
|
+
import { errorHandler } from '../../../src/server/middleware/error-handler'
|
|
31
|
+
import { sessionMiddleware } from '../../../src/server/lib/session'
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// TEST SETUP
|
|
35
|
+
// ============================================================================
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a database wrapper that adds the `execute` method
|
|
39
|
+
* The better-sqlite3 drizzle doesn't have execute, but D1 does
|
|
40
|
+
*/
|
|
41
|
+
function createTestDb() {
|
|
42
|
+
const db = getDb()
|
|
43
|
+
return new Proxy(db, {
|
|
44
|
+
get(target, prop) {
|
|
45
|
+
if (prop === 'execute') {
|
|
46
|
+
return target.run.bind(target)
|
|
47
|
+
}
|
|
48
|
+
return (target as any)[prop]
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Helper to create a user with a specific role in an account
|
|
55
|
+
*/
|
|
56
|
+
async function createUserWithRole(
|
|
57
|
+
accountId: string,
|
|
58
|
+
role: Role,
|
|
59
|
+
options?: { email?: string; name?: string }
|
|
60
|
+
): Promise<{
|
|
61
|
+
user: Awaited<ReturnType<typeof createUser>>
|
|
62
|
+
sessionId: string
|
|
63
|
+
headers: Record<string, string>
|
|
64
|
+
}> {
|
|
65
|
+
const user = await createUser({
|
|
66
|
+
email: options?.email ?? `${role.toLowerCase()}-user-${crypto.randomUUID().slice(0, 8)}@example.com`,
|
|
67
|
+
name: options?.name ?? `${role} User`,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
await addUserToAccount(user.id, accountId, role)
|
|
71
|
+
|
|
72
|
+
const { sessionId, headers } = await createUserSession(user.id, {
|
|
73
|
+
email: user.email,
|
|
74
|
+
name: user.name,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { user, sessionId, headers }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Helper to create an audit log entry directly in the database
|
|
82
|
+
*/
|
|
83
|
+
function createAuditLog(options: {
|
|
84
|
+
accountId: string
|
|
85
|
+
userId: string
|
|
86
|
+
entity: string
|
|
87
|
+
entityId: string
|
|
88
|
+
action: 'INSERT' | 'UPDATE' | 'DELETE' | 'LOGIN' | 'LOGOUT' | 'SIGNUP' | 'TOKEN_REFRESH' | 'LOGIN_FAILED'
|
|
89
|
+
changes?: Record<string, unknown> | null
|
|
90
|
+
timestamp?: string
|
|
91
|
+
}) {
|
|
92
|
+
const sqlite = getSqlite()
|
|
93
|
+
const id = crypto.randomUUID()
|
|
94
|
+
const transactionId = crypto.randomUUID()
|
|
95
|
+
const timestamp = options.timestamp ?? new Date().toISOString()
|
|
96
|
+
|
|
97
|
+
sqlite.prepare(`
|
|
98
|
+
INSERT INTO audit_logs (id, transaction_id, account_id, user_id, entity, entity_id, action, changes, ip_address, user_agent, timestamp)
|
|
99
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
100
|
+
`).run(
|
|
101
|
+
id,
|
|
102
|
+
transactionId,
|
|
103
|
+
options.accountId,
|
|
104
|
+
options.userId,
|
|
105
|
+
options.entity,
|
|
106
|
+
options.entityId,
|
|
107
|
+
options.action,
|
|
108
|
+
options.changes ? JSON.stringify(options.changes) : null,
|
|
109
|
+
'127.0.0.1',
|
|
110
|
+
'IntegrationTest/1.0',
|
|
111
|
+
timestamp
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return { id, transactionId, timestamp }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ============================================================================
|
|
118
|
+
// TESTS
|
|
119
|
+
// ============================================================================
|
|
120
|
+
|
|
121
|
+
describe('Multi-Tenancy Isolation', () => {
|
|
122
|
+
let app: Hono<HonoEnv>
|
|
123
|
+
let env: TestEnv
|
|
124
|
+
|
|
125
|
+
beforeAll(() => {
|
|
126
|
+
env = getEnv()
|
|
127
|
+
app = new Hono<HonoEnv>()
|
|
128
|
+
|
|
129
|
+
// Set up error handler
|
|
130
|
+
app.onError(errorHandler)
|
|
131
|
+
|
|
132
|
+
// Set up middleware to inject test environment
|
|
133
|
+
app.use('*', async (c, next) => {
|
|
134
|
+
// Inject environment bindings
|
|
135
|
+
;(c as any).env = env
|
|
136
|
+
|
|
137
|
+
// Set up database
|
|
138
|
+
const db = createTestDb()
|
|
139
|
+
c.set('db', db)
|
|
140
|
+
|
|
141
|
+
// Set up request context variables
|
|
142
|
+
c.set('transactionId', crypto.randomUUID())
|
|
143
|
+
c.set('ip', '127.0.0.1')
|
|
144
|
+
c.set('userAgent', 'IntegrationTest/1.0')
|
|
145
|
+
|
|
146
|
+
await next()
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Session middleware - reads session from KV and sets sessionData in context
|
|
150
|
+
app.use('*', sessionMiddleware())
|
|
151
|
+
|
|
152
|
+
// Mount API routes (includes sessionAuth and accountMiddleware)
|
|
153
|
+
app.route('/api', api)
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// ============================================================================
|
|
157
|
+
// USER ACCESS ISOLATION TESTS
|
|
158
|
+
// ============================================================================
|
|
159
|
+
|
|
160
|
+
describe('User access isolation', () => {
|
|
161
|
+
it('should only view users in accessible accounts', async () => {
|
|
162
|
+
const scenario = await createMultiTenantScenario()
|
|
163
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
164
|
+
|
|
165
|
+
// Create users in the accessible account
|
|
166
|
+
const userInAccessibleAccount = await createUser({
|
|
167
|
+
email: 'accessible-account-user@example.com',
|
|
168
|
+
name: 'Accessible Account User',
|
|
169
|
+
})
|
|
170
|
+
await addUserToAccount(userInAccessibleAccount.id, accountWithAdminAccess.account.id, 'VIEWER')
|
|
171
|
+
|
|
172
|
+
// Create users in an account without access
|
|
173
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
174
|
+
const userInInaccessibleAccount = await createUser({
|
|
175
|
+
email: 'inaccessible-account-user@example.com',
|
|
176
|
+
name: 'Inaccessible Account User',
|
|
177
|
+
})
|
|
178
|
+
await addUserToAccount(userInInaccessibleAccount.id, accountWithoutAccess.id, 'VIEWER')
|
|
179
|
+
|
|
180
|
+
// Request users from the accessible account - should work
|
|
181
|
+
const res = await app.request('/api/users', {
|
|
182
|
+
method: 'GET',
|
|
183
|
+
headers: {
|
|
184
|
+
...scenario.headers,
|
|
185
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
186
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
187
|
+
},
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
expect(res.status).toBe(200)
|
|
191
|
+
const body = await res.json()
|
|
192
|
+
const userIds = body.data.map((u: any) => u.id)
|
|
193
|
+
|
|
194
|
+
// Should include user from accessible account
|
|
195
|
+
expect(userIds).toContain(userInAccessibleAccount.id)
|
|
196
|
+
// Should NOT include user from inaccessible account
|
|
197
|
+
expect(userIds).not.toContain(userInInaccessibleAccount.id)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should NOT be able to view a specific user from another account', async () => {
|
|
201
|
+
const scenario = await createMultiTenantScenario()
|
|
202
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
203
|
+
|
|
204
|
+
// Create a user in another account
|
|
205
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
206
|
+
const otherUser = await createUser({
|
|
207
|
+
email: 'other-account-specific@example.com',
|
|
208
|
+
name: 'Other Account User',
|
|
209
|
+
})
|
|
210
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'VIEWER')
|
|
211
|
+
|
|
212
|
+
// Try to get the other user from the accessible account context
|
|
213
|
+
const res = await app.request(`/api/users/${otherUser.id}`, {
|
|
214
|
+
method: 'GET',
|
|
215
|
+
headers: {
|
|
216
|
+
...scenario.headers,
|
|
217
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
218
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
219
|
+
},
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Should return 404 (not 403) for security - don't reveal user exists
|
|
223
|
+
expect(res.status).toBe(404)
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
it('should NOT be able to update users in other accounts', async () => {
|
|
227
|
+
const scenario = await createMultiTenantScenario()
|
|
228
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
229
|
+
|
|
230
|
+
// Create a user in another account
|
|
231
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
232
|
+
const otherUser = await createUser({
|
|
233
|
+
email: 'update-other-account@example.com',
|
|
234
|
+
name: 'Update Other Account User',
|
|
235
|
+
})
|
|
236
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'VIEWER')
|
|
237
|
+
|
|
238
|
+
// Try to update the other user
|
|
239
|
+
const res = await app.request(`/api/users/${otherUser.id}`, {
|
|
240
|
+
method: 'PATCH',
|
|
241
|
+
headers: {
|
|
242
|
+
...scenario.headers,
|
|
243
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
244
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
245
|
+
'Content-Type': 'application/json',
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({ name: 'Should Not Update' }),
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
// Should return 404 (not 403) for security
|
|
251
|
+
expect(res.status).toBe(404)
|
|
252
|
+
|
|
253
|
+
// Verify user was not updated
|
|
254
|
+
const sqlite = getSqlite()
|
|
255
|
+
const row = sqlite.prepare('SELECT name FROM users WHERE id = ?').get(otherUser.id) as { name: string }
|
|
256
|
+
expect(row.name).toBe('Update Other Account User')
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
it('should NOT be able to delete users in other accounts', async () => {
|
|
260
|
+
const scenario = await createMultiTenantScenario()
|
|
261
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
262
|
+
|
|
263
|
+
// Create a user in another account
|
|
264
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
265
|
+
const otherUser = await createUser({
|
|
266
|
+
email: 'delete-other-account@example.com',
|
|
267
|
+
name: 'Delete Other Account User',
|
|
268
|
+
})
|
|
269
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'VIEWER')
|
|
270
|
+
|
|
271
|
+
// Try to delete the other user
|
|
272
|
+
const res = await app.request(`/api/users/${otherUser.id}`, {
|
|
273
|
+
method: 'DELETE',
|
|
274
|
+
headers: {
|
|
275
|
+
...scenario.headers,
|
|
276
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
277
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
278
|
+
},
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
// Should return 404 (not 403) for security
|
|
282
|
+
expect(res.status).toBe(404)
|
|
283
|
+
|
|
284
|
+
// Verify user was not deleted
|
|
285
|
+
const sqlite = getSqlite()
|
|
286
|
+
const row = sqlite.prepare('SELECT deleted_at FROM users WHERE id = ?').get(otherUser.id) as { deleted_at: string | null }
|
|
287
|
+
expect(row.deleted_at).toBeNull()
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
// ============================================================================
|
|
292
|
+
// ACCOUNT ACCESS ISOLATION TESTS
|
|
293
|
+
// ============================================================================
|
|
294
|
+
|
|
295
|
+
describe('Account access isolation', () => {
|
|
296
|
+
it('should only view accounts user has access to', async () => {
|
|
297
|
+
const scenario = await createMultiTenantScenario()
|
|
298
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
299
|
+
|
|
300
|
+
// Request accounts list
|
|
301
|
+
const res = await app.request('/api/accounts', {
|
|
302
|
+
method: 'GET',
|
|
303
|
+
headers: {
|
|
304
|
+
...scenario.headers,
|
|
305
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
306
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
307
|
+
},
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
expect(res.status).toBe(200)
|
|
311
|
+
const body = await res.json()
|
|
312
|
+
const accountIds = body.data.map((a: any) => a.id)
|
|
313
|
+
|
|
314
|
+
// Should include all accounts with access
|
|
315
|
+
for (const { account } of scenario.accounts.withAccess) {
|
|
316
|
+
expect(accountIds).toContain(account.id)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Should NOT include accounts without access
|
|
320
|
+
for (const account of scenario.accounts.withoutAccess) {
|
|
321
|
+
expect(accountIds).not.toContain(account.id)
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
|
|
325
|
+
it('should return 404 (not 403) when viewing other accounts', async () => {
|
|
326
|
+
const scenario = await createMultiTenantScenario()
|
|
327
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
328
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
329
|
+
|
|
330
|
+
// Try to get an account without access
|
|
331
|
+
const res = await app.request(`/api/accounts/${accountWithoutAccess.id}`, {
|
|
332
|
+
method: 'GET',
|
|
333
|
+
headers: {
|
|
334
|
+
...scenario.headers,
|
|
335
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
336
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
337
|
+
},
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
// Should return 404 for security - don't reveal account exists
|
|
341
|
+
expect(res.status).toBe(404)
|
|
342
|
+
})
|
|
343
|
+
|
|
344
|
+
it('should NOT be able to update other accounts', async () => {
|
|
345
|
+
const scenario = await createMultiTenantScenario()
|
|
346
|
+
const accountWithManagerAccess = scenario.accounts.withAccess.find(a => a.role === 'MANAGER')!
|
|
347
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
348
|
+
|
|
349
|
+
// Try to update an account without access
|
|
350
|
+
const res = await app.request(`/api/accounts/${accountWithoutAccess.id}`, {
|
|
351
|
+
method: 'PATCH',
|
|
352
|
+
headers: {
|
|
353
|
+
...scenario.headers,
|
|
354
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
355
|
+
'account-id': accountWithManagerAccess.account.id,
|
|
356
|
+
'Content-Type': 'application/json',
|
|
357
|
+
},
|
|
358
|
+
body: JSON.stringify({ name: 'Should Not Update Account' }),
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
// Should return 404 for security
|
|
362
|
+
expect(res.status).toBe(404)
|
|
363
|
+
|
|
364
|
+
// Verify account was not updated
|
|
365
|
+
const sqlite = getSqlite()
|
|
366
|
+
const row = sqlite.prepare('SELECT name FROM accounts WHERE id = ?').get(accountWithoutAccess.id) as { name: string }
|
|
367
|
+
expect(row.name).toBe(accountWithoutAccess.name)
|
|
368
|
+
})
|
|
369
|
+
|
|
370
|
+
it('should NOT be able to delete other accounts (even with ADMIN role)', async () => {
|
|
371
|
+
const scenario = await createMultiTenantScenario()
|
|
372
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
373
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
374
|
+
|
|
375
|
+
// Try to delete an account without access
|
|
376
|
+
const res = await app.request(`/api/accounts/${accountWithoutAccess.id}`, {
|
|
377
|
+
method: 'DELETE',
|
|
378
|
+
headers: {
|
|
379
|
+
...scenario.headers,
|
|
380
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
381
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Should return 403 (requires super-admin) or 404
|
|
386
|
+
expect([403, 404]).toContain(res.status)
|
|
387
|
+
|
|
388
|
+
// Verify account was not deleted
|
|
389
|
+
const sqlite = getSqlite()
|
|
390
|
+
const row = sqlite.prepare('SELECT deleted_at FROM accounts WHERE id = ?').get(accountWithoutAccess.id) as { deleted_at: string | null }
|
|
391
|
+
expect(row.deleted_at).toBeNull()
|
|
392
|
+
})
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
// ============================================================================
|
|
396
|
+
// INVITATION ISOLATION TESTS
|
|
397
|
+
// ============================================================================
|
|
398
|
+
|
|
399
|
+
describe('Invitation isolation', () => {
|
|
400
|
+
it('should only view invitations in accessible accounts', async () => {
|
|
401
|
+
const scenario = await createMultiTenantScenario()
|
|
402
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
403
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
404
|
+
|
|
405
|
+
// Create invitation in accessible account
|
|
406
|
+
const invitationInAccessible = await createInvitation({
|
|
407
|
+
accountId: accountWithAdminAccess.account.id,
|
|
408
|
+
email: 'accessible-invitation@example.com',
|
|
409
|
+
role: 'VIEWER',
|
|
410
|
+
invitedById: scenario.user.id,
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// Create user in inaccessible account to create invitation
|
|
414
|
+
const otherUser = await createUser({
|
|
415
|
+
email: 'other-inviter@example.com',
|
|
416
|
+
name: 'Other Inviter',
|
|
417
|
+
})
|
|
418
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'ADMIN')
|
|
419
|
+
|
|
420
|
+
// Create invitation in inaccessible account
|
|
421
|
+
const invitationInInaccessible = await createInvitation({
|
|
422
|
+
accountId: accountWithoutAccess.id,
|
|
423
|
+
email: 'inaccessible-invitation@example.com',
|
|
424
|
+
role: 'VIEWER',
|
|
425
|
+
invitedById: otherUser.id,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Request invitations from the accessible account
|
|
429
|
+
const res = await app.request('/api/invitations', {
|
|
430
|
+
method: 'GET',
|
|
431
|
+
headers: {
|
|
432
|
+
...scenario.headers,
|
|
433
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
434
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
435
|
+
},
|
|
436
|
+
})
|
|
437
|
+
|
|
438
|
+
expect(res.status).toBe(200)
|
|
439
|
+
const body = await res.json()
|
|
440
|
+
const invitationIds = body.data.map((i: any) => i.id)
|
|
441
|
+
|
|
442
|
+
// Should include invitation from accessible account
|
|
443
|
+
expect(invitationIds).toContain(invitationInAccessible.id)
|
|
444
|
+
// Should NOT include invitation from inaccessible account
|
|
445
|
+
expect(invitationIds).not.toContain(invitationInInaccessible.id)
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
it('should NOT be able to create invitations for other accounts', async () => {
|
|
449
|
+
const scenario = await createMultiTenantScenario()
|
|
450
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
451
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
452
|
+
|
|
453
|
+
const testEmail = 'cross-tenant-invite@example.com'
|
|
454
|
+
|
|
455
|
+
// Create an invitation in the authorized account context
|
|
456
|
+
const res = await app.request('/api/invitations', {
|
|
457
|
+
method: 'POST',
|
|
458
|
+
headers: {
|
|
459
|
+
...scenario.headers,
|
|
460
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
461
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
462
|
+
'Content-Type': 'application/json',
|
|
463
|
+
},
|
|
464
|
+
body: JSON.stringify({
|
|
465
|
+
email: testEmail,
|
|
466
|
+
role: 'VIEWER',
|
|
467
|
+
}),
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
expect(res.status).toBe(200)
|
|
471
|
+
|
|
472
|
+
// Verify the invitation was created in the correct account by checking database
|
|
473
|
+
const sqlite = getSqlite()
|
|
474
|
+
const invitation = sqlite.prepare(
|
|
475
|
+
'SELECT account_id FROM invitations WHERE email = ?'
|
|
476
|
+
).get(testEmail) as { account_id: string }
|
|
477
|
+
|
|
478
|
+
// The invitation should be in the account from the header, not any other
|
|
479
|
+
expect(invitation.account_id).toBe(accountWithAdminAccess.account.id)
|
|
480
|
+
expect(invitation.account_id).not.toBe(accountWithoutAccess.id)
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
it('should NOT be able to revoke invitations in other accounts', async () => {
|
|
484
|
+
const scenario = await createMultiTenantScenario()
|
|
485
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
486
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
487
|
+
|
|
488
|
+
// Create user in inaccessible account
|
|
489
|
+
const otherUser = await createUser({
|
|
490
|
+
email: 'other-revoke-inviter@example.com',
|
|
491
|
+
name: 'Other Revoke Inviter',
|
|
492
|
+
})
|
|
493
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'ADMIN')
|
|
494
|
+
|
|
495
|
+
// Create invitation in inaccessible account
|
|
496
|
+
const invitation = await createInvitation({
|
|
497
|
+
accountId: accountWithoutAccess.id,
|
|
498
|
+
email: 'revoke-cross-tenant@example.com',
|
|
499
|
+
role: 'VIEWER',
|
|
500
|
+
invitedById: otherUser.id,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
// Try to revoke the invitation from accessible account context
|
|
504
|
+
const res = await app.request(`/api/invitations/${invitation.id}`, {
|
|
505
|
+
method: 'DELETE',
|
|
506
|
+
headers: {
|
|
507
|
+
...scenario.headers,
|
|
508
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
509
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
// Should return 404 for security
|
|
514
|
+
expect(res.status).toBe(404)
|
|
515
|
+
|
|
516
|
+
// Verify invitation still exists
|
|
517
|
+
const sqlite = getSqlite()
|
|
518
|
+
const row = sqlite.prepare('SELECT id FROM invitations WHERE id = ?').get(invitation.id)
|
|
519
|
+
expect(row).not.toBeNull()
|
|
520
|
+
})
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// ============================================================================
|
|
524
|
+
// AUDIT LOG ISOLATION TESTS
|
|
525
|
+
// ============================================================================
|
|
526
|
+
|
|
527
|
+
describe('Audit log isolation', () => {
|
|
528
|
+
it('should only view audit logs for accessible accounts', async () => {
|
|
529
|
+
const scenario = await createMultiTenantScenario()
|
|
530
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
531
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
532
|
+
|
|
533
|
+
// Create audit log in accessible account
|
|
534
|
+
const auditInAccessible = createAuditLog({
|
|
535
|
+
accountId: accountWithAdminAccess.account.id,
|
|
536
|
+
userId: scenario.user.id,
|
|
537
|
+
entity: 'User',
|
|
538
|
+
entityId: crypto.randomUUID(),
|
|
539
|
+
action: 'INSERT',
|
|
540
|
+
changes: { name: 'Accessible Audit' },
|
|
541
|
+
})
|
|
542
|
+
|
|
543
|
+
// Create user in inaccessible account for audit log
|
|
544
|
+
const otherUser = await createUser({
|
|
545
|
+
email: 'other-audit-user@example.com',
|
|
546
|
+
name: 'Other Audit User',
|
|
547
|
+
})
|
|
548
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'ADMIN')
|
|
549
|
+
|
|
550
|
+
// Create audit log in inaccessible account
|
|
551
|
+
const auditInInaccessible = createAuditLog({
|
|
552
|
+
accountId: accountWithoutAccess.id,
|
|
553
|
+
userId: otherUser.id,
|
|
554
|
+
entity: 'User',
|
|
555
|
+
entityId: crypto.randomUUID(),
|
|
556
|
+
action: 'INSERT',
|
|
557
|
+
changes: { name: 'Inaccessible Audit' },
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
// Request audit logs from the accessible account
|
|
561
|
+
const res = await app.request('/api/audits', {
|
|
562
|
+
method: 'GET',
|
|
563
|
+
headers: {
|
|
564
|
+
...scenario.headers,
|
|
565
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
566
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
567
|
+
},
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
expect(res.status).toBe(200)
|
|
571
|
+
const body = await res.json()
|
|
572
|
+
const auditIds = body.data.map((a: any) => a.id)
|
|
573
|
+
|
|
574
|
+
// Should include audit from accessible account
|
|
575
|
+
expect(auditIds).toContain(auditInAccessible.id)
|
|
576
|
+
// Should NOT include audit from inaccessible account
|
|
577
|
+
expect(auditIds).not.toContain(auditInInaccessible.id)
|
|
578
|
+
})
|
|
579
|
+
|
|
580
|
+
it('should NOT be able to view audit logs from other accounts via filtering', async () => {
|
|
581
|
+
const scenario = await createMultiTenantScenario()
|
|
582
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
583
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
584
|
+
|
|
585
|
+
// Create user and audit log in inaccessible account
|
|
586
|
+
const otherUser = await createUser({
|
|
587
|
+
email: 'other-filter-audit@example.com',
|
|
588
|
+
name: 'Other Filter Audit User',
|
|
589
|
+
})
|
|
590
|
+
await addUserToAccount(otherUser.id, accountWithoutAccess.id, 'ADMIN')
|
|
591
|
+
|
|
592
|
+
const specificEntityId = crypto.randomUUID()
|
|
593
|
+
createAuditLog({
|
|
594
|
+
accountId: accountWithoutAccess.id,
|
|
595
|
+
userId: otherUser.id,
|
|
596
|
+
entity: 'User',
|
|
597
|
+
entityId: specificEntityId,
|
|
598
|
+
action: 'INSERT',
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
// Try to filter by the specific entityId from the inaccessible account
|
|
602
|
+
const res = await app.request(`/api/audits?entityId=${specificEntityId}`, {
|
|
603
|
+
method: 'GET',
|
|
604
|
+
headers: {
|
|
605
|
+
...scenario.headers,
|
|
606
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
607
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
608
|
+
},
|
|
609
|
+
})
|
|
610
|
+
|
|
611
|
+
expect(res.status).toBe(200)
|
|
612
|
+
const body = await res.json()
|
|
613
|
+
|
|
614
|
+
// Should return empty - the audit is in another account
|
|
615
|
+
expect(body.data.length).toBe(0)
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
// ============================================================================
|
|
620
|
+
// ACCOUNT SWITCHING TESTS
|
|
621
|
+
// ============================================================================
|
|
622
|
+
|
|
623
|
+
describe('Account switching', () => {
|
|
624
|
+
it('should enforce different roles in different accounts', async () => {
|
|
625
|
+
const scenario = await createMultiTenantScenario()
|
|
626
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
627
|
+
const accountWithViewerAccess = scenario.accounts.withAccess.find(a => a.role === 'VIEWER')!
|
|
628
|
+
|
|
629
|
+
// Create a target user in both accounts
|
|
630
|
+
const targetUser = await createUser({
|
|
631
|
+
email: 'target-for-switching@example.com',
|
|
632
|
+
name: 'Target For Switching',
|
|
633
|
+
})
|
|
634
|
+
await addUserToAccount(targetUser.id, accountWithAdminAccess.account.id, 'VIEWER')
|
|
635
|
+
await addUserToAccount(targetUser.id, accountWithViewerAccess.account.id, 'VIEWER')
|
|
636
|
+
|
|
637
|
+
// Try to update user in account where user has ADMIN role - should succeed
|
|
638
|
+
const adminRes = await app.request(`/api/users/${targetUser.id}`, {
|
|
639
|
+
method: 'PATCH',
|
|
640
|
+
headers: {
|
|
641
|
+
...scenario.headers,
|
|
642
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
643
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
644
|
+
'Content-Type': 'application/json',
|
|
645
|
+
},
|
|
646
|
+
body: JSON.stringify({ name: 'Admin Updated' }),
|
|
647
|
+
})
|
|
648
|
+
expect(adminRes.status).toBe(200)
|
|
649
|
+
|
|
650
|
+
// Try to update user in account where user has VIEWER role - should fail
|
|
651
|
+
const viewerRes = await app.request(`/api/users/${targetUser.id}`, {
|
|
652
|
+
method: 'PATCH',
|
|
653
|
+
headers: {
|
|
654
|
+
...scenario.headers,
|
|
655
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
656
|
+
'account-id': accountWithViewerAccess.account.id,
|
|
657
|
+
'Content-Type': 'application/json',
|
|
658
|
+
},
|
|
659
|
+
body: JSON.stringify({ name: 'Viewer Updated' }),
|
|
660
|
+
})
|
|
661
|
+
expect(viewerRes.status).toBe(403)
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it('should allow same user to view users in both accounts with correct filtering', async () => {
|
|
665
|
+
const scenario = await createMultiTenantScenario()
|
|
666
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
667
|
+
const accountWithViewerAccess = scenario.accounts.withAccess.find(a => a.role === 'VIEWER')!
|
|
668
|
+
|
|
669
|
+
// Create users unique to each account
|
|
670
|
+
const userInAccount1 = await createUser({
|
|
671
|
+
email: 'unique-to-admin-account@example.com',
|
|
672
|
+
name: 'Unique To Admin Account',
|
|
673
|
+
})
|
|
674
|
+
await addUserToAccount(userInAccount1.id, accountWithAdminAccess.account.id, 'VIEWER')
|
|
675
|
+
|
|
676
|
+
const userInAccount2 = await createUser({
|
|
677
|
+
email: 'unique-to-viewer-account@example.com',
|
|
678
|
+
name: 'Unique To Viewer Account',
|
|
679
|
+
})
|
|
680
|
+
await addUserToAccount(userInAccount2.id, accountWithViewerAccess.account.id, 'VIEWER')
|
|
681
|
+
|
|
682
|
+
// Get users from admin account
|
|
683
|
+
const res1 = await app.request('/api/users', {
|
|
684
|
+
method: 'GET',
|
|
685
|
+
headers: {
|
|
686
|
+
...scenario.headers,
|
|
687
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
688
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
689
|
+
},
|
|
690
|
+
})
|
|
691
|
+
expect(res1.status).toBe(200)
|
|
692
|
+
const body1 = await res1.json()
|
|
693
|
+
const userIds1 = body1.data.map((u: any) => u.id)
|
|
694
|
+
expect(userIds1).toContain(userInAccount1.id)
|
|
695
|
+
expect(userIds1).not.toContain(userInAccount2.id)
|
|
696
|
+
|
|
697
|
+
// Get users from viewer account
|
|
698
|
+
const res2 = await app.request('/api/users', {
|
|
699
|
+
method: 'GET',
|
|
700
|
+
headers: {
|
|
701
|
+
...scenario.headers,
|
|
702
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
703
|
+
'account-id': accountWithViewerAccess.account.id,
|
|
704
|
+
},
|
|
705
|
+
})
|
|
706
|
+
expect(res2.status).toBe(200)
|
|
707
|
+
const body2 = await res2.json()
|
|
708
|
+
const userIds2 = body2.data.map((u: any) => u.id)
|
|
709
|
+
expect(userIds2).toContain(userInAccount2.id)
|
|
710
|
+
expect(userIds2).not.toContain(userInAccount1.id)
|
|
711
|
+
})
|
|
712
|
+
|
|
713
|
+
it('should correctly enforce invitation permissions across accounts', async () => {
|
|
714
|
+
const scenario = await createMultiTenantScenario()
|
|
715
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
716
|
+
const accountWithViewerAccess = scenario.accounts.withAccess.find(a => a.role === 'VIEWER')!
|
|
717
|
+
|
|
718
|
+
// Try to create invitation in admin account - should succeed
|
|
719
|
+
const adminInviteRes = await app.request('/api/invitations', {
|
|
720
|
+
method: 'POST',
|
|
721
|
+
headers: {
|
|
722
|
+
...scenario.headers,
|
|
723
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
724
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
725
|
+
'Content-Type': 'application/json',
|
|
726
|
+
},
|
|
727
|
+
body: JSON.stringify({
|
|
728
|
+
email: 'admin-invite-test@example.com',
|
|
729
|
+
role: 'VIEWER',
|
|
730
|
+
}),
|
|
731
|
+
})
|
|
732
|
+
expect(adminInviteRes.status).toBe(200)
|
|
733
|
+
|
|
734
|
+
// Try to create invitation in viewer account - should fail (VIEWER cannot invite)
|
|
735
|
+
const viewerInviteRes = await app.request('/api/invitations', {
|
|
736
|
+
method: 'POST',
|
|
737
|
+
headers: {
|
|
738
|
+
...scenario.headers,
|
|
739
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
740
|
+
'account-id': accountWithViewerAccess.account.id,
|
|
741
|
+
'Content-Type': 'application/json',
|
|
742
|
+
},
|
|
743
|
+
body: JSON.stringify({
|
|
744
|
+
email: 'viewer-invite-test@example.com',
|
|
745
|
+
role: 'VIEWER',
|
|
746
|
+
}),
|
|
747
|
+
})
|
|
748
|
+
expect(viewerInviteRes.status).toBe(403)
|
|
749
|
+
})
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
// ============================================================================
|
|
753
|
+
// SUPER ADMIN OVERRIDE TESTS
|
|
754
|
+
// ============================================================================
|
|
755
|
+
|
|
756
|
+
describe('Super admin override', () => {
|
|
757
|
+
it('should allow super admin to access any account', async () => {
|
|
758
|
+
// Create a super admin
|
|
759
|
+
const superAdmin = await createSuperAdmin({
|
|
760
|
+
email: 'super-admin-override@example.com',
|
|
761
|
+
name: 'Super Admin Override',
|
|
762
|
+
})
|
|
763
|
+
|
|
764
|
+
// Create an account for the super admin context
|
|
765
|
+
const adminContext = await createAccount({ name: 'Super Admin Context' })
|
|
766
|
+
await addUserToAccount(superAdmin.id, adminContext.id, 'ADMIN')
|
|
767
|
+
|
|
768
|
+
// Create another account with users
|
|
769
|
+
const otherAccount = await createAccount({ name: 'Other Account For Super' })
|
|
770
|
+
const otherUser = await createUser({
|
|
771
|
+
email: 'other-for-super@example.com',
|
|
772
|
+
name: 'Other For Super',
|
|
773
|
+
})
|
|
774
|
+
await addUserToAccount(otherUser.id, otherAccount.id, 'VIEWER')
|
|
775
|
+
|
|
776
|
+
const { headers } = await createUserSession(superAdmin.id, {
|
|
777
|
+
email: superAdmin.email,
|
|
778
|
+
name: superAdmin.name,
|
|
779
|
+
isSuperAdmin: true,
|
|
780
|
+
})
|
|
781
|
+
|
|
782
|
+
// Super admin should be able to see the other account
|
|
783
|
+
const res = await app.request(`/api/accounts/${otherAccount.id}`, {
|
|
784
|
+
method: 'GET',
|
|
785
|
+
headers: {
|
|
786
|
+
...headers,
|
|
787
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
788
|
+
'account-id': adminContext.id,
|
|
789
|
+
},
|
|
790
|
+
})
|
|
791
|
+
|
|
792
|
+
expect(res.status).toBe(200)
|
|
793
|
+
const body = await res.json()
|
|
794
|
+
expect(body.data.id).toBe(otherAccount.id)
|
|
795
|
+
})
|
|
796
|
+
|
|
797
|
+
it('should allow super admin to view all users across all accounts', async () => {
|
|
798
|
+
// Create a super admin
|
|
799
|
+
const superAdmin = await createSuperAdmin({
|
|
800
|
+
email: 'super-admin-all-users@example.com',
|
|
801
|
+
name: 'Super Admin All Users',
|
|
802
|
+
})
|
|
803
|
+
|
|
804
|
+
// Create an account for the super admin context
|
|
805
|
+
const adminContext = await createAccount({ name: 'Super Admin Users Context' })
|
|
806
|
+
await addUserToAccount(superAdmin.id, adminContext.id, 'ADMIN')
|
|
807
|
+
|
|
808
|
+
// Create multiple accounts with users
|
|
809
|
+
const account1 = await createAccount({ name: 'Account 1 For Super' })
|
|
810
|
+
const user1 = await createUser({
|
|
811
|
+
email: 'user1-for-super@example.com',
|
|
812
|
+
name: 'User 1 For Super',
|
|
813
|
+
})
|
|
814
|
+
await addUserToAccount(user1.id, account1.id, 'VIEWER')
|
|
815
|
+
|
|
816
|
+
const account2 = await createAccount({ name: 'Account 2 For Super' })
|
|
817
|
+
const user2 = await createUser({
|
|
818
|
+
email: 'user2-for-super@example.com',
|
|
819
|
+
name: 'User 2 For Super',
|
|
820
|
+
})
|
|
821
|
+
await addUserToAccount(user2.id, account2.id, 'VIEWER')
|
|
822
|
+
|
|
823
|
+
const { headers } = await createUserSession(superAdmin.id, {
|
|
824
|
+
email: superAdmin.email,
|
|
825
|
+
name: superAdmin.name,
|
|
826
|
+
isSuperAdmin: true,
|
|
827
|
+
})
|
|
828
|
+
|
|
829
|
+
// Super admin can see user in account1
|
|
830
|
+
const res1 = await app.request(`/api/users/${user1.id}`, {
|
|
831
|
+
method: 'GET',
|
|
832
|
+
headers: {
|
|
833
|
+
...headers,
|
|
834
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
835
|
+
'account-id': adminContext.id,
|
|
836
|
+
},
|
|
837
|
+
})
|
|
838
|
+
expect(res1.status).toBe(200)
|
|
839
|
+
|
|
840
|
+
// Super admin can see user in account2
|
|
841
|
+
const res2 = await app.request(`/api/users/${user2.id}`, {
|
|
842
|
+
method: 'GET',
|
|
843
|
+
headers: {
|
|
844
|
+
...headers,
|
|
845
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
846
|
+
'account-id': adminContext.id,
|
|
847
|
+
},
|
|
848
|
+
})
|
|
849
|
+
expect(res2.status).toBe(200)
|
|
850
|
+
})
|
|
851
|
+
|
|
852
|
+
it('should allow super admin to modify any account', async () => {
|
|
853
|
+
// Create a super admin
|
|
854
|
+
const superAdmin = await createSuperAdmin({
|
|
855
|
+
email: 'super-admin-modify@example.com',
|
|
856
|
+
name: 'Super Admin Modify',
|
|
857
|
+
})
|
|
858
|
+
|
|
859
|
+
// Create an account for the super admin context
|
|
860
|
+
const adminContext = await createAccount({ name: 'Super Admin Modify Context' })
|
|
861
|
+
await addUserToAccount(superAdmin.id, adminContext.id, 'ADMIN')
|
|
862
|
+
|
|
863
|
+
// Create another account to modify
|
|
864
|
+
const targetAccount = await createAccount({
|
|
865
|
+
name: 'Target Account For Super',
|
|
866
|
+
description: 'Original Description',
|
|
867
|
+
})
|
|
868
|
+
|
|
869
|
+
const { headers } = await createUserSession(superAdmin.id, {
|
|
870
|
+
email: superAdmin.email,
|
|
871
|
+
name: superAdmin.name,
|
|
872
|
+
isSuperAdmin: true,
|
|
873
|
+
})
|
|
874
|
+
|
|
875
|
+
// Super admin should be able to update the account
|
|
876
|
+
const res = await app.request(`/api/accounts/${targetAccount.id}`, {
|
|
877
|
+
method: 'PATCH',
|
|
878
|
+
headers: {
|
|
879
|
+
...headers,
|
|
880
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
881
|
+
'account-id': adminContext.id,
|
|
882
|
+
'Content-Type': 'application/json',
|
|
883
|
+
},
|
|
884
|
+
body: JSON.stringify({
|
|
885
|
+
name: 'Super Admin Updated Name',
|
|
886
|
+
description: 'Super Admin Updated Description',
|
|
887
|
+
}),
|
|
888
|
+
})
|
|
889
|
+
|
|
890
|
+
expect(res.status).toBe(200)
|
|
891
|
+
const body = await res.json()
|
|
892
|
+
expect(body.data.name).toBe('Super Admin Updated Name')
|
|
893
|
+
expect(body.data.description).toBe('Super Admin Updated Description')
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
it('should allow super admin to view audit logs across all accounts', async () => {
|
|
897
|
+
// Create a super admin
|
|
898
|
+
const superAdmin = await createSuperAdmin({
|
|
899
|
+
email: 'super-admin-audits@example.com',
|
|
900
|
+
name: 'Super Admin Audits',
|
|
901
|
+
})
|
|
902
|
+
|
|
903
|
+
// Create an account for the super admin context
|
|
904
|
+
const adminContext = await createAccount({ name: 'Super Admin Audits Context' })
|
|
905
|
+
await addUserToAccount(superAdmin.id, adminContext.id, 'ADMIN')
|
|
906
|
+
|
|
907
|
+
// Create audit logs in different accounts
|
|
908
|
+
const account1 = await createAccount({ name: 'Audit Account 1' })
|
|
909
|
+
const user1 = await createUser({
|
|
910
|
+
email: 'audit-user1@example.com',
|
|
911
|
+
name: 'Audit User 1',
|
|
912
|
+
})
|
|
913
|
+
await addUserToAccount(user1.id, account1.id, 'ADMIN')
|
|
914
|
+
|
|
915
|
+
const audit1 = createAuditLog({
|
|
916
|
+
accountId: account1.id,
|
|
917
|
+
userId: user1.id,
|
|
918
|
+
entity: 'User',
|
|
919
|
+
entityId: crypto.randomUUID(),
|
|
920
|
+
action: 'INSERT',
|
|
921
|
+
changes: { name: 'Audit 1' },
|
|
922
|
+
})
|
|
923
|
+
|
|
924
|
+
const account2 = await createAccount({ name: 'Audit Account 2' })
|
|
925
|
+
const user2 = await createUser({
|
|
926
|
+
email: 'audit-user2@example.com',
|
|
927
|
+
name: 'Audit User 2',
|
|
928
|
+
})
|
|
929
|
+
await addUserToAccount(user2.id, account2.id, 'ADMIN')
|
|
930
|
+
|
|
931
|
+
const audit2 = createAuditLog({
|
|
932
|
+
accountId: account2.id,
|
|
933
|
+
userId: user2.id,
|
|
934
|
+
entity: 'User',
|
|
935
|
+
entityId: crypto.randomUUID(),
|
|
936
|
+
action: 'INSERT',
|
|
937
|
+
changes: { name: 'Audit 2' },
|
|
938
|
+
})
|
|
939
|
+
|
|
940
|
+
const { headers } = await createUserSession(superAdmin.id, {
|
|
941
|
+
email: superAdmin.email,
|
|
942
|
+
name: superAdmin.name,
|
|
943
|
+
isSuperAdmin: true,
|
|
944
|
+
})
|
|
945
|
+
|
|
946
|
+
// Super admin should see all audit logs
|
|
947
|
+
const res = await app.request('/api/audits', {
|
|
948
|
+
method: 'GET',
|
|
949
|
+
headers: {
|
|
950
|
+
...headers,
|
|
951
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
952
|
+
'account-id': adminContext.id,
|
|
953
|
+
},
|
|
954
|
+
})
|
|
955
|
+
|
|
956
|
+
expect(res.status).toBe(200)
|
|
957
|
+
const body = await res.json()
|
|
958
|
+
const auditIds = body.data.map((a: any) => a.id)
|
|
959
|
+
|
|
960
|
+
// Should include audits from both accounts
|
|
961
|
+
expect(auditIds).toContain(audit1.id)
|
|
962
|
+
expect(auditIds).toContain(audit2.id)
|
|
963
|
+
})
|
|
964
|
+
|
|
965
|
+
it('should allow super admin to delete any account', async () => {
|
|
966
|
+
// Create a super admin
|
|
967
|
+
const superAdmin = await createSuperAdmin({
|
|
968
|
+
email: 'super-admin-delete@example.com',
|
|
969
|
+
name: 'Super Admin Delete',
|
|
970
|
+
})
|
|
971
|
+
|
|
972
|
+
// Create an account for the super admin context
|
|
973
|
+
const adminContext = await createAccount({ name: 'Super Admin Delete Context' })
|
|
974
|
+
await addUserToAccount(superAdmin.id, adminContext.id, 'ADMIN')
|
|
975
|
+
|
|
976
|
+
// Create another account to delete
|
|
977
|
+
const targetAccount = await createAccount({ name: 'Account To Be Deleted By Super' })
|
|
978
|
+
|
|
979
|
+
const { headers } = await createUserSession(superAdmin.id, {
|
|
980
|
+
email: superAdmin.email,
|
|
981
|
+
name: superAdmin.name,
|
|
982
|
+
isSuperAdmin: true,
|
|
983
|
+
})
|
|
984
|
+
|
|
985
|
+
// Super admin should be able to delete the account
|
|
986
|
+
const res = await app.request(`/api/accounts/${targetAccount.id}`, {
|
|
987
|
+
method: 'DELETE',
|
|
988
|
+
headers: {
|
|
989
|
+
...headers,
|
|
990
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
991
|
+
'account-id': adminContext.id,
|
|
992
|
+
},
|
|
993
|
+
})
|
|
994
|
+
|
|
995
|
+
expect(res.status).toBe(204)
|
|
996
|
+
|
|
997
|
+
// Verify the account was soft-deleted
|
|
998
|
+
const sqlite = getSqlite()
|
|
999
|
+
const row = sqlite.prepare('SELECT deleted_at FROM accounts WHERE id = ?').get(targetAccount.id) as { deleted_at: string | null }
|
|
1000
|
+
expect(row.deleted_at).not.toBeNull()
|
|
1001
|
+
})
|
|
1002
|
+
})
|
|
1003
|
+
|
|
1004
|
+
// ============================================================================
|
|
1005
|
+
// CROSS-TENANT DATA LEAKAGE PREVENTION
|
|
1006
|
+
// ============================================================================
|
|
1007
|
+
|
|
1008
|
+
describe('Cross-tenant data leakage prevention', () => {
|
|
1009
|
+
it('should not leak user existence across accounts via response timing', async () => {
|
|
1010
|
+
const scenario = await createMultiTenantScenario()
|
|
1011
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
1012
|
+
|
|
1013
|
+
// Create a user in another account
|
|
1014
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
1015
|
+
const existingUser = await createUser({
|
|
1016
|
+
email: 'timing-test-user@example.com',
|
|
1017
|
+
name: 'Timing Test User',
|
|
1018
|
+
})
|
|
1019
|
+
await addUserToAccount(existingUser.id, accountWithoutAccess.id, 'VIEWER')
|
|
1020
|
+
|
|
1021
|
+
// Non-existent user ID
|
|
1022
|
+
const nonExistentId = crypto.randomUUID()
|
|
1023
|
+
|
|
1024
|
+
// Both should return 404 (same response for security)
|
|
1025
|
+
const resExisting = await app.request(`/api/users/${existingUser.id}`, {
|
|
1026
|
+
method: 'GET',
|
|
1027
|
+
headers: {
|
|
1028
|
+
...scenario.headers,
|
|
1029
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
1030
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
1031
|
+
},
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
const resNonExistent = await app.request(`/api/users/${nonExistentId}`, {
|
|
1035
|
+
method: 'GET',
|
|
1036
|
+
headers: {
|
|
1037
|
+
...scenario.headers,
|
|
1038
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
1039
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
1040
|
+
},
|
|
1041
|
+
})
|
|
1042
|
+
|
|
1043
|
+
// Both should return the same status code (404)
|
|
1044
|
+
expect(resExisting.status).toBe(404)
|
|
1045
|
+
expect(resNonExistent.status).toBe(404)
|
|
1046
|
+
})
|
|
1047
|
+
|
|
1048
|
+
it('should not leak account existence across tenants via response timing', async () => {
|
|
1049
|
+
const scenario = await createMultiTenantScenario()
|
|
1050
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
1051
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
1052
|
+
|
|
1053
|
+
// Non-existent account ID
|
|
1054
|
+
const nonExistentId = crypto.randomUUID()
|
|
1055
|
+
|
|
1056
|
+
// Both should return 404
|
|
1057
|
+
const resExisting = await app.request(`/api/accounts/${accountWithoutAccess.id}`, {
|
|
1058
|
+
method: 'GET',
|
|
1059
|
+
headers: {
|
|
1060
|
+
...scenario.headers,
|
|
1061
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
1062
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
1063
|
+
},
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
const resNonExistent = await app.request(`/api/accounts/${nonExistentId}`, {
|
|
1067
|
+
method: 'GET',
|
|
1068
|
+
headers: {
|
|
1069
|
+
...scenario.headers,
|
|
1070
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
1071
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
1072
|
+
},
|
|
1073
|
+
})
|
|
1074
|
+
|
|
1075
|
+
// Both should return the same status code (404)
|
|
1076
|
+
expect(resExisting.status).toBe(404)
|
|
1077
|
+
expect(resNonExistent.status).toBe(404)
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
it('should prevent enumeration attacks on user IDs', async () => {
|
|
1081
|
+
const scenario = await createMultiTenantScenario()
|
|
1082
|
+
const accountWithAdminAccess = scenario.accounts.withAccess.find(a => a.role === 'ADMIN')!
|
|
1083
|
+
|
|
1084
|
+
// Create multiple users in another account
|
|
1085
|
+
const accountWithoutAccess = scenario.accounts.withoutAccess[0]
|
|
1086
|
+
const users = await Promise.all(
|
|
1087
|
+
Array.from({ length: 3 }, async (_, i) => {
|
|
1088
|
+
const user = await createUser({
|
|
1089
|
+
email: `enum-test-${i}@example.com`,
|
|
1090
|
+
name: `Enum Test User ${i}`,
|
|
1091
|
+
})
|
|
1092
|
+
await addUserToAccount(user.id, accountWithoutAccess.id, 'VIEWER')
|
|
1093
|
+
return user
|
|
1094
|
+
})
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
// Try to enumerate users - all should return 404
|
|
1098
|
+
for (const user of users) {
|
|
1099
|
+
const res = await app.request(`/api/users/${user.id}`, {
|
|
1100
|
+
method: 'GET',
|
|
1101
|
+
headers: {
|
|
1102
|
+
...scenario.headers,
|
|
1103
|
+
'User-Agent': 'IntegrationTest/1.0',
|
|
1104
|
+
'account-id': accountWithAdminAccess.account.id,
|
|
1105
|
+
},
|
|
1106
|
+
})
|
|
1107
|
+
|
|
1108
|
+
expect(res.status).toBe(404)
|
|
1109
|
+
}
|
|
1110
|
+
})
|
|
1111
|
+
})
|
|
1112
|
+
})
|