feat(users): require password confirmation when creating an app user
Both create paths (Users > Add User page and the CreateUserModal used during device onboarding) now have a Confirm Password field. A mismatch shows an inline error and blocks the create call, so a typo can't silently become the user's Firebase Auth password. The confirm value is client-side only and is never sent to the backend. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,12 +11,13 @@ export default function CreateUserModal({ open, onClose, onCreated }) {
|
|||||||
const [email, setEmail] = useState('')
|
const [email, setEmail] = useState('')
|
||||||
const [displayName, setDisplayName] = useState('')
|
const [displayName, setDisplayName] = useState('')
|
||||||
const [password, setPassword] = useState('')
|
const [password, setPassword] = useState('')
|
||||||
|
const [confirm, setConfirm] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
setEmail(''); setDisplayName(''); setPassword(''); setError('')
|
setEmail(''); setDisplayName(''); setPassword(''); setConfirm(''); setError('')
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
@@ -29,6 +30,10 @@ export default function CreateUserModal({ open, onClose, onCreated }) {
|
|||||||
setError('Password must be at least 6 characters.')
|
setError('Password must be at least 6 characters.')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (password !== confirm) {
|
||||||
|
setError('Passwords do not match.')
|
||||||
|
return
|
||||||
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const created = await api.post('/users', {
|
const created = await api.post('/users', {
|
||||||
@@ -88,6 +93,16 @@ export default function CreateUserModal({ open, onClose, onCreated }) {
|
|||||||
placeholder="Min. 6 characters"
|
placeholder="Min. 6 characters"
|
||||||
hint="Used to sign into the mobile app immediately."
|
hint="Used to sign into the mobile app immediately."
|
||||||
/>
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Confirm Password"
|
||||||
|
name="confirm_password"
|
||||||
|
type="password"
|
||||||
|
value={confirm}
|
||||||
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="Re-enter password"
|
||||||
|
error={confirm && password !== confirm ? 'Passwords do not match.' : undefined}
|
||||||
|
/>
|
||||||
{error && (
|
{error && (
|
||||||
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-danger)' }}>{error}</p>
|
<p style={{ fontSize: 'var(--font-size-sm)', color: 'var(--color-danger)' }}>{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -319,6 +319,7 @@ export default function UserForm() {
|
|||||||
quickSettingsPIN: '',
|
quickSettingsPIN: '',
|
||||||
password: '',
|
password: '',
|
||||||
})
|
})
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('')
|
||||||
const [uid, setUid] = useState('')
|
const [uid, setUid] = useState('')
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -356,6 +357,10 @@ export default function UserForm() {
|
|||||||
const handleSubmit = async (e) => {
|
const handleSubmit = async (e) => {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setError('')
|
setError('')
|
||||||
|
if (!isEdit && form.password !== confirmPassword) {
|
||||||
|
setError('Passwords do not match.')
|
||||||
|
return
|
||||||
|
}
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
@@ -437,16 +442,28 @@ export default function UserForm() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{!isEdit && (
|
{!isEdit && (
|
||||||
<FormField
|
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
|
||||||
label="Password"
|
<FormField
|
||||||
name="password"
|
label="Password"
|
||||||
type="password"
|
name="password"
|
||||||
value={form.password}
|
type="password"
|
||||||
onChange={set('password')}
|
value={form.password}
|
||||||
required
|
onChange={set('password')}
|
||||||
placeholder="Min. 6 characters"
|
required
|
||||||
hint="Used to sign into the mobile app immediately — this creates the Firebase Auth account."
|
placeholder="Min. 6 characters"
|
||||||
/>
|
hint="Used to sign into the mobile app immediately — this creates the Firebase Auth account."
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
label="Confirm Password"
|
||||||
|
name="confirm_password"
|
||||||
|
type="password"
|
||||||
|
value={confirmPassword}
|
||||||
|
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
placeholder="Re-enter password"
|
||||||
|
error={confirmPassword && form.password !== confirmPassword ? 'Passwords do not match.' : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<StatusToggle value={form.status} onChange={setVal('status')} />
|
<StatusToggle value={form.status} onChange={setVal('status')} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user