[2b] User and department management - #30
Conversation
- Add admin Departments API (create + list, scoped to an organization) - Add admin Users listing API (scoped to an organization) to support the assignment UI - Add admin UserDepartments API to assign a user to one or more departments via a set-replace PUT, with primary-department resolution and organization-scoped validation - Enrich GET /api/me with departmentIds/primaryDepartmentId resolved live from the UserDepartment table, so admin-driven assignment changes are reflected in the authenticated user's session without requiring a token refresh - Add Admin Web pages for departments (create/list) and user department assignment, with an organization picker shared across both - Add unit and integration test coverage for department CRUD, user listing, assignment (including primary-department edge cases), and session enrichment Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
|
Warning Review limit reached
Next review available in: 24 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (22)
📝 WalkthroughWalkthroughThe PR adds organization-scoped department management, user-department assignment, admin UI workflows, and live department assignments to ChangesDepartment administration
Estimated code review effort: 4 (Complex) | ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Note Unit test generation is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Generating unit tests... This may take up to 20 minutes. |
|
✅ Unit tests committed locally. Commit: |
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (3)
apps/web/app/admin/departments/api.ts (1)
28-45: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAlign error handling with
createDepartment.
fetchDepartmentsdiscards the response body on failure and throws onlyFailed to fetch departments: ${res.status}.createDepartmentin the same file parses the error body for a specific message. Apply the same pattern here so a 404 "Organization not found" or similar backend message reaches the caller instead of a bare status code.♻️ Proposed refactor
if (!res.ok) { - throw new Error(`Failed to fetch departments: ${res.status}`); + const errBody = await parseJsonSafe<{ message?: string }>(res); + throw new Error(errBody?.message ?? `Failed to fetch departments: ${res.status}`); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/admin/departments/api.ts` around lines 28 - 45, Update fetchDepartments to handle non-OK responses using the same error-body parsing pattern as createDepartment, extracting and propagating the backend’s specific message while retaining the existing status-based fallback when no message is available.apps/api/src/admin/user-departments/user-departments.integration.spec.ts (1)
93-109: 🩺 Stability & Availability | 🔵 TrivialAdd a cross-organization authorization test once the ownership check exists.
The
authorizationblock tests 401 and 403 but does not cover anadmintoken from one organization targeting a user in a different organization. Add this test once the organization-ownership check is implemented inuser-department.service.ts(see the linked comment there).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/api/src/admin/user-departments/user-departments.integration.spec.ts` around lines 93 - 109, Add an integration test in the authorization block that issues an admin token for one organization, targets a user belonging to another organization through the user-departments endpoint, and asserts the ownership check rejects the request with the expected authorization status. Implement this after the organization-ownership check in user-department service is available, following the existing request and token setup patterns.apps/web/app/admin/users/api.ts (1)
5-26: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winConsolidate duplicate admin API helpers.
apps/web/app/admin/users/api.ts,apps/web/app/admin/departments/api.ts, andapps/web/app/admin/organizations/api.tsall define the samegetAdminToken,authHeaders, andparseJsonSafehelpers. Extract these into one shared admin API helper underapps/web/app/admin/_libor a similar private layer to prevent drift and keep maintenance localized.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@apps/web/app/admin/users/api.ts` around lines 5 - 26, Extract getAdminToken, authHeaders, and parseJsonSafe into one shared private admin API helper under the admin _lib layer, then update the users, departments, and organizations API modules to import and reuse those functions. Remove the duplicate local definitions while preserving their current behavior and types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/api/src/admin/departments/department.service.ts`:
- Around line 10-17: Move the duplicated isPrismaUniqueConstraintError helper
into a shared Prisma error utility, then remove the local definitions from
department.service.ts and user.service.ts and import the shared function in both
callers. Preserve the existing P2002 classification behavior and update
references to use the shared symbol.
In `@apps/api/src/admin/departments/departments.controller.ts`:
- Around line 30-33: Update DepartmentsController authorization so every
department read and create operation validates that the authenticated user’s
organizationId matches the request organizationId before calling
DepartmentService.findAllByOrganization() or create(). Use the validated
`@AuthenticatedUser`() identity and preserve the existing AdminRoleGuard role
check, rejecting mismatches without accessing or modifying departments.
In `@apps/api/src/admin/user-departments/user-department.service.ts`:
- Around line 53-111: Require the authenticated admin’s organization membership
for all listed user and department operations: in
apps/api/src/admin/users/admin-users.controller.ts:24-37, compare
req.user.organizationId with :organizationId before findAllByOrganization; in
apps/api/src/admin/users/admin-user.service.ts:13-27, accept and enforce the
admin organization when listing users; in
apps/api/src/admin/user-departments/user-departments.controller.ts:34-66, read
req.user.organizationId and pass it to findForUser and assign; in
apps/api/src/admin/user-departments/user-department.service.ts:27-111, require
and validate that organization against the target user and departments before
reading or updating assignments.
In `@apps/api/src/admin/user-departments/user-departments.controller.ts`:
- Around line 54-62: Update the departmentIds validation in the controller to
require every array element to be a string before calling
UserDepartmentService.assign, while preserving the existing non-empty-array
check and controlled BadRequestException response for invalid payloads.
In `@apps/web/app/admin/_components/OrgPicker.tsx`:
- Around line 40-42: Update the organization selector around the empty option in
OrgPicker so users can clear an existing selection, either by removing the
disabled attribute or by adding an equivalent clear-selection control; ensure
the empty value can reach the existing basePath branch.
In `@apps/web/app/admin/users/api.ts`:
- Around line 64-85: Update assignUserDepartments to serialize an explicit
sentinel value for primaryDepartmentId when no primary department is selected,
rather than omitting the field. Preserve the provided department ID when
present, and ensure the request body always communicates either the selected
primary or the backend-supported clear value.
In `@apps/web/app/admin/users/AssignDepartmentsForm.tsx`:
- Around line 18-38: Ensure AssignDepartmentsForm resets its local checked and
primary state when the selected user changes; preferably key the
AssignDepartmentsForm instance by userId at the AssignmentPanel call site so it
remounts with the new currentAssignments.
---
Nitpick comments:
In `@apps/api/src/admin/user-departments/user-departments.integration.spec.ts`:
- Around line 93-109: Add an integration test in the authorization block that
issues an admin token for one organization, targets a user belonging to another
organization through the user-departments endpoint, and asserts the ownership
check rejects the request with the expected authorization status. Implement this
after the organization-ownership check in user-department service is available,
following the existing request and token setup patterns.
In `@apps/web/app/admin/departments/api.ts`:
- Around line 28-45: Update fetchDepartments to handle non-OK responses using
the same error-body parsing pattern as createDepartment, extracting and
propagating the backend’s specific message while retaining the existing
status-based fallback when no message is available.
In `@apps/web/app/admin/users/api.ts`:
- Around line 5-26: Extract getAdminToken, authHeaders, and parseJsonSafe into
one shared private admin API helper under the admin _lib layer, then update the
users, departments, and organizations API modules to import and reuse those
functions. Remove the duplicate local definitions while preserving their current
behavior and types.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 01deca4f-be10-4d5d-8748-03876129bd49
📒 Files selected for processing (36)
apps/api/src/__mocks__/db-client.mock.tsapps/api/src/admin/admin.module.tsapps/api/src/admin/departments/department.dto.tsapps/api/src/admin/departments/department.service.spec.tsapps/api/src/admin/departments/department.service.tsapps/api/src/admin/departments/departments.controller.tsapps/api/src/admin/departments/departments.integration.spec.tsapps/api/src/admin/user-departments/user-department.dto.tsapps/api/src/admin/user-departments/user-department.service.spec.tsapps/api/src/admin/user-departments/user-department.service.tsapps/api/src/admin/user-departments/user-departments.controller.tsapps/api/src/admin/user-departments/user-departments.integration.spec.tsapps/api/src/admin/users/admin-user.service.spec.tsapps/api/src/admin/users/admin-user.service.tsapps/api/src/admin/users/admin-users.controller.tsapps/api/src/admin/users/user.dto.tsapps/api/src/auth/auth.controller.tsapps/api/src/auth/auth.module.tsapps/api/src/auth/auth.types.tsapps/api/src/auth/session.integration.spec.tsapps/api/src/auth/user.service.spec.tsapps/api/src/auth/user.service.tsapps/web/app/admin/_components/OrgPicker.tsxapps/web/app/admin/departments/DeptForm.tsxapps/web/app/admin/departments/DeptTable.tsxapps/web/app/admin/departments/actions.tsapps/web/app/admin/departments/api.tsapps/web/app/admin/departments/page.tsxapps/web/app/admin/departments/types.tsapps/web/app/admin/organizations/OrgTable.tsxapps/web/app/admin/users/AssignDepartmentsForm.tsxapps/web/app/admin/users/UserTable.tsxapps/web/app/admin/users/actions.tsapps/web/app/admin/users/api.tsapps/web/app/admin/users/page.tsxapps/web/app/admin/users/types.ts
| export async function assignUserDepartments( | ||
| userId: string, | ||
| departmentIds: string[], | ||
| primaryDepartmentId?: string, | ||
| ): Promise<{ result?: UserDepartmentsResponse; error?: string }> { | ||
| const res = await fetch( | ||
| `${API_URL}/api/admin/users/${encodeURIComponent(userId)}/departments`, | ||
| { | ||
| method: "PUT", | ||
| headers: { ...authHeaders(), "Content-Type": "application/json" }, | ||
| body: JSON.stringify({ departmentIds, primaryDepartmentId }), | ||
| signal: AbortSignal.timeout(5000), | ||
| }, | ||
| ); | ||
|
|
||
| if (!res.ok) { | ||
| const errBody = await parseJsonSafe<{ message?: string }>(res); | ||
| return { error: errBody?.message ?? "Failed to assign departments" }; | ||
| } | ||
|
|
||
| return { result: (await res.json()) as UserDepartmentsResponse }; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Inspect the user-department DTO and service to see how primaryDepartmentId is validated/applied.
fd -a 'user-department.dto.ts|user-department.service.ts' apps/api/src/admin/user-departments -x cat -n {}Repository: andrmaz/cortex
Length of output: 5875
🏁 Script executed:
#!/bin/bash
# Inspect the API route/controller dispatches and client callers around primaryDepartmentId to understand available forms and whether clear handling exists elsewhere.
set -e
echo "== route files =="
fd -a -t f '.*' apps/api/src/admin/user-departments apps/web/app/admin/users -g '*.ts' -g '*.tsx' | sed 's/^/FILE: /'
echo
echo "== relevant snippets =="
rg -n "assignUserDepartments|primaryDepartmentId|departments" apps/api/src/admin/user-departments apps/web/app/admin/users -g '*.ts' -g '*.tsx' -C 3Repository: andrmaz/cortex
Length of output: 34673
Send a sentinel value when the user has no primary department.
An omitted primaryDepartmentId triggers default resolution by the existing primary or the first selected department; it does not clear isPrimary. The primary radio is required for each selected department, so admins have no way to leave the user without a primary unless the backend also accepts an explicit clear value.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@apps/web/app/admin/users/api.ts` around lines 64 - 85, Update
assignUserDepartments to serialize an explicit sentinel value for
primaryDepartmentId when no primary department is selected, rather than omitting
the field. Preserve the provided department ID when present, and ensure the
request body always communicates either the selected primary or the
backend-supported clear value.
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment (not approved): Cursor Bugbot previously reported unresolved findings (including a high-severity issue) that still need human attention. No reviewers were assigned because the only available collaborator is the PR author.
Sent by Cursor Approval Agent: Pull Request Router and Approver
- Gate admin routes with session JWT verification via admin layout - Forward API calls with caller session token instead of env service token - Reset AssignDepartmentsForm state when userId changes (key prop) - Validate selected user belongs to org before showing assignment panel Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
| @@ -0,0 +1,75 @@ | |||
| import { adminAuthHeaders } from "../_lib/admin-auth"; | |||
There was a problem hiding this comment.
Fixed: removed the process-wide CORTEX_ADMIN_TOKEN forwarding. API helpers now call adminAuthHeaders(), which runs requireAdminAccess() — reading the session JWT from the cortex_access_token cookie and verifying admin role via GET /api/me. An admin/layout.tsx gate blocks unauthenticated callers before any privileged fetch runs.
|
|
||
| return ( | ||
| <AssignDepartmentsForm | ||
| key={userId} |
There was a problem hiding this comment.
Fixed stale form state: added key={userId} so AssignDepartmentsForm remounts (and resets checked/primary state) whenever the selected user changes or after a successful save + revalidation.
| fetchUserDepartments(userId), | ||
| ]); | ||
|
|
||
| const userInOrg = users.some((u) => u.id === userId); |
There was a problem hiding this comment.
Fixed org membership mismatch: AssignmentPanel now loads the org user list and returns an error banner when userId is not in that organization, instead of showing departments for one org against a user from another.
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment (not approved): Cursor Bugbot completed as skipped and reported an unresolved high-severity finding that still needs human attention. No reviewers were assigned because the only available collaborator is the PR author.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@apps/web/app/admin/_lib/admin-auth.ts`:
- Around line 24-37: Wrap the fetch and response parsing in the admin-auth flow
with try/catch so network, timeout, abort, and malformed-JSON failures are
converted to AdminAccessError. Preserve the existing Invalid or expired session
and Admin role required errors, while rethrowing other failures as
AdminAccessError with a clear session-verification message.
- Around line 18-45: Repeated admin authentication checks trigger multiple
/api/me requests within one request. In
apps/web/app/admin/_lib/admin-auth.ts:18-45, wrap requireAdminAccess in React’s
per-request cache while preserving its validation and token-return behavior;
adminAuthHeaders should continue reusing requireAdminAccess. The call sites in
apps/web/app/admin/organizations/api.ts:1-1, 17-17, 33-33, 51-54, and 75-78
require no direct changes because the memoized requireAdminAccess call will
deduplicate their checks.
🪄 Autofix (Beta)
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 25b684ad-48fe-4050-9277-e08a92871ae0
📒 Files selected for processing (10)
apps/api/src/admin/departments/departments.integration.spec.tsapps/api/src/admin/user-departments/user-departments.integration.spec.tsapps/api/src/admin/users/admin-users.integration.spec.tsapps/api/src/auth/user.service.spec.tsapps/web/app/admin/_lib/admin-auth.tsapps/web/app/admin/departments/api.tsapps/web/app/admin/layout.tsxapps/web/app/admin/organizations/api.tsapps/web/app/admin/users/api.tsapps/web/app/admin/users/page.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- apps/web/app/admin/departments/api.ts
- apps/web/app/admin/users/api.ts
- apps/api/src/auth/user.service.spec.ts
- apps/web/app/admin/users/page.tsx
|
Note Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it. Fixes Applied SuccessfullyFixed 1 file(s) based on 2 unresolved review comments. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 2 unresolved review comments. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
- Extract shared isPrismaUniqueConstraintError helper - Enforce org-scoped admin access on department/user endpoints - Validate departmentIds elements are non-empty strings - Allow clearing organization selection in OrgPicker - Add integration tests for cross-org and invalid payloads Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
| * Narrows an unknown thrown value to a Prisma unique-constraint error (P2002). | ||
| * Avoids importing Prisma runtime types into the CommonJS API workspace. | ||
| */ | ||
| export function isPrismaUniqueConstraintError(err: unknown): boolean { |
There was a problem hiding this comment.
Fixed: moved isPrismaUniqueConstraintError into apps/api/src/common/prisma-errors.ts and updated department.service.ts, user.service.ts, and organization.service.ts to import the shared helper.
| @Req() req: RequestWithUser, | ||
| @Param("organizationId") organizationId: string, | ||
| ): Promise<DepartmentResponseDto[]> { | ||
| assertAdminOrganizationAccess(req.user.organizationId, organizationId); |
There was a problem hiding this comment.
Fixed: DepartmentsController now calls assertAdminOrganizationAccess(req.user.organizationId, organizationId) before list/create operations. Added integration coverage for cross-org requests (403).
| departmentId, | ||
| isPrimary: departmentId === primaryDepartmentId, | ||
| }, | ||
| }), |
There was a problem hiding this comment.
Fixed: admin org membership is enforced end-to-end.
- Controllers pass
req.user.organizationIdinto user-department service methods. ensureUserInOrganizationrejects users outside the admin's org with 403.AdminUsersControllerandDepartmentsControlleruse the same org guard.- Integration tests cover cross-org access.
| throw new BadRequestException("departmentIds must be a non-empty array"); | ||
| } | ||
| if ( | ||
| body.departmentIds.some((id) => typeof id !== "string" || id.length === 0) |
There was a problem hiding this comment.
Fixed: added validation that every departmentIds element is a non-empty string before calling the service. Integration test covers { departmentIds: [123] } → 400.
| }} | ||
| style={selectStyle} | ||
| > | ||
| <option value="">Select an organization…</option> |
There was a problem hiding this comment.
Fixed: removed disabled from the empty option so admins can clear the organization selection; the existing onChange handler already navigates to basePath when the value is empty.
There was a problem hiding this comment.
Stale comment
Left a non-blocking comment (not approved): Cursor Bugbot completed as skipped, and an unresolved high-severity Bugbot finding still needs human attention. No reviewers were assigned because the only assignable collaborator is the PR author.
Sent by Cursor Approval Agent: Pull Request Router and Approver
|
@cursoragent Fix the unresolved high-severity Bugbot finding mentioned in #30 (review) |
|
CI is green and the fix is already on the branch. The Bugbot finding is resolved. SummaryThe high-severity Bugbot finding ("Admin cookie never set") was already fixed by a concurrent commit ( What the landed fix does: it wires a real OAuth redirect chain — a new How I verified it: I spun up a mock API server simulating
I also re-ran |
The admin auth module read cortex_access_token from cookies but nothing ever set it after removing CORTEX_ADMIN_TOKEN. Add login/callback/logout routes in the web app, redirect the API Google callback to /auth/callback, and show a sign-in link on the admin gate. Co-authored-by: Andrea Mazzucchelli <andrmaz@users.noreply.github.com>
|
|
||
| const API_URL = process.env["CORTEX_API_URL"] ?? "http://localhost:3001"; | ||
|
|
||
| export async function setAccessTokenCookie(token: string): Promise<void> { |
There was a problem hiding this comment.
Fixed: the cortex_access_token cookie is now set after Google OAuth completes.
- Added
setAccessTokenCookie/clearAccessTokenCookiehelpers here /auth/loginredirects to the API Google OAuth flow/auth/callbackreadsaccessTokenand sets the httpOnly session cookie (8h, matching JWT expiry)- API
GET /auth/google/callbacknow redirects to the web callback instead of returning JSON - Admin gate shows a "Sign in with Google" link when unauthenticated
Flow: /auth/login → API OAuth → /auth/callback?accessToken=… → cookie set → /admin/organizations
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 197ed26. Configure here.
| const token = this.authService.issueToken(authenticatedUser); | ||
|
|
||
| callbackUrl.searchParams.set("accessToken", token); | ||
| res.redirect(callbackUrl.toString()); |
There was a problem hiding this comment.
JWT exposed in redirect URL
High Severity
The Google OAuth callback now redirects with the JWT in the accessToken query parameter. This exposes the session credential in browser history, referrer headers, and server/proxy access logs, creating a security risk.
Reviewed by Cursor Bugbot for commit 197ed26. Configure here.
| const message = | ||
| error instanceof Error ? error.message : "Authentication failed"; | ||
| callbackUrl.searchParams.set("error", message); | ||
| res.redirect(callbackUrl.toString()); |
There was a problem hiding this comment.
OAuth errors leak domain
Medium Severity
On OAuth failure, the googleCallback endpoint redirects with raw exception messages in the error query parameter. These messages can expose sensitive data, like a user's email domain, in URLs, logs, and browser history.
Reviewed by Cursor Bugbot for commit 197ed26. Configure here.
|
|
||
| /** Starts Google OAuth on the API; callback redirects back to set the session cookie. */ | ||
| export function GET(): never { | ||
| redirect(`${API_URL}/auth/google`); |
There was a problem hiding this comment.
Wrong default API port
High Severity
The web app defaults CORTEX_API_URL to http://localhost:3001. When this environment variable is unset, OAuth login and admin API calls fail because the API actually runs on port 4000.
Reviewed by Cursor Bugbot for commit 197ed26. Configure here.
There was a problem hiding this comment.
Left a non-blocking comment (not approved): Cursor Bugbot completed as skipped and reported unresolved findings (including high-severity issues) that still need human attention. No reviewers were assigned because the only assignable collaborator is the PR author.
Sent by Cursor Approval Agent: Pull Request Router and Approver






What changed
Builds on #3 [2a] Organization CRUD to add admin department management and user-department assignment, plus session enrichment so downstream request paths can resolve department scope per user.
API (
apps/api)apps/api/src/admin/departments/):GET/POST /api/admin/organizations/:organizationId/departments— create and list departments scoped to an organization, with 404 on unknown org and 409 on duplicate name within the org.apps/api/src/admin/users/):GET /api/admin/organizations/:organizationId/users— lists users in an org, needed so the admin UI can pick a user to assign.apps/api/src/admin/user-departments/):GET/PUT /api/admin/users/:userId/departments— set-replaces a user's department assignments (add/remove departments in one call) and designates exactly one primary department. Validates that everydepartmentIdbelongs to the user's own organization, and clears existing primary flags before writing the new set inside a transaction so the DB's "one primary per user" partial unique index is never violated mid-write.apps/api/src/auth/):GET /api/menow returnsdepartmentIdsandprimaryDepartmentIdalongside the existing JWT claims (id,email,organizationId,role). This is resolved live from theUserDepartmenttable on every request rather than signed into the JWT, so admin-driven reassignment is visible immediately without waiting for token expiry/reissuance.AdminRoleGuard(401 unauthenticated / 403 non-admin).Web (
apps/web)/admin/departments— organization picker, department list, create-department form./admin/users— organization picker, user list, and a department-assignment form (checkboxes + primary radio) per selected user.Tests
DepartmentService,AdminUserService,UserDepartmentService(including primary-department resolution order, dedup, cross-org validation, and transaction shape), andUserService.getDepartmentAssignments.PrismaServicemocked, matching the existing pattern) covering:GET /api/me) reflecting a fresh assignment on an already-issued token, and the empty/no-assignment case.Verification
pnpm test— all 152 API tests + existing shared package tests pass.pnpm check-types/pnpm lint— clean across all workspaces.pnpm --filter @cortex/web build— succeeds; new/admin/departmentsand/admin/usersroutes render.@cortex/api— this repo has a documented, pre-existing gap (seedocs/agents/cursor-cloud.md) where the compiled API can't start yet (db/clientpath resolution + missing Prisma driver adapter), unrelated to this change. Verified instead via the Jest integration tests, per that doc's guidance.Acceptance criteria
GET /api/me).Summary by CodeRabbit