Skip to content

Commit f775b29

Browse files
authored
fix(settings): protect generated keys and identity transitions (#8761)
* fix(settings): protect generated keys and identity transitions * fix(settings): keep identity transitions guarded until redirect
1 parent 9ab24e9 commit f775b29

12 files changed

Lines changed: 310 additions & 36 deletions

File tree

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

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,13 +91,15 @@ export function Admin() {
9191

9292
const [workflowId, setWorkflowId] = useState('')
9393
const [targetWorkspaceId, setTargetWorkspaceId] = useState('')
94+
const [impersonatingUserId, setImpersonatingUserId] = useState<string | null>(null)
95+
const isImpersonating = impersonatingUserId !== null || impersonateUser.isPending
9496
const discardImport = () => {
9597
setWorkflowId('')
9698
setTargetWorkspaceId('')
9799
}
98100
const importGuard = useSettingsUnsavedGuard({
99101
isDirty: Boolean(workflowId.trim() || targetWorkspaceId.trim()),
100-
navigationBlocked: importWorkflow.isPending,
102+
navigationBlocked: importWorkflow.isPending || isImpersonating,
101103
onDiscard: discardImport,
102104
})
103105

@@ -109,7 +111,6 @@ export function Admin() {
109111
const [searchInput, setSearchInput] = useState(searchQuery)
110112
const [pendingAction, setPendingAction] = useState<PendingUserAction | null>(null)
111113
const [banReason, setBanReason] = useState('')
112-
const [impersonatingUserId, setImpersonatingUserId] = useState<string | null>(null)
113114
const [impersonationGuardError, setImpersonationGuardError] = useState<string | null>(null)
114115
const [isAddUserOpen, setIsAddUserOpen] = useState(false)
115116
const [provisionWarning, setProvisionWarning] = useState<string | null>(null)
@@ -151,6 +152,7 @@ export function Admin() {
151152
}
152153

153154
const handleImpersonate = (userId: string, email: string) => {
155+
if (isImpersonating || importWorkflow.isPending) return
154156
setImpersonationGuardError(null)
155157
if (session?.user?.role !== 'admin') {
156158
setImpersonatingUserId(null)
@@ -180,7 +182,7 @@ export function Admin() {
180182
const handleImport = () => {
181183
const sourceId = workflowId.trim()
182184
const targetId = targetWorkspaceId.trim()
183-
if (!sourceId || !targetId || importWorkflow.isPending || impersonateUser.isPending) return
185+
if (!sourceId || !targetId || importWorkflow.isPending || isImpersonating) return
184186
importWorkflow.mutate(
185187
{ workflowId: sourceId, targetWorkspaceId: targetId },
186188
{
@@ -244,7 +246,7 @@ export function Admin() {
244246
<Chip
245247
aria-label={`Impersonate ${u.email}`}
246248
onClick={() => handleImpersonate(u.id, u.email)}
247-
disabled={importWorkflow.isPending || pendingUserIds.has(u.id)}
249+
disabled={importWorkflow.isPending || isImpersonating || pendingUserIds.has(u.id)}
248250
>
249251
{impersonatingUserId === u.id ? 'Switching...' : 'Impersonate'}
250252
</Chip>
@@ -359,7 +361,7 @@ export function Admin() {
359361
importWorkflow.reset()
360362
}}
361363
placeholder='Source workflow ID'
362-
disabled={importWorkflow.isPending || impersonateUser.isPending}
364+
disabled={importWorkflow.isPending || isImpersonating}
363365
/>
364366
<ChipInput
365367
value={targetWorkspaceId}
@@ -368,14 +370,14 @@ export function Admin() {
368370
importWorkflow.reset()
369371
}}
370372
placeholder='Target workspace ID'
371-
disabled={importWorkflow.isPending || impersonateUser.isPending}
373+
disabled={importWorkflow.isPending || isImpersonating}
372374
/>
373375
<Button
374376
variant='primary'
375377
onClick={handleImport}
376378
disabled={
377379
importWorkflow.isPending ||
378-
impersonateUser.isPending ||
380+
isImpersonating ||
379381
!workflowId.trim() ||
380382
!targetWorkspaceId.trim()
381383
}

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

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
'use client'
22

3-
import { useCallback, useMemo, useState } from 'react'
3+
import { useMemo, useState } from 'react'
44
import {
55
Badge,
66
Button,
7+
Chip,
78
ChipCopyInput,
89
ChipInput,
910
ChipModalTabs,
@@ -361,19 +362,23 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
361362
const [newExpiry, setNewExpiry] = useState('')
362363
const [approvalReference, setApprovalReference] = useState('')
363364
const [generatedKey, setGeneratedKey] = useState<string | null>(null)
364-
const discardDraft = useCallback(() => {
365+
const resetForm = () => {
365366
setNewName('')
366367
setNewExpiry('')
367368
setApprovalReference('')
368-
}, [])
369+
}
369370
useSettingsUnsavedGuard({
370-
isDirty: Boolean(newName.trim() || newExpiry || approvalReference.trim()),
371+
isDirty: Boolean(newName.trim() || newExpiry || approvalReference.trim() || generatedKey),
371372
navigationBlocked: generateLicense.isPending,
372-
onDiscard: discardDraft,
373+
onDiscard: () => {
374+
resetForm()
375+
setGeneratedKey(null)
376+
},
373377
})
374378

375-
const handleGenerate = useCallback(() => {
376-
if (!newName.trim() || !approvalReference.trim() || generateLicense.isPending) return
379+
const handleGenerate = () => {
380+
if (!newName.trim() || !approvalReference.trim() || generateLicense.isPending || generatedKey)
381+
return
377382
generateLicense.mutate(
378383
{
379384
name: newName.trim(),
@@ -383,18 +388,11 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
383388
{
384389
onSuccess: (result) => {
385390
setGeneratedKey(result.license_key)
386-
discardDraft()
391+
resetForm()
387392
},
388393
}
389394
)
390-
}, [
391-
newName,
392-
newExpiry,
393-
approvalReference,
394-
generateLicense.mutate,
395-
generateLicense.isPending,
396-
discardDraft,
397-
])
395+
}
398396

399397
return (
400398
<div className='flex flex-col gap-5'>
@@ -405,10 +403,7 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
405403
<ChipInput
406404
disabled={generateLicense.isPending}
407405
value={newName}
408-
onChange={(e) => {
409-
setNewName(e.target.value)
410-
setGeneratedKey(null)
411-
}}
406+
onChange={(e) => setNewName(e.target.value)}
412407
placeholder='e.g. Acme Corp'
413408
className='w-[200px]'
414409
/>
@@ -437,7 +432,12 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
437432
variant='primary'
438433
className='h-[32px]'
439434
onClick={handleGenerate}
440-
disabled={generateLicense.isPending || !newName.trim() || !approvalReference.trim()}
435+
disabled={
436+
generateLicense.isPending ||
437+
Boolean(generatedKey) ||
438+
!newName.trim() ||
439+
!approvalReference.trim()
440+
}
441441
>
442442
{generateLicense.isPending ? 'Generating...' : 'Generate'}
443443
</Button>
@@ -448,7 +448,10 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) {
448448
<p className='text-[var(--text-secondary)] text-caption'>
449449
License key (only shown once):
450450
</p>
451-
<ChipCopyInput value={generatedKey} copyLabel='Copy license key' />
451+
<div className='flex items-center gap-2'>
452+
<ChipCopyInput value={generatedKey} copyLabel='Copy license key' className='flex-1' />
453+
<Chip onClick={() => setGeneratedKey(null)}>Done</Chip>
454+
</div>
452455
</div>
453456
)}
454457

0 commit comments

Comments
 (0)