docs(ST-1001..1008): document QR-login flow in functional spec + persona

Voeg F-01b (Inloggen via mobiel via QR-pairing) toe aan de functional spec met
acceptatiecriteria, randgevallen en datamodel. Beveiligingsuitgangspunt
expliciet: mobileSecret in URL-fragment en HttpOnly desktop-cookie zodat geheim
materiaal nooit in URL-paden of access logs belandt.

Lars-persona krijgt de bijbehorende use-case (publieke/geleende laptops bij
klantbezoek of familie) zodat de feature een herkenbare aanleiding heeft in v1.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-04-27 21:23:43 +02:00
parent 308ff57789
commit 7bfb2a786a
2 changed files with 40 additions and 0 deletions

View file

@ -54,6 +54,45 @@ Gebruikers kunnen een account aanmaken en inloggen met gebruikersnaam en wachtwo
---
### F-01b: Inloggen via mobiel (QR-pairing)
**Prioriteit:** v1 — Belangrijk
**Persona:** Lars (publieke demo-laptops), Dina (klantapparatuur)
**Omschrijving:**
Een gebruiker kan op een (publieke of gedeelde) desktop inloggen zonder zijn wachtwoord te typen, door op zijn al-ingelogde mobiele apparaat een QR-code te scannen die de desktop toont. Na een expliciete tap op "Bevestig" op de mobiel raakt de desktop binnen 12 seconden ingelogd. De flow is bedoeld om typen op vreemde toetsenborden, shoulder-surfing en autofill-history te vermijden.
**Verloop:**
1. Op het login-scherm klikt de desktop-gebruiker op *"Inloggen via mobiel"*. De server maakt een eenmalige pairing-rij aan (status `pending`, vervalt na 2 minuten) met twee gescheiden geheimen: `mobileSecret` (voor de mobiel) en `desktopToken` (HttpOnly cookie voor de desktop).
2. De desktop toont een QR-code. De code bevat een URL met `mobileSecret` in het URL-fragment (`#s=…`) — dit fragment wordt door browsers nooit naar servers gestuurd, dus belandt niet in access logs of analytics.
3. De gebruiker scant met zijn telefoon. De OS-camera opent de URL in de mobiele Scrum4Me-tab. Een Client Component leest het fragment en POST't `mobileSecret` in de body naar de approve-endpoint.
4. De mobiele bevestigingspagina toont *"Inloggen op {browser-omschrijving} ({IP}) als {jouw-gebruikersnaam}?"* met een Bevestig- en Annuleer-knop. Na een tap op Bevestig wordt de pairing approved (status `approved`, vervaltijd verlengd naar 5 minuten); de desktop ontvangt dit binnen 12 seconden via een SSE-stream die geauthenticeerd is met het HttpOnly desktop-cookie.
5. De desktop claimt de sessie atomisch (eenmalig consumeerbaar), krijgt zijn iron-session cookie en wordt naar `/dashboard` doorgestuurd.
**Acceptatiecriteria:**
- [ ] Knop "Inloggen via mobiel" zichtbaar op `/login` naast het wachtwoord-formulier
- [ ] QR-code vernieuwt automatisch na 2 minuten via een "Vernieuwen"-knop
- [ ] Mobiele bevestigingspagina toont browser/UA en best-effort IP van de desktop
- [ ] Demo-gebruiker kan niet als approver fungeren — duidelijke foutmelding "Niet beschikbaar in demo-modus"
- [ ] Paired-sessie heeft een eigen TTL van 8 uur (korter dan reguliere wachtwoord-login) en is herkenbaar via een `paired`-vlag in het session-payload
- [ ] Een tweede claim met dezelfde pairing geeft `410 Gone` (one-time use)
- [ ] `mobileSecret` komt nergens in een GET-URL voor (alleen in URL-fragment of POST-body); `desktopToken` staat alleen in een HttpOnly cookie met `Path=/api/auth/pair`, `Max-Age=120`, `SameSite=Lax`
- [ ] In `nginx`/Vercel access logs is geen secret-materiaal terug te vinden (acceptatietest)
**Randgevallen:**
- QR vervalt voordat mobiel scant → mobiele pagina toont "Pairing verlopen, vraag een nieuwe QR-code op"; desktop toont "Vernieuwen"-knop
- Pairing approved maar desktop claimt niet binnen 5 minuten → atomic update faalt; pairing-rij wordt automatisch genegeerd; gebruiker start opnieuw
- Gebruiker scant een phishing-QR vanaf een willekeurige website → mobiele bevestiging toont onbekende UA/IP; expliciete bevestiging vereist; de gebruiker kan annuleren
- Gebruiker is op de mobiel niet ingelogd → middleware-guard van `/m/pair` redirectt naar `/login` met return-URL
- Gebruiker logt zichzelf uit op de mobiel terwijl de pairing nog `pending` is → approve faalt op auth-check
**Data:**
- Nieuw: `login_pairings` (id, secret_hash, desktop_token_hash, status, user_id?, desktop_ua?, desktop_ip?, created_at, expires_at, approved_at?, consumed_at?)
- Postgres-trigger op `login_pairings` publiceert via `pg_notify('scrum4me_pairing', …)`
- Sessie-payload: nieuwe optionele `paired: boolean` en `pairedExpiresAt: number`
---
### F-02: Roltoewijzing
**Prioriteit:** v1 — Fundament voor v2

View file

@ -36,6 +36,7 @@ Elke repository heeft een `TODO.md` die hij bijhoudt zolang een project actief i
- Elke avond binnen één minuut weten welke taak het meest urgent is per project
- Claude Code laten oppakken wat open staat zonder zelf de context te hoeven herstellen
- Achteraf kunnen zien wat er gedaan is en hoe (implementatieplan, commit, testresultaat)
- Op klantbezoek of bij familie zijn side projects kunnen demonstreren op een geleende laptop, zonder dat hij zijn wachtwoord op een vreemd toetsenbord hoeft te typen — door zijn telefoon (waar hij al ingelogd is) een QR-code op het scherm te laten scannen
### Frustraties om te vermijden