73 lines
2.5 KiB
Markdown
73 lines
2.5 KiB
Markdown
# Profile Form Validation Design
|
|
|
|
## Problem
|
|
|
|
The self-service (`/manage/profile`) and admin (`/admin/users/{id}/profile`) profile
|
|
forms have minimal validation: email only checks for `@`, phone number has no format
|
|
validation at all. Validation logic is duplicated inline in both route handlers.
|
|
|
|
## Decision
|
|
|
|
Use Pydantic types for field validation via a shared `ProfileUpdate` model.
|
|
|
|
- **Email**: `pydantic.EmailStr` (uses `email-validator` under the hood)
|
|
- **Phone**: `pydantic_extra_types.phone_numbers.PhoneNumberValidator` with
|
|
`number_format="E164"` — accepts various input formats, normalizes to strict
|
|
E.164 (`+46701234567`). Backed by Google's `libphonenumber`.
|
|
- **Picture URL**: custom Pydantic validator (http/https scheme, has netloc)
|
|
- **Field lengths**: `Field(max_length=...)` on each field
|
|
|
|
## New dependency
|
|
|
|
`pydantic-extra-types[phonenumbers]` — brings in the `phonenumbers` package.
|
|
|
|
## Shared validation module
|
|
|
|
`src/porchlight/validation.py`:
|
|
|
|
```python
|
|
from typing import Annotated
|
|
from pydantic import BaseModel, EmailStr, Field
|
|
from pydantic_extra_types.phone_numbers import PhoneNumberValidator
|
|
|
|
E164Phone = Annotated[
|
|
str, PhoneNumberValidator(number_format="E164")
|
|
]
|
|
|
|
class ProfileUpdate(BaseModel):
|
|
given_name: str = Field(default="", max_length=255)
|
|
family_name: str = Field(default="", max_length=255)
|
|
preferred_username: str = Field(default="", max_length=255)
|
|
email: EmailStr | None = None
|
|
phone_number: E164Phone | None = None
|
|
picture: str = Field(default="", max_length=2048) # + URL validator
|
|
locale: str = Field(default="", max_length=20)
|
|
```
|
|
|
|
Route handlers instantiate `ProfileUpdate` from form data, catch `ValidationError`,
|
|
and return the first user-friendly error as the existing `HTMLResponse` with
|
|
`role="alert"`.
|
|
|
|
## Route changes
|
|
|
|
Both `manage/routes.py` and `admin/routes.py` profile POST handlers will:
|
|
1. Parse form data through `ProfileUpdate`
|
|
2. Use validated/normalized values from the model
|
|
3. Replace all inline validation with a single try/except
|
|
|
|
## Template changes
|
|
|
|
Add `pattern` and `title` attributes on the phone input for browser-native hints
|
|
about E.164 format.
|
|
|
|
## Validation: server-side only
|
|
|
|
No client-side JS validation. The HTML `type="email"` and `type="tel"` attributes
|
|
already provide basic browser hints. Server-side validation via htmx response
|
|
handles all error display.
|
|
|
|
## Tests
|
|
|
|
- Unit tests for `ProfileUpdate` (valid/invalid emails, phones, URLs, lengths)
|
|
- Integration tests for manage profile POST (happy path + validation errors)
|
|
- Existing admin tests may be tightened
|