Skip to content

feat(api): add users tRPC router with CRUD operations and admin access control#792

Merged
aaight merged 2 commits intodevfrom
feature/users-trpc-router
Mar 14, 2026
Merged

feat(api): add users tRPC router with CRUD operations and admin access control#792
aaight merged 2 commits intodevfrom
feature/users-trpc-router

Conversation

@aaight
Copy link
Copy Markdown
Collaborator

@aaight aaight commented Mar 14, 2026

Summary

  • Add src/api/routers/users.ts with list, create, update, delete procedures
  • All procedures use adminProcedure middleware (admin/superadmin only)
  • Bcrypt password hashing in router layer (matching create-admin-user.ts pattern)
  • Security rules: prevent self-deletion, prevent self-demotion, only superadmins can grant/revoke superadmin role
  • Register usersRouter in appRouter as users in src/api/router.ts
  • 26 unit tests covering all procedures and access control rules

Test plan

  • list returns org-scoped user list without passwordHash
  • list throws UNAUTHORIZED for unauthenticated requests
  • list throws FORBIDDEN for members
  • create hashes password with bcrypt before saving
  • create rejects superadmin role assignment by non-superadmins (FORBIDDEN)
  • create allows superadmins to create superadmin users
  • update allows sparse updates (name, email, role, password)
  • update prevents self-demotion (FORBIDDEN when changing own role)
  • update prevents non-superadmins from assigning superadmin role
  • update verifies org ownership (NOT_FOUND for cross-org access)
  • delete prevents self-deletion (FORBIDDEN)
  • delete verifies org ownership before deleting
  • All procedures throw UNAUTHORIZED when unauthenticated and FORBIDDEN for members
  • All 4655 unit tests pass
  • Lint and type checks pass

Trello card: https://trello.com/c/69b4f7503a01d76fed8352b2

🤖 Generated with Claude Code

Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Solid implementation that follows existing codebase patterns well. However, there are two security gaps where the PR description claims "only superadmins can grant/revoke superadmin role" but the code doesn't fully enforce this.

Code Issues

Blocking

1. Missing superadmin revocation guard (src/api/routers/users.ts:80-84)

The update procedure checks if (input.role === 'superadmin' && ctx.user.role !== 'superadmin') — this prevents granting superadmin, but does NOT prevent a regular admin from revoking superadmin status by changing a superadmin's role to member or admin. The PR description claims "only superadmins can grant/revoke superadmin role" but only the grant side is enforced.

Fix: add a guard that checks the target user's current role:

if (targetUser.role === 'superadmin' && input.role !== 'superadmin' && ctx.user.role !== 'superadmin') {
    throw new TRPCError({ code: 'FORBIDDEN', message: 'Only superadmins can change a superadmin user role' });
}

2. Admin can delete superadmin users (src/api/routers/users.ts:105-121)

The delete procedure has no check preventing a regular admin from deleting a user who has the superadmin role. This is a privilege hierarchy violation — a lower-privileged admin should not be able to remove a higher-privileged superadmin.

Fix: add a guard after fetching the target user:

if (targetUser.role === 'superadmin' && ctx.user.role !== 'superadmin') {
    throw new TRPCError({ code: 'FORBIDDEN', message: 'Only superadmins can delete superadmin users' });
}

Should Fix

3. Misleading test assertion (tests/unit/api/routers/users.test.ts:47-64)

The test "returns org-scoped user list without passwordHash" passes trivially because the mock data itself doesn't contain passwordHash. The not.toHaveProperty('passwordHash') assertion tests the mock, not the actual behavior. The real guarantee comes from listOrgUsers in the repository selecting specific columns. Consider removing the misleading assertion or noting it as a documentation comment.

- Prevent regular admins from revoking superadmin role (update procedure)
- Prevent regular admins from deleting superadmin users (delete procedure)
- Add tests covering both new security guards
- Replace misleading passwordHash assertion with explanatory comment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Collaborator

@nhopeatall nhopeatall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Clean, well-structured users CRUD router that follows existing codebase patterns. Access control logic is comprehensive, tests are thorough, and CI is green. Two minor suggestions below but nothing blocking.

Should Fix

  • src/api/routers/users.ts:41 — The create mutation doesn't handle duplicate email errors. Since users.email has a unique constraint, inserting a duplicate will throw a raw Drizzle/pg error that surfaces as an opaque INTERNAL_SERVER_ERROR. Other routers in this codebase handle uniqueness conflicts explicitly (e.g., runs.ts uses CONFLICT). Consider catching the DB error and throwing TRPCError({ code: 'CONFLICT', message: 'Email already in use' }).

  • src/api/routers/users.ts:24password: z.string().min(1) allows single-character passwords. While the create-admin-user.ts tool doesn't enforce a minimum either, this is a user-facing API endpoint where a reasonable minimum (e.g., min(8)) would be appropriate. Same applies to the update schema on line 65.

@aaight aaight merged commit fd7ec5c into dev Mar 14, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants