ui-syncup 0.3.13 → 0.4.0-beta.2
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/.agents/skills/ai-spec-workflow/SKILL.md +58 -0
- package/.agents/skills/ai-spec-workflow/references/AI_SPECIFICATION_WORKFLOW.md +1434 -0
- package/.agents/skills/ai-spec-workflow/references/templates/design-template.md +729 -0
- package/.agents/skills/ai-spec-workflow/references/templates/requirements-template.md +179 -0
- package/.agents/skills/ai-spec-workflow/references/templates/tasks-template.md +501 -0
- package/.agents/skills/animation-designer/SKILL.md +688 -0
- package/.agents/skills/animation-designer/manifest.yaml +44 -0
- package/.agents/skills/brainstorming/SKILL.md +54 -0
- package/.agents/skills/contract-driven-ui/SKILL.md +270 -0
- package/.agents/skills/dispatching-parallel-agents/SKILL.md +180 -0
- package/.agents/skills/executing-plans/SKILL.md +76 -0
- package/.agents/skills/executing-specs/SKILL.md +53 -0
- package/.agents/skills/finishing-a-development-branch/SKILL.md +200 -0
- package/.agents/skills/github-workflow-automation/SKILL.md +846 -0
- package/.agents/skills/react-best-practices/AGENTS.md +2249 -0
- package/.agents/skills/react-best-practices/README.md +123 -0
- package/.agents/skills/react-best-practices/SKILL.md +121 -0
- package/.agents/skills/react-best-practices/metadata.json +15 -0
- package/.agents/skills/react-best-practices/rules/_sections.md +46 -0
- package/.agents/skills/react-best-practices/rules/_template.md +28 -0
- package/.agents/skills/react-best-practices/rules/advanced-event-handler-refs.md +55 -0
- package/.agents/skills/react-best-practices/rules/advanced-use-latest.md +49 -0
- package/.agents/skills/react-best-practices/rules/async-api-routes.md +38 -0
- package/.agents/skills/react-best-practices/rules/async-defer-await.md +80 -0
- package/.agents/skills/react-best-practices/rules/async-dependencies.md +36 -0
- package/.agents/skills/react-best-practices/rules/async-parallel.md +28 -0
- package/.agents/skills/react-best-practices/rules/async-suspense-boundaries.md +99 -0
- package/.agents/skills/react-best-practices/rules/bundle-barrel-imports.md +59 -0
- package/.agents/skills/react-best-practices/rules/bundle-conditional.md +31 -0
- package/.agents/skills/react-best-practices/rules/bundle-defer-third-party.md +49 -0
- package/.agents/skills/react-best-practices/rules/bundle-dynamic-imports.md +35 -0
- package/.agents/skills/react-best-practices/rules/bundle-preload.md +50 -0
- package/.agents/skills/react-best-practices/rules/client-event-listeners.md +74 -0
- package/.agents/skills/react-best-practices/rules/client-swr-dedup.md +56 -0
- package/.agents/skills/react-best-practices/rules/js-batch-dom-css.md +82 -0
- package/.agents/skills/react-best-practices/rules/js-cache-function-results.md +80 -0
- package/.agents/skills/react-best-practices/rules/js-cache-property-access.md +28 -0
- package/.agents/skills/react-best-practices/rules/js-cache-storage.md +70 -0
- package/.agents/skills/react-best-practices/rules/js-combine-iterations.md +32 -0
- package/.agents/skills/react-best-practices/rules/js-early-exit.md +50 -0
- package/.agents/skills/react-best-practices/rules/js-hoist-regexp.md +45 -0
- package/.agents/skills/react-best-practices/rules/js-index-maps.md +37 -0
- package/.agents/skills/react-best-practices/rules/js-length-check-first.md +49 -0
- package/.agents/skills/react-best-practices/rules/js-min-max-loop.md +82 -0
- package/.agents/skills/react-best-practices/rules/js-set-map-lookups.md +24 -0
- package/.agents/skills/react-best-practices/rules/js-tosorted-immutable.md +57 -0
- package/.agents/skills/react-best-practices/rules/rendering-activity.md +26 -0
- package/.agents/skills/react-best-practices/rules/rendering-animate-svg-wrapper.md +47 -0
- package/.agents/skills/react-best-practices/rules/rendering-conditional-render.md +40 -0
- package/.agents/skills/react-best-practices/rules/rendering-content-visibility.md +38 -0
- package/.agents/skills/react-best-practices/rules/rendering-hoist-jsx.md +46 -0
- package/.agents/skills/react-best-practices/rules/rendering-hydration-no-flicker.md +82 -0
- package/.agents/skills/react-best-practices/rules/rendering-svg-precision.md +28 -0
- package/.agents/skills/react-best-practices/rules/rerender-defer-reads.md +39 -0
- package/.agents/skills/react-best-practices/rules/rerender-dependencies.md +45 -0
- package/.agents/skills/react-best-practices/rules/rerender-derived-state.md +29 -0
- package/.agents/skills/react-best-practices/rules/rerender-functional-setstate.md +74 -0
- package/.agents/skills/react-best-practices/rules/rerender-lazy-state-init.md +58 -0
- package/.agents/skills/react-best-practices/rules/rerender-memo.md +44 -0
- package/.agents/skills/react-best-practices/rules/rerender-transitions.md +40 -0
- package/.agents/skills/react-best-practices/rules/server-after-nonblocking.md +73 -0
- package/.agents/skills/react-best-practices/rules/server-cache-lru.md +41 -0
- package/.agents/skills/react-best-practices/rules/server-cache-react.md +26 -0
- package/.agents/skills/react-best-practices/rules/server-parallel-fetching.md +79 -0
- package/.agents/skills/react-best-practices/rules/server-serialization.md +38 -0
- package/.agents/skills/react-ui-patterns/SKILL.md +289 -0
- package/.agents/skills/receiving-code-review/SKILL.md +213 -0
- package/.agents/skills/requesting-code-review/SKILL.md +105 -0
- package/.agents/skills/requesting-code-review/code-reviewer.md +146 -0
- package/.agents/skills/reviewing-code/SKILL.md +28 -0
- package/.agents/skills/shadcn/SKILL.md +240 -0
- package/.agents/skills/shadcn/agents/openai.yml +5 -0
- package/.agents/skills/shadcn/assets/shadcn-small.png +0 -0
- package/.agents/skills/shadcn/assets/shadcn.png +0 -0
- package/.agents/skills/shadcn/cli.md +255 -0
- package/.agents/skills/shadcn/customization.md +202 -0
- package/.agents/skills/shadcn/evals/evals.json +47 -0
- package/.agents/skills/shadcn/mcp.md +94 -0
- package/.agents/skills/shadcn/rules/base-vs-radix.md +306 -0
- package/.agents/skills/shadcn/rules/composition.md +195 -0
- package/.agents/skills/shadcn/rules/forms.md +192 -0
- package/.agents/skills/shadcn/rules/icons.md +101 -0
- package/.agents/skills/shadcn/rules/styling.md +162 -0
- package/.agents/skills/steering-creation/SKILL.md +221 -0
- package/.agents/skills/steering-creation/references/STEERING_CREATION_INSTRUCTION.md +850 -0
- package/.agents/skills/subagent-driven-development/SKILL.md +240 -0
- package/.agents/skills/subagent-driven-development/code-quality-reviewer-prompt.md +20 -0
- package/.agents/skills/subagent-driven-development/implementer-prompt.md +78 -0
- package/.agents/skills/subagent-driven-development/spec-reviewer-prompt.md +61 -0
- package/.agents/skills/systematic-debugging/CREATION-LOG.md +119 -0
- package/.agents/skills/systematic-debugging/SKILL.md +296 -0
- package/.agents/skills/systematic-debugging/condition-based-waiting-example.ts +158 -0
- package/.agents/skills/systematic-debugging/condition-based-waiting.md +115 -0
- package/.agents/skills/systematic-debugging/defense-in-depth.md +122 -0
- package/.agents/skills/systematic-debugging/find-polluter.sh +63 -0
- package/.agents/skills/systematic-debugging/root-cause-tracing.md +169 -0
- package/.agents/skills/systematic-debugging/test-academic.md +14 -0
- package/.agents/skills/systematic-debugging/test-pressure-1.md +58 -0
- package/.agents/skills/systematic-debugging/test-pressure-2.md +68 -0
- package/.agents/skills/systematic-debugging/test-pressure-3.md +69 -0
- package/.agents/skills/test-driven-development/SKILL.md +371 -0
- package/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/.agents/skills/using-git-worktrees/SKILL.md +217 -0
- package/.agents/skills/using-superpowers/SKILL.md +87 -0
- package/.agents/skills/verification-before-completion/SKILL.md +139 -0
- package/.agents/skills/web-design-guidelines/SKILL.md +36 -0
- package/.agents/skills/writing-plans/SKILL.md +116 -0
- package/.agents/skills/writing-skills/SKILL.md +655 -0
- package/.agents/skills/writing-skills/anthropic-best-practices.md +1150 -0
- package/.agents/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/.agents/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/.agents/skills/writing-skills/persuasion-principles.md +187 -0
- package/.agents/skills/writing-skills/render-graphs.js +168 -0
- package/.agents/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/.ai/steering/product.md +51 -0
- package/.ai/steering/structure.md +275 -0
- package/.ai/steering/tech.md +188 -0
- package/.claude/agents/database-architect.md +96 -0
- package/.claude/agents/deployment-pipeline-architect.md +122 -0
- package/.claude/agents/nextjs-expert.md +69 -0
- package/.claude/agents/ui-design-expert.md +106 -0
- package/.claudeignore +69 -0
- package/.dockerignore +8 -0
- package/.env.development +86 -0
- package/.env.example +171 -0
- package/.env.production +139 -0
- package/.env.test +58 -0
- package/.gitattributes +2 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +20 -0
- package/.github/PULL_REQUEST_TEMPLATE.md +23 -0
- package/.github/workflows/ci.yml +64 -0
- package/.github/workflows/release.yml +174 -0
- package/.nvmrc +1 -0
- package/.releaserc.json +18 -0
- package/.vercelignore +73 -0
- package/AGENTS.md +544 -0
- package/CHANGELOG.md +69 -0
- package/CODE_OF_CONDUCT.md +21 -0
- package/CONTRIBUTING.md +32 -0
- package/Dockerfile +84 -0
- package/LICENSE +21 -0
- package/README.md +328 -59
- package/SECURITY.md +16 -0
- package/bun.lock +3853 -0
- package/cli/README.md +94 -0
- package/cli/bun.lock +306 -0
- package/cli/index.ts +96 -0
- package/cli/package-lock.json +2157 -0
- package/cli/package.json +30 -0
- package/cli/src/commands/backup.ts +78 -0
- package/cli/src/commands/doctor.ts +82 -0
- package/cli/src/commands/init.ts +234 -0
- package/cli/src/commands/logs.ts +26 -0
- package/cli/src/commands/open.ts +23 -0
- package/cli/src/commands/remove.ts +44 -0
- package/cli/src/commands/restart.ts +21 -0
- package/cli/src/commands/restore.ts +90 -0
- package/cli/src/commands/start.ts +26 -0
- package/cli/src/commands/status.ts +25 -0
- package/cli/src/commands/stop.ts +20 -0
- package/cli/src/commands/upgrade.ts +28 -0
- package/cli/src/lib/docker.ts +40 -0
- package/cli/src/lib/env.ts +42 -0
- package/cli/src/lib/ui.ts +43 -0
- package/cli/tsconfig.json +13 -0
- package/cli/tsup.config.ts +12 -0
- package/components.json +24 -0
- package/docker/README.md +430 -0
- package/docker/compose.dev-minio.yml +30 -0
- package/docker/compose.dev.yml +39 -0
- package/docker/compose.local.yml +84 -0
- package/docker/compose.yml +153 -0
- package/docs/VERSIONING.md +101 -0
- package/docs/database/DRIZZLE_COMMANDS_EXPLAINED.md +1779 -0
- package/docs/database/DRIZZLE_ZOD_POSTGRESQL_INSTRUCTION.md +646 -0
- package/docs/database/MIGRATION_BEST_PRACTICES.md +601 -0
- package/docs/database/MIGRATION_ROLLBACK.md +1080 -0
- package/docs/database/MIGRATION_SYSTEM.md +165 -0
- package/docs/database/MIGRATION_TROUBLESHOOTING.md +881 -0
- package/docs/development/ENVIRONMENT_CONFIG.md +896 -0
- package/docs/development/LOCAL_DEVELOPMENT.md +456 -0
- package/docs/development/REMOTE_DATABASE_SETUP.md +786 -0
- package/docs/development/STORAGE_SETUP.md +207 -0
- package/docs/development/SUPABASE_LOCAL_SETUP.md +178 -0
- package/docs/development/TESTING.md +714 -0
- package/docs/feature-architectures/LOADING_ARCHITECTURE.md +343 -0
- package/docs/feature-architectures/NOTIFICATION_ARCHITECTURE.md +858 -0
- package/docs/feature-architectures/RATE_LIMIT_RESET.md +147 -0
- package/docs/feature-architectures/RBAC.md +1132 -0
- package/docs/feature-architectures/RESOURCE_LIMITS.md +69 -0
- package/docs/feature-architectures/SECURITY.md +284 -0
- package/docs/feature-architectures/WORKSPACES.md +278 -0
- package/docs/plans/admin-setup-wizard-routing-plan.md +623 -0
- package/drizzle/0000_purple_wilson_fisk.sql +360 -0
- package/drizzle/0001_drop_instance_public_url.sql +1 -0
- package/drizzle/meta/0000_snapshot.json +3118 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +13 -0
- package/eslint.config.mjs +44 -0
- package/install.sh +180 -0
- package/next.config.ts +91 -0
- package/package.json +128 -22
- package/playwright.config.ts +70 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/logo.svg +11 -0
- package/public/next.svg +1 -0
- package/public/playground/CPM-101/as-is-image.jpg +0 -0
- package/public/playground/CPM-101/to-be-image.jpg +0 -0
- package/public/playground/TEST-1/LinkedIn-skeleton-screen.png +0 -0
- package/public/playground/TEST-1/https___dev-to-uploads.s3.amazonaws.com_uploads_articles_vuahe90ka1mkx9aepmea.webp +0 -0
- package/public/playground/TEST-1/linkedin_skeletonscreen.jpg +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/scripts/__tests__/migrate.integration.test.ts +642 -0
- package/scripts/__tests__/migrate.property.test.ts +1714 -0
- package/scripts/__tests__/migrate.test.ts +536 -0
- package/scripts/admin-reset-password.ts +114 -0
- package/scripts/check-email-queue.ts +99 -0
- package/scripts/check-sessions.ts +50 -0
- package/scripts/db-pull-data.sh +73 -0
- package/scripts/force-verify-email.sh +13 -0
- package/scripts/migrate.ts +693 -0
- package/scripts/process-email-queue.ts +26 -0
- package/scripts/reset-db.ts +47 -0
- package/scripts/reset-rate-limit.sh +26 -0
- package/scripts/reset-remote-db.sql +31 -0
- package/scripts/retry-failed-emails.ts +67 -0
- package/scripts/seed.ts +605 -0
- package/scripts/setup-monitoring.sh +440 -0
- package/scripts/sync-migration-tracking.ts +113 -0
- package/scripts/test-ci-error-handling.sh +237 -0
- package/scripts/test-ci-workflow.sh +200 -0
- package/scripts/test-migration.sh +151 -0
- package/scripts/validate-env.ts +25 -0
- package/scripts/validate-migration-system.ts +566 -0
- package/scripts/verify-ci-status-reporting.sh +206 -0
- package/scripts/verify-user-email.sql +22 -0
- package/scripts/verify-vercel-integration.ts +292 -0
- package/seed_data.md +54 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/error.tsx +89 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/loading.tsx +101 -0
- package/src/app/(protected)/(team)/(routes)/[projectSlug]/page.tsx +91 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/README.md +192 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/error.tsx +58 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/loading.tsx +14 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/not-found.tsx +47 -0
- package/src/app/(protected)/(team)/(routes)/issue/[issueKey]/page.tsx +91 -0
- package/src/app/(protected)/(team)/projects/page.tsx +16 -0
- package/src/app/(protected)/(team)/team/settings/(section)/instance/page.tsx +52 -0
- package/src/app/(protected)/(team)/team/settings/(section)/integrations/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/(section)/integrations/page.tsx +23 -0
- package/src/app/(protected)/(team)/team/settings/(section)/members/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/(section)/members/page.tsx +35 -0
- package/src/app/(protected)/(team)/team/settings/layout.tsx +72 -0
- package/src/app/(protected)/(team)/team/settings/loading.tsx +5 -0
- package/src/app/(protected)/(team)/team/settings/page.tsx +71 -0
- package/src/app/(protected)/dev/auth/README.md +151 -0
- package/src/app/(protected)/dev/auth/page.tsx +590 -0
- package/src/app/(protected)/layout.test.tsx +209 -0
- package/src/app/(protected)/layout.tsx +28 -0
- package/src/app/(protected)/onboarding/page.tsx +27 -0
- package/src/app/(protected)/settings/integrations/page.tsx +23 -0
- package/src/app/(protected)/settings/layout.tsx +26 -0
- package/src/app/(protected)/settings/notifications/page.tsx +26 -0
- package/src/app/(protected)/settings/other/page.tsx +23 -0
- package/src/app/(protected)/settings/page.tsx +23 -0
- package/src/app/(protected)/settings/preferences/page.tsx +23 -0
- package/src/app/(protected)/settings/security/page.tsx +37 -0
- package/src/app/(public)/forgot-password/page.tsx +20 -0
- package/src/app/(public)/invite/project/[token]/error.tsx +50 -0
- package/src/app/(public)/invite/project/[token]/loading.tsx +39 -0
- package/src/app/(public)/invite/project/[token]/page.tsx +156 -0
- package/src/app/(public)/layout.tsx +9 -0
- package/src/app/(public)/privacy-policy/page.tsx +12 -0
- package/src/app/(public)/reset-password/page.tsx +37 -0
- package/src/app/(public)/setup/__tests__/page.test.tsx +30 -0
- package/src/app/(public)/setup/page.tsx +17 -0
- package/src/app/(public)/share/issue/[token]/page.tsx +51 -0
- package/src/app/(public)/sign-in/page.tsx +55 -0
- package/src/app/(public)/sign-up/page.tsx +23 -0
- package/src/app/(public)/verify-email/page.tsx +22 -0
- package/src/app/(public)/verify-email-confirm/page.tsx +40 -0
- package/src/app/api/auth/[...all]/route.ts +6 -0
- package/src/app/api/auth/delete-account/route.ts +134 -0
- package/src/app/api/auth/dev/force-verify/route.ts +180 -0
- package/src/app/api/auth/dev/reset-rate-limit/route.ts +144 -0
- package/src/app/api/auth/dev/sessions/route.ts +172 -0
- package/src/app/api/auth/forgot-password/__tests__/forgot-password.property.test.ts +397 -0
- package/src/app/api/auth/forgot-password/route.ts +277 -0
- package/src/app/api/auth/logout/route.ts +115 -0
- package/src/app/api/auth/me/route.ts +123 -0
- package/src/app/api/auth/providers/__tests__/route.test.ts +236 -0
- package/src/app/api/auth/providers/route.ts +119 -0
- package/src/app/api/auth/resend-verification/route.ts +262 -0
- package/src/app/api/auth/reset-password/__tests__/reset-password.property.test.ts +493 -0
- package/src/app/api/auth/reset-password/__tests__/route.test.ts +284 -0
- package/src/app/api/auth/reset-password/route.ts +251 -0
- package/src/app/api/auth/verify-email/route.ts +232 -0
- package/src/app/api/example-cors/route.ts +61 -0
- package/src/app/api/health/route.ts +14 -0
- package/src/app/api/invite/project/[token]/__tests__/accept-invitation.integration.test.ts +348 -0
- package/src/app/api/invite/project/[token]/decline/route.ts +99 -0
- package/src/app/api/invite/project/[token]/route.ts +269 -0
- package/src/app/api/issues/[issueId]/activities/route.ts +213 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/comments/[commentId]/route.ts +486 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/comments/route.ts +283 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/read/route.ts +242 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/[annotationId]/route.ts +534 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/annotations/route.ts +514 -0
- package/src/app/api/issues/[issueId]/attachments/[attachmentId]/route.ts +161 -0
- package/src/app/api/issues/[issueId]/attachments/route.ts +376 -0
- package/src/app/api/issues/[issueId]/route.ts +516 -0
- package/src/app/api/notifications/[id]/read/route.ts +131 -0
- package/src/app/api/notifications/__tests__/notifications.integration.test.ts +350 -0
- package/src/app/api/notifications/read-all/route.ts +72 -0
- package/src/app/api/notifications/route.ts +148 -0
- package/src/app/api/notifications/unread-count/route.ts +77 -0
- package/src/app/api/projects/[id]/activities/route.ts +174 -0
- package/src/app/api/projects/[id]/invitations/[invitationId]/resend/route.ts +99 -0
- package/src/app/api/projects/[id]/invitations/[invitationId]/route.ts +96 -0
- package/src/app/api/projects/[id]/invitations/route.ts +254 -0
- package/src/app/api/projects/[id]/issues/route.ts +452 -0
- package/src/app/api/projects/[id]/join/route.ts +207 -0
- package/src/app/api/projects/[id]/members/[memberId]/route.ts +364 -0
- package/src/app/api/projects/[id]/members/me/route.ts +121 -0
- package/src/app/api/projects/[id]/members/route.ts +129 -0
- package/src/app/api/projects/[id]/route.ts +476 -0
- package/src/app/api/projects/route.ts +394 -0
- package/src/app/api/setup/admin/route.ts +255 -0
- package/src/app/api/setup/complete/__tests__/route.test.ts +60 -0
- package/src/app/api/setup/complete/route.ts +244 -0
- package/src/app/api/setup/config/route.ts +195 -0
- package/src/app/api/setup/export/route.ts +111 -0
- package/src/app/api/setup/health/route.ts +74 -0
- package/src/app/api/setup/import/route.ts +154 -0
- package/src/app/api/setup/status/route.ts +82 -0
- package/src/app/api/setup/workspace/route.ts +252 -0
- package/src/app/api/teams/[teamId]/export/route.ts +115 -0
- package/src/app/api/teams/[teamId]/invitations/[invitationId]/resend/route.ts +132 -0
- package/src/app/api/teams/[teamId]/invitations/[invitationId]/route.ts +117 -0
- package/src/app/api/teams/[teamId]/invitations/route.ts +363 -0
- package/src/app/api/teams/[teamId]/members/[userId]/route.ts +335 -0
- package/src/app/api/teams/[teamId]/members/route.ts +184 -0
- package/src/app/api/teams/[teamId]/members/search/route.ts +202 -0
- package/src/app/api/teams/[teamId]/route.ts +423 -0
- package/src/app/api/teams/[teamId]/switch/route.ts +140 -0
- package/src/app/api/teams/[teamId]/transfer-ownership/route.ts +212 -0
- package/src/app/api/teams/invitations/[token]/accept/route.ts +140 -0
- package/src/app/api/teams/invitations/by-id/[id]/accept/route.ts +98 -0
- package/src/app/api/teams/invitations/by-id/[id]/decline/route.ts +90 -0
- package/src/app/api/teams/route.ts +278 -0
- package/src/app/api/uploads/media/route.ts +118 -0
- package/src/app/api/uploads/presigned/route.ts +49 -0
- package/src/app/api/user/linked-accounts/route.ts +35 -0
- package/src/app/email-preview/page.tsx +11 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/global-error.tsx +21 -0
- package/src/app/layout.tsx +50 -0
- package/src/app/page.tsx +5 -0
- package/src/components/icons/atlassian-icon.tsx +22 -0
- package/src/components/icons/index.ts +1 -0
- package/src/components/layout/SIDEBAR_LAYOUT_BEST_PRACTICES.md +240 -0
- package/src/components/layout/app-shell-header-store.tsx +20 -0
- package/src/components/layout/app-shell-skeleton.tsx +89 -0
- package/src/components/layout/app-shell-wrapper.tsx +32 -0
- package/src/components/layout/app-shell.test.tsx +155 -0
- package/src/components/layout/app-shell.tsx +100 -0
- package/src/components/shared/headers/app-header-configurator.tsx +42 -0
- package/src/components/shared/headers/app-header.tsx +103 -0
- package/src/components/shared/headers/header-user-menu.tsx +247 -0
- package/src/components/shared/headers/index.ts +44 -0
- package/src/components/shared/headers/page-header.tsx +25 -0
- package/src/components/shared/notifications/__tests__/notification-bell.test.tsx +159 -0
- package/src/components/shared/notifications/__tests__/notification-dropdown.test.tsx +296 -0
- package/src/components/shared/notifications/__tests__/notification-item.test.tsx +328 -0
- package/src/components/shared/notifications/index.ts +45 -0
- package/src/components/shared/notifications/notification-actions.tsx +295 -0
- package/src/components/shared/notifications/notification-bell-button.tsx +77 -0
- package/src/components/shared/notifications/notification-dropdown.tsx +160 -0
- package/src/components/shared/notifications/notification-group-item.tsx +268 -0
- package/src/components/shared/notifications/notification-item.tsx +193 -0
- package/src/components/shared/notifications/notification-load-more.tsx +50 -0
- package/src/components/shared/notifications/notification-panel.tsx +49 -0
- package/src/components/shared/notifications/utils.tsx +127 -0
- package/src/components/shared/permission-guard/index.ts +1 -0
- package/src/components/shared/permission-guard/permission-tooltip.tsx +45 -0
- package/src/components/shared/relative-time.tsx +53 -0
- package/src/components/shared/section-container.tsx +32 -0
- package/src/components/shared/service-status-banner.tsx +121 -0
- package/src/components/shared/settings-sidebar/index.ts +2 -0
- package/src/components/shared/settings-sidebar/team-setting-aside.tsx +97 -0
- package/src/components/shared/settings-sidebar/user-settings-aside.tsx +66 -0
- package/src/components/shared/sidebar/app-sidebar.tsx +146 -0
- package/src/components/shared/sidebar/index.ts +36 -0
- package/src/components/shared/sidebar/sidebar-main.tsx +81 -0
- package/src/components/shared/sidebar/sidebar-project.tsx +61 -0
- package/src/components/shared/sidebar/sidebar-team-avatar.tsx +126 -0
- package/src/components/shared/sidebar/sidebar-team-switcher.tsx +185 -0
- package/src/components/shared/sidebar/type.ts +97 -0
- package/src/components/ui/alert-dialog.tsx +157 -0
- package/src/components/ui/alert.tsx +66 -0
- package/src/components/ui/avatar-upload.tsx +147 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/breadcrumb.tsx +109 -0
- package/src/components/ui/button.tsx +60 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/collapsible.tsx +33 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/empty.tsx +104 -0
- package/src/components/ui/field.tsx +244 -0
- package/src/components/ui/image-cropper-dialog.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/optimized-image.tsx +220 -0
- package/src/components/ui/pagination.tsx +127 -0
- package/src/components/ui/popover.tsx +48 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +187 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +139 -0
- package/src/components/ui/sidebar.tsx +733 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/sonner.tsx +40 -0
- package/src/components/ui/spinner.tsx +16 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +23 -0
- package/src/components/ui/tooltip.tsx +61 -0
- package/src/config/__tests__/workspace.property.test.ts +40 -0
- package/src/config/auth.ts +62 -0
- package/src/config/integrations.ts +126 -0
- package/src/config/quotas.ts +20 -0
- package/src/config/roles.ts +463 -0
- package/src/config/settings-nav.ts +39 -0
- package/src/config/team-settings-nav.ts +37 -0
- package/src/config/user-settings-nav.ts +42 -0
- package/src/config/version.ts +1 -0
- package/src/config/workspace.ts +64 -0
- package/src/features/annotations/README.md +283 -0
- package/src/features/annotations/api/annotations-api.ts +194 -0
- package/src/features/annotations/api/comments-api.ts +147 -0
- package/src/features/annotations/api/index.ts +71 -0
- package/src/features/annotations/api/save-annotation.ts +150 -0
- package/src/features/annotations/api/schemas.ts +142 -0
- package/src/features/annotations/components/annotated-attachment-view.tsx +576 -0
- package/src/features/annotations/components/annotation-action-sheet.tsx +140 -0
- package/src/features/annotations/components/annotation-annotations-panel.tsx +213 -0
- package/src/features/annotations/components/annotation-box.tsx +539 -0
- package/src/features/annotations/components/annotation-canvas.tsx +534 -0
- package/src/features/annotations/components/annotation-comment-input.tsx +145 -0
- package/src/features/annotations/components/annotation-context-menu.tsx +164 -0
- package/src/features/annotations/components/annotation-drawer.tsx +231 -0
- package/src/features/annotations/components/annotation-layer.tsx +271 -0
- package/src/features/annotations/components/annotation-pin.tsx +318 -0
- package/src/features/annotations/components/annotation-popover.tsx +562 -0
- package/src/features/annotations/components/annotation-thread-panel.tsx +485 -0
- package/src/features/annotations/components/annotation-thread-preview.tsx +195 -0
- package/src/features/annotations/components/annotation-toolbar.tsx +244 -0
- package/src/features/annotations/components/keyboard-shortcuts-modal.tsx +79 -0
- package/src/features/annotations/docs/ANNOTATIONS_ARCHITECTURE.md +67 -0
- package/src/features/annotations/docs/ANNOTATION_SAVE_ARCHITECTURE.md +422 -0
- package/src/features/annotations/docs/ANNOTATION_SAVE_FEATURE.md +408 -0
- package/src/features/annotations/docs/BOX_ANNOTATION_GUIDE.md +542 -0
- package/src/features/annotations/docs/NEXTSTEP.md +28 -0
- package/src/features/annotations/docs/STALE_CLOSURE_FIX.md +344 -0
- package/src/features/annotations/docs/UNDO_REDO_QUICK_START.md +545 -0
- package/src/features/annotations/docs/local_first_canvas_autosave_architecture.md +674 -0
- package/src/features/annotations/examples/complete-example.tsx +266 -0
- package/src/features/annotations/examples/save-annotation-example.tsx +309 -0
- package/src/features/annotations/hooks/__tests__/use-annotation-permissions.property.test.tsx +493 -0
- package/src/features/annotations/hooks/index.ts +36 -0
- package/src/features/annotations/hooks/use-annotation-batch-save.ts +109 -0
- package/src/features/annotations/hooks/use-annotation-comments.ts +353 -0
- package/src/features/annotations/hooks/use-annotation-drafts.ts +137 -0
- package/src/features/annotations/hooks/use-annotation-edit-state.ts +99 -0
- package/src/features/annotations/hooks/use-annotation-history-tracker.ts +159 -0
- package/src/features/annotations/hooks/use-annotation-integration.ts +916 -0
- package/src/features/annotations/hooks/use-annotation-permissions.ts +210 -0
- package/src/features/annotations/hooks/use-annotation-popover.ts +175 -0
- package/src/features/annotations/hooks/use-annotation-save.ts +208 -0
- package/src/features/annotations/hooks/use-annotation-tools.ts +237 -0
- package/src/features/annotations/hooks/use-annotations-with-history.ts +332 -0
- package/src/features/annotations/hooks/use-auto-save.ts +94 -0
- package/src/features/annotations/index.ts +111 -0
- package/src/features/annotations/types/annotation.ts +201 -0
- package/src/features/annotations/types/index.ts +28 -0
- package/src/features/annotations/utils/history-manager.ts +73 -0
- package/src/features/annotations/utils/index.ts +2 -0
- package/src/features/annotations/utils/map-attachments-to-threads.ts +28 -0
- package/src/features/annotations/utils/position-comment-input.ts +136 -0
- package/src/features/annotations/utils/re-sequence-labels.ts +92 -0
- package/src/features/annotations/utils/validate-annotation-label.ts +120 -0
- package/src/features/auth/api/types.ts +101 -0
- package/src/features/auth/components/__tests__/role-gate.test.tsx +448 -0
- package/src/features/auth/components/__tests__/social-login-buttons.test.tsx +313 -0
- package/src/features/auth/components/auth-card.tsx +36 -0
- package/src/features/auth/components/forgot-password-form.tsx +115 -0
- package/src/features/auth/components/index.ts +14 -0
- package/src/features/auth/components/invite-code-input.tsx +155 -0
- package/src/features/auth/components/invited-user-form.tsx +309 -0
- package/src/features/auth/components/onboarding-form.tsx +195 -0
- package/src/features/auth/components/password-strength-indicator.tsx +113 -0
- package/src/features/auth/components/reset-password-form.tsx +138 -0
- package/src/features/auth/components/role-gate.tsx +124 -0
- package/src/features/auth/components/self-registration-choice.tsx +153 -0
- package/src/features/auth/components/sign-in-form.tsx +159 -0
- package/src/features/auth/components/sign-up-form.tsx +158 -0
- package/src/features/auth/components/social-login-buttons.tsx +219 -0
- package/src/features/auth/hooks/__tests__/use-onboarding.test.tsx +109 -0
- package/src/features/auth/hooks/__tests__/use-session.test.tsx +160 -0
- package/src/features/auth/hooks/index.ts +15 -0
- package/src/features/auth/hooks/use-accept-invitation.ts +194 -0
- package/src/features/auth/hooks/use-delete-account.ts +86 -0
- package/src/features/auth/hooks/use-force-verify.ts +89 -0
- package/src/features/auth/hooks/use-forgot-password.ts +144 -0
- package/src/features/auth/hooks/use-link-account.ts +78 -0
- package/src/features/auth/hooks/use-linked-accounts.ts +88 -0
- package/src/features/auth/hooks/use-onboarding.ts +159 -0
- package/src/features/auth/hooks/use-resend-verification.ts +139 -0
- package/src/features/auth/hooks/use-reset-password.ts +151 -0
- package/src/features/auth/hooks/use-reset-rate-limit.ts +56 -0
- package/src/features/auth/hooks/use-self-registration.ts +202 -0
- package/src/features/auth/hooks/use-session.ts +81 -0
- package/src/features/auth/hooks/use-sessions.ts +59 -0
- package/src/features/auth/hooks/use-sign-in.ts +234 -0
- package/src/features/auth/hooks/use-sign-out.ts +88 -0
- package/src/features/auth/hooks/use-sign-up.ts +194 -0
- package/src/features/auth/hooks/use-unlink-account.ts +100 -0
- package/src/features/auth/hooks/use-verify-email-token.ts +125 -0
- package/src/features/auth/index.ts +75 -0
- package/src/features/auth/screens/forgot-password-screen.tsx +33 -0
- package/src/features/auth/screens/index.ts +7 -0
- package/src/features/auth/screens/onboarding-screen.tsx +49 -0
- package/src/features/auth/screens/reset-password-screen.tsx +33 -0
- package/src/features/auth/screens/sign-in-screen.tsx +61 -0
- package/src/features/auth/screens/sign-up-screen.tsx +37 -0
- package/src/features/auth/screens/verify-email-confirm-screen.tsx +286 -0
- package/src/features/auth/screens/verify-email-screen.tsx +146 -0
- package/src/features/auth/types/index.ts +14 -0
- package/src/features/auth/utils/__tests__/validators.test.ts +331 -0
- package/src/features/auth/utils/password-strength.ts +129 -0
- package/src/features/auth/utils/validators.ts +124 -0
- package/src/features/email-preview/actions/render-email.ts +21 -0
- package/src/features/email-preview/screens/email-preview-screen.tsx +81 -0
- package/src/features/folder-scaffold-template/index.ts +0 -0
- package/src/features/instance-settings/components/index.ts +6 -0
- package/src/features/instance-settings/components/instance-settings-form.tsx +180 -0
- package/src/features/instance-settings/components/instance-status-display.tsx +158 -0
- package/src/features/instance-settings/index.ts +7 -0
- package/src/features/instance-settings/screens/index.ts +5 -0
- package/src/features/instance-settings/screens/instance-settings-screen.tsx +59 -0
- package/src/features/issues/README.md +330 -0
- package/src/features/issues/api/create-issue.ts +19 -0
- package/src/features/issues/api/delete-issue.ts +27 -0
- package/src/features/issues/api/get-issue-activities.ts +58 -0
- package/src/features/issues/api/get-issue-details.ts +25 -0
- package/src/features/issues/api/get-project-issues-server.ts +44 -0
- package/src/features/issues/api/get-project-issues.ts +21 -0
- package/src/features/issues/api/index.ts +44 -0
- package/src/features/issues/api/update-issue.ts +31 -0
- package/src/features/issues/api/upload-attachment.ts +81 -0
- package/src/features/issues/components/activity-timeline.tsx +440 -0
- package/src/features/issues/components/canvas-state-indicator.tsx +90 -0
- package/src/features/issues/components/centered-canvas-view.tsx +739 -0
- package/src/features/issues/components/image-selector.tsx +123 -0
- package/src/features/issues/components/image-upload-zone.tsx +262 -0
- package/src/features/issues/components/infinite-canvas-background.tsx +163 -0
- package/src/features/issues/components/inline-editable-select.tsx +173 -0
- package/src/features/issues/components/inline-editable-text.tsx +225 -0
- package/src/features/issues/components/inline-editable-textarea.tsx +219 -0
- package/src/features/issues/components/inline-editable-user-select.tsx +202 -0
- package/src/features/issues/components/issue-deletion-dialog.tsx +142 -0
- package/src/features/issues/components/issue-details-panel.tsx +101 -0
- package/src/features/issues/components/issues-create-dialog.tsx +578 -0
- package/src/features/issues/components/issues-list-filter.tsx +312 -0
- package/src/features/issues/components/issues-list.tsx +151 -0
- package/src/features/issues/components/issues-priority-badge.tsx +77 -0
- package/src/features/issues/components/issues-status-badge.tsx +100 -0
- package/src/features/issues/components/metadata-section.tsx +389 -0
- package/src/features/issues/components/optimized-attachment-view.tsx +528 -0
- package/src/features/issues/components/optimized-image.tsx +257 -0
- package/src/features/issues/components/panel-header.tsx +186 -0
- package/src/features/issues/components/preload.ts +31 -0
- package/src/features/issues/components/priority-selector.tsx +101 -0
- package/src/features/issues/components/responsive-issue-layout-skeleton.tsx +139 -0
- package/src/features/issues/components/responsive-issue-layout.tsx +617 -0
- package/src/features/issues/components/status-selector.tsx +320 -0
- package/src/features/issues/components/type-selector.tsx +102 -0
- package/src/features/issues/components/upload-progress-overlay.tsx +35 -0
- package/src/features/issues/components/uploaded-image-preview.tsx +173 -0
- package/src/features/issues/components/workflow-control.tsx +318 -0
- package/src/features/issues/components/zoom-controls.tsx +150 -0
- package/src/features/issues/config/index.ts +47 -0
- package/src/features/issues/config/options.ts +323 -0
- package/src/features/issues/config/workflow.ts +102 -0
- package/src/features/issues/docs/ARCHITECTURE_DIAGRAM.md +321 -0
- package/src/features/issues/docs/BACKEND_ARCHITECTURE.md +194 -0
- package/src/features/issues/docs/IMAGE_COMPONENTS_ARCHITECTURE.md +363 -0
- package/src/features/issues/docs/IMAGE_COMPONENTS_IMPORTS.md +412 -0
- package/src/features/issues/docs/IMPLEMENTATION_CHECKLIST.md +210 -0
- package/src/features/issues/docs/ROUTE_SETUP_COMPLETE.md +242 -0
- package/src/features/issues/hooks/index.ts +78 -0
- package/src/features/issues/hooks/use-canvas-transform.ts +255 -0
- package/src/features/issues/hooks/use-create-issue.ts +28 -0
- package/src/features/issues/hooks/use-elastic-scroll.ts +296 -0
- package/src/features/issues/hooks/use-issue-activities.ts +71 -0
- package/src/features/issues/hooks/use-issue-delete.ts +84 -0
- package/src/features/issues/hooks/use-issue-details.ts +70 -0
- package/src/features/issues/hooks/use-issue-filters.ts +50 -0
- package/src/features/issues/hooks/use-issue-update.ts +93 -0
- package/src/features/issues/hooks/use-keyboard-shortcuts.ts +104 -0
- package/src/features/issues/hooks/use-optimized-image.ts +228 -0
- package/src/features/issues/hooks/use-project-issues.ts +14 -0
- package/src/features/issues/index.ts +65 -0
- package/src/features/issues/screens/issue-details-screen.tsx +207 -0
- package/src/features/issues/screens/issue-details-skeletons.tsx +295 -0
- package/src/features/issues/screens/issue-share-screen.tsx +56 -0
- package/src/features/issues/types/index.ts +48 -0
- package/src/features/issues/types/issue.ts +291 -0
- package/src/features/issues/utils/filter-issues.ts +141 -0
- package/src/features/issues/utils/index.ts +14 -0
- package/src/features/legal/index.ts +1 -0
- package/src/features/legal/screens/privacy-policy-screen.tsx +307 -0
- package/src/features/notifications/api/get-notifications.ts +58 -0
- package/src/features/notifications/api/get-unread-count.ts +37 -0
- package/src/features/notifications/api/index.ts +35 -0
- package/src/features/notifications/api/mark-all-as-read.ts +37 -0
- package/src/features/notifications/api/mark-as-read.ts +41 -0
- package/src/features/notifications/api/types.ts +109 -0
- package/src/features/notifications/hooks/__tests__/use-notification-subscription.test.ts +206 -0
- package/src/features/notifications/hooks/index.ts +28 -0
- package/src/features/notifications/hooks/use-mark-all-as-read.ts +106 -0
- package/src/features/notifications/hooks/use-mark-as-read.ts +106 -0
- package/src/features/notifications/hooks/use-notification-subscription.ts +244 -0
- package/src/features/notifications/hooks/use-notification-toast.ts +161 -0
- package/src/features/notifications/hooks/use-notifications.ts +80 -0
- package/src/features/notifications/hooks/use-unread-count.ts +60 -0
- package/src/features/notifications/index.ts +48 -0
- package/src/features/notifications/utils/group-notifications.ts +152 -0
- package/src/features/notifications/utils/index.ts +9 -0
- package/src/features/projects/api/create-invitation.ts +45 -0
- package/src/features/projects/api/create-project.ts +64 -0
- package/src/features/projects/api/delete-project.ts +50 -0
- package/src/features/projects/api/get-project-activities.ts +43 -0
- package/src/features/projects/api/get-project-members.ts +53 -0
- package/src/features/projects/api/get-project.ts +49 -0
- package/src/features/projects/api/get-projects.ts +61 -0
- package/src/features/projects/api/index.ts +27 -0
- package/src/features/projects/api/join-project.ts +52 -0
- package/src/features/projects/api/leave-project.ts +51 -0
- package/src/features/projects/api/list-invitations.ts +36 -0
- package/src/features/projects/api/remove-member.ts +60 -0
- package/src/features/projects/api/resend-invitation.ts +36 -0
- package/src/features/projects/api/revoke-invitation.ts +36 -0
- package/src/features/projects/api/types.ts +286 -0
- package/src/features/projects/api/update-member-role.ts +70 -0
- package/src/features/projects/api/update-project.ts +69 -0
- package/src/features/projects/components/__tests__/project-detail-activity-feed.test.tsx +106 -0
- package/src/features/projects/components/__tests__/project-invitation-dialog.test.tsx +211 -0
- package/src/features/projects/components/__tests__/project-member-manager-dialog.test.tsx +254 -0
- package/src/features/projects/components/index.ts +21 -0
- package/src/features/projects/components/project-actions.tsx +248 -0
- package/src/features/projects/components/project-create-dialog.tsx +410 -0
- package/src/features/projects/components/project-detail-activity-feed.tsx +206 -0
- package/src/features/projects/components/project-detail-header.tsx +103 -0
- package/src/features/projects/components/project-detail-overview.tsx +128 -0
- package/src/features/projects/components/project-icon-selector.test.tsx +49 -0
- package/src/features/projects/components/project-icon-selector.tsx +76 -0
- package/src/features/projects/components/project-invitation-dialog.tsx +368 -0
- package/src/features/projects/components/project-issues.tsx +128 -0
- package/src/features/projects/components/project-leave-button.tsx +69 -0
- package/src/features/projects/components/project-list-card.tsx +246 -0
- package/src/features/projects/components/project-list-filters.tsx +320 -0
- package/src/features/projects/components/project-member-manager-dialog.tsx +419 -0
- package/src/features/projects/components/project-settings-dialog.tsx +204 -0
- package/src/features/projects/components/project-stats.tsx +46 -0
- package/src/features/projects/components/project-title-section.tsx +78 -0
- package/src/features/projects/config/icons.ts +91 -0
- package/src/features/projects/hooks/index.ts +28 -0
- package/src/features/projects/hooks/use-create-invitation.ts +83 -0
- package/src/features/projects/hooks/use-create-project.ts +77 -0
- package/src/features/projects/hooks/use-delete-project.ts +84 -0
- package/src/features/projects/hooks/use-join-project.ts +43 -0
- package/src/features/projects/hooks/use-leave-project.ts +84 -0
- package/src/features/projects/hooks/use-project-activities.ts +39 -0
- package/src/features/projects/hooks/use-project-filters.ts +86 -0
- package/src/features/projects/hooks/use-project-invitations.ts +66 -0
- package/src/features/projects/hooks/use-project-members.ts +57 -0
- package/src/features/projects/hooks/use-project.ts +67 -0
- package/src/features/projects/hooks/use-projects.ts +49 -0
- package/src/features/projects/hooks/use-recent-projects.ts +58 -0
- package/src/features/projects/hooks/use-remove-member.ts +89 -0
- package/src/features/projects/hooks/use-resend-invitation.ts +68 -0
- package/src/features/projects/hooks/use-revoke-invitation.ts +71 -0
- package/src/features/projects/hooks/use-team-member-suggestions.ts +133 -0
- package/src/features/projects/hooks/use-update-member-role.ts +92 -0
- package/src/features/projects/hooks/use-update-project.ts +88 -0
- package/src/features/projects/index.ts +91 -0
- package/src/features/projects/screens/index.ts +3 -0
- package/src/features/projects/screens/invitation-acceptance-screen.tsx +320 -0
- package/src/features/projects/screens/project-detail-screen-wrapper.tsx +47 -0
- package/src/features/projects/screens/project-detail-screen.tsx +661 -0
- package/src/features/projects/screens/projects-list-screen.tsx +161 -0
- package/src/features/projects/types/index.ts +59 -0
- package/src/features/projects/utils/format-helpers.ts +16 -0
- package/src/features/projects/utils/index.ts +2 -0
- package/src/features/projects/utils/role-helpers.ts +25 -0
- package/src/features/setup/api/complete-setup.ts +21 -0
- package/src/features/setup/api/create-admin.ts +21 -0
- package/src/features/setup/api/create-first-workspace.ts +21 -0
- package/src/features/setup/api/get-instance-status.ts +12 -0
- package/src/features/setup/api/get-service-health.ts +12 -0
- package/src/features/setup/api/index.ts +44 -0
- package/src/features/setup/api/save-instance-config.ts +21 -0
- package/src/features/setup/api/types.ts +122 -0
- package/src/features/setup/components/__tests__/setup-wizard-ui.test.tsx +362 -0
- package/src/features/setup/components/admin-account-step.tsx +205 -0
- package/src/features/setup/components/first-workspace-step.tsx +120 -0
- package/src/features/setup/components/index.ts +9 -0
- package/src/features/setup/components/instance-config-step.tsx +107 -0
- package/src/features/setup/components/mail-config-step.tsx +205 -0
- package/src/features/setup/components/sample-data-step.tsx +131 -0
- package/src/features/setup/components/service-health-step.tsx +180 -0
- package/src/features/setup/components/service-status-badge.tsx +50 -0
- package/src/features/setup/components/setup-progress.tsx +103 -0
- package/src/features/setup/components/setup-wizard.tsx +169 -0
- package/src/features/setup/hooks/index.ts +12 -0
- package/src/features/setup/hooks/use-complete-setup.ts +23 -0
- package/src/features/setup/hooks/use-create-admin.ts +25 -0
- package/src/features/setup/hooks/use-create-first-workspace.ts +24 -0
- package/src/features/setup/hooks/use-instance-status.ts +21 -0
- package/src/features/setup/hooks/use-save-instance-config.ts +23 -0
- package/src/features/setup/hooks/use-service-health.ts +21 -0
- package/src/features/setup/hooks/use-setup-wizard.ts +152 -0
- package/src/features/setup/hooks/use-workspace-mode.ts +19 -0
- package/src/features/setup/index.ts +30 -0
- package/src/features/setup/screens/index.ts +1 -0
- package/src/features/setup/screens/setup-screen.tsx +40 -0
- package/src/features/setup/types/index.ts +69 -0
- package/src/features/setup/utils/index.ts +78 -0
- package/src/features/team-settings/components/index.ts +39 -0
- package/src/features/team-settings/components/loading-states.tsx +296 -0
- package/src/features/team-settings/components/permission-guard.tsx +23 -0
- package/src/features/team-settings/components/settings-card.tsx +22 -0
- package/src/features/team-settings/components/settings-context-provider.tsx +51 -0
- package/src/features/team-settings/components/settings-error-boundary.tsx +366 -0
- package/src/features/team-settings/components/settings-navigation.tsx +87 -0
- package/src/features/team-settings/components/settings-section.tsx +23 -0
- package/src/features/team-settings/components/team-danger-zone.tsx +275 -0
- package/src/features/team-settings/components/team-information-form.tsx +116 -0
- package/src/features/team-settings/components/team-invitations-list.tsx +463 -0
- package/src/features/team-settings/components/team-members-list.tsx +342 -0
- package/src/features/team-settings/components/team-permission-guard.tsx +56 -0
- package/src/features/team-settings/components/team-setting-integrations.tsx +27 -0
- package/src/features/team-settings/components/team-setting-member.tsx +28 -0
- package/src/features/team-settings/components/team-settings-general.tsx +131 -0
- package/src/features/team-settings/components/transfer-ownership-modal.tsx +164 -0
- package/src/features/team-settings/components/unauthorized-access.tsx +52 -0
- package/src/features/team-settings/hooks/__tests__/use-team-settings.test.tsx +139 -0
- package/src/features/team-settings/hooks/index.ts +1 -0
- package/src/features/team-settings/hooks/use-team-settings.ts +148 -0
- package/src/features/team-settings/hooks/use-transfer-ownership.ts +45 -0
- package/src/features/team-settings/index.ts +25 -0
- package/src/features/team-settings/screens/index.ts +1 -0
- package/src/features/team-settings/screens/team-settings-screen.tsx +33 -0
- package/src/features/team-settings/types/index.ts +78 -0
- package/src/features/team-settings/utils/index.ts +6 -0
- package/src/features/team-settings/utils/mock-data.ts +40 -0
- package/src/features/teams/api/cancel-invitation.ts +25 -0
- package/src/features/teams/api/create-invitation.ts +28 -0
- package/src/features/teams/api/create-team.ts +14 -0
- package/src/features/teams/api/delete-team.ts +19 -0
- package/src/features/teams/api/get-invitations.ts +25 -0
- package/src/features/teams/api/get-team-members.ts +25 -0
- package/src/features/teams/api/get-team.ts +13 -0
- package/src/features/teams/api/get-teams.ts +19 -0
- package/src/features/teams/api/index.ts +49 -0
- package/src/features/teams/api/leave-team.ts +22 -0
- package/src/features/teams/api/remove-member.ts +25 -0
- package/src/features/teams/api/resend-invitation.ts +26 -0
- package/src/features/teams/api/switch-team.ts +13 -0
- package/src/features/teams/api/types.ts +122 -0
- package/src/features/teams/api/update-member-roles.ts +28 -0
- package/src/features/teams/api/update-team.ts +17 -0
- package/src/features/teams/components/create-team-dialog.tsx +105 -0
- package/src/features/teams/hooks/__tests__/cache-invalidation.property.test.tsx +268 -0
- package/src/features/teams/hooks/index.ts +35 -0
- package/src/features/teams/hooks/use-can-manage-members.ts +21 -0
- package/src/features/teams/hooks/use-can-manage-team.ts +21 -0
- package/src/features/teams/hooks/use-cancel-invitation.ts +52 -0
- package/src/features/teams/hooks/use-create-invitation.ts +59 -0
- package/src/features/teams/hooks/use-create-team.ts +38 -0
- package/src/features/teams/hooks/use-delete-team.ts +43 -0
- package/src/features/teams/hooks/use-invitations.ts +31 -0
- package/src/features/teams/hooks/use-leave-team.ts +43 -0
- package/src/features/teams/hooks/use-remove-member.ts +58 -0
- package/src/features/teams/hooks/use-resend-invitation.ts +52 -0
- package/src/features/teams/hooks/use-switch-team.ts +41 -0
- package/src/features/teams/hooks/use-team-members.ts +31 -0
- package/src/features/teams/hooks/use-team-permissions.ts +102 -0
- package/src/features/teams/hooks/use-team.ts +30 -0
- package/src/features/teams/hooks/use-teams.ts +26 -0
- package/src/features/teams/hooks/use-update-member-roles.ts +64 -0
- package/src/features/teams/hooks/use-update-team.ts +47 -0
- package/src/features/teams/index.ts +111 -0
- package/src/features/user-settings/actions/set-password.ts +63 -0
- package/src/features/user-settings/api/index.ts +16 -0
- package/src/features/user-settings/components/__tests__/security-settings.test.tsx +125 -0
- package/src/features/user-settings/components/delete-account-dialog.tsx +185 -0
- package/src/features/user-settings/components/index.ts +13 -0
- package/src/features/user-settings/components/integrations-list.tsx +152 -0
- package/src/features/user-settings/components/notification-preferences.tsx +112 -0
- package/src/features/user-settings/components/other-settings.tsx +126 -0
- package/src/features/user-settings/components/password-section.tsx +297 -0
- package/src/features/user-settings/components/security-settings.tsx +184 -0
- package/src/features/user-settings/components/user-preferences.tsx +146 -0
- package/src/features/user-settings/hooks/index.ts +8 -0
- package/src/features/user-settings/hooks/use-notification-preferences.ts +65 -0
- package/src/features/user-settings/hooks/use-user-preferences.ts +52 -0
- package/src/features/user-settings/index.ts +22 -0
- package/src/features/user-settings/screens/index.ts +11 -0
- package/src/features/user-settings/screens/integrations-screen.tsx +24 -0
- package/src/features/user-settings/screens/notifications-screen.tsx +29 -0
- package/src/features/user-settings/screens/other-settings-screen.tsx +24 -0
- package/src/features/user-settings/screens/setting-preferences-screen.tsx +24 -0
- package/src/features/user-settings/screens/user-settings-screen.tsx +23 -0
- package/src/features/user-settings/types/index.ts +42 -0
- package/src/features/user-settings/utils/index.ts +8 -0
- package/src/hooks/use-long-press.ts +196 -0
- package/src/hooks/use-media-upload.ts +95 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/hooks/use-team.ts +32 -0
- package/src/instrumentation.ts +14 -0
- package/src/lib/__tests__/auth-config.test.ts +166 -0
- package/src/lib/__tests__/config.test.ts +42 -0
- package/src/lib/__tests__/db.test.ts +101 -0
- package/src/lib/__tests__/env.property.test.ts +197 -0
- package/src/lib/__tests__/env.test.ts +177 -0
- package/src/lib/__tests__/health-check.test.ts +87 -0
- package/src/lib/__tests__/oauth-errors.test.ts +184 -0
- package/src/lib/__tests__/oauth-redirect.test.ts +224 -0
- package/src/lib/__tests__/oauth-scopes.test.ts +268 -0
- package/src/lib/__tests__/storage.test.ts +58 -0
- package/src/lib/__tests__/url-validator.test.ts +232 -0
- package/src/lib/api-client.ts +93 -0
- package/src/lib/auth-client.ts +5 -0
- package/src/lib/auth-config.ts +146 -0
- package/src/lib/auth.ts +209 -0
- package/src/lib/config.ts +228 -0
- package/src/lib/cors.ts +96 -0
- package/src/lib/date.ts +16 -0
- package/src/lib/db.ts +88 -0
- package/src/lib/env.ts +489 -0
- package/src/lib/feedback.ts +29 -0
- package/src/lib/health-check.ts +229 -0
- package/src/lib/logger.ts +204 -0
- package/src/lib/oauth-errors.ts +138 -0
- package/src/lib/performance.ts +367 -0
- package/src/lib/query-server.ts +35 -0
- package/src/lib/query.tsx +43 -0
- package/src/lib/resend.ts +36 -0
- package/src/lib/security-headers.ts +165 -0
- package/src/lib/setup-status.ts +34 -0
- package/src/lib/storage.ts +150 -0
- package/src/lib/supabase-client.ts +58 -0
- package/src/lib/testing/test-db.ts +75 -0
- package/src/lib/url-validator.ts +168 -0
- package/src/lib/utils.ts +6 -0
- package/src/mocks/README.md +42 -0
- package/src/mocks/activity.fixtures.ts +281 -0
- package/src/mocks/annotation.fixtures.ts +325 -0
- package/src/mocks/attachment.fixtures.ts +80 -0
- package/src/mocks/email.fixtures.ts +61 -0
- package/src/mocks/index.ts +33 -0
- package/src/mocks/issue.fixtures.ts +364 -0
- package/src/mocks/landing.fixtures.ts +118 -0
- package/src/mocks/notification.fixtures.ts +111 -0
- package/src/mocks/project-activity.fixtures.ts +69 -0
- package/src/mocks/project-invitation.fixtures.ts +95 -0
- package/src/mocks/project-member.fixtures.ts +93 -0
- package/src/mocks/project.fixtures.ts +249 -0
- package/src/mocks/settings.fixtures.ts +56 -0
- package/src/mocks/share.fixtures.ts +48 -0
- package/src/mocks/team-member.fixtures.ts +147 -0
- package/src/mocks/team.fixtures.ts +35 -0
- package/src/mocks/user-settings.fixtures.ts +77 -0
- package/src/mocks/user.fixtures.ts +21 -0
- package/src/providers/theme-provider.tsx +11 -0
- package/src/proxy/__tests__/proxy-setup-gate.test.ts +43 -0
- package/src/proxy.ts +169 -0
- package/src/server/annotations/__tests__/annotation-limit.property.test.ts +291 -0
- package/src/server/annotations/__tests__/attachment-deletion-cascade.property.test.ts +385 -0
- package/src/server/annotations/__tests__/comment-delete-authorization.property.test.ts +419 -0
- package/src/server/annotations/__tests__/sanitize.property.test.ts +302 -0
- package/src/server/annotations/annotation-service.ts +305 -0
- package/src/server/annotations/comment-service.ts +288 -0
- package/src/server/annotations/index.ts +61 -0
- package/src/server/annotations/permission-utils.ts +125 -0
- package/src/server/annotations/sanitize.ts +134 -0
- package/src/server/annotations/types.ts +161 -0
- package/src/server/audit/audit-service.ts +85 -0
- package/src/server/audit/index.ts +11 -0
- package/src/server/audit/types.ts +75 -0
- package/src/server/auth/__tests__/README.md +368 -0
- package/src/server/auth/__tests__/account-linking.integration.test.ts +410 -0
- package/src/server/auth/__tests__/auth-integration.test.ts +811 -0
- package/src/server/auth/__tests__/cookies.test.ts +337 -0
- package/src/server/auth/__tests__/login.property.test.ts +428 -0
- package/src/server/auth/__tests__/oauth-integration.test.ts +555 -0
- package/src/server/auth/__tests__/password.test.ts +194 -0
- package/src/server/auth/__tests__/rate-limiter.test.ts +450 -0
- package/src/server/auth/__tests__/rbac.test.ts +474 -0
- package/src/server/auth/__tests__/session.test.ts +599 -0
- package/src/server/auth/__tests__/signup.property.test.ts +224 -0
- package/src/server/auth/__tests__/token-encryption.test.ts +171 -0
- package/src/server/auth/__tests__/tokens.test.ts +476 -0
- package/src/server/auth/__tests__/verify-email.property.test.ts +372 -0
- package/src/server/auth/cookies.ts +184 -0
- package/src/server/auth/password.ts +94 -0
- package/src/server/auth/rate-limiter.ts +257 -0
- package/src/server/auth/rbac.ts +1168 -0
- package/src/server/auth/session.ts +392 -0
- package/src/server/auth/token-encryption.ts +201 -0
- package/src/server/auth/tokens.ts +397 -0
- package/src/server/db/schema/account.ts +21 -0
- package/src/server/db/schema/annotation-read-status.ts +57 -0
- package/src/server/db/schema/better-auth-verifications.ts +27 -0
- package/src/server/db/schema/email-jobs.ts +24 -0
- package/src/server/db/schema/index.ts +20 -0
- package/src/server/db/schema/instance-settings.ts +42 -0
- package/src/server/db/schema/issue-activities.ts +97 -0
- package/src/server/db/schema/issue-attachments.ts +101 -0
- package/src/server/db/schema/issues.ts +139 -0
- package/src/server/db/schema/notifications.ts +119 -0
- package/src/server/db/schema/project-activities.ts +116 -0
- package/src/server/db/schema/project-invitations.ts +34 -0
- package/src/server/db/schema/project-members.ts +29 -0
- package/src/server/db/schema/projects.ts +46 -0
- package/src/server/db/schema/sessions.ts +17 -0
- package/src/server/db/schema/team-invitations.ts +22 -0
- package/src/server/db/schema/team-members.ts +18 -0
- package/src/server/db/schema/teams.ts +15 -0
- package/src/server/db/schema/user-roles.ts +14 -0
- package/src/server/db/schema/users.ts +17 -0
- package/src/server/db/schema/verification-tokens.ts +15 -0
- package/src/server/email/README.md +258 -0
- package/src/server/email/__tests__/client.property.test.ts +218 -0
- package/src/server/email/__tests__/payload.property.test.ts +205 -0
- package/src/server/email/__tests__/queue.test.ts +407 -0
- package/src/server/email/__tests__/render-template.test.tsx +172 -0
- package/src/server/email/client.ts +70 -0
- package/src/server/email/index.ts +20 -0
- package/src/server/email/providers/console-provider.ts +43 -0
- package/src/server/email/providers/index.ts +5 -0
- package/src/server/email/providers/resend-provider.ts +59 -0
- package/src/server/email/providers/smtp-provider.ts +65 -0
- package/src/server/email/queue.ts +365 -0
- package/src/server/email/render-template.tsx +117 -0
- package/src/server/email/templates/layout.tsx +66 -0
- package/src/server/email/templates/ownership-transfer-email.tsx +75 -0
- package/src/server/email/templates/password-reset-email.tsx +72 -0
- package/src/server/email/templates/project-invitation-email.tsx +70 -0
- package/src/server/email/templates/security-alert-email.tsx +161 -0
- package/src/server/email/templates/team-invitation-email.tsx +68 -0
- package/src/server/email/templates/verification-email.tsx +61 -0
- package/src/server/email/templates/welcome-email.tsx +60 -0
- package/src/server/email/worker.ts +182 -0
- package/src/server/issues/activity-service.ts +422 -0
- package/src/server/issues/attachment-service.ts +379 -0
- package/src/server/issues/index.ts +68 -0
- package/src/server/issues/issue-service.ts +569 -0
- package/src/server/issues/types.ts +233 -0
- package/src/server/notifications/__tests__/notification-service.integration.test.ts +405 -0
- package/src/server/notifications/index.ts +13 -0
- package/src/server/notifications/notification-service.ts +526 -0
- package/src/server/notifications/types.ts +234 -0
- package/src/server/projects/__tests__/activity-logging.integration.test.ts +319 -0
- package/src/server/projects/__tests__/invitation-email.integration.test.ts +258 -0
- package/src/server/projects/__tests__/invitation-service.integration.test.ts +651 -0
- package/src/server/projects/__tests__/member-service.property.test.ts +397 -0
- package/src/server/projects/__tests__/project-service.property.test.ts +116 -0
- package/src/server/projects/activity-service.ts +548 -0
- package/src/server/projects/index.ts +11 -0
- package/src/server/projects/invitation-service.ts +773 -0
- package/src/server/projects/member-service.ts +458 -0
- package/src/server/projects/project-service.ts +491 -0
- package/src/server/projects/schemas.ts +310 -0
- package/src/server/projects/types.ts +166 -0
- package/src/server/projects/utils.ts +128 -0
- package/src/server/setup/__tests__/health-check.property.test.ts +70 -0
- package/src/server/setup/__tests__/sample-data.property.test.ts +82 -0
- package/src/server/setup/__tests__/setup.property.test.ts +95 -0
- package/src/server/setup/health-check-service.ts +294 -0
- package/src/server/setup/index.ts +35 -0
- package/src/server/setup/sample-data-service.ts +233 -0
- package/src/server/setup/setup-service.ts +229 -0
- package/src/server/setup/types.ts +96 -0
- package/src/server/teams/__tests__/export.property.test.ts +542 -0
- package/src/server/teams/__tests__/get-members.integration.test.ts +105 -0
- package/src/server/teams/__tests__/invitation-flow.integration.test.ts +402 -0
- package/src/server/teams/__tests__/invitations.property.test.ts +235 -0
- package/src/server/teams/__tests__/member-management-flow.integration.test.ts +306 -0
- package/src/server/teams/__tests__/members.property.test.ts +180 -0
- package/src/server/teams/__tests__/ownership-transfer.property.test.ts +173 -0
- package/src/server/teams/__tests__/resource-limits.property.test.ts +382 -0
- package/src/server/teams/__tests__/team-context-management.integration.test.ts +396 -0
- package/src/server/teams/__tests__/team-context.property.test.ts +854 -0
- package/src/server/teams/__tests__/team-creation-flow.integration.test.ts +310 -0
- package/src/server/teams/__tests__/team-creation.property.test.ts +280 -0
- package/src/server/teams/errors.ts +396 -0
- package/src/server/teams/export-service.ts +383 -0
- package/src/server/teams/index.ts +10 -0
- package/src/server/teams/invitation-service.ts +708 -0
- package/src/server/teams/member-service.ts +334 -0
- package/src/server/teams/resource-limits.ts +211 -0
- package/src/server/teams/slug.ts +58 -0
- package/src/server/teams/team-context.ts +648 -0
- package/src/server/teams/team-service.ts +660 -0
- package/src/server/teams/types.ts +81 -0
- package/src/server/teams/validation.ts +209 -0
- package/src/styles/globals.css +160 -0
- package/src/types/deployment.ts +39 -0
- package/supabase/config.toml +357 -0
- package/supabase/seed.sql +480 -0
- package/tests/e2e/.gitkeep +0 -0
- package/tests/e2e/QUICK_START.md +98 -0
- package/tests/e2e/README.md +301 -0
- package/tests/e2e/auth.spec.ts +583 -0
- package/tests/e2e/global-setup.ts +23 -0
- package/tests/e2e/global-teardown.ts +23 -0
- package/tests/e2e/helpers/auth-helpers.ts +310 -0
- package/tests/e2e/helpers/test-fixtures.ts +286 -0
- package/tests/e2e/smoke-test.spec.ts +330 -0
- package/tsconfig.json +48 -0
- package/vitest.config.ts +50 -0
- package/vitest.setup.ts +56 -0
- package/dist/index.js +0 -778
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
// src/server/auth/rbac.ts
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Role-Based Access Control (RBAC) Utilities
|
|
5
|
+
*
|
|
6
|
+
* This module provides server-side utilities for managing roles and permissions.
|
|
7
|
+
* All role assignments and permission checks should go through these functions.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { and, eq } from "drizzle-orm";
|
|
11
|
+
import { db } from "@/lib/db";
|
|
12
|
+
import { userRoles } from "@/server/db/schema/user-roles";
|
|
13
|
+
import {
|
|
14
|
+
ALL_ROLES,
|
|
15
|
+
type Permission,
|
|
16
|
+
PROJECT_ROLES,
|
|
17
|
+
type ProjectRole,
|
|
18
|
+
type Role,
|
|
19
|
+
ROLE_PERMISSIONS,
|
|
20
|
+
TEAM_MANAGEMENT_ROLES,
|
|
21
|
+
type TeamManagementRole,
|
|
22
|
+
TEAM_OPERATIONAL_ROLES,
|
|
23
|
+
TEAM_OPERATIONAL_ROLE_HIERARCHY,
|
|
24
|
+
type TeamOperationalRole,
|
|
25
|
+
TEAM_ROLES,
|
|
26
|
+
type TeamRole,
|
|
27
|
+
} from "@/config/roles";
|
|
28
|
+
import { logger } from "@/lib/logger";
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// TYPES
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
export interface UserRole {
|
|
35
|
+
id: string;
|
|
36
|
+
userId: string;
|
|
37
|
+
role: Role;
|
|
38
|
+
resourceType: "team" | "project";
|
|
39
|
+
resourceId: string;
|
|
40
|
+
createdAt: Date;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RoleAssignment {
|
|
44
|
+
userId: string;
|
|
45
|
+
role: Role;
|
|
46
|
+
resourceType: "team" | "project";
|
|
47
|
+
resourceId: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PermissionCheck {
|
|
51
|
+
userId: string;
|
|
52
|
+
permission: Permission;
|
|
53
|
+
resourceId: string;
|
|
54
|
+
resourceType?: "team" | "project";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// ROLE ASSIGNMENT
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Assign a role to a user for a specific resource.
|
|
63
|
+
*
|
|
64
|
+
* @param assignment - Role assignment details
|
|
65
|
+
* @returns The created user role record
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // Assign team owner role
|
|
70
|
+
* await assignRole({
|
|
71
|
+
* userId: 'user_123',
|
|
72
|
+
* role: TEAM_ROLES.TEAM_OWNER,
|
|
73
|
+
* resourceType: 'team',
|
|
74
|
+
* resourceId: 'team_456',
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // Assign project editor role
|
|
78
|
+
* await assignRole({
|
|
79
|
+
* userId: 'user_123',
|
|
80
|
+
* role: PROJECT_ROLES.PROJECT_EDITOR,
|
|
81
|
+
* resourceType: 'project',
|
|
82
|
+
* resourceId: 'project_789',
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export async function assignRole(
|
|
87
|
+
assignment: RoleAssignment
|
|
88
|
+
): Promise<UserRole> {
|
|
89
|
+
const { userId, role, resourceType, resourceId } = assignment;
|
|
90
|
+
|
|
91
|
+
// Validate role exists
|
|
92
|
+
if (!Object.values(ALL_ROLES).includes(role)) {
|
|
93
|
+
throw new Error(`Invalid role: ${role}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Validate resource type matches role type
|
|
97
|
+
if (resourceType === "team" && !Object.values(TEAM_ROLES).includes(role as TeamRole)) {
|
|
98
|
+
throw new Error(`Role ${role} is not a team role`);
|
|
99
|
+
}
|
|
100
|
+
if (resourceType === "project" && !Object.values(PROJECT_ROLES).includes(role as ProjectRole)) {
|
|
101
|
+
throw new Error(`Role ${role} is not a project role`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Check if role already exists
|
|
105
|
+
const existing = await db
|
|
106
|
+
.select()
|
|
107
|
+
.from(userRoles)
|
|
108
|
+
.where(
|
|
109
|
+
and(
|
|
110
|
+
eq(userRoles.userId, userId),
|
|
111
|
+
eq(userRoles.role, role),
|
|
112
|
+
eq(userRoles.resourceType, resourceType),
|
|
113
|
+
eq(userRoles.resourceId, resourceId)
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
.limit(1);
|
|
117
|
+
|
|
118
|
+
if (existing.length > 0) {
|
|
119
|
+
logger.info("rbac.assign_role.already_exists", {
|
|
120
|
+
userId,
|
|
121
|
+
role,
|
|
122
|
+
resourceType,
|
|
123
|
+
resourceId,
|
|
124
|
+
});
|
|
125
|
+
return existing[0] as UserRole;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Insert role
|
|
129
|
+
const [userRole] = await db
|
|
130
|
+
.insert(userRoles)
|
|
131
|
+
.values({
|
|
132
|
+
userId,
|
|
133
|
+
role,
|
|
134
|
+
resourceType,
|
|
135
|
+
resourceId,
|
|
136
|
+
})
|
|
137
|
+
.returning();
|
|
138
|
+
|
|
139
|
+
logger.info("rbac.assign_role.success", {
|
|
140
|
+
userId,
|
|
141
|
+
role,
|
|
142
|
+
resourceType,
|
|
143
|
+
resourceId,
|
|
144
|
+
roleId: userRole.id,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
return userRole as UserRole;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Assign multiple roles to a user at once (transactional).
|
|
152
|
+
*
|
|
153
|
+
* @param assignments - Array of role assignments
|
|
154
|
+
* @returns Array of created user role records
|
|
155
|
+
*/
|
|
156
|
+
export async function assignRoles(
|
|
157
|
+
assignments: RoleAssignment[]
|
|
158
|
+
): Promise<UserRole[]> {
|
|
159
|
+
if (assignments.length === 0) {
|
|
160
|
+
return [];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate all roles
|
|
164
|
+
for (const assignment of assignments) {
|
|
165
|
+
if (!Object.values(ALL_ROLES).includes(assignment.role)) {
|
|
166
|
+
throw new Error(`Invalid role: ${assignment.role}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Insert all roles in a transaction
|
|
171
|
+
const roles = await db.transaction(async (tx) => {
|
|
172
|
+
const results: UserRole[] = [];
|
|
173
|
+
|
|
174
|
+
for (const assignment of assignments) {
|
|
175
|
+
const [userRole] = await tx
|
|
176
|
+
.insert(userRoles)
|
|
177
|
+
.values({
|
|
178
|
+
userId: assignment.userId,
|
|
179
|
+
role: assignment.role,
|
|
180
|
+
resourceType: assignment.resourceType,
|
|
181
|
+
resourceId: assignment.resourceId,
|
|
182
|
+
})
|
|
183
|
+
.returning();
|
|
184
|
+
|
|
185
|
+
results.push(userRole as UserRole);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return results;
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
logger.info("rbac.assign_roles.success", {
|
|
192
|
+
count: assignments.length,
|
|
193
|
+
userId: assignments[0].userId,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
return roles;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Remove a role from a user.
|
|
201
|
+
*
|
|
202
|
+
* @param assignment - Role assignment to remove
|
|
203
|
+
* @returns True if role was removed, false if it didn't exist
|
|
204
|
+
*/
|
|
205
|
+
export async function removeRole(assignment: RoleAssignment): Promise<boolean> {
|
|
206
|
+
const { userId, role, resourceType, resourceId } = assignment;
|
|
207
|
+
|
|
208
|
+
const result = await db
|
|
209
|
+
.delete(userRoles)
|
|
210
|
+
.where(
|
|
211
|
+
and(
|
|
212
|
+
eq(userRoles.userId, userId),
|
|
213
|
+
eq(userRoles.role, role),
|
|
214
|
+
eq(userRoles.resourceType, resourceType),
|
|
215
|
+
eq(userRoles.resourceId, resourceId)
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
.returning();
|
|
219
|
+
|
|
220
|
+
const removed = result.length > 0;
|
|
221
|
+
|
|
222
|
+
if (removed) {
|
|
223
|
+
logger.info("rbac.remove_role.success", {
|
|
224
|
+
userId,
|
|
225
|
+
role,
|
|
226
|
+
resourceType,
|
|
227
|
+
resourceId,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return removed;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Update a user's role for a resource (remove old role, assign new role).
|
|
236
|
+
*
|
|
237
|
+
* IMPORTANT: When demoting a TEAM_EDITOR to TEAM_VIEWER/TEAM_MEMBER,
|
|
238
|
+
* this function checks if the user is a PROJECT_OWNER on any projects.
|
|
239
|
+
* If they are, the demotion is blocked to prevent orphaned projects.
|
|
240
|
+
*
|
|
241
|
+
* @param userId - User ID
|
|
242
|
+
* @param oldRole - Current role to remove
|
|
243
|
+
* @param newRole - New role to assign
|
|
244
|
+
* @param resourceType - Resource type
|
|
245
|
+
* @param resourceId - Resource ID
|
|
246
|
+
* @throws Error if user is PROJECT_OWNER and being demoted from TEAM_EDITOR
|
|
247
|
+
*/
|
|
248
|
+
export async function updateRole(
|
|
249
|
+
userId: string,
|
|
250
|
+
oldRole: Role,
|
|
251
|
+
newRole: Role,
|
|
252
|
+
resourceType: "team" | "project",
|
|
253
|
+
resourceId: string
|
|
254
|
+
): Promise<void> {
|
|
255
|
+
// Check for PROJECT_OWNER demotion edge case
|
|
256
|
+
if (
|
|
257
|
+
resourceType === "team" &&
|
|
258
|
+
oldRole === "WORKSPACE_EDITOR" &&
|
|
259
|
+
(newRole === "WORKSPACE_VIEWER" || newRole === "WORKSPACE_MEMBER")
|
|
260
|
+
) {
|
|
261
|
+
await validateProjectOwnershipBeforeDemotion(userId, resourceId);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
await db.transaction(async (tx) => {
|
|
265
|
+
// Remove old role
|
|
266
|
+
await tx
|
|
267
|
+
.delete(userRoles)
|
|
268
|
+
.where(
|
|
269
|
+
and(
|
|
270
|
+
eq(userRoles.userId, userId),
|
|
271
|
+
eq(userRoles.role, oldRole),
|
|
272
|
+
eq(userRoles.resourceType, resourceType),
|
|
273
|
+
eq(userRoles.resourceId, resourceId)
|
|
274
|
+
)
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Assign new role
|
|
278
|
+
await tx.insert(userRoles).values({
|
|
279
|
+
userId,
|
|
280
|
+
role: newRole,
|
|
281
|
+
resourceType,
|
|
282
|
+
resourceId,
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
logger.info("rbac.update_role.success", {
|
|
287
|
+
userId,
|
|
288
|
+
oldRole,
|
|
289
|
+
newRole,
|
|
290
|
+
resourceType,
|
|
291
|
+
resourceId,
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ============================================================================
|
|
296
|
+
// ROLE QUERIES
|
|
297
|
+
// ============================================================================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get all roles for a user.
|
|
301
|
+
*
|
|
302
|
+
* @param userId - User ID
|
|
303
|
+
* @returns Array of user roles
|
|
304
|
+
*/
|
|
305
|
+
export async function getUserRoles(userId: string): Promise<UserRole[]> {
|
|
306
|
+
const roles = await db
|
|
307
|
+
.select()
|
|
308
|
+
.from(userRoles)
|
|
309
|
+
.where(eq(userRoles.userId, userId));
|
|
310
|
+
|
|
311
|
+
return roles as UserRole[];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Get all roles for a user in a specific team.
|
|
316
|
+
*
|
|
317
|
+
* @param userId - User ID
|
|
318
|
+
* @param teamId - Team ID
|
|
319
|
+
* @returns Array of team roles
|
|
320
|
+
*/
|
|
321
|
+
export async function getUserTeamRoles(
|
|
322
|
+
userId: string,
|
|
323
|
+
teamId: string
|
|
324
|
+
): Promise<UserRole[]> {
|
|
325
|
+
const roles = await db
|
|
326
|
+
.select()
|
|
327
|
+
.from(userRoles)
|
|
328
|
+
.where(
|
|
329
|
+
and(
|
|
330
|
+
eq(userRoles.userId, userId),
|
|
331
|
+
eq(userRoles.resourceType, "team"),
|
|
332
|
+
eq(userRoles.resourceId, teamId)
|
|
333
|
+
)
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
return roles as UserRole[];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Get all roles for a user in a specific project.
|
|
341
|
+
*
|
|
342
|
+
* @param userId - User ID
|
|
343
|
+
* @param projectId - Project ID
|
|
344
|
+
* @returns Array of project roles
|
|
345
|
+
*/
|
|
346
|
+
export async function getUserProjectRoles(
|
|
347
|
+
userId: string,
|
|
348
|
+
projectId: string
|
|
349
|
+
): Promise<UserRole[]> {
|
|
350
|
+
const roles = await db
|
|
351
|
+
.select()
|
|
352
|
+
.from(userRoles)
|
|
353
|
+
.where(
|
|
354
|
+
and(
|
|
355
|
+
eq(userRoles.userId, userId),
|
|
356
|
+
eq(userRoles.resourceType, "project"),
|
|
357
|
+
eq(userRoles.resourceId, projectId)
|
|
358
|
+
)
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
return roles as UserRole[];
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Get the highest team role for a user in a team.
|
|
366
|
+
*
|
|
367
|
+
* @param userId - User ID
|
|
368
|
+
* @param teamId - Team ID
|
|
369
|
+
* @returns Highest team role or null if user has no team roles
|
|
370
|
+
*/
|
|
371
|
+
export async function getHighestTeamRole(
|
|
372
|
+
userId: string,
|
|
373
|
+
teamId: string
|
|
374
|
+
): Promise<TeamRole | null> {
|
|
375
|
+
const roles = await getUserTeamRoles(userId, teamId);
|
|
376
|
+
|
|
377
|
+
if (roles.length === 0) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Find highest role by hierarchy
|
|
382
|
+
const teamRoleHierarchy = {
|
|
383
|
+
[TEAM_ROLES.TEAM_VIEWER]: 1,
|
|
384
|
+
[TEAM_ROLES.TEAM_MEMBER]: 2,
|
|
385
|
+
[TEAM_ROLES.TEAM_EDITOR]: 3,
|
|
386
|
+
[TEAM_ROLES.TEAM_ADMIN]: 4,
|
|
387
|
+
[TEAM_ROLES.TEAM_OWNER]: 5,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let highestRole = roles[0].role as TeamRole;
|
|
391
|
+
let highestLevel = teamRoleHierarchy[highestRole];
|
|
392
|
+
|
|
393
|
+
for (const role of roles) {
|
|
394
|
+
const level = teamRoleHierarchy[role.role as TeamRole];
|
|
395
|
+
if (level > highestLevel) {
|
|
396
|
+
highestRole = role.role as TeamRole;
|
|
397
|
+
highestLevel = level;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
return highestRole;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get the highest project role for a user in a project.
|
|
406
|
+
*
|
|
407
|
+
* @param userId - User ID
|
|
408
|
+
* @param projectId - Project ID
|
|
409
|
+
* @returns Highest project role or null if user has no project roles
|
|
410
|
+
*/
|
|
411
|
+
export async function getHighestProjectRole(
|
|
412
|
+
userId: string,
|
|
413
|
+
projectId: string
|
|
414
|
+
): Promise<ProjectRole | null> {
|
|
415
|
+
const roles = await getUserProjectRoles(userId, projectId);
|
|
416
|
+
|
|
417
|
+
if (roles.length === 0) {
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Find highest role by hierarchy
|
|
422
|
+
const projectRoleHierarchy = {
|
|
423
|
+
[PROJECT_ROLES.PROJECT_VIEWER]: 1,
|
|
424
|
+
[PROJECT_ROLES.PROJECT_DEVELOPER]: 2,
|
|
425
|
+
[PROJECT_ROLES.PROJECT_EDITOR]: 3,
|
|
426
|
+
[PROJECT_ROLES.PROJECT_OWNER]: 4,
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
let highestRole = roles[0].role as ProjectRole;
|
|
430
|
+
let highestLevel = projectRoleHierarchy[highestRole];
|
|
431
|
+
|
|
432
|
+
for (const role of roles) {
|
|
433
|
+
const level = projectRoleHierarchy[role.role as ProjectRole];
|
|
434
|
+
if (level > highestLevel) {
|
|
435
|
+
highestRole = role.role as ProjectRole;
|
|
436
|
+
highestLevel = level;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return highestRole;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* Check if a user has a specific role.
|
|
445
|
+
*
|
|
446
|
+
* For team roles, this queries the team_members table (single source of truth).
|
|
447
|
+
* For project roles, this queries the project_members table.
|
|
448
|
+
*
|
|
449
|
+
* @param userId - User ID
|
|
450
|
+
* @param role - Role to check
|
|
451
|
+
* @param resourceType - Resource type
|
|
452
|
+
* @param resourceId - Resource ID
|
|
453
|
+
* @returns True if user has the role
|
|
454
|
+
*/
|
|
455
|
+
export async function hasRole(
|
|
456
|
+
userId: string,
|
|
457
|
+
role: Role,
|
|
458
|
+
resourceType: "team" | "project",
|
|
459
|
+
resourceId: string
|
|
460
|
+
): Promise<boolean> {
|
|
461
|
+
if (resourceType === "team") {
|
|
462
|
+
// Query team_members table (single source of truth for team roles)
|
|
463
|
+
const { teamMembers } = await import("@/server/db/schema/team-members");
|
|
464
|
+
|
|
465
|
+
const teamMember = await db
|
|
466
|
+
.select()
|
|
467
|
+
.from(teamMembers)
|
|
468
|
+
.where(
|
|
469
|
+
and(
|
|
470
|
+
eq(teamMembers.teamId, resourceId),
|
|
471
|
+
eq(teamMembers.userId, userId)
|
|
472
|
+
)
|
|
473
|
+
)
|
|
474
|
+
.limit(1);
|
|
475
|
+
|
|
476
|
+
if (teamMember.length === 0) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const member = teamMember[0];
|
|
481
|
+
// Check if the requested role matches either management or operational role
|
|
482
|
+
return member.managementRole === role || member.operationalRole === role;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
if (resourceType === "project") {
|
|
486
|
+
// Query project_members table (single source of truth for project roles)
|
|
487
|
+
const { projectMembers } = await import("@/server/db/schema/project-members");
|
|
488
|
+
|
|
489
|
+
const projectMember = await db
|
|
490
|
+
.select()
|
|
491
|
+
.from(projectMembers)
|
|
492
|
+
.where(
|
|
493
|
+
and(
|
|
494
|
+
eq(projectMembers.projectId, resourceId),
|
|
495
|
+
eq(projectMembers.userId, userId)
|
|
496
|
+
)
|
|
497
|
+
)
|
|
498
|
+
.limit(1);
|
|
499
|
+
|
|
500
|
+
if (projectMember.length === 0) {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return projectMember[0].role === role;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Fallback: query user_roles for unknown resource types (backwards compatibility)
|
|
508
|
+
const result = await db
|
|
509
|
+
.select()
|
|
510
|
+
.from(userRoles)
|
|
511
|
+
.where(
|
|
512
|
+
and(
|
|
513
|
+
eq(userRoles.userId, userId),
|
|
514
|
+
eq(userRoles.role, role),
|
|
515
|
+
eq(userRoles.resourceType, resourceType),
|
|
516
|
+
eq(userRoles.resourceId, resourceId)
|
|
517
|
+
)
|
|
518
|
+
)
|
|
519
|
+
.limit(1);
|
|
520
|
+
|
|
521
|
+
return result.length > 0;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// ============================================================================
|
|
525
|
+
// PERMISSION CHECKS
|
|
526
|
+
// ============================================================================
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Check if a user has a specific permission for a resource.
|
|
530
|
+
*
|
|
531
|
+
* @param check - Permission check details
|
|
532
|
+
* @returns True if user has the permission
|
|
533
|
+
*
|
|
534
|
+
* @example
|
|
535
|
+
* ```ts
|
|
536
|
+
* // Check if user can create issues in a project
|
|
537
|
+
* const canCreate = await hasPermission({
|
|
538
|
+
* userId: 'user_123',
|
|
539
|
+
* permission: PERMISSIONS.ISSUE_CREATE,
|
|
540
|
+
* resourceId: 'project_456',
|
|
541
|
+
* resourceType: 'project',
|
|
542
|
+
* });
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
export async function hasPermission(check: PermissionCheck): Promise<boolean> {
|
|
546
|
+
const { userId, permission, resourceId, resourceType } = check;
|
|
547
|
+
|
|
548
|
+
// ============================================================================
|
|
549
|
+
// CONSOLIDATED PERMISSION CHECK
|
|
550
|
+
// Query the single source of truth table based on resource type:
|
|
551
|
+
// - team resources → team_members table
|
|
552
|
+
// - project resources → project_members table
|
|
553
|
+
// ============================================================================
|
|
554
|
+
|
|
555
|
+
if (resourceType === "project") {
|
|
556
|
+
// Query project_members for project permissions
|
|
557
|
+
const { projectMembers } = await import("@/server/db/schema/project-members");
|
|
558
|
+
|
|
559
|
+
const projectMember = await db
|
|
560
|
+
.select()
|
|
561
|
+
.from(projectMembers)
|
|
562
|
+
.where(
|
|
563
|
+
and(
|
|
564
|
+
eq(projectMembers.projectId, resourceId),
|
|
565
|
+
eq(projectMembers.userId, userId)
|
|
566
|
+
)
|
|
567
|
+
)
|
|
568
|
+
.limit(1);
|
|
569
|
+
|
|
570
|
+
if (projectMember.length > 0) {
|
|
571
|
+
const member = projectMember[0];
|
|
572
|
+
const rolePermissions = ROLE_PERMISSIONS[member.role as Role];
|
|
573
|
+
if (rolePermissions?.includes(permission)) {
|
|
574
|
+
return true;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
if (resourceType === "team") {
|
|
581
|
+
// Query team_members for team permissions (single source of truth)
|
|
582
|
+
const { teamMembers } = await import("@/server/db/schema/team-members");
|
|
583
|
+
|
|
584
|
+
const teamMember = await db
|
|
585
|
+
.select()
|
|
586
|
+
.from(teamMembers)
|
|
587
|
+
.where(
|
|
588
|
+
and(
|
|
589
|
+
eq(teamMembers.teamId, resourceId),
|
|
590
|
+
eq(teamMembers.userId, userId)
|
|
591
|
+
)
|
|
592
|
+
)
|
|
593
|
+
.limit(1);
|
|
594
|
+
|
|
595
|
+
if (teamMember.length > 0) {
|
|
596
|
+
const member = teamMember[0];
|
|
597
|
+
|
|
598
|
+
// Check management role permissions (TEAM_OWNER, TEAM_ADMIN)
|
|
599
|
+
if (member.managementRole) {
|
|
600
|
+
const managementPermissions = ROLE_PERMISSIONS[member.managementRole as Role];
|
|
601
|
+
if (managementPermissions?.includes(permission)) {
|
|
602
|
+
return true;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Check operational role permissions (TEAM_EDITOR, TEAM_MEMBER, TEAM_VIEWER)
|
|
607
|
+
if (member.operationalRole) {
|
|
608
|
+
const operationalPermissions = ROLE_PERMISSIONS[member.operationalRole as Role];
|
|
609
|
+
if (operationalPermissions?.includes(permission)) {
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Fallback: query user_roles for unknown resource types (backwards compatibility)
|
|
618
|
+
const roles = await db
|
|
619
|
+
.select()
|
|
620
|
+
.from(userRoles)
|
|
621
|
+
.where(
|
|
622
|
+
and(
|
|
623
|
+
eq(userRoles.userId, userId),
|
|
624
|
+
eq(userRoles.resourceId, resourceId)
|
|
625
|
+
)
|
|
626
|
+
);
|
|
627
|
+
|
|
628
|
+
for (const userRole of roles) {
|
|
629
|
+
const rolePermissions = ROLE_PERMISSIONS[userRole.role as Role];
|
|
630
|
+
if (rolePermissions?.includes(permission)) {
|
|
631
|
+
return true;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Check if a user has any of the specified permissions for a resource.
|
|
642
|
+
*
|
|
643
|
+
* @param userId - User ID
|
|
644
|
+
* @param permissions - Array of permissions to check
|
|
645
|
+
* @param resourceId - Resource ID
|
|
646
|
+
* @param resourceType - Resource type (optional)
|
|
647
|
+
* @returns True if user has at least one of the permissions
|
|
648
|
+
*/
|
|
649
|
+
export async function hasAnyPermission(
|
|
650
|
+
userId: string,
|
|
651
|
+
permissions: Permission[],
|
|
652
|
+
resourceId: string,
|
|
653
|
+
resourceType?: "team" | "project"
|
|
654
|
+
): Promise<boolean> {
|
|
655
|
+
for (const permission of permissions) {
|
|
656
|
+
const has = await hasPermission({
|
|
657
|
+
userId,
|
|
658
|
+
permission,
|
|
659
|
+
resourceId,
|
|
660
|
+
resourceType,
|
|
661
|
+
});
|
|
662
|
+
if (has) {
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Check if a user has all of the specified permissions for a resource.
|
|
671
|
+
*
|
|
672
|
+
* @param userId - User ID
|
|
673
|
+
* @param permissions - Array of permissions to check
|
|
674
|
+
* @param resourceId - Resource ID
|
|
675
|
+
* @param resourceType - Resource type (optional)
|
|
676
|
+
* @returns True if user has all of the permissions
|
|
677
|
+
*/
|
|
678
|
+
export async function hasAllPermissions(
|
|
679
|
+
userId: string,
|
|
680
|
+
permissions: Permission[],
|
|
681
|
+
resourceId: string,
|
|
682
|
+
resourceType?: "team" | "project"
|
|
683
|
+
): Promise<boolean> {
|
|
684
|
+
for (const permission of permissions) {
|
|
685
|
+
const has = await hasPermission({
|
|
686
|
+
userId,
|
|
687
|
+
permission,
|
|
688
|
+
resourceId,
|
|
689
|
+
resourceType,
|
|
690
|
+
});
|
|
691
|
+
if (!has) {
|
|
692
|
+
return false;
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return true;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Get all permissions for a user in a resource.
|
|
700
|
+
*
|
|
701
|
+
* @param userId - User ID
|
|
702
|
+
* @param resourceId - Resource ID
|
|
703
|
+
* @param resourceType - Resource type (optional)
|
|
704
|
+
* @returns Array of permissions
|
|
705
|
+
*/
|
|
706
|
+
export async function getUserPermissions(
|
|
707
|
+
userId: string,
|
|
708
|
+
resourceId: string,
|
|
709
|
+
resourceType?: "team" | "project"
|
|
710
|
+
): Promise<Permission[]> {
|
|
711
|
+
// Collect all permissions from all roles (deduplicated)
|
|
712
|
+
const permissionsSet = new Set<Permission>();
|
|
713
|
+
|
|
714
|
+
if (resourceType === "project") {
|
|
715
|
+
// Query project_members for project permissions
|
|
716
|
+
const { projectMembers } = await import("@/server/db/schema/project-members");
|
|
717
|
+
|
|
718
|
+
const projectMember = await db
|
|
719
|
+
.select()
|
|
720
|
+
.from(projectMembers)
|
|
721
|
+
.where(
|
|
722
|
+
and(
|
|
723
|
+
eq(projectMembers.projectId, resourceId),
|
|
724
|
+
eq(projectMembers.userId, userId)
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
.limit(1);
|
|
728
|
+
|
|
729
|
+
if (projectMember.length > 0) {
|
|
730
|
+
const member = projectMember[0];
|
|
731
|
+
const rolePermissions = ROLE_PERMISSIONS[member.role as Role];
|
|
732
|
+
if (rolePermissions) {
|
|
733
|
+
for (const permission of rolePermissions) {
|
|
734
|
+
permissionsSet.add(permission);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return Array.from(permissionsSet);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
if (resourceType === "team") {
|
|
742
|
+
// Query team_members for team permissions (single source of truth)
|
|
743
|
+
const { teamMembers } = await import("@/server/db/schema/team-members");
|
|
744
|
+
|
|
745
|
+
const teamMember = await db
|
|
746
|
+
.select()
|
|
747
|
+
.from(teamMembers)
|
|
748
|
+
.where(
|
|
749
|
+
and(
|
|
750
|
+
eq(teamMembers.teamId, resourceId),
|
|
751
|
+
eq(teamMembers.userId, userId)
|
|
752
|
+
)
|
|
753
|
+
)
|
|
754
|
+
.limit(1);
|
|
755
|
+
|
|
756
|
+
if (teamMember.length > 0) {
|
|
757
|
+
const member = teamMember[0];
|
|
758
|
+
|
|
759
|
+
// Add management role permissions
|
|
760
|
+
if (member.managementRole) {
|
|
761
|
+
const managementPermissions = ROLE_PERMISSIONS[member.managementRole as Role];
|
|
762
|
+
if (managementPermissions) {
|
|
763
|
+
for (const permission of managementPermissions) {
|
|
764
|
+
permissionsSet.add(permission);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Add operational role permissions
|
|
770
|
+
if (member.operationalRole) {
|
|
771
|
+
const operationalPermissions = ROLE_PERMISSIONS[member.operationalRole as Role];
|
|
772
|
+
if (operationalPermissions) {
|
|
773
|
+
for (const permission of operationalPermissions) {
|
|
774
|
+
permissionsSet.add(permission);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return Array.from(permissionsSet);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Fallback: query user_roles for unknown resource types
|
|
783
|
+
const roles = await db
|
|
784
|
+
.select()
|
|
785
|
+
.from(userRoles)
|
|
786
|
+
.where(
|
|
787
|
+
and(
|
|
788
|
+
eq(userRoles.userId, userId),
|
|
789
|
+
eq(userRoles.resourceId, resourceId)
|
|
790
|
+
)
|
|
791
|
+
);
|
|
792
|
+
|
|
793
|
+
for (const userRole of roles) {
|
|
794
|
+
const rolePermissions = ROLE_PERMISSIONS[userRole.role as Role];
|
|
795
|
+
if (rolePermissions) {
|
|
796
|
+
for (const permission of rolePermissions) {
|
|
797
|
+
permissionsSet.add(permission);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
return Array.from(permissionsSet);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
|
|
806
|
+
// ============================================================================
|
|
807
|
+
// TWO-TIER ROLE HELPERS
|
|
808
|
+
// ============================================================================
|
|
809
|
+
|
|
810
|
+
/**
|
|
811
|
+
* Get a user's management role for a team.
|
|
812
|
+
*
|
|
813
|
+
* @param userId - User ID
|
|
814
|
+
* @param teamId - Team ID
|
|
815
|
+
* @returns Management role or null if user has no management role
|
|
816
|
+
*/
|
|
817
|
+
export async function getManagementRole(
|
|
818
|
+
userId: string,
|
|
819
|
+
teamId: string
|
|
820
|
+
): Promise<TeamManagementRole | null> {
|
|
821
|
+
const roles = await db
|
|
822
|
+
.select()
|
|
823
|
+
.from(userRoles)
|
|
824
|
+
.where(
|
|
825
|
+
and(
|
|
826
|
+
eq(userRoles.userId, userId),
|
|
827
|
+
eq(userRoles.resourceType, "team"),
|
|
828
|
+
eq(userRoles.resourceId, teamId)
|
|
829
|
+
)
|
|
830
|
+
);
|
|
831
|
+
|
|
832
|
+
for (const role of roles) {
|
|
833
|
+
if (role.role === "WORKSPACE_OWNER" || role.role === "WORKSPACE_ADMIN") {
|
|
834
|
+
return role.role as TeamManagementRole;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return null;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Get a user's operational role for a team.
|
|
843
|
+
*
|
|
844
|
+
* @param userId - User ID
|
|
845
|
+
* @param teamId - Team ID
|
|
846
|
+
* @returns Operational role or null if user has no operational role
|
|
847
|
+
*/
|
|
848
|
+
export async function getOperationalRole(
|
|
849
|
+
userId: string,
|
|
850
|
+
teamId: string
|
|
851
|
+
): Promise<TeamOperationalRole | null> {
|
|
852
|
+
const roles = await db
|
|
853
|
+
.select()
|
|
854
|
+
.from(userRoles)
|
|
855
|
+
.where(
|
|
856
|
+
and(
|
|
857
|
+
eq(userRoles.userId, userId),
|
|
858
|
+
eq(userRoles.resourceType, "team"),
|
|
859
|
+
eq(userRoles.resourceId, teamId)
|
|
860
|
+
)
|
|
861
|
+
);
|
|
862
|
+
|
|
863
|
+
for (const role of roles) {
|
|
864
|
+
if (
|
|
865
|
+
role.role === "WORKSPACE_EDITOR" ||
|
|
866
|
+
role.role === "WORKSPACE_MEMBER" ||
|
|
867
|
+
role.role === "WORKSPACE_VIEWER"
|
|
868
|
+
) {
|
|
869
|
+
return role.role as TeamOperationalRole;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
/**
|
|
877
|
+
* Ensure a user has a specific operational role.
|
|
878
|
+
* If user doesn't have the role, assign it.
|
|
879
|
+
* If user has a lower role, upgrade it.
|
|
880
|
+
*
|
|
881
|
+
* IMPORTANT: This function only upgrades, never downgrades.
|
|
882
|
+
* Use updateRole() directly for downgrades (which includes PROJECT_OWNER validation).
|
|
883
|
+
*
|
|
884
|
+
* @param userId - User ID
|
|
885
|
+
* @param teamId - Team ID
|
|
886
|
+
* @param role - Desired operational role
|
|
887
|
+
*/
|
|
888
|
+
export async function ensureOperationalRole(
|
|
889
|
+
userId: string,
|
|
890
|
+
teamId: string,
|
|
891
|
+
role: TeamOperationalRole
|
|
892
|
+
): Promise<void> {
|
|
893
|
+
const currentRole = await getOperationalRole(userId, teamId);
|
|
894
|
+
|
|
895
|
+
if (!currentRole) {
|
|
896
|
+
// User has no operational role, assign it
|
|
897
|
+
await assignRole({
|
|
898
|
+
userId,
|
|
899
|
+
role,
|
|
900
|
+
resourceType: "team",
|
|
901
|
+
resourceId: teamId,
|
|
902
|
+
});
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (currentRole === role) {
|
|
907
|
+
// User already has the desired role
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
// Check if we need to upgrade
|
|
912
|
+
const currentLevel = TEAM_OPERATIONAL_ROLE_HIERARCHY[currentRole];
|
|
913
|
+
const desiredLevel = TEAM_OPERATIONAL_ROLE_HIERARCHY[role];
|
|
914
|
+
|
|
915
|
+
if (desiredLevel > currentLevel) {
|
|
916
|
+
// Upgrade to higher role (safe, no PROJECT_OWNER check needed)
|
|
917
|
+
await updateRole(userId, currentRole, role, "team", teamId);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
// Don't downgrade automatically - must be done explicitly via updateRole()
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/**
|
|
924
|
+
* Validate that a user can be demoted from TEAM_EDITOR.
|
|
925
|
+
* Checks if user is PROJECT_OWNER on any projects in the team.
|
|
926
|
+
*
|
|
927
|
+
* @param userId - User ID
|
|
928
|
+
* @param teamId - Team ID
|
|
929
|
+
* @throws Error with project details if user is PROJECT_OWNER
|
|
930
|
+
*/
|
|
931
|
+
async function validateProjectOwnershipBeforeDemotion(
|
|
932
|
+
userId: string,
|
|
933
|
+
teamId: string
|
|
934
|
+
): Promise<void> {
|
|
935
|
+
// Get all projects where user is PROJECT_OWNER
|
|
936
|
+
const ownedProjects = await db
|
|
937
|
+
.select()
|
|
938
|
+
.from(userRoles)
|
|
939
|
+
.where(
|
|
940
|
+
and(
|
|
941
|
+
eq(userRoles.userId, userId),
|
|
942
|
+
eq(userRoles.role, "PROJECT_OWNER"),
|
|
943
|
+
eq(userRoles.resourceType, "project")
|
|
944
|
+
)
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
if (ownedProjects.length === 0) {
|
|
948
|
+
// User is not a PROJECT_OWNER, demotion is safe
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
// TODO: Fetch project names from projects table when it's implemented
|
|
953
|
+
// For now, just use project IDs
|
|
954
|
+
const projectIds = ownedProjects.map((p) => p.resourceId);
|
|
955
|
+
|
|
956
|
+
logger.warn("rbac.demotion_blocked.project_owner", {
|
|
957
|
+
userId,
|
|
958
|
+
teamId,
|
|
959
|
+
projectCount: projectIds.length,
|
|
960
|
+
projectIds,
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
throw new Error(
|
|
964
|
+
`DEMOTION_BLOCKED: User is PROJECT_OWNER on ${projectIds.length} project(s). ` +
|
|
965
|
+
`Transfer ownership first. Project IDs: ${projectIds.join(", ")}`
|
|
966
|
+
);
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* Get all projects where a user is PROJECT_OWNER.
|
|
971
|
+
* Useful for UI to show which projects need ownership transfer.
|
|
972
|
+
*
|
|
973
|
+
* @param userId - User ID
|
|
974
|
+
* @returns Array of project IDs where user is owner
|
|
975
|
+
*/
|
|
976
|
+
export async function getOwnedProjects(userId: string): Promise<string[]> {
|
|
977
|
+
const ownedProjects = await db
|
|
978
|
+
.select()
|
|
979
|
+
.from(userRoles)
|
|
980
|
+
.where(
|
|
981
|
+
and(
|
|
982
|
+
eq(userRoles.userId, userId),
|
|
983
|
+
eq(userRoles.role, "PROJECT_OWNER"),
|
|
984
|
+
eq(userRoles.resourceType, "project")
|
|
985
|
+
)
|
|
986
|
+
);
|
|
987
|
+
|
|
988
|
+
return ownedProjects.map((p) => p.resourceId);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Auto-promote user to TEAM_EDITOR when they get PROJECT_OWNER or PROJECT_EDITOR role.
|
|
993
|
+
*
|
|
994
|
+
* @param userId - User ID
|
|
995
|
+
* @param teamId - Team ID
|
|
996
|
+
*/
|
|
997
|
+
export async function autoPromoteToEditor(
|
|
998
|
+
userId: string,
|
|
999
|
+
teamId: string
|
|
1000
|
+
): Promise<void> {
|
|
1001
|
+
await ensureOperationalRole(userId, teamId, "WORKSPACE_EDITOR");
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Safely demote a user from TEAM_EDITOR to TEAM_VIEWER/TEAM_MEMBER.
|
|
1006
|
+
* Transfers PROJECT_OWNER roles to a new owner before demotion.
|
|
1007
|
+
*
|
|
1008
|
+
* @param userId - User ID to demote
|
|
1009
|
+
* @param teamId - Team ID
|
|
1010
|
+
* @param newOperationalRole - New operational role (TEAM_VIEWER or TEAM_MEMBER)
|
|
1011
|
+
* @param projectOwnershipTransfers - Map of projectId -> newOwnerId for ownership transfers
|
|
1012
|
+
*
|
|
1013
|
+
* @example
|
|
1014
|
+
* ```ts
|
|
1015
|
+
* // Demote user and transfer their project ownerships
|
|
1016
|
+
* await demoteWithOwnershipTransfer(
|
|
1017
|
+
* 'user_123',
|
|
1018
|
+
* 'team_456',
|
|
1019
|
+
* 'WORKSPACE_VIEWER',
|
|
1020
|
+
* {
|
|
1021
|
+
* 'project_1': 'user_789', // Transfer project_1 to user_789
|
|
1022
|
+
* 'project_2': 'user_789', // Transfer project_2 to user_789
|
|
1023
|
+
* }
|
|
1024
|
+
* );
|
|
1025
|
+
* ```
|
|
1026
|
+
*/
|
|
1027
|
+
export async function demoteWithOwnershipTransfer(
|
|
1028
|
+
userId: string,
|
|
1029
|
+
teamId: string,
|
|
1030
|
+
newOperationalRole: "WORKSPACE_VIEWER" | "WORKSPACE_MEMBER",
|
|
1031
|
+
projectOwnershipTransfers: Record<string, string>
|
|
1032
|
+
): Promise<void> {
|
|
1033
|
+
// Get all projects where user is PROJECT_OWNER
|
|
1034
|
+
const ownedProjectIds = await getOwnedProjects(userId);
|
|
1035
|
+
|
|
1036
|
+
// Validate that all owned projects have a transfer target
|
|
1037
|
+
for (const projectId of ownedProjectIds) {
|
|
1038
|
+
if (!projectOwnershipTransfers[projectId]) {
|
|
1039
|
+
throw new Error(
|
|
1040
|
+
`Missing ownership transfer for project ${projectId}. ` +
|
|
1041
|
+
`All owned projects must have a new owner assigned.`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Perform transfers and demotion in a transaction
|
|
1047
|
+
await db.transaction(async (tx) => {
|
|
1048
|
+
// Transfer ownership for each project
|
|
1049
|
+
for (const [projectId, newOwnerId] of Object.entries(projectOwnershipTransfers)) {
|
|
1050
|
+
// Remove old owner's PROJECT_OWNER role
|
|
1051
|
+
await tx
|
|
1052
|
+
.delete(userRoles)
|
|
1053
|
+
.where(
|
|
1054
|
+
and(
|
|
1055
|
+
eq(userRoles.userId, userId),
|
|
1056
|
+
eq(userRoles.role, "PROJECT_OWNER"),
|
|
1057
|
+
eq(userRoles.resourceType, "project"),
|
|
1058
|
+
eq(userRoles.resourceId, projectId)
|
|
1059
|
+
)
|
|
1060
|
+
);
|
|
1061
|
+
|
|
1062
|
+
// Assign PROJECT_OWNER to new owner
|
|
1063
|
+
await tx.insert(userRoles).values({
|
|
1064
|
+
userId: newOwnerId,
|
|
1065
|
+
role: "PROJECT_OWNER",
|
|
1066
|
+
resourceType: "project",
|
|
1067
|
+
resourceId: projectId,
|
|
1068
|
+
});
|
|
1069
|
+
|
|
1070
|
+
// Auto-promote new owner to TEAM_EDITOR
|
|
1071
|
+
await autoPromoteToEditor(newOwnerId, teamId);
|
|
1072
|
+
|
|
1073
|
+
logger.info("rbac.project_ownership_transferred", {
|
|
1074
|
+
projectId,
|
|
1075
|
+
fromUserId: userId,
|
|
1076
|
+
toUserId: newOwnerId,
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// Now safe to demote the user
|
|
1081
|
+
const currentRole = await getOperationalRole(userId, teamId);
|
|
1082
|
+
if (currentRole) {
|
|
1083
|
+
await tx
|
|
1084
|
+
.delete(userRoles)
|
|
1085
|
+
.where(
|
|
1086
|
+
and(
|
|
1087
|
+
eq(userRoles.userId, userId),
|
|
1088
|
+
eq(userRoles.role, currentRole),
|
|
1089
|
+
eq(userRoles.resourceType, "team"),
|
|
1090
|
+
eq(userRoles.resourceId, teamId)
|
|
1091
|
+
)
|
|
1092
|
+
);
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
// Assign new operational role
|
|
1096
|
+
await tx.insert(userRoles).values({
|
|
1097
|
+
userId,
|
|
1098
|
+
role: newOperationalRole,
|
|
1099
|
+
resourceType: "team",
|
|
1100
|
+
resourceId: teamId,
|
|
1101
|
+
});
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
logger.info("rbac.demote_with_transfer.success", {
|
|
1105
|
+
userId,
|
|
1106
|
+
teamId,
|
|
1107
|
+
newRole: newOperationalRole,
|
|
1108
|
+
projectsTransferred: Object.keys(projectOwnershipTransfers).length,
|
|
1109
|
+
});
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
// ============================================================================
|
|
1113
|
+
// AUTHORIZATION GUARDS
|
|
1114
|
+
// ============================================================================
|
|
1115
|
+
|
|
1116
|
+
/**
|
|
1117
|
+
* Require a user to have a specific permission, or throw an error.
|
|
1118
|
+
*
|
|
1119
|
+
* @param check - Permission check details
|
|
1120
|
+
* @throws Error if user doesn't have the permission
|
|
1121
|
+
*/
|
|
1122
|
+
export async function requirePermission(check: PermissionCheck): Promise<void> {
|
|
1123
|
+
const has = await hasPermission(check);
|
|
1124
|
+
|
|
1125
|
+
if (!has) {
|
|
1126
|
+
logger.warn("rbac.permission_denied", {
|
|
1127
|
+
userId: check.userId,
|
|
1128
|
+
permission: check.permission,
|
|
1129
|
+
resourceId: check.resourceId,
|
|
1130
|
+
resourceType: check.resourceType,
|
|
1131
|
+
});
|
|
1132
|
+
|
|
1133
|
+
throw new Error(
|
|
1134
|
+
`Permission denied: ${check.permission} on ${check.resourceType || "resource"} ${check.resourceId}`
|
|
1135
|
+
);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Require a user to have a specific role, or throw an error.
|
|
1141
|
+
*
|
|
1142
|
+
* @param userId - User ID
|
|
1143
|
+
* @param role - Required role
|
|
1144
|
+
* @param resourceType - Resource type
|
|
1145
|
+
* @param resourceId - Resource ID
|
|
1146
|
+
* @throws Error if user doesn't have the role
|
|
1147
|
+
*/
|
|
1148
|
+
export async function requireRole(
|
|
1149
|
+
userId: string,
|
|
1150
|
+
role: Role,
|
|
1151
|
+
resourceType: "team" | "project",
|
|
1152
|
+
resourceId: string
|
|
1153
|
+
): Promise<void> {
|
|
1154
|
+
const has = await hasRole(userId, role, resourceType, resourceId);
|
|
1155
|
+
|
|
1156
|
+
if (!has) {
|
|
1157
|
+
logger.warn("rbac.role_denied", {
|
|
1158
|
+
userId,
|
|
1159
|
+
role,
|
|
1160
|
+
resourceType,
|
|
1161
|
+
resourceId,
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
throw new Error(
|
|
1165
|
+
`Role required: ${role} on ${resourceType} ${resourceId}`
|
|
1166
|
+
);
|
|
1167
|
+
}
|
|
1168
|
+
}
|