docs: add profile form validation design
This commit is contained in:
parent
0435b81c5a
commit
a3d4ac1bfd
1 changed files with 73 additions and 0 deletions
73
docs/plans/2026-02-20-profile-validation-design.md
Normal file
73
docs/plans/2026-02-20-profile-validation-design.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# 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
|
||||
Loading…
Add table
Add a link
Reference in a new issue