Skip to content

Added user access management page#300

Merged
danielemery merged 2 commits intomainfrom
approve-a-new-user
Feb 23, 2026
Merged

Added user access management page#300
danielemery merged 2 commits intomainfrom
approve-a-new-user

Conversation

@danielemery
Copy link
Owner

@danielemery danielemery commented Feb 23, 2026

Introduced a new /users page for admins to approve or reject
pending user signups. Includes GraphQL queries/mutations for
pending and rejected users, custom hooks, and a MANAGE_USERS
authorization action gated to the ADMIN role. Added "Users"
nav link visible only to admins.

Co-Authored-By: Claude noreply@anthropic.com

Summary by CodeRabbit

  • New Features
    • Added a Users management page for admins to handle registration workflow.
    • Admins can view and approve or reject pending user applications.
    • Admins can review rejected applications (with rejection details) and re-approve users.
    • Navigation now shows a Users link for users with management permissions.

Introduced a new /users page for admins to approve or reject
pending user signups. Includes GraphQL queries/mutations for
pending and rejected users, custom hooks, and a MANAGE_USERS
authorization action gated to the ADMIN role. Added "Users"
nav link visible only to admins.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

📝 Walkthrough

Walkthrough

Adds a user management feature: a Users nav item gated by a new MANAGE_USERS permission, a /users route serving a PendingUsers page, GraphQL queries/mutations for pending/rejected users, hooks to fetch them, and approve/reject actions wired to UI mutations and refetching.

Changes

Cohort / File(s) Summary
Navigation & Routing
src/NavigationBar.tsx, src/app.tsx
Added canManageUsers:boolean prop to NavigationBar; conditionally render Users link (desktop & mobile); wired canManageUsers from userCanPerformAction(user, 'MANAGE_USERS'); added /users route to authenticated flow with PendingUsers component.
Queries & Mutations
src/queries/user.ts
Added GraphQL operations: PENDING_USERS, REJECTED_USERS, APPROVE_USER, REJECT_USER.
Data Hooks
src/hooks/usePendingUsers.hook.ts, src/hooks/useRejectedUsers.hook.ts
New hooks usePendingUsers() and useRejectedUsers() using Apollo useQuery with network-only fetchPolicy and notifyOnNetworkStatusChange; return loading, data, and refetch.
User Management UI
src/pages/users/PendingUsers.tsx
New PendingUsers page rendering Pending and Rejected users tables; supports approve/reject actions that call mutations and refetch affected lists; shows loading and empty states.
Authorization Policy
src/services/authorization.policy.ts
Added 'MANAGE_USERS' to Action union and included it in allowedActions.ADMIN.

Sequence Diagram(s)

sequenceDiagram
  participant Admin as Admin (Browser)
  participant UI as PendingUsers UI
  participant Hook as usePendingUsers / useRejectedUsers
  participant GraphQL as GraphQL API
  participant DB as Database

  Admin->>UI: Click "Approve" / "Reject"
  UI->>Hook: call mutation (APPROVE_USER / REJECT_USER)
  Hook->>GraphQL: send mutation
  GraphQL->>DB: update user status
  DB-->>GraphQL: persist result
  GraphQL-->>Hook: mutation response
  Hook-->>UI: resolve, call refetch()
  UI->>Hook: refetch PENDING_USERS / REJECTED_USERS
  Hook->>GraphQL: send query (PENDING_USERS / REJECTED_USERS)
  GraphQL->>DB: query lists
  DB-->>GraphQL: return lists
  GraphQL-->>Hook: query response
  Hook-->>UI: render updated lists
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰
I hopped to the nav and found a door,
Pending folks waiting, who could ask for more?
Approve with a nibble, reject with a thump—
I bounce, I refetch, I tidy the rump! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Added user access management page' accurately reflects the main objective of the pull request, which is to introduce a new /users page for administrators to manage user signups.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch approve-a-new-user

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (3)
src/hooks/useRejectedUsers.hook.ts (1)

17-21: refetch return type loses the Promise.

According to the Apollo Client docs, useQuery's refetch has the signature (variables?: Partial<TVariables>) => Promise<ApolloQueryResult<TData>>. Declaring it as () => void in the public hook signature is technically valid TypeScript (void widening), but callers can never chain on or await the returned promise if they need to. The same applies to usePendingUsers.

♻️ Proposed fix
-import { useQuery } from '@apollo/client';
+import { ApolloQueryResult, useQuery } from '@apollo/client';

