Require password confirmation on account creation (#40)

Both account-creation surfaces now require the password twice:

- Web signup (invite-based self-service): SignupComplete gains a
  password_confirm field with a model_validator backstop server-side,
  plus a client-side match check in SignupPage.tsx for immediate
  feedback -- the client check is the primary UX, the server check is
  defense in depth so the guarantee doesn't rely on the client alone.
- CLI (python -m app.cli create-user): password is now an optional
  positional argument. If omitted, prompts interactively via getpass
  (hidden input) twice, retrying on mismatch -- matching what "entered
  twice and verified" actually means for a human typing blind. Passing
  the password directly as before still works unchanged, for scripted/
  automated provisioning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-17 09:48:23 -06:00
co-authored by Claude Sonnet 5
parent dbf9bfa902
commit 2466e76af1
6 changed files with 161 additions and 11 deletions
+2 -1
View File
@@ -9,9 +9,10 @@ export function completeSignup(
token: string,
username: string,
password: string,
passwordConfirm: string,
): Promise<User> {
return apiFetch<User>('/api/signup', {
method: 'POST',
body: JSON.stringify({ token, username, password }),
body: JSON.stringify({ token, username, password, password_confirm: passwordConfirm }),
})
}
+18 -1
View File
@@ -18,6 +18,7 @@ export function SignupPage() {
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [passwordConfirm, setPasswordConfirm] = useState('')
const [error, setError] = useState<string | null>(null)
const [submitting, setSubmitting] = useState(false)
@@ -40,9 +41,13 @@ export function SignupPage() {
async function handleSubmit(e: FormEvent) {
e.preventDefault()
setError(null)
if (password !== passwordConfirm) {
setError("Passwords don't match")
return
}
setSubmitting(true)
try {
const newUser = await completeSignup(token, username, password)
const newUser = await completeSignup(token, username, password, passwordConfirm)
updateUser(newUser)
navigate('/rooms')
} catch (err) {
@@ -92,6 +97,18 @@ export function SignupPage() {
onChange={(e) => setPassword(e.target.value)}
required
minLength={8}
autoComplete="new-password"
/>
</label>
<label>
Confirm password
<input
type="password"
value={passwordConfirm}
onChange={(e) => setPasswordConfirm(e.target.value)}
required
minLength={8}
autoComplete="new-password"
/>
</label>
{error && <p className="login-error">{error}</p>}