Skip to content

Commit 02ac5da

Browse files
waleedlatif1claude
andcommitted
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>
1 parent fec89ef commit 02ac5da

File tree

5 files changed

+39
-13
lines changed

5 files changed

+39
-13
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,13 +158,13 @@ interface SettingsPageProps {
158158
export function SettingsPage({ section }: SettingsPageProps) {
159159
const searchParams = useSearchParams()
160160
const mcpServerId = searchParams.get('mcpServerId')
161-
const { data: session } = useSession()
161+
const { data: session, isPending: sessionLoading } = useSession()
162162

163163
const isAdminRole = session?.user?.role === 'admin'
164164
const effectiveSection =
165165
!isBillingEnabled && (section === 'subscription' || section === 'team')
166166
? 'general'
167-
: section === 'admin' && !isAdminRole
167+
: section === 'admin' && !sessionLoading && !isAdminRole
168168
? 'general'
169169
: section
170170

apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -70,12 +70,23 @@ export function Admin() {
7070
}
7171
}
7272

73-
const actionPending = setUserRole.isPending || banUser.isPending || unbanUser.isPending
74-
const pendingUserId =
75-
(setUserRole.isPending ? (setUserRole.variables as { userId?: string })?.userId : undefined) ??
76-
(banUser.isPending ? (banUser.variables as { userId?: string })?.userId : undefined) ??
77-
(unbanUser.isPending ? (unbanUser.variables as { userId?: string })?.userId : undefined) ??
78-
null
73+
const pendingUserIds = useMemo(() => {
74+
const ids = new Set<string>()
75+
if (setUserRole.isPending && (setUserRole.variables as { userId?: string })?.userId)
76+
ids.add((setUserRole.variables as { userId: string }).userId)
77+
if (banUser.isPending && (banUser.variables as { userId?: string })?.userId)
78+
ids.add((banUser.variables as { userId: string }).userId)
79+
if (unbanUser.isPending && (unbanUser.variables as { userId?: string })?.userId)
80+
ids.add((unbanUser.variables as { userId: string }).userId)
81+
return ids
82+
}, [
83+
setUserRole.isPending,
84+
setUserRole.variables,
85+
banUser.isPending,
86+
banUser.variables,
87+
unbanUser.isPending,
88+
unbanUser.variables,
89+
])
7990
return (
8091
<div className='flex h-full flex-col gap-[24px]'>
8192
<div className='flex items-center justify-between'>
@@ -204,7 +215,7 @@ export function Admin() {
204215
role: u.role === 'admin' ? 'user' : 'admin',
205216
})
206217
}
207-
disabled={actionPending && pendingUserId === u.id}
218+
disabled={pendingUserIds.has(u.id)}
208219
>
209220
{u.role === 'admin' ? 'Demote' : 'Promote'}
210221
</Button>
@@ -213,7 +224,7 @@ export function Admin() {
213224
variant='active'
214225
className='h-[28px] px-[8px] text-[12px]'
215226
onClick={() => unbanUser.mutate({ userId: u.id })}
216-
disabled={actionPending && pendingUserId === u.id}
227+
disabled={pendingUserIds.has(u.id)}
217228
>
218229
Unban
219230
</Button>
@@ -242,7 +253,7 @@ export function Admin() {
242253
}
243254
)
244255
}}
245-
disabled={actionPending && pendingUserId === u.id}
256+
disabled={pendingUserIds.has(u.id)}
246257
>
247258
Confirm
248259
</Button>
@@ -265,7 +276,7 @@ export function Admin() {
265276
setBanUserId(u.id)
266277
setBanReason('')
267278
}}
268-
disabled={actionPending && pendingUserId === u.id}
279+
disabled={pendingUserIds.has(u.id)}
269280
>
270281
Ban
271282
</Button>

apps/sim/lib/auth/auth.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ const logger = createLogger('Auth')
7979
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
8080
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
8181

82+
const blockedSignupDomains = env.BLOCKED_SIGNUP_DOMAINS
83+
? new Set(env.BLOCKED_SIGNUP_DOMAINS.split(',').map((d) => d.trim().toLowerCase()))
84+
: null
85+
8286
const validStripeKey = env.STRIPE_SECRET_KEY
8387

8488
let stripeClient = null
@@ -599,6 +603,16 @@ export const auth = betterAuth({
599603
}
600604
}
601605

606+
if (ctx.path.startsWith('/sign-up') && blockedSignupDomains) {
607+
const requestEmail = ctx.body?.email?.toLowerCase()
608+
if (requestEmail) {
609+
const emailDomain = requestEmail.split('@')[1]
610+
if (emailDomain && blockedSignupDomains.has(emailDomain)) {
611+
throw new Error('Sign-ups from this email domain are not allowed.')
612+
}
613+
}
614+
}
615+
602616
if (ctx.path === '/oauth2/authorize' || ctx.path === '/oauth2/token') {
603617
const clientId = (ctx.query?.client_id ?? ctx.body?.client_id) as string | undefined
604618
if (clientId && isMetadataUrl(clientId)) {

apps/sim/lib/core/config/env.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const env = createEnv({
2424
DISABLE_AUTH: z.boolean().optional(), // Bypass authentication entirely (self-hosted only, creates anonymous session)
2525
ALLOWED_LOGIN_EMAILS: z.string().optional(), // Comma-separated list of allowed email addresses for login
2626
ALLOWED_LOGIN_DOMAINS: z.string().optional(), // Comma-separated list of allowed email domains for login
27+
BLOCKED_SIGNUP_DOMAINS: z.string().optional(), // Comma-separated list of email domains blocked from signing up (e.g., "gmail.com,yahoo.com")
2728
ENCRYPTION_KEY: z.string().min(32), // Key for encrypting sensitive data
2829
API_ENCRYPTION_KEY: z.string().min(32).optional(), // Dedicated key for encrypting API keys (optional for OSS)
2930
INTERNAL_API_SECRET: z.string().min(32), // Secret for internal API authentication

packages/db/migrations/0177_wise_puma.sql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ UPDATE "user" SET "role" = 'admin' WHERE "is_super_user" = true;--> statement-br
44
ALTER TABLE "user" ADD COLUMN "banned" boolean DEFAULT false;--> statement-breakpoint
55
ALTER TABLE "user" ADD COLUMN "ban_reason" text;--> statement-breakpoint
66
ALTER TABLE "user" ADD COLUMN "ban_expires" timestamp;--> statement-breakpoint
7-
ALTER TABLE "user" DROP COLUMN "is_super_user";
7+
ALTER TABLE "user" DROP COLUMN "is_super_user";

0 commit comments

Comments
 (0)