import { keepPreviousData } from '@tanstack/react-query' import { useDebounce } from '@uidotdev/usehooks' import { LOCAL_STORAGE_KEYS, useParams } from 'common' import { ChevronDown, User as IconUser, Loader2, Search, X } from 'lucide-react' import { useMemo, useState } from 'react' import { toast } from 'sonner' import { Button, cn, Collapsible, CollapsibleContent, CollapsibleTrigger, DropdownMenuSeparator, Input, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, ScrollArea, Switch, Tabs_Shadcn_, TabsContent_Shadcn_, TabsList_Shadcn_, TabsTrigger_Shadcn_, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import { getAvatarUrl, getDisplayName } from '../Auth/Users/Users.utils' import AlertError from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { User, useUsersInfiniteQuery } from '@/data/auth/users-infinite-query' import { useCustomAccessTokenHookDetails } from '@/hooks/misc/useCustomAccessTokenHookDetails' import { useLocalStorage } from '@/hooks/misc/useLocalStorage' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' import { useRoleImpersonationStateSnapshot } from '@/state/role-impersonation-state' import type { ResponseError } from '@/types' type AuthenticatorAssuranceLevels = 'aal1' | 'aal2' export const UserImpersonationSelector = () => { const [searchText, setSearchText] = useState('') const [aal, setAal] = useState('aal1') const [externalUserId, setExternalUserId] = useState('') const [additionalClaims, setAdditionalClaims] = useState('') const { id: tableId } = useParams() const [selectedTab, setSelectedTab] = useState<'user' | 'external'>('user') const [previousSearches, setPreviousSearches] = useLocalStorage( LOCAL_STORAGE_KEYS.USER_IMPERSONATION_SELECTOR_PREVIOUS_SEARCHES(tableId!), [] ) const state = useRoleImpersonationStateSnapshot() const debouncedSearchText = useDebounce(searchText, 300) const { data: project } = useSelectedProjectQuery() const { data, isSuccess, isPending: isLoading, isError, error, isFetching, isPlaceholderData, } = useUsersInfiniteQuery( { projectRef: project?.ref, connectionString: project?.connectionString, keywords: debouncedSearchText.trim().toLocaleLowerCase(), }, { placeholderData: keepPreviousData, } ) const users = useMemo(() => data?.pages.flatMap((page) => page.result) ?? [], [data?.pages]) const isSearching = isPlaceholderData && isFetching const impersonatingUser = state.role?.type === 'postgrest' && state.role.role === 'authenticated' && state.role.userType === 'native' && state.role.user // Check if we're currently impersonating an external auth user (e.g. OAuth, SAML) // This is used to show the correct UI state and impersonation details const isExternalAuthImpersonating = state.role?.type === 'postgrest' && state.role.role === 'authenticated' && state.role.userType === 'external' && state.role.externalAuth const customAccessTokenHookDetails = useCustomAccessTokenHookDetails(project?.ref) const [isImpersonateLoading, setIsImpersonateLoading] = useState(false) async function impersonateUser(user: User) { setIsImpersonateLoading(true) setPreviousSearches((prev) => { // Remove if already present const filtered = prev.filter((u) => u.id !== user.id) // Add new user to the start of the list (last used first) const updated = [user, ...filtered] // Keep only the last 6 return updated.slice(0, 5) }) if (customAccessTokenHookDetails?.type === 'https') { toast.info( 'Please note that HTTPS custom access token hooks are not yet supported in the dashboard.' ) } try { await state.setRole( { type: 'postgrest', role: 'authenticated', userType: 'native', user, aal, }, customAccessTokenHookDetails ) } catch (error) { toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`) } setIsImpersonateLoading(false) } // Impersonates an external auth user (e.g. OAuth, SAML) by setting the sub and any additional claims // This allows testing RLS policies for external auth users without needing to set up the full OAuth/SAML flow async function impersonateExternalUser() { setIsImpersonateLoading(true) let parsedClaims = {} try { parsedClaims = additionalClaims ? JSON.parse(additionalClaims) : {} } catch (e) { toast.error('Invalid JSON in additional claims') setIsImpersonateLoading(false) return } try { await state.setRole( { type: 'postgrest', role: 'authenticated', userType: 'external', externalAuth: { sub: externalUserId, additionalClaims: parsedClaims, }, aal, }, customAccessTokenHookDetails ) } catch (error) { toast.error(`Failed to impersonate user: ${(error as ResponseError).message}`) } setIsImpersonateLoading(false) } function stopImpersonating() { state.setRole(undefined) } function toggleAalState() { setAal((prev) => (prev === 'aal2' ? 'aal1' : 'aal2')) } const displayName = impersonatingUser ? getDisplayName( impersonatingUser, impersonatingUser.email ?? impersonatingUser.phone ?? impersonatingUser.id ?? 'Unknown' ) : isExternalAuthImpersonating ? state.role.externalAuth.sub : undefined // Clear all search history function clearSearchHistory() { setPreviousSearches([]) } return ( <>

{displayName ? `Impersonating ${displayName}` : 'Impersonate a user'}

{!impersonatingUser && !isExternalAuthImpersonating ? "Select a user to respect your database's RLS policies for that particular user." : "Results will respect your database's RLS policies for this user."}

{impersonatingUser && ( )} {isExternalAuthImpersonating && ( )} {!impersonatingUser && !isExternalAuthImpersonating && ( setSelectedTab(value)}> Project user External user Test RLS policies with external auth providers like Clerk or Auth0 by providing a user ID and optional claims.
setSearchText(e.target.value)} value={searchText} /> {isSearching ? ( ) : ( )} {searchText && ( setSearchText('')}> Clear search )} {isLoading && (
Loading users...
)} {isError && } {isSuccess && (users.length > 0 ? (
    {users.map((user) => (
  • ))}
) : (

No users found

))} <> {previousSearches.length > 0 && (
{previousSearches.length > 0 ? ( <>

Recents

3 ? 'h-36' : 'h-auto')} >
    {previousSearches.map((search) => (
  • ))}
) : (
No recent searches
)}
)}
setExternalUserId(e.target.value)} /> setAdditionalClaims(e.target.value)} />
)}
{/* Check for both regular user and external auth impersonation since they use different data structures but both need to be handled for displaying impersonation UI */} {!impersonatingUser && !isExternalAuthImpersonating ? ( <>

Advanced options

MFA assurance level

AAL1 verifies users via standard login methods, while AAL2 adds a second authentication factor. If you're not using MFA, you can leave this on AAL1. Learn more about MFA{' '} here.

AAL1

AAL2

) : null} ) } // Base interface for shared impersonation row props to reduce // duplication between user and external auth impersonation displays interface BaseImpersonatingRowProps { onClick: () => void aal: AuthenticatorAssuranceLevels displayName: string avatarUrl?: string isImpersonating: boolean isLoading?: boolean } const BaseImpersonatingRow = ({ onClick, aal, displayName, avatarUrl, isImpersonating = false, isLoading = false, }: BaseImpersonatingRowProps) => { return (
{avatarUrl ? ( {displayName} ) : (
)} {displayName}{' '} {aal === 'aal2' ? 'AAL2' : 'AAL1'}
) } const UserImpersonatingRow = ({ user, onClick, isImpersonating = false, isLoading = false, aal, }: UserRowProps & { aal: AuthenticatorAssuranceLevels }) => { const avatarUrl = getAvatarUrl(user) const displayName = getDisplayName(user, user.email ?? user.phone ?? user.id ?? 'Unknown') + (user.is_anonymous ? ' (anonymous)' : '') return ( onClick(user)} aal={aal} displayName={displayName} avatarUrl={avatarUrl} isImpersonating={isImpersonating} isLoading={isLoading} /> ) } interface ExternalAuthImpersonatingRowProps { sub: string onClick: () => void aal: AuthenticatorAssuranceLevels isLoading?: boolean } const ExternalAuthImpersonatingRow = ({ sub, onClick, aal, isLoading = false, }: ExternalAuthImpersonatingRowProps) => { return ( ) } interface UserRowProps { user: User onClick: (user: User) => void isImpersonating?: boolean isLoading?: boolean } const UserRow = ({ user, onClick, isImpersonating = false, isLoading = false }: UserRowProps) => { const avatarUrl = getAvatarUrl(user) const emailOrPhone = user.email || user.phone const displayName = getDisplayName(user, '') const isAnonymous = user.is_anonymous const showDisplayName = displayName && displayName !== emailOrPhone return (
{avatarUrl ? ( {displayName ) : (
)} {emailOrPhone} {showDisplayName && ( <> {displayName} {isAnonymous ? ' (anonymous)' : ''} )} {user?.id?.slice(0, 8)}
) }