feat: wire ProfileUpdate validation into manage profile route

This commit is contained in:
Johan Lundberg 2026-03-10 14:01:47 +01:00
parent 3cbf7cda5f
commit 5fd63d61ff
No known key found for this signature in database
GPG key ID: A6C152738D03C7D1
2 changed files with 173 additions and 30 deletions

View file

@ -1,12 +1,13 @@
from base64 import urlsafe_b64decode
from urllib.parse import urlparse
from fastapi import APIRouter, Form, Request, Response
from fastapi.responses import HTMLResponse, JSONResponse, RedirectResponse
from fido2.webauthn import PublicKeyCredentialDescriptor, PublicKeyCredentialType
from pydantic import ValidationError
from porchlight.dependencies import get_session_user
from porchlight.models import PasswordCredential, WebAuthnCredential
from porchlight.validation import ProfileUpdate
router = APIRouter(prefix="/manage", tags=["manage"])
@ -213,41 +214,51 @@ async def update_profile(
userid, _username = session_user
# Validate field lengths
for field_name, value, max_len in [
("Given name", given_name, 255),
("Family name", family_name, 255),
("Display name", preferred_username, 255),
("Email", email, 255),
("Phone number", phone_number, 50),
("Picture URL", picture, 2048),
("Locale", locale, 20),
]:
if len(value) > max_len:
return HTMLResponse(f'<div role="alert">{field_name} is too long</div>')
# Validate email format
if email and "@" not in email:
return HTMLResponse('<div role="alert">Invalid email address</div>')
# Validate picture URL format
if picture:
parsed = urlparse(picture)
if parsed.scheme not in ("http", "https") or not parsed.netloc:
return HTMLResponse('<div role="alert">Picture URL must be a valid HTTP or HTTPS URL</div>')
try:
profile = ProfileUpdate(
given_name=given_name,
family_name=family_name,
preferred_username=preferred_username,
email=email,
phone_number=phone_number,
picture=picture,
locale=locale,
)
except ValidationError as exc:
error = exc.errors()[0]
field = error["loc"][-1] if error["loc"] else "input"
msg = error["msg"]
# Produce user-friendly field labels
labels = {
"given_name": "Given name",
"family_name": "Family name",
"preferred_username": "Display name",
"email": "Email",
"phone_number": "Phone number",
"picture": "Picture URL",
"locale": "Locale",
}
label = labels.get(str(field), str(field))
# Use custom message for value errors (e.g. picture URL), generic pydantic message otherwise
if error["type"] == "value_error":
# Strip Pydantic's "Value error, " prefix to get the original message
display_msg = msg.removeprefix("Value error, ")
else:
display_msg = f"{label}: {msg}"
return HTMLResponse(f'<div role="alert">{display_msg}</div>')
user_repo = request.app.state.user_repo
user = await user_repo.get_by_userid(userid)
updated = user.model_copy(
update={
"given_name": given_name or None,
"family_name": family_name or None,
"preferred_username": preferred_username or None,
"email": email or None,
"phone_number": phone_number or None,
"picture": picture or None,
"locale": locale or None,
"given_name": profile.given_name or None,
"family_name": profile.family_name or None,
"preferred_username": profile.preferred_username or None,
"email": profile.email,
"phone_number": profile.phone_number,
"picture": profile.picture,
"locale": profile.locale or None,
}
)
await user_repo.update(updated)