Skip to content

Commit 25a03f1

Browse files
waleedlatif1claude
andauthored
feat(auth): migrate to better-auth admin plugin with unified Admin tab (#3612)
* feat(auth): migrate to better-auth admin plugin * feat(settings): add unified Admin tab with user management 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. * fix(db): backfill existing super users to admin role in migration Add UPDATE statement to promote is_super_user=true rows to role='admin' before dropping the is_super_user column, preventing silent demotion. * fix(admin): resolve type errors in admin tab - 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 * improvement(auth): remove /api/user/super-user route, use session role 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. * chore(auth): remove redundant role override in customSession The admin plugin already includes role on the user object. No need to manually spread it in customSession. * improvement(queries): clean up admin-users hooks per React Query best 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 * fix(admin): address review feedback on admin tab - 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 * fix(settings): align superUserModeEnabled default to false everywhere Three places defaulted to true while admin tab and sidebar used false. Align all to false so new admins see consistent behavior. * fix(admin): fix stale pendingUserId, add isPending guard and error feedback - 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 * fix(admin): concurrent pending users Set, session loading guard, domain 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> * fix(admin): close OAuth domain bypass, fix stale errors, deduplicate 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> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 35c42ba commit 25a03f1

File tree

24 files changed

+14103
-230
lines changed

24 files changed

+14103
-230
lines changed

apps/sim/app/_shell/providers/session-provider.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type AppSession = {
1313
emailVerified?: boolean
1414
name?: string | null
1515
image?: string | null
16+
role?: string
1617
createdAt?: Date
1718
updatedAt?: Date
1819
} | null

apps/sim/app/api/user/super-user/route.ts

Lines changed: 0 additions & 42 deletions
This file was deleted.

apps/sim/app/api/users/me/settings/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export async function GET() {
7272
emailPreferences: userSettings.emailPreferences ?? {},
7373
billingUsageNotificationsEnabled: userSettings.billingUsageNotificationsEnabled ?? true,
7474
showTrainingControls: userSettings.showTrainingControls ?? false,
75-
superUserModeEnabled: userSettings.superUserModeEnabled ?? true,
75+
superUserModeEnabled: userSettings.superUserModeEnabled ?? false,
7676
errorNotificationsEnabled: userSettings.errorNotificationsEnabled ?? true,
7777
snapToGridSize: userSettings.snapToGridSize ?? 0,
7878
showActionBar: userSettings.showActionBar ?? true,

apps/sim/app/templates/[id]/template.tsx

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
148148
const [currentUserOrgRoles, setCurrentUserOrgRoles] = useState<
149149
Array<{ organizationId: string; role: string }>
150150
>([])
151-
const [isSuperUser, setIsSuperUser] = useState(false)
151+
const isSuperUser = session?.user?.role === 'admin'
152152
const [isUsing, setIsUsing] = useState(false)
153153
const [isEditing, setIsEditing] = useState(false)
154154
const [isApproving, setIsApproving] = useState(false)
@@ -186,21 +186,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
186186
}
187187
}
188188

189-
const fetchSuperUserStatus = async () => {
190-
if (!currentUserId) return
191-
192-
try {
193-
const response = await fetch('/api/user/super-user')
194-
if (response.ok) {
195-
const data = await response.json()
196-
setIsSuperUser(data.isSuperUser || false)
197-
}
198-
} catch (error) {
199-
logger.error('Error fetching super user status:', error)
200-
}
201-
}
202-
203-
fetchSuperUserStatus()
204189
fetchUserOrganizations()
205190
}, [currentUserId])
206191