...

 export function useRejectedUsers(): {
   loading: boolean;
   data: { rejectedUsers: RejectedUser[] } | undefined;
-  refetch: () => void;
+  refetch: () => Promise<ApolloQueryResult<{ rejectedUsers: RejectedUser[] }>>;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/useRejectedUsers.hook.ts` around lines 17 - 21, The hook signature
for useRejectedUsers (and similarly usePendingUsers) declares refetch as () =>
void which loses the Promise returned by Apollo's useQuery; update the public
return type so refetch matches useQuery's signature, e.g. refetch: (variables?:
Partial<any>) => Promise<ApolloQueryResult<{ rejectedUsers: RejectedUser[] }>>
(import ApolloQueryResult) or use the correct generic TVariables/TData types
used by useQuery, ensuring callers can await or chain the returned promise;
change the refetch type in the exported function return to the Promise-based
signature and mirror the same fix in usePendingUsers.
src/hooks/usePendingUsers.hook.ts (1)

12-16: Same refetch: () => void narrowing concern as useRejectedUsers.

For consistency, consider updating this hook's return type to expose the full Promise<ApolloQueryResult<...>> return just like the suggested fix for useRejectedUsers.hook.ts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/usePendingUsers.hook.ts` around lines 12 - 16, The hook
usePendingUsers currently types refetch as a void-returning function which
narrows callers; change its return type so refetch matches the underlying Apollo
client's signature (Promise<ApolloQueryResult<{ pendingUsers: PendingUser[]
}>>). Update the exported return type for usePendingUsers to expose loading,
data: { pendingUsers: PendingUser[] } | undefined, and refetch: (variables?:
Partial<Record<string, any>>) => Promise<ApolloQueryResult<{ pendingUsers:
PendingUser[] }>> (or the exact ApolloQueryResult generic used elsewhere) so it
is consistent with useRejectedUsers and callers can await the query result.
src/pages/users/PendingUsers.tsx (1)

15-16: No in-flight guard on action buttons — duplicate mutations possible.

The loading state from useMutation is discarded (only the mutate function is destructured on Lines 15–16). While a mutation is in flight, all Approve and Reject buttons remain enabled, so a quick double-click fires the same mutation twice.

♻️ Proposed fix — capture loading state and disable buttons
-  const [approveUser] = useMutation(APPROVE_USER);
-  const [rejectUser] = useMutation(REJECT_USER);
+  const [approveUser, { loading: approveLoading }] = useMutation(APPROVE_USER);
+  const [rejectUser, { loading: rejectLoading }] = useMutation(REJECT_USER);

Then pass disabled to each button:

-                  <Button onClick={() => handleApprove(user.id, refetchPending)}>Approve</Button>
-                  <Button danger onClick={() => handleReject(user.id)}>
+                  <Button disabled={approveLoading || rejectLoading} onClick={() => handleApprove(user.id, refetchPending)}>Approve</Button>
+                  <Button danger disabled={approveLoading || rejectLoading} onClick={() => handleReject(user.id)}>
-                  <Button onClick={() => handleApprove(user.id, refetchRejected)}>Approve</Button>
+                  <Button disabled={approveLoading || rejectLoading} onClick={() => handleApprove(user.id, refetchRejected)}>Approve</Button>

Also applies to: 54-55, 96-96

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/pages/users/PendingUsers.tsx` around lines 15 - 16, Capture each
mutation's loading flag from useMutation (e.g., const [approveUser, { loading:
approving }] = useMutation(APPROVE_USER) and const [rejectUser, { loading:
rejecting }] = useMutation(REJECT_USER)) and pass the appropriate disabled prop
to the corresponding Approve and Reject buttons in the PendingUsers component so
the button is disabled while its mutation is in-flight; if you need per-row
locking, derive a per-user in-flight map keyed by user id when invoking
approveUser/rejectUser and use that map to disable only that row's buttons.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/app.tsx`:
- Line 66: The /users Route currently uses the generic USE_APP guard (which
allows plain USER) so any authenticated user can reach <PendingUsers />; change
the route to require the MANAGE_USERS permission instead — e.g., replace or wrap
the Route entry for path='/users' (element={<PendingUsers />}) so it is rendered
only under the authorization guard/component that checks for MANAGE_USERS (not
USER/USE_APP), or add an explicit permission check before rendering
<PendingUsers />; ensure the guard/prop you use matches your app's auth helpers
(MANAGE_USERS, USE_APP, Route/Authorized wrapper) so only users with
MANAGE_USERS can access the page.

