Skip to content

feat: admin role page#316

Open
Krish120003 wants to merge 1 commit intomainfrom
03-23-feat_admin_role_page
Open

feat: admin role page#316
Krish120003 wants to merge 1 commit intomainfrom
03-23-feat_admin_role_page

Conversation

@Krish120003
Copy link
Copy Markdown
Contributor

@Krish120003 Krish120003 commented Mar 24, 2026

TL;DR

Added admin user management functionality with role-based access control and a dedicated admin interface for managing user roles.

What changed?

  • Created /admin page with authentication and role-based access control that only allows admin users
  • Added AdminUserManagement component with user search functionality and role assignment interface
  • Enhanced TeacherDashboard to show admin-specific features and "Manage Users" button for admin users
  • Updated role detection logic in the main app to use getCurrentUserRole API and treat both teachers and admins as instructors
  • Added new backend queries and mutations:
    • getCurrentUserRole - gets the current user's role
    • searchUsers - searches and returns enriched user data (admin-only)
    • updateUserRole - updates user roles with validation (admin-only)
  • Added role validation preventing admins from removing their own admin access
  • Updated header component to recognize /admin route

How to test?

  1. Sign in as an admin user and navigate to /admin to access user management
  2. Search for users by name, email, username, or role
  3. Try updating user roles using the role buttons
  4. Verify non-admin users cannot access the admin page
  5. Test that admins cannot remove their own admin privileges
  6. Check that the "Manage Users" button appears in the teacher dashboard for admin users

Why make this change?

This enables proper user role management within the application, allowing administrators to promote users to teachers or other admins without requiring direct database access. The role-based access control ensures only authorized users can modify permissions, while the search functionality makes it easy to find and manage users at scale.

@Krish120003 Krish120003 self-assigned this Mar 24, 2026
Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@Krish120003 Krish120003 marked this pull request as ready for review March 24, 2026 03:13
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Mar 24, 2026

Greptile Summary

This PR introduces a complete admin user-management feature: a new /admin route with server-side auth/role gating, an AdminUserManagement client component for searching users and updating their roles, backend queries/mutations (getCurrentUserRole, searchUsers, updateUserRole), and accompanying tests. The getDashboardRole logic in /app is also simplified to call the new API instead of inferring the role from a classroom query.