apps/sim/app/workspace/[workspaceId]/settings/[section]/settings.tsx

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import dynamic from 'next/dynamic'
44
import { useSearchParams } from 'next/navigation'
55
import { Skeleton } from '@/components/emcn'
6+
import { useSession } from '@/lib/auth/auth-client'
7+
import { AdminSkeleton } from '@/app/workspace/[workspaceId]/settings/components/admin/admin-skeleton'
68
import { ApiKeysSkeleton } from '@/app/workspace/[workspaceId]/settings/components/api-keys/api-key-skeleton'
79
import { BYOKSkeleton } from '@/app/workspace/[workspaceId]/settings/components/byok/byok-skeleton'
810
import { CopilotSkeleton } from '@/app/workspace/[workspaceId]/settings/components/copilot/copilot-skeleton'
911
import { CredentialSetsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credential-sets/credential-sets-skeleton'
1012
import { CredentialsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/credentials/credential-skeleton'
1113
import { CustomToolsSkeleton } from '@/app/workspace/[workspaceId]/settings/components/custom-tools/custom-tool-skeleton'
12-
import { DebugSkeleton } from '@/app/workspace/[workspaceId]/settings/components/debug/debug-skeleton'
1314
import { GeneralSkeleton } from '@/app/workspace/[workspaceId]/settings/components/general/general-skeleton'
1415
import { InboxSkeleton } from '@/app/workspace/[workspaceId]/settings/components/inbox/inbox-skeleton'
1516
import { McpSkeleton } from '@/app/workspace/[workspaceId]/settings/components/mcp/mcp-skeleton'
@@ -130,10 +131,10 @@ const Inbox = dynamic(
130131
import('@/app/workspace/[workspaceId]/settings/components/inbox/inbox').then((m) => m.Inbox),
131132
{ loading: () => <InboxSkeleton /> }
132133
)
133-
const Debug = dynamic(
134+
const Admin = dynamic(
134135
() =>
135-
import('@/app/workspace/[workspaceId]/settings/components/debug/debug').then((m) => m.Debug),
136-
{ loading: () => <DebugSkeleton /> }
136+
import('@/app/workspace/[workspaceId]/settings/components/admin/admin').then((m) => m.Admin),
137+
{ loading: () => <AdminSkeleton /> }
137138
)
138139
const RecentlyDeleted = dynamic(
139140
() =>
@@ -157,9 +158,15 @@ interface SettingsPageProps {
157158
export function SettingsPage({ section }: SettingsPageProps) {
158159
const searchParams = useSearchParams()
159160
const mcpServerId = searchParams.get('mcpServerId')
161+
const { data: session, isPending: sessionLoading } = useSession()
160162

163+
const isAdminRole = session?.user?.role === 'admin'
161164
const effectiveSection =
162-
!isBillingEnabled && (section === 'subscription' || section === 'team') ? 'general' : section
165+
!isBillingEnabled && (section === 'subscription' || section === 'team')
166+
? 'general'
167+
: section === 'admin' && !sessionLoading && !isAdminRole
168+
? 'general'
169+
: section
163170

164171
const label =
165172
allNavigationItems.find((item) => item.id === effectiveSection)?.label ?? effectiveSection
@@ -185,7 +192,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
185192
{effectiveSection === 'workflow-mcp-servers' && <WorkflowMcpServers />}
186193
{effectiveSection === 'inbox' && <Inbox />}
187194
{effectiveSection === 'recently-deleted' && <RecentlyDeleted />}
188-
{effectiveSection === 'debug' && <Debug />}
195+
{effectiveSection === 'admin' && <Admin />}
189196
</div>
190197
)
191198
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Skeleton } from '@/components/emcn'
2+
3+
export function AdminSkeleton() {
4+
return (
5+
<div className='flex h-full flex-col gap-[24px]'>
6+
<div className='flex items-center justify-between'>
7+
<Skeleton className='h-[14px] w-[120px]' />
8+
<Skeleton className='h-[20px] w-[36px] rounded-full' />
9+
</div>
10+
<div className='flex flex-col gap-[8px]'>
11+
<Skeleton className='h-[14px] w-[340px]' />
12+
<div className='flex gap-[8px]'>
13+
<Skeleton className='h-9 flex-1 rounded-[6px]' />
14+
<Skeleton className='h-9 w-[80px] rounded-[6px]' />
15+
</div>
16+
</div>
17+
<div className='flex flex-col gap-[8px]'>
18+
<Skeleton className='h-[14px] w-[120px]' />
19+
<Skeleton className='h-[200px] w-full rounded-[8px]' />
20+
</div>
21+
</div>
22+
)
23+
}

0 commit comments

Comments
 (0)