In `@src/pages/users/PendingUsers.tsx`:
- Around line 18-27: handleApprove and handleReject currently call
approveUser/rejectUser without error handling so network/server failures are
swallowed; wrap each mutation call (approveUser and rejectUser) in try/catch,
await the mutation inside the try, call
refetch()/refetchPending()/refetchRejected() only on success, and in the catch
set/display an error to the admin (e.g., via component state, a toast, or an
alert) using the caught error.message; alternatively, check and handle the error
field returned from the useMutation tuple after the await and surface that to
the user so failures are not silent.

---

Nitpick comments:
In `@src/hooks/usePendingUsers.hook.ts`:
- Around line 12-16: The hook usePendingUsers currently types refetch as a
void-returning function which narrows callers; change its return type so refetch
matches the underlying Apollo client's signature (Promise<ApolloQueryResult<{
pendingUsers: PendingUser[] }>>). Update the exported return type for
usePendingUsers to expose loading, data: { pendingUsers: PendingUser[] } |
undefined, and refetch: (variables?: Partial<Record<string, any>>) =>
Promise<ApolloQueryResult<{ pendingUsers: PendingUser[] }>> (or the exact
ApolloQueryResult generic used elsewhere) so it is consistent with
useRejectedUsers and callers can await the query result.

In `@src/hooks/useRejectedUsers.hook.ts`:
- Around line 17-21: The hook signature for useRejectedUsers (and similarly
usePendingUsers) declares refetch as () => void which loses the Promise returned
by Apollo's useQuery; update the public return type so refetch matches
useQuery's signature, e.g. refetch: (variables?: Partial<any>) =>
Promise<ApolloQueryResult<{ rejectedUsers: RejectedUser[] }>> (import
ApolloQueryResult) or use the correct generic TVariables/TData types used by
useQuery, ensuring callers can await or chain the returned promise; change the
refetch type in the exported function return to the Promise-based signature and
mirror the same fix in usePendingUsers.

In `@src/pages/users/PendingUsers.tsx`:
- Around line 15-16: Capture each mutation's loading flag from useMutation
(e.g., const [approveUser, { loading: approving }] = useMutation(APPROVE_USER)
and const [rejectUser, { loading: rejecting }] = useMutation(REJECT_USER)) and
pass the appropriate disabled prop to the corresponding Approve and Reject
buttons in the PendingUsers component so the button is disabled while its
mutation is in-flight; if you need per-row locking, derive a per-user in-flight
map keyed by user id when invoking approveUser/rejectUser and use that map to
disable only that row's buttons.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 934a7a8 and 95f06b6.

📒 Files selected for processing (7)
  • src/NavigationBar.tsx
  • src/app.tsx
  • src/hooks/usePendingUsers.hook.ts
  • src/hooks/useRejectedUsers.hook.ts
  • src/pages/users/PendingUsers.tsx
  • src/queries/user.ts
  • src/services/authorization.policy.ts

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/app.tsx (1)

40-40: Optional: compute canManageUsers once to avoid the duplicate userCanPerformAction call.

userCanPerformAction(user, 'MANAGE_USERS') is evaluated twice — once for the NavigationBar prop and once for the route guard — each render cycle. Extracting it to a local variable keeps both usages in sync and makes future changes (e.g. memoising) a one-liner.

♻️ Proposed refactor
+  const canManageUsers = userCanPerformAction(user, 'MANAGE_USERS');
+
   return (
     <div className='min-h-screen flex flex-col'>
       <NavigationBar
         ...
-        canManageUsers={userCanPerformAction(user, 'MANAGE_USERS')}
+        canManageUsers={canManageUsers}
       />
       ...
-            {userCanPerformAction(user, 'MANAGE_USERS') && <Route path='/users' element={<PendingUsers />} />}
+            {canManageUsers && <Route path='/users' element={<PendingUsers />} />}

Also applies to: 66-66

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/app.tsx` at line 40, Compute the permission once and reuse it: create a
local constant (e.g. const canManageUsers = userCanPerformAction(user,
'MANAGE_USERS')) and replace both places that call userCanPerformAction (the
NavigationBar prop canManageUsers and the route guard check around line ~66) to
use that constant; if desired later, wrap the computation in useMemo keyed on
user to memoize.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/app.tsx`:
- Line 40: Compute the permission once and reuse it: create a local constant
(e.g. const canManageUsers = userCanPerformAction(user, 'MANAGE_USERS')) and
replace both places that call userCanPerformAction (the NavigationBar prop
canManageUsers and the route guard check around line ~66) to use that constant;
if desired later, wrap the computation in useMemo keyed on user to memoize.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95f06b6 and c182a3d.

📒 Files selected for processing (1)
  • src/app.tsx

@danielemery danielemery merged commit de03582 into main Feb 23, 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.

1 participant