Key points:

  • Missing issue link — the PR description does not include a resolves #issue-id reference, which is required by the contributing guidelines.
  • PR scope — the PR touches multiple pages (/app, /admin), two components, backend logic, and tests simultaneously. Per the project's conventions, components should be developed in isolation and integrated in separate PRs.
  • searchUsers full table scanctx.db.query("users").collect() fetches every user row, then issues one authComponent.getAnyUserById call per user before filtering. This will become a bottleneck and may hit Convex limits as user count grows.
  • Fragile self-demotion guard in the UIuser.isCurrentUser && role !== "admin" only disables Student/Teacher buttons for the current user, leaving the Admin button technically enabled. In practice the current-role check also disables it, but the intent is obscured and a simpler user.isCurrentUser guard would be both correct and clearer.
  • clsx not usedclassName={\...${roleBadgeClassName(...)}`}should useclsx` per the project convention.
  • Relative importspackages/backend/convex/web/user.ts and the new test file use relative imports (../helpers/roles, ../../convex/...) instead of absolute paths.

Confidence Score: 3/5

  • Functional but the searchUsers full table scan is a real scalability issue, and the PR violates several project conventions (missing issue link, too broad in scope).
  • The feature works correctly and the backend enforces auth/role guards properly. However, the searchUsers full .collect() + N auth-profile lookups will degrade as user count grows and could hit Convex query limits in production. Combined with the missing resolves #issue-id (a required PR template field), the scope-of-change convention violations, and the fragile self-demotion guard, a targeted round of fixes before merge is warranted.
  • packages/backend/convex/web/user.ts (scalability of searchUsers) and apps/nextjs/src/components/admin-user-management.tsx (clsx + self-demotion guard)

Important Files Changed

Filename Overview
apps/nextjs/src/app/admin/page.tsx New server-rendered admin page with auth and role gating; straightforward and clean.
apps/nextjs/src/components/admin-user-management.tsx New client component for user search and role assignment; has a string-template class-name concatenation (should use clsx) and a fragile self-demotion guard that could be simplified to user.isCurrentUser.
packages/backend/convex/web/user.ts Adds getCurrentUserRole, searchUsers, and updateUserRole; searchUsers does a full .collect() table scan and issues N auth-profile lookups before filtering, which will degrade at scale. Also uses relative imports throughout instead of absolute paths.
packages/backend/tests/web_api/web.user.test.ts New test suite covering getCurrentUserRole, searchUsers (auth denial + happy path), and updateUserRole; good coverage of the added mutations. Uses relative imports instead of absolute paths per project convention.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: apps/nextjs/src/components/admin-user-management.tsx
Line: 142-145

Comment:
**Use `clsx` for conditional class names**

String template interpolation is used to combine static and dynamic class names here, which violates the project convention of using `clsx` for conditional class composition.

```suggestion
                    <span
                      className={clsx("rounded-full px-2 py-0.5 text-xs font-medium", roleBadgeClassName(user.role))}
```

Add `import { clsx } from "clsx";` (or wherever `clsx` is imported from in this project) at the top of the file.

**Rule Used:** Use the clsx utility for conditional CSS class nam... ([source](https://app.greptile.com/review/custom-context?memory=db90354d-2e07-48d1-b0b1-61bc79768a32))

**Learnt From**
[deltahacks/landing-12#14](https://github.com/deltahacks/landing-12/pull/14)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/backend/convex/web/user.ts
Line: 3-6

Comment:
**Relative imports instead of absolute paths**

All four imports here use relative paths (`../helpers/roles`, `../_generated/server`, `../auth`). The project convention requires absolute paths (e.g., starting with `~/` or a package alias) instead of relative paths in TypeScript files.

The same pattern appears in the test file at `packages/backend/tests/web_api/web.user.test.ts` lines 3–8, where all imports use `../../convex/...` and `../helpers/...` relative paths.

**Rule Used:** Always use absolute paths (starting with '~/' or s... ([source](https://app.greptile.com/review/custom-context?memory=b2963068-1d68-42ee-accd-2fab5b14abcd))

**Learnt From**
[deltahacks/landing-12#5](https://github.com/deltahacks/landing-12/pull/5)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/backend/convex/web/user.ts
Line: 120

Comment:
**Full table scan on every search request**

`ctx.db.query("users").collect()` loads every user record unconditionally, then for each one issues a separate `authComponent.getAnyUserById` call. With N users that's N sequential auth-profile lookups followed by in-memory filtering.

Even though the result is sliced to 50, all N records and all N auth lookups happen first. As the user base grows this will become very slow and could exceed Convex query execution limits.

Consider paginating the scan or building an index that allows filtering before enrichment, so only the matching users need their auth profiles fetched.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/nextjs/src/components/admin-user-management.tsx
Line: 164-167

Comment:
**Self-demotion guard only blocks non-admin roles, not all role changes**

The condition `user.isCurrentUser && role !== "admin"` disables the Student and Teacher buttons for the current user but leaves the Admin button **enabled** on their own row. Because `user.role === role` already disables it when they are already admin, in practice all three buttons end up disabled — but the intent is fragile and confusing.

If the current admin's role were ever non-admin when this page is rendered (edge case in a race), the Admin button on their own row would not be disabled, potentially allowing an unexpected self-role change.

A cleaner and safer guard is:

```suggestion
                    const isDisabled =
                      isPending ||
                      user.role === role ||
                      user.isCurrentUser;
```

This disables all role-change buttons for the current user's own row, which matches what the backend enforces (`Admins cannot remove their own admin access`) and makes the protection explicit and unconditional.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "feat: admin role page" | Re-trigger Greptile

Comment on lines +142 to +145
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${roleBadgeClassName(
user.role,
)}`}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Use clsx for conditional class names

String template interpolation is used to combine static and dynamic class names here, which violates the project convention of using clsx for conditional class composition.

Suggested change
<span
className={`rounded-full px-2 py-0.5 text-xs font-medium ${roleBadgeClassName(
user.role,
)}`}
<span
className={clsx("rounded-full px-2 py-0.5 text-xs font-medium", roleBadgeClassName(user.role))}

Add import { clsx } from "clsx"; (or wherever clsx is imported from in this project) at the top of the file.

Rule Used: Use the clsx utility for conditional CSS class nam... (source)

Learnt From
deltahacks/landing-12#14

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/nextjs/src/components/admin-user-management.tsx
Line: 142-145

Comment:
**Use `clsx` for conditional class names**

String template interpolation is used to combine static and dynamic class names here, which violates the project convention of using `clsx` for conditional class composition.

```suggestion
                    <span
                      className={clsx("rounded-full px-2 py-0.5 text-xs font-medium", roleBadgeClassName(user.role))}
```

Add `import { clsx } from "clsx";` (or wherever `clsx` is imported from in this project) at the top of the file.

**Rule Used:** Use the clsx utility for conditional CSS class nam... ([source](https://app.greptile.com/review/custom-context?memory=db90354d-2e07-48d1-b0b1-61bc79768a32))

**Learnt From**
[deltahacks/landing-12#14](https://github.com/deltahacks/landing-12/pull/14)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +3 to +6
import type { UserRole } from "../helpers/roles";
import { mutation, MutationCtx, query, QueryCtx } from "../_generated/server";
import { authComponent } from "../auth";
import { getUserRole as getStoredUserRole } from "../helpers/roles";
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Relative imports instead of absolute paths

All four imports here use relative paths (../helpers/roles, ../_generated/server, ../auth). The project convention requires absolute paths (e.g., starting with ~/ or a package alias) instead of relative paths in TypeScript files.

The same pattern appears in the test file at packages/backend/tests/web_api/web.user.test.ts lines 3–8, where all imports use ../../convex/... and ../helpers/... relative paths.

Rule Used: Always use absolute paths (starting with '~/' or s... (source)

Learnt From
deltahacks/landing-12#5

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/backend/convex/web/user.ts
Line: 3-6

Comment:
**Relative imports instead of absolute paths**

All four imports here use relative paths (`../helpers/roles`, `../_generated/server`, `../auth`). The project convention requires absolute paths (e.g., starting with `~/` or a package alias) instead of relative paths in TypeScript files.

The same pattern appears in the test file at `packages/backend/tests/web_api/web.user.test.ts` lines 3–8, where all imports use `../../convex/...` and `../helpers/...` relative paths.

**Rule Used:** Always use absolute paths (starting with '~/' or s... ([source](https://app.greptile.com/review/custom-context?memory=b2963068-1d68-42ee-accd-2fab5b14abcd))

**Learnt From**
[deltahacks/landing-12#5](https://github.com/deltahacks/landing-12/pull/5)

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

await requireAdmin(ctx, authUser._id);

const normalizedSearch = args.searchTerm?.trim().toLowerCase() ?? "";
const users = await ctx.db.query("users").collect();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Full table scan on every search request

ctx.db.query("users").collect() loads every user record unconditionally, then for each one issues a separate authComponent.getAnyUserById call. With N users that's N sequential auth-profile lookups followed by in-memory filtering.

Even though the result is sliced to 50, all N records and all N auth lookups happen first. As the user base grows this will become very slow and could exceed Convex query execution limits.

Consider paginating the scan or building an index that allows filtering before enrichment, so only the matching users need their auth profiles fetched.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/backend/convex/web/user.ts
Line: 120

Comment:
**Full table scan on every search request**

`ctx.db.query("users").collect()` loads every user record unconditionally, then for each one issues a separate `authComponent.getAnyUserById` call. With N users that's N sequential auth-profile lookups followed by in-memory filtering.

Even though the result is sliced to 50, all N records and all N auth lookups happen first. As the user base grows this will become very slow and could exceed Convex query execution limits.

Consider paginating the scan or building an index that allows filtering before enrichment, so only the matching users need their auth profiles fetched.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think the search term should be part of the sql query so we don't have to return the whole user table. but i don't think this app will ever have more than 5 users to it's fine

Comment on lines +164 to +167
const isDisabled =
isPending ||
user.role === role ||
(user.isCurrentUser && role !== "admin");
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Self-demotion guard only blocks non-admin roles, not all role changes

The condition user.isCurrentUser && role !== "admin" disables the Student and Teacher buttons for the current user but leaves the Admin button enabled on their own row. Because user.role === role already disables it when they are already admin, in practice all three buttons end up disabled — but the intent is fragile and confusing.

If the current admin's role were ever non-admin when this page is rendered (edge case in a race), the Admin button on their own row would not be disabled, potentially allowing an unexpected self-role change.

A cleaner and safer guard is:

Suggested change
const isDisabled =
isPending ||
user.role === role ||
(user.isCurrentUser && role !== "admin");
const isDisabled =
isPending ||
user.role === role ||
user.isCurrentUser;

This disables all role-change buttons for the current user's own row, which matches what the backend enforces (Admins cannot remove their own admin access) and makes the protection explicit and unconditional.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/nextjs/src/components/admin-user-management.tsx
Line: 164-167

Comment:
**Self-demotion guard only blocks non-admin roles, not all role changes**

The condition `user.isCurrentUser && role !== "admin"` disables the Student and Teacher buttons for the current user but leaves the Admin button **enabled** on their own row. Because `user.role === role` already disables it when they are already admin, in practice all three buttons end up disabled — but the intent is fragile and confusing.

If the current admin's role were ever non-admin when this page is rendered (edge case in a race), the Admin button on their own row would not be disabled, potentially allowing an unexpected self-role change.

A cleaner and safer guard is:

```suggestion
                    const isDisabled =
                      isPending ||
                      user.role === role ||
                      user.isCurrentUser;
```

This disables all role-change buttons for the current user's own row, which matches what the backend enforces (`Admins cannot remove their own admin access`) and makes the protection explicit and unconditional.

How can I resolve this? If you propose a fix, please make it concise.

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