Vercel detecteert @prisma/client en runt automatisch `prisma generate` zonder --generator filter. Daardoor probeerde de erd-generator op Vercel te draaien en faalde op libnss3.so (puppeteer/Chrome niet beschikbaar in de build container). Cascading: de Prisma-client werd niet ge-update, runtime kreeg oude enum-waarden (ACTIVE i.p.v. OPEN). ERD is dev-only documentatie en niet meer in productie nodig. Generator + dependency + npm scripts + de gegenereerde svg verwijderd. README, prisma-client pattern en architecture docs bijgewerkt. Build script blijft `prisma generate && next build` zodat de client ook bij Vercel build-cache-hits opnieuw wordt gegenereerd.
216 lines
9.4 KiB
Markdown
216 lines
9.4 KiB
Markdown
---
|
||
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 <token> 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
|
||
<DemoTooltip show={isDemo}>
|
||
<Button disabled={isDemo} onClick={() => !isDemo && handleAction()}>
|
||
Actie
|
||
</Button>
|
||
</DemoTooltip>
|
||
```
|
||
|
||
**Let op:** drag-and-drop handles (`⠿`) blijven verborgen voor demo (`{!isDemo && <span {...listeners} />}`) — 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.
|