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:
2026-09-30 15:40:51 +03:00
co-authored by Claude Opus 5.5
parent c1df3b5aa3
commit 36f9540a89
2 changed files with 43 additions and 11 deletions
@@ -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>
)} )}
+27 -10
View File
@@ -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>