diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
index 9d7094ab17f..5a2daaa70ca 100644
--- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx
@@ -10,10 +10,13 @@ import {
adminParsers,
adminUrlKeys,
} from '@/app/workspace/[workspaceId]/settings/components/admin/search-params'
+import { useRecentImpersonations } from '@/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations'
import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
import { SettingsPanel } from '@/app/workspace/[workspaceId]/settings/components/settings-panel'
import {
+ type AdminUser,
useAdminUsers,
+ useAdminUsersByEmails,
useBanUser,
useImpersonateUser,
useSetUserRole,
@@ -25,6 +28,16 @@ import { clearUserData } from '@/stores'
const PAGE_SIZE = 20 as const
+const USER_TABLE_HEADER = (
+
+ Name
+ Email
+ Role
+ Status
+ Actions
+
+)
+
const MOTHERSHIP_ENV_OPTIONS: { value: MothershipEnvironment; label: string }[] = [
{ value: 'default', label: 'Default' },
{ value: 'dev', label: 'Dev' },
@@ -43,6 +56,8 @@ export function Admin() {
const banUser = useBanUser()
const unbanUser = useUnbanUser()
const impersonateUser = useImpersonateUser()
+ const { recentEmails, recordImpersonation } = useRecentImpersonations()
+ const { data: recentUsers } = useAdminUsersByEmails(recentEmails)
const [workflowId, setWorkflowId] = useState('')
const [targetWorkspaceId, setTargetWorkspaceId] = useState('')
@@ -94,7 +109,7 @@ export function Admin() {
}
}
- const handleImpersonate = (userId: string) => {
+ const handleImpersonate = (userId: string, email: string) => {
setImpersonationGuardError(null)
if (session?.user?.role !== 'admin') {
setImpersonatingUserId(null)
@@ -111,6 +126,7 @@ export function Admin() {
setImpersonatingUserId(null)
},
onSuccess: async () => {
+ recordImpersonation(email)
await clearUserData()
window.location.assign('/workspace')
},
@@ -156,6 +172,119 @@ export function Admin() {
impersonateUser.variables,
impersonatingUserId,
])
+
+ const renderUserRow = (u: AdminUser) => (
+
+
+ {u.name || '—'}
+ {u.email}
+
+ {u.role || 'user'}
+
+
+ {u.banned ? Banned : Active}
+
+
+ {u.id !== session?.user?.id && (
+ <>
+
+
+ {u.banned ? (
+
+ ) : (
+
+ )}
+ >
+ )}
+
+
+ {banUserId === u.id && !u.banned && (
+
+ setBanReason(e.target.value)}
+ placeholder='Reason (optional)'
+ className='flex-1'
+ />
+
+
+ )}
+
+ )
+
return (
@@ -275,149 +404,16 @@ export function Admin() {
)}
- {searchQuery.length > 0 && usersData && (
+ {searchQuery.length > 0 && usersData ? (
<>
-
- Name
- Email
- Role
- Status
- Actions
-
+ {USER_TABLE_HEADER}
{usersData.users.length === 0 && (
No users found.
)}
- {usersData.users.map((u) => (
-
-
-
- {u.name || '—'}
-
- {u.email}
-
-
- {u.role || 'user'}
-
-
-
- {u.banned ? (
- Banned
- ) : (
- Active
- )}
-
-
- {u.id !== session?.user?.id && (
- <>
-
-
- {u.banned ? (
-
- ) : (
-
- )}
- >
- )}
-
-
- {banUserId === u.id && !u.banned && (
-
- setBanReason(e.target.value)}
- placeholder='Reason (optional)'
- className='flex-1'
- />
-
-
- )}
-
- ))}
+ {usersData.users.map((u) => renderUserRow(u))}
{totalPages > 1 && (
@@ -450,6 +446,15 @@ export function Admin() {
)}
>
+ ) : (
+ searchQuery.length === 0 &&
+ recentUsers &&
+ recentUsers.length > 0 && (
+
+ {USER_TABLE_HEADER}
+ {recentUsers.map((u) => renderUserRow(u))}
+
+ )
)}
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations.ts b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations.ts
new file mode 100644
index 00000000000..4a1eecb92e3
--- /dev/null
+++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/use-recent-impersonations.ts
@@ -0,0 +1,34 @@
+import { useCallback, useState } from 'react'
+import { RECENT_IMPERSONATIONS_STORAGE_KEY } from '@/stores'
+
+const MAX_RECENT = 5
+
+function readRecentEmails(): string[] {
+ try {
+ const parsed = JSON.parse(localStorage.getItem(RECENT_IMPERSONATIONS_STORAGE_KEY) ?? '[]')
+ if (!Array.isArray(parsed)) return []
+ return parsed.filter((e): e is string => typeof e === 'string').slice(0, MAX_RECENT)
+ } catch {
+ return []
+ }
+}
+
+/**
+ * Last {@link MAX_RECENT} emails the admin impersonated, most recent first,
+ * persisted in localStorage on this browser.
+ */
+export function useRecentImpersonations() {
+ const [recentEmails, setRecentEmails] = useState(() => readRecentEmails())
+
+ const recordImpersonation = useCallback((email: string) => {
+ const next = [email, ...readRecentEmails().filter((e) => e !== email)].slice(0, MAX_RECENT)
+ try {
+ localStorage.setItem(RECENT_IMPERSONATIONS_STORAGE_KEY, JSON.stringify(next))
+ } catch {
+ /* best-effort: recording must never block the impersonation transition */
+ }
+ setRecentEmails(next)
+ }, [])
+
+ return { recentEmails, recordImpersonation }
+}
diff --git a/apps/sim/hooks/queries/admin-users.ts b/apps/sim/hooks/queries/admin-users.ts
index 658b6ebe009..afa3fa7c893 100644
--- a/apps/sim/hooks/queries/admin-users.ts
+++ b/apps/sim/hooks/queries/admin-users.ts
@@ -12,9 +12,10 @@ export const adminUserKeys = {
lists: () => [...adminUserKeys.all, 'list'] as const,
list: (offset: number, limit: number, searchQuery: string) =>
[...adminUserKeys.lists(), offset, limit, searchQuery] as const,
+ byEmails: (emails: string[]) => [...adminUserKeys.lists(), 'byEmails', emails] as const,
}
-interface AdminUser {
+export interface AdminUser {
id: string
name: string
email: string
@@ -81,6 +82,41 @@ async function fetchAdminUsers(
}
}
+async function fetchAdminUsersByEmails(
+ emails: string[],
+ signal?: AbortSignal
+): Promise {
+ const results = await Promise.all(
+ emails.map(async (email) => {
+ const { data, error } = await client.admin.listUsers(
+ {
+ query: {
+ limit: 1,
+ filterField: 'email',
+ filterValue: email,
+ filterOperator: 'eq',
+ },
+ },
+ { signal }
+ )
+ if (error) throw new Error(error.message ?? 'Failed to fetch user')
+ const user = (data?.users ?? [])[0]
+ return user ? mapUser(user) : null
+ })
+ )
+ return results.filter((u): u is AdminUser => u !== null)
+}
+
+/** Resolves each email to its exact-match user; unmatched emails are dropped. */
+export function useAdminUsersByEmails(emails: string[]) {
+ return useQuery({
+ queryKey: adminUserKeys.byEmails(emails),
+ queryFn: ({ signal }) => fetchAdminUsersByEmails(emails, signal),
+ enabled: emails.length > 0,
+ staleTime: ADMIN_USER_LIST_STALE_TIME,
+ })
+}
+
export function useAdminUsers(offset: number, limit: number, searchQuery: string) {
return useQuery({
queryKey: adminUserKeys.list(offset, limit, searchQuery),
diff --git a/apps/sim/stores/index.ts b/apps/sim/stores/index.ts
index 71d4c54ba23..529cb891a55 100644
--- a/apps/sim/stores/index.ts
+++ b/apps/sim/stores/index.ts
@@ -12,6 +12,9 @@ import { useWorkflowStore } from '@/stores/workflows/workflow/store'
const logger = createLogger('Stores')
+/** localStorage key for the admin recent-impersonations list; kept through clearUserData. */
+export const RECENT_IMPERSONATIONS_STORAGE_KEY = 'recent-impersonations'
+
/**
* Reset all Zustand stores and React Query caches to initial state.
*/
@@ -50,8 +53,7 @@ export async function clearUserData(): Promise {
try {
resetAllStores()
- // Clear localStorage except for essential app settings
- const keysToKeep = ['next-favicon', 'theme']
+ const keysToKeep = ['next-favicon', 'theme', RECENT_IMPERSONATIONS_STORAGE_KEY]
const keysToRemove = Object.keys(localStorage).filter((key) => !keysToKeep.includes(key))
keysToRemove.forEach((key) => localStorage.removeItem(key))