Skip to content

Commit 45d8918

Browse files
fix(admin): prevent duplicate user creation
1 parent 712aeec commit 45d8918

2 files changed

Lines changed: 23 additions & 4 deletions

File tree

apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.test.tsx

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,25 @@ describe('AddUserModal', () => {
206206
password: 'canary-password',
207207
emailVerified: true,
208208
},
209-
{ onSuccess: expect.any(Function) }
209+
{ onSuccess: expect.any(Function), onSettled: expect.any(Function) }
210210
)
211211
expect(onOpenChange).toHaveBeenCalledWith(false)
212212
expect(onCreated).toHaveBeenCalledWith(CREATED_USER)
213213
})
214214

215+
it('ignores repeated submissions before the pending state renders', async () => {
216+
await renderModal()
217+
await fillRequiredFields()
218+
219+
await act(async () => {
220+
const addUserButton = buttonLabelled('Add user')
221+
addUserButton.dispatchEvent(new MouseEvent('click', { bubbles: true }))
222+
addUserButton.dispatchEvent(new MouseEvent('click', { bubbles: true }))
223+
})
224+
225+
expect(mockMutate).toHaveBeenCalledTimes(1)
226+
})
227+
215228
it('supports unverified accounts without exposing a platform-role control', async () => {
216229
mockMutate.mockImplementation(
217230
(_input: AddUserInput, options: { onSuccess: (user: AdminUser) => void }) => {
@@ -231,6 +244,7 @@ describe('AddUserModal', () => {
231244
expect(container.querySelector('[aria-label="Platform role"]')).toBeNull()
232245
expect(mockMutate).toHaveBeenCalledWith(expect.objectContaining({ emailVerified: false }), {
233246
onSuccess: expect.any(Function),
247+
onSettled: expect.any(Function),
234248
})
235249
})
236250

apps/sim/app/workspace/[workspaceId]/settings/components/admin/add-user-modal.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
'use client'
22

3-
import { useState } from 'react'
3+
import { useRef, useState } from 'react'
44
import {
55
ChipModal,
66
ChipModalBody,
@@ -26,6 +26,7 @@ interface AddUserModalProps {
2626

2727
export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProps) {
2828
const addUser = useAddUser()
29+
const submissionInFlightRef = useRef(false)
2930
const [name, setName] = useState('')
3031
const [email, setEmail] = useState('')
3132
const [password, setPassword] = useState('')
@@ -55,13 +56,14 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
5556
}
5657

5758
const handleClose = () => {
58-
if (addUser.isPending) return
59+
if (submissionInFlightRef.current || addUser.isPending) return
5960
reset()
6061
onOpenChange(false)
6162
}
6263

6364
const handleAddUser = () => {
64-
if (!canSubmit) return
65+
if (!canSubmit || submissionInFlightRef.current) return
66+
submissionInFlightRef.current = true
6567
addUser.reset()
6668
addUser.mutate(
6769
{
@@ -76,6 +78,9 @@ export function AddUserModal({ open, onOpenChange, onCreated }: AddUserModalProp
7678
onOpenChange(false)
7779
onCreated(user)
7880
},
81+
onSettled: () => {
82+
submissionInFlightRef.current = false
83+
},
7984
}
8085
)
8186
}

0 commit comments

Comments
 (0)