--- title: "Authentication, Sessions & Demo Policy" status: active audience: [maintainer, contributor] language: nl last_updated: 2026-05-03 related: [qr-pairing.md](./qr-pairing.md) --- ## Authenticatieflow ``` Registratie: POST /register → valideer username/wachtwoord → bcrypt hash → opslaan in DB → iron-session cookie zetten → redirect /dashboard Inloggen: POST /login → gebruiker ophalen op username → bcrypt vergelijken → bij match: iron-session cookie zetten → redirect /dashboard → bij mismatch: generieke foutmelding (geen onderscheid) Sessie per request: proxy.ts → sessiecookie-aanwezigheid controleren → beschermde routes: redirect /login als geen sessiecookie aanwezig is → app layout valideert de volledige sessie server-side API-aanroepen (Claude Code): Authorization: Bearer header → SHA-256 hash → opzoeken in api_tokens → revoked_at null check → user_id ophalen → is_demo check voor schrijfrechten Uitloggen: Server Action → iron-session vernietigen → redirect /login ``` --- ## Demo-user policy (ST-1110) Demo-gebruikers (`is_demo = true` in de database, `isDemo: true` in de iron-session) hebben volledig read-only toegang. Bescherming is drielaags: ### Laag 1 — Middleware-guard (proxy.ts) `proxy.ts` blokkeert alle non-GET requests op `/api/*` voor demo-gebruikers voordat de route handler draait (defense in depth). Implementatie gebruikt `unsealData` direct (geen `getIronSession`) omdat `request.cookies` in middleware `RequestCookies` is, niet de volledige `CookieStore`. ```ts // Whitelist: paden die demo mag aanroepen ondanks non-GET const DEMO_WRITE_ALLOWLIST = [ '/api/cron/', // machine-auth, irrelevant voor demo ] // pair/start en pair/claim staan NIET in de allowlist — zie Laag 2 ``` ### Laag 2 — Per-route guards (Server Actions & Route Handlers) Elke schrijfactie controleert `session.isDemo` vóór DB-toegang: ```ts if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' } ``` **QR-pairing (M10):** - `pair/start`: isDemo-check via `getIronSession(await cookies(), sessionOptions)` — blokkeert demo-desktops - `pair/claim`: check `pairing.user?.is_demo` na DB-read — blokkeert demo-users die op mobiel hebben goedgekeurd - `pair/approve` en `pair/cancel`: waren al geblokkeerd vóór ST-1110 **Realtime SSE en cron-routes:** niet relevant voor demo-bescherming (SSE is read-only, cron gebruikt Bearer-auth). ### Laag 3 — UI-laag (DemoTooltip) Alle write-knoppen zijn `disabled` met een `DemoTooltip show={isDemo}` wrapper zodat demo-bezoekers de app-mogelijkheden kunnen zien. Consistente component: `components/shared/demo-tooltip.tsx`. Patroon: ```tsx ``` **Let op:** drag-and-drop handles (`⠿`) blijven verborgen voor demo (`{!isDemo && }`) — dragging is geen UI-showcase maar zou nep-optimistische updates triggeren. --- ## Claude job queue (M13 — ST-1111) Developers kunnen vanuit de Task Detail Dialog een lokale Claude Code-sessie inschakelen. De job queue zorgt voor coördinatie en realtime-status. ### State machine ``` QUEUED → CLAIMED (snapshot capture) → RUNNING → DONE → FAILED → CANCELLED (door user) CLAIMED → QUEUED (stale claim cleanup, >30min; snapshot gewist) QUEUED → CLAIMED (re-claim na stale reset; snapshot refreshed) ``` **Snapshot-rationale:** bij atomic claim schrijft `wait_for_job` de dan-actuele `task.implementation_plan` naar `claude_jobs.plan_snapshot`. Dit veld blijft bevroren terwijl de job loopt — ook als een gebruiker `update_task_plan` aanroept. Zo kan een toekomstige verify-tool drift detecteren tussen de baseline (snapshot) en de actuele plan. Jobs zonder snapshot (NULL) zijn aangemaakt vóór deze feature en worden als "no baseline" gemarkeerd. ### ClaudeJob model ``` claude_jobs id, user_id, product_id, task_id status: ClaudeJobStatus (QUEUED|CLAIMED|RUNNING|DONE|FAILED|CANCELLED) claimed_by_token_id (FK → api_tokens, nullable) claimed_at, started_at, finished_at plan_snapshot: String? — bevroren snapshot van task.implementation_plan bij claim branch, pushed_at, summary, error verify_result: VerifyResult? (ALIGNED|PARTIAL|EMPTY|DIVERGENT) @@index([user_id, status]) @@index([task_id, status]) @@index([status, claimed_at]) — voor stale-claim cleanup ``` **VerifyResult enum** — vergelijking van de git-diff in de worktree versus `plan_snapshot`: | Waarde | Betekenis | |---|---| | `ALIGNED` | Diff dekt het plan volledig — implementatie klopt met de intentie | | `PARTIAL` | Diff dekt slechts een deel van het plan — waarschuwing, maar geen blocker | | `EMPTY` | Geen codewijzigingen in de diff — blocker, tenzij de task `verify_only=true` heeft | | `DIVERGENT` | Diff bevat significant meer dan het plan — review extra zorgvuldig | **`verify_only` op Task** — wanneer `true` mag de agent de task als DONE markeren ook als de diff leeg is. Bedoeld voor taken die expliciet om verificatie (niet implementatie) vragen. **`pushed_at`** — timestamp waarop de agent de feature-branch naar origin heeft gepusht. Aanwezig zodra de push slaagde; absent als er geen wijzigingen waren of de push mislukte. ### NOTIFY/LISTEN flow ``` UI klikt 'Voer uit' → enqueueClaudeJobAction() Server Action → prisma.claudeJob.create(QUEUED) → prisma.$executeRaw pg_notify('scrum4me_changes', {type:'claude_job_enqueued',...}) → /api/realtime/solo SSE server-side filter: user_id + product_id → EventSource.onmessage browser: handleJobEvent() → useSoloStore.claudeJobsByTaskId map → SoloTaskCard pill + dialog-footer update ``` ### Idempotency `enqueueClaudeJobAction` weigert een tweede enqueue als er al een job bestaat met `status IN (QUEUED, CLAIMED, RUNNING)`. Teruggestuurde fout bevat het bestaande `jobId` zodat de UI ernaar kan linken. ### Auto-promote task-status op job-overgangen Twee Postgres-triggers houden `task.status` in sync met `claude_job.status` zodat de Solo-kaart altijd in de juiste kolom staat: - **`claude_job_claim_to_task`** (`prisma/migrations/20260501130000_promote_task_to_in_progress_on_claim`): bij INSERT met status `CLAIMED|RUNNING` of UPDATE OF status naar `CLAIMED|RUNNING`, promoot de bijbehorende task van `TO_DO` naar `IN_PROGRESS`. Forceert niet vanuit andere status — handmatige overrides (REVIEW, DONE) blijven staan. - **`claude_job_status_to_task`** (`prisma/migrations/20260501110000_sync_task_status_from_claude_job`): bij DONE zet de task ook op `DONE`. Idempotent: skip wanneer task al DONE is. De bestaande `notify_task_change`-trigger op `tasks` vuurt automatisch de pg_notify naar `/api/realtime/solo` zodat de UI direct synct — geen extra plumbing in de SSE-handler nodig. ### Hybride-ready De huidige implementatie verwacht een lokale Claude Code-sessie die `wait_for_job` aanroept vanuit `madhura68/scrum4me-mcp`. Toekomstige uitbreiding naar Vercel Sandbox (serverless agent) vereist alleen een nieuw claim-endpoint — het datamodel en SSE-flow zijn ongewijzigd. ## Environment variables | Variabele | Doel | Waar te vinden | |---|---|---| | `DATABASE_URL` | Prisma database-verbinding | Neon dashboard → Connection string (pooled) | | `DIRECT_URL` | Directe verbinding voor migraties én voor de LISTEN/NOTIFY-verbinding van het Solo Paneel realtime-endpoint | Neon dashboard → Connection string (unpooled) | | `SESSION_SECRET` | Versleutelingssleutel voor iron-session | Genereer met `openssl rand -base64 32` | | `NODE_ENV` | Omgevingsmodus | Automatisch gezet door Vercel / Node | `.env.example`: ```bash # Database DATABASE_URL="postgresql://user:password@host/dbname?sslmode=require" DIRECT_URL="postgresql://user:password@host/dbname?sslmode=require" # Sessie SESSION_SECRET="vervang-dit-met-openssl-rand-base64-32-output" # Optioneel NODE_ENV="development" ``` --- ## Deployment **Hosting:** Vercel (Hobby — gratis voor v1) **CI/CD:** GitHub Actions → lint + typecheck + `prisma validate` op elke PR; Vercel deploy automatisch bij merge naar `main` **Database (cloud):** Neon — migraties via `prisma migrate deploy` in de Vercel build-stap **Database (lokaal):** Neon (gratis tier) — `npx prisma db push` synchroniseert schema **Prisma generatie:** `prisma generate` (single client generator) **Seeding:** `npx prisma db seed` laadt de testdata uit het Product Backlog document ### Deployment checklist (pre-launch) - [ ] `DATABASE_URL` en `DIRECT_URL` gezet in Vercel dashboard (Neon connection strings) - [ ] `SESSION_SECRET` gezet in Vercel dashboard (min. 32 tekens) - [ ] `prisma migrate deploy` uitgevoerd op productiedatabase - [ ] Demo-gebruiker aangemaakt via seed of handmatig - [ ] API-token aangemaakt en getest met `curl`-aanroep naar `/api/products` - [ ] Vercel Analytics actief in het Vercel dashboard na eerste productiebezoek - [ ] Vercel preview-deployments getest op een PR - [ ] `next build` lokaal geslaagd zonder TypeScript-fouten --- ## Kostenscattting | Service | Plan | Maandelijkse kosten | |---|---|---| | Vercel | Hobby | Gratis | | Neon | Free tier (0.5 GB, 190 compute-uren) | Gratis | | GitHub | Free | Gratis | | Domein | Eigen domein (optioneel) | ~€1–2/maand | | **Totaal** | | **€0–2/maand** | > Bij groei naar meerdere gebruikers (v2): Neon Launch plan (~$19/maand) en Vercel Pro (~$20/maand) zijn de eerste stappen omhoog.