Implemented phase 6 for the POC prototype in the Website service.

This commit is contained in:
javier committed 2026-09-21 13:53:13 +02:00
1 parent 256d22df40
commit f2f445827b
40 files changed
+1906 -37

No files matched your search

+7 -4
View File
@@ -52,9 +52,9 @@ LogRequestsMiddleware
→ FileMiddleware (serves Resources/Static)
RootController (GET / → landing page, language negotiated; GET /<lang> → its other editions)
CatalogueController (GET /search, GET /listing/{slug}, GET /shop/{slug}, GET /grading-guide → rendered per request)
SessionController (GET /sign-in, POST /sign-in, POST /sign-out)
SessionController (GET /sign-in, POST /sign-in, POST /sign-out, GET/POST /recover, GET /recover/{token})
AccountController (GET/POST /welcome, GET /account, POST /account/…, GET/POST /sell/open-a-shop)
CollectingController (GET/POST /wants…, GET /series, GET /series/{slug}, GET /compare)
CollectingController (GET/POST /wants…, GET/POST /collection, GET /series…, GET /compare, GET /events)
PasskeyController (POST /sign-in/passkey/options, POST /sign-in/passkey/finish → JSON)
ListingFormController (GET /sell/new, POST /sell/new → multipart, for a shop's owner)
TradeController (offer sheet, messages, sale records, seller dashboard → signed-in accounts only)
@@ -91,7 +91,8 @@ How it holds together:
- **Per-viewer pages**: the controllers read the viewer from the request and send those pages `Cache-Control: private, no-cache`. The landing page stays cached for signed-out visitors and is rendered per request for signed-in ones.
- **Adding a passkey** happens from the account screen alone: the account's own session is the proof of who is asking.
- **Sign-out** is a `POST` form on the account screen, so a link on another site cannot trigger it. The not-found page always renders signed out.
- **Left out**: passkey recovery.
- **Recovery**: `/recover` hands out a one-time link (`RecoveryLinks`: in memory, good once, for an hour); following it runs a WebAuthn registration that replaces every passkey on the account and signs in. **Stubbed**: with no mail to send, the link is shown on the screen — so anyone who knows an email can recover its account. The design's 24-hour hold, and the cancel from another device, are not built.
- **States**: a cancelled passkey prompt says so, and a browser without WebAuthn — or without scripts — is told it can't use passkeys.
The signatures need a WebAuthn client, so the ceremonies are checked in a browser rather than in `swift test`: `Tests/Browser/passkeys.py` drives headless Chrome with a virtual authenticator over the DevTools protocol (Python standard library only; the script's header has the commands).
@@ -107,7 +108,7 @@ The signatures need a WebAuthn client, so the ceremonies are checked in a browse
- **The rules live in `Persistence`**: `TradeRepository` decides who may offer, answer, or take a step; `TradeController` only maps a refusal to the sheet re-rendered with a `422`, or to a redirect that changes nothing.
- **Privacy**: a conversation or sale the account is no party to is not found, rather than forbidden.
- **Demo accounts**: every seeded shop has an owner; sign in as `joost@kestrelrow.example` to sell for Kestrel Row Comics.
- **Left out**: counter-offers, problem reports and disputes, the PDF record, price guidance, and vacation mode.
- **Left out**: counter-offers, problem reports and disputes, the PDF record, price guidance, and vacation mode. Terms, privacy, and seller rules have no screens: the design has none to build them from.
### Accounts and collecting
- **First sign-in**: a new account — joined by passkey or through the demo sign-in — meets `/welcome` once, picks its display name, and goes on to what it came for.
@@ -115,6 +116,8 @@ The signatures need a WebAuthn client, so the ceremonies are checked in a browse
- **Open a shop**: one form — the shop's public identity, private or business seller (a business states its KvK number, as Dutch consumer law asks), and policies — then straight on to the first listing. A new shop starts unverified and unrated.
- **Want list**: a want is words a listing's title must carry, within a grade and a price, among copies shipping from the EU. Matches listed since the list was last looked at are new, and ring the header's bell. Saving a copy, and a gap in a series, both fill the want form.
- **Series**: `Persistence` seeds a small catalogue of runs, which is what lets a gap show: every issue of the run, the cheapest copy of each, and *alert me* where none is listed. A series sellers list but the catalogue lacks is made up from its listings, without gaps.
- **Collection**: the issues an account owns of each series, added from a form (`1-6, 9, 12`) or a switch per issue on the series screen — which then dims what is owned and counts the gaps listed right now.
- **Events**: a seeded calendar of cons and the shops trading at each. A seller picks theirs on the dashboard; a listing and its shop say where a copy can be picked up; and `pickup=` narrows a search to the shops at one event, or at any.
- **Compare**: up to three copies of an issue, ordered by what each costs delivered to the Netherlands, flagging a customs border.
Every link on the site leads somewhere: `Tests/Browser/links.py` crawls them all — signed out, as a buyer, and as a seller — against a running service.