Skip to content

Commit d53a41f

Browse files
author
Theodore Li
committed
Add explicit role check
1 parent d209682 commit d53a41f

File tree

4 files changed

+115
-49
lines changed

4 files changed

+115
-49
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { Banner } from '@/components/emcn'
5+
import { useStopImpersonating } from '@/hooks/queries/admin-users'
6+
import { useSession } from '@/lib/auth/auth-client'
7+
8+
function getImpersonationBannerText(userLabel: string, userEmail?: string) {
9+
return `Impersonating ${userLabel}${userEmail ? ` (${userEmail})` : ''}. Changes will apply to this account until you switch back.`
10+
}
11+
12+
export function ImpersonationBanner() {
13+
const { data: session, isPending } = useSession()
14+
const stopImpersonating = useStopImpersonating()
15+
const [isRedirecting, setIsRedirecting] = useState(false)
16+
const userLabel = session?.user?.name || 'this user'
17+
const userEmail = session?.user?.email
18+
19+
if (isPending || !session?.session?.impersonatedBy) {
20+
return null
21+
}
22+
23+
return (
24+
<Banner
25+
variant='destructive'
26+
text={getImpersonationBannerText(userLabel, userEmail)}
27+
textClassName='text-red-700 dark:text-red-300'
28+
actionLabel={stopImpersonating.isPending || isRedirecting ? 'Returning...' : 'Stop impersonating'}
29+
actionVariant='destructive'
30+
actionDisabled={stopImpersonating.isPending || isRedirecting}
31+
onAction={() =>
32+
stopImpersonating.mutate(undefined, {
33+
onError: () => {
34+
setIsRedirecting(false)
35+
},
36+
onSuccess: () => {
37+
setIsRedirecting(true)
38+
window.location.assign('/workspace')
39+
},
40+
})
41+
}
42+
/>
43+
)
44+
}

apps/sim/app/workspace/[workspaceId]/layout.tsx

Lines changed: 2 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,11 @@
1-
import { Banner, Button, ToastProvider } from '@/components/emcn'
1+
import { ToastProvider } from '@/components/emcn'
22
import { GlobalCommandsProvider } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
3+
import { ImpersonationBanner } from '@/app/workspace/[workspaceId]/impersonation-banner'
34
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
45
import { SettingsLoader } from '@/app/workspace/[workspaceId]/providers/settings-loader'
56
import { WorkspacePermissionsProvider } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
6-
import { useSession } from '@/lib/auth/auth-client'
7-
import { useStopImpersonating } from '@/hooks/queries/admin-users'
87
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
98

10-
function ImpersonationBanner() {
11-
const { data: session, isPending } = useSession()
12-
const stopImpersonating = useStopImpersonating()
13-
const userLabel = session?.user?.name || 'this user'
14-
const userEmail = session?.user?.email
15-
16-
if (isPending || !session?.session?.impersonatedBy) {
17-
return null
18-
}
19-
20-
return (
21-
<Banner variant='destructive'>
22-
<div className='mx-auto flex max-w-[1400px] items-center justify-between gap-[12px]'>
23-
<p className='text-[13px] text-red-700 dark:text-red-300'>
24-
Impersonating {userLabel}
25-
{userEmail ? ` (${userEmail})` : ''}. Changes will apply to this account until you switch
26-
back.
27-
</p>
28-
<Button
29-
variant='destructive'
30-
className='h-[28px] shrink-0 px-[8px] text-[12px]'
31-
onClick={() =>
32-
stopImpersonating.mutate(undefined, {
33-
onSuccess: () => {
34-
window.location.assign('/workspace')
35-
},
36-
})
37-
}
38-
disabled={stopImpersonating.isPending}
39-
>
40-
{stopImpersonating.isPending ? 'Returning...' : 'Stop impersonating'}
41-
</Button>
42-
</div>
43-
</Banner>
44-
)
45-
}
46-
479
export default function WorkspaceLayout({ children }: { children: React.ReactNode }) {
4810
return (
4911
<ToastProvider>

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

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export function Admin() {
3737
const [searchQuery, setSearchQuery] = useState('')
3838
const [banUserId, setBanUserId] = useState<string | null>(null)
3939
const [banReason, setBanReason] = useState('')
40+
const [impersonatingUserId, setImpersonatingUserId] = useState<string | null>(null)
41+
const [impersonationGuardError, setImpersonationGuardError] = useState<string | null>(null)
4042

4143
const {
4244
data: usersData,
@@ -70,10 +72,21 @@ export function Admin() {
7072
}
7173

7274
const handleImpersonate = (userId: string) => {
75+
setImpersonationGuardError(null)
76+
if (session?.user?.role !== 'admin') {
77+
setImpersonatingUserId(null)
78+
setImpersonationGuardError('Only admins can impersonate users.')
79+
return
80+
}
81+
82+
setImpersonatingUserId(userId)
7383
impersonateUser.reset()
7484
impersonateUser.mutate(
7585
{ userId },
7686
{
87+
onError: () => {
88+
setImpersonatingUserId(null)
89+
},
7790
onSuccess: () => {
7891
window.location.assign('/workspace')
7992
},
@@ -91,6 +104,7 @@ export function Admin() {
91104
ids.add((unbanUser.variables as { userId: string }).userId)
92105
if (impersonateUser.isPending && (impersonateUser.variables as { userId?: string })?.userId)
93106
ids.add((impersonateUser.variables as { userId: string }).userId)
107+
if (impersonatingUserId) ids.add(impersonatingUserId)
94108
return ids
95109
}, [
96110
setUserRole.isPending,
@@ -101,6 +115,7 @@ export function Admin() {
101115
unbanUser.variables,
102116
impersonateUser.isPending,
103117
impersonateUser.variables,
118+
impersonatingUserId,
104119
])
105120
return (
106121
<div className='flex h-full flex-col gap-[24px]'>
@@ -170,10 +185,15 @@ export function Admin() {
170185
</p>
171186
)}
172187

173-
{(setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error) && (
188+
{(setUserRole.error ||
189+
banUser.error ||
190+
unbanUser.error ||
191+
impersonateUser.error ||
192+
impersonationGuardError) && (
174193
<p className='text-[13px] text-[var(--text-error)]'>
175-
{(setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error)
176-
?.message ??
194+
{impersonationGuardError ||
195+
(setUserRole.error || banUser.error || unbanUser.error || impersonateUser.error)
196+
?.message ||
177197
'Action failed. Please try again.'}
178198
</p>
179199
)}
@@ -234,9 +254,10 @@ export function Admin() {
234254
onClick={() => handleImpersonate(u.id)}
235255
disabled={pendingUserIds.has(u.id)}
236256
>
237-
{impersonateUser.isPending &&
238-
(impersonateUser.variables as { userId?: string } | undefined)?.userId ===
239-
u.id
257+
{(impersonatingUserId === u.id ||
258+
(impersonateUser.isPending &&
259+
(impersonateUser.variables as { userId?: string } | undefined)
260+
?.userId === u.id))
240261
? 'Switching...'
241262
: 'Impersonate'}
242263
</Button>

apps/sim/components/emcn/components/banner/banner.tsx

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import type { HTMLAttributes, ReactNode } from 'react'
44
import { cva, type VariantProps } from 'class-variance-authority'
55
import { cn } from '@/lib/core/utils/cn'
6+
import { Button, type ButtonProps } from '@/components/emcn/components/button/button'
67

78
const bannerVariants = cva('shrink-0 px-[24px] py-[10px]', {
89
variants: {
@@ -19,13 +20,51 @@ const bannerVariants = cva('shrink-0 px-[24px] py-[10px]', {
1920
export interface BannerProps
2021
extends HTMLAttributes<HTMLDivElement>,
2122
VariantProps<typeof bannerVariants> {
22-
children: ReactNode
23+
actionClassName?: string
24+
actionDisabled?: boolean
25+
actionLabel?: ReactNode
26+
actionProps?: Omit<ButtonProps, 'children' | 'className' | 'disabled' | 'onClick' | 'variant'>
27+
actionVariant?: ButtonProps['variant']
28+
children?: ReactNode
29+
contentClassName?: string
30+
onAction?: () => void
31+
text?: ReactNode
32+
textClassName?: string
2333
}
2434

25-
export function Banner({ className, variant, children, ...props }: BannerProps) {
35+
export function Banner({
36+
actionClassName,
37+
actionDisabled,
38+
actionLabel,
39+
actionProps,
40+
actionVariant = 'default',
41+
children,
42+
className,
43+
contentClassName,
44+
onAction,
45+
text,
46+
textClassName,
47+
variant,
48+
...props
49+
}: BannerProps) {
2650
return (
2751
<div className={cn(bannerVariants({ variant }), className)} {...props}>
28-
{children}
52+
{children ?? (
53+
<div className={cn('mx-auto flex max-w-[1400px] items-center justify-between gap-[12px]', contentClassName)}>
54+
<p className={cn('text-[13px]', textClassName)}>{text}</p>
55+
{actionLabel ? (
56+
<Button
57+
variant={actionVariant}
58+
className={cn('h-[28px] shrink-0 px-[8px] text-[12px]', actionClassName)}
59+
onClick={onAction}
60+
disabled={actionDisabled}
61+
{...actionProps}
62+
>
63+
{actionLabel}
64+
</Button>
65+
) : null}
66+
</div>
67+
)}
2968
</div>
3069
)
3170
}

0 commit comments

Comments
 (0)