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 [displayName, setDisplayName] = useState('')
const [password, setPassword] = useState('')
const [confirm, setConfirm] = useState('')
const [saving, setSaving] = useState(false)
const [error, setError] = useState('')
useEffect(() => {
if (!open) return
setEmail(''); setDisplayName(''); setPassword(''); setError('')
setEmail(''); setDisplayName(''); setPassword(''); setConfirm(''); setError('')
}, [open])
const handleCreate = async () => {
@@ -29,6 +30,10 @@ export default function CreateUserModal({ open, onClose, onCreated }) {
setError('Password must be at least 6 characters.')
return
}
if (password !== confirm) {
setError('Passwords do not match.')
return
}
setSaving(true)
try {
const created = await api.post('/users', {
@@ -88,6 +93,16 @@ export default function CreateUserModal({ open, onClose, onCreated }) {
placeholder="Min. 6 characters"
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 && (
<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: '',
password: '',
})
const [confirmPassword, setConfirmPassword] = useState('')
const [uid, setUid] = useState('')
const [loading, setLoading] = useState(false)
const [saving, setSaving] = useState(false)
@@ -356,6 +357,10 @@ export default function UserForm() {
const handleSubmit = async (e) => {
e.preventDefault()
setError('')
if (!isEdit && form.password !== confirmPassword) {
setError('Passwords do not match.')
return
}
setSaving(true)
try {
if (isEdit) {
@@ -437,16 +442,28 @@ export default function UserForm() {
/>
</div>
{!isEdit && (
<FormField
label="Password"
name="password"
type="password"
value={form.password}
onChange={set('password')}
required
placeholder="Min. 6 characters"
hint="Used to sign into the mobile app immediately — this creates the Firebase Auth account."
/>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 'var(--space-4)' }}>
<FormField
label="Password"
name="password"
type="password"
value={form.password}
onChange={set('password')}
required
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')} />
</div>