Skip to content

feat(auth): migrate to better-auth admin plugin with unified Admin tab#3612

Merged
waleedlatif1 merged 12 commits intostagingfrom
waleedlatif1/better-auth-admin
Mar 17, 2026
Merged

feat(auth): migrate to better-auth admin plugin with unified Admin tab#3612
waleedlatif1 merged 12 commits intostagingfrom
waleedlatif1/better-auth-admin

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 16, 2026

Summary

  • Replace custom isSuperUser boolean with better-auth's native admin plugin (role, banned, banReason, banExpires on user table, impersonatedBy on session table)
  • Add unified Admin settings tab consolidating super admin toggle, workflow import, and user management (list, set role, ban/unban)
  • Remove redundant /api/user/super-user API route — role now available directly from session via customSession + admin plugin
  • Backfill migration promotes existing is_super_user = true rows to role = 'admin' before dropping the column

Changes

  • Schema: isSuperUserrole text field + banned/banReason/banExpires columns, impersonatedBy on session
  • Auth config: Register admin() server plugin and adminClient() client plugin
  • Session: user.role exposed via better-auth's customSession — no custom override needed
  • Admin tab: New settings tab with React Query hooks (useAdminUsers, useSetUserRole, useBanUser, useUnbanUser) following project patterns
  • Navigation: requiresAdminRole gate (checks role only, not mode toggle) so admin tab stays accessible
  • Cleanup: Delete Debug tab, super-user route, useSuperUserStatus hook

Test plan

  • Verify migration backfills existing super users to admin role
  • Confirm admin tab visible for admin-role users, hidden for regular users
  • Test super admin mode toggle persists correctly
  • Test workflow import from Admin tab
  • Test user management: list, promote/demote, ban/unban
  • Verify session includes user.role field after login

@vercel
Copy link

vercel bot commented Mar 16, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Mar 17, 2026 10:03pm

Request Review

@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR migrates super-user management from a custom boolean column to better-auth's native admin plugin, replacing the old is_super_user field with role, banned, ban_reason, and ban_expires columns, and consolidating all admin controls into a new unified Admin settings tab that replaces the old Debug tab.

Key changes:

  • DB schema and migration: new columns added with a backfill step that promotes existing super-users to role = 'admin' before the old column is dropped — no data loss
  • Auth config: admin() server plugin and adminClient() browser plugin registered; customSession pass-through exposes user.role in the session
  • New Admin settings tab with React Query hooks for listing users, setting roles, banning/unbanning; session-loading guard prevents a redirect flash for admin users navigating directly to the admin URL
  • Redundant /api/user/super-user route and useSuperUserStatus hook removed; superUserModeEnabled defaults unified to false across all call sites

Issues found:

  • The new BLOCKED_SIGNUP_DOMAINS feature in auth.ts only guards the email/password /sign-up path. OAuth-based account creation (social providers) uses different paths and bypasses this check entirely, so users from blocked domains can still register via social login
  • The admin and access-control navigation items share the same ShieldCheck icon, making them visually indistinguishable when both are visible
  • requiresSuperUser on NavigationItem and its corresponding sidebar check are now dead code — no navigation items use this field after the Debug tab was removed

Confidence Score: 3/5

  • Mostly safe to merge, but the new BLOCKED_SIGNUP_DOMAINS feature has a meaningful security gap that should be addressed before relying on it in production.
  • The core migration (schema, admin plugin, session wiring) is clean, the backfill is correct, and the previously-reported issues have all been fixed in follow-up commits. The score is lowered because the new BLOCKED_SIGNUP_DOMAINS env var silently fails to block OAuth sign-ups — if the operator deploys with this setting expecting it to enforce domain restrictions, blocked users can trivially bypass it via social login.
  • apps/sim/lib/auth/auth.ts — the BLOCKED_SIGNUP_DOMAINS middleware does not cover OAuth/social sign-up paths.

Important Files Changed

Filename Overview
apps/sim/lib/auth/auth.ts Adds the admin() plugin and a new BLOCKED_SIGNUP_DOMAINS middleware check. The domain-block only guards /sign-up paths, leaving OAuth-based account creation unprotected — users from blocked domains can still register via social providers.
apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx New 321-line Admin settings tab consolidating super-admin toggle, workflow import, and paginated user management (promote/demote, ban/unban). Previously reported issues (banReason bleed, default mismatch, pendingUserId Set, mutation errors) are all addressed in the head commit.
apps/sim/hooks/queries/admin-users.ts New hooks wrapping better-auth's client.admin API for listing users, setting roles, banning, and unbanning. Follows project query patterns with React Query, keepPreviousData for pagination, and cache invalidation on settle.
packages/db/migrations/0177_wise_puma.sql Migration adds role, banned, ban_reason, ban_expires to user and impersonated_by to session. Includes the backfill UPDATE … WHERE is_super_user = true before dropping the column — no data loss for existing super users.
apps/sim/app/workspace/[workspaceId]/settings/navigation.ts Replaces debug/requiresSuperUser item with admin/requiresAdminRole. Both admin and access-control items now share the ShieldCheck icon; requiresSuperUser interface field and sidebar check become dead code.
packages/db/schema.ts Replaces isSuperUser: boolean with role: text, banned: boolean, banReason: text, banExpires: timestamp on the user table and adds impersonatedBy: text to session — aligns the Drizzle schema with better-auth admin plugin expectations.
apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx Swaps Debug dynamic import for Admin, adds session-aware guard that redirects non-admins away from /settings/admin, and respects the sessionLoading flag to prevent a flash to General on first render for admin users.
apps/sim/lib/auth/auth-client.ts Adds adminClient() plugin to the better-auth client, enabling client.admin.* methods in the browser for the new Admin settings tab.
apps/sim/hooks/queries/user-profile.ts Removes the now-redundant useSuperUserStatus hook and userProfileKeys.superUser cache key — super-user status is derived directly from session.user.role instead of a separate API round-trip.

Sequence Diagram

sequenceDiagram
    participant Browser
    participant AdminUI as Admin Settings Tab
    participant AuthAPI as better-auth admin endpoints
    participant DB as PostgreSQL

    Browser->>AdminUI: Navigate to settings/admin
    AdminUI->>AdminUI: Check session user role
    alt role is not admin
        AdminUI->>Browser: Redirect to settings/general
    end

    Browser->>AdminUI: Click Load Users
    AdminUI->>AuthAPI: listUsers with limit and offset
    AuthAPI->>DB: Query user table
    DB-->>AuthAPI: Return paginated rows
    AuthAPI-->>AdminUI: Users array with total count
    AdminUI-->>Browser: Render user management table

    Browser->>AdminUI: Click Promote or Demote button
    AdminUI->>AuthAPI: setRole with userId and target role
    AuthAPI->>DB: Update role column
    AuthAPI-->>AdminUI: Success
    AdminUI->>AdminUI: Invalidate adminUsers cache

    Browser->>AdminUI: Click Ban then enter reason then Confirm
    AdminUI->>AuthAPI: banUser with userId and optional reason
    AuthAPI->>DB: Set banned true and store reason
    AuthAPI-->>AdminUI: Success
    AdminUI->>AdminUI: Invalidate adminUsers cache
Loading

Comments Outside Diff (1)

  1. apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx, line 118-122 (link)

    P2 requiresSuperUser check is now dead code

    After this PR, no NavigationItem in allNavigationItems sets requiresSuperUser: true — the former debug item that used it has been replaced by the admin item (which uses requiresAdminRole). The block at lines 118–122 is therefore unreachable.

    The requiresSuperUser field also still exists on the NavigationItem interface in navigation.ts. Leaving both in place is misleading: a future developer adding an item with requiresSuperUser: true would expect it to gate on the old boolean flag, but isSuperUser is now computed from session?.user?.role === 'admin', so the semantics have silently shifted.

    Consider removing requiresSuperUser from the NavigationItem interface and deleting this dead block, or add a comment explaining the retained semantics.

Last reviewed commit: 02ac5da

Consolidate superuser features into a single Admin settings tab:
- Super admin mode toggle (moved from General)
- Workflow import (moved from Debug)
- User management via better-auth admin (list, set role, ban/unban)

Replace Debug tab with Admin tab gated by requiresAdminRole.
Add React Query hooks for admin user operations.
Add UPDATE statement to promote is_super_user=true rows to role='admin'
before dropping the is_super_user column, preventing silent demotion.
- Fix cn import path to @/lib/core/utils/cn
- Use valid Badge variants (blue/gray/red/green instead of secondary/destructive)
- Type setRole param as 'user' | 'admin' union
Include user.role in customSession so it's available client-side.
Replace all useSuperUserStatus() calls with session.user.role === 'admin'.
Delete the now-redundant /api/user/super-user endpoint.
The admin plugin already includes role on the user object.
No need to manually spread it in customSession.
… practices

- Remove unsafe unknown/Record casting, use better-auth typed response
- Add placeholderData: keepPreviousData for paginated variable-key query
- Remove nullable types where defaults are always applied
@waleedlatif1 waleedlatif1 changed the title feat(auth): migrate to better-auth admin plugin feat(auth): migrate to better-auth admin plugin with unified Admin tab Mar 16, 2026
@waleedlatif1
Copy link
Collaborator Author

@greptile

- Fix superUserModeEnabled default to false (matches sidebar behavior)
- Reset banReason when switching ban target to prevent state bleed
- Guard admin section render with session role check for direct URL access
@waleedlatif1
Copy link
Collaborator Author

@greptile

Three places defaulted to true while admin tab and sidebar used false.
Align all to false so new admins see consistent behavior.
@cursor
Copy link

cursor bot commented Mar 17, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

@greptile

@waleedlatif1
Copy link
Collaborator Author

@greptile

…edback

- Only read mutation.variables when mutation isPending (prevents stale ID)
- Add isPending guard to super user mode toggle (prevents concurrent mutations)
- Show inline error message when setRole/ban/unban mutations fail
@cursor
Copy link

cursor bot commented Mar 17, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

@greptile

…in blocking

- Replace pendingUserId scalar with pendingUserIds Set (useMemo) so concurrent
  mutations across different users each disable their own row correctly
- Add sessionLoading guard to admin section redirect to prevent flash on direct
  /settings/admin navigation before session resolves
- Add BLOCKED_SIGNUP_DOMAINS env var and before-hook for email domain denylist,
  parsed once at module init as a Set for O(1) per-request lookups
- Add trailing newline to migration file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@greptile

…icon

- Add databaseHooks.user.create.before to enforce BLOCKED_SIGNUP_DOMAINS at
  the model level, covering all signup vectors (email, OAuth, social) not just
  /sign-up paths
- Call .reset() on each mutation before firing to clear stale error state from
  previous operations
- Change Admin nav icon from ShieldCheck to Lock to avoid duplicate with
  Access Control tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@waleedlatif1 waleedlatif1 merged commit 25a03f1 into staging Mar 17, 2026
6 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/better-auth-admin branch March 17, 2026 22:04
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