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
+18 -2
View File
@@ -7,6 +7,7 @@ created by an operator running this script directly on the app server.
import argparse
import asyncio
import base64
import getpass
from pydantic import ValidationError
@@ -15,6 +16,15 @@ from app.schemas.user import UserCreate
from app.services.auth_service import DuplicateUserError, register_user
def _prompt_password() -> str:
while True:
password = getpass.getpass("Password: ")
confirm = getpass.getpass("Confirm password: ")
if password == confirm:
return password
print("Passwords didn't match -- try again.")
async def _create_user(username: str, email: str, password: str, is_admin: bool) -> None:
try:
data = UserCreate(username=username, email=email, password=password)
@@ -69,7 +79,12 @@ def main() -> None:
create_user = subparsers.add_parser("create-user", help="Create a new user account")
create_user.add_argument("username")
create_user.add_argument("email")
create_user.add_argument("password")
create_user.add_argument(
"password",
nargs="?",
default=None,
help="If omitted, you'll be prompted interactively (hidden input, entered twice to confirm).",
)
create_user.add_argument("--admin", action="store_true", help="Grant is_site_admin")
subparsers.add_parser("generate-vapid-keys", help="Generate a VAPID key pair for push notifications")
@@ -77,7 +92,8 @@ def main() -> None:
args = parser.parse_args()
if args.command == "create-user":
asyncio.run(_create_user(args.username, args.email, args.password, args.admin))
password = args.password if args.password is not None else _prompt_password()
asyncio.run(_create_user(args.username, args.email, password, args.admin))
elif args.command == "generate-vapid-keys":
_generate_vapid_keys()