Add analytics and documentation updates

This commit is contained in:
Janpeter Visser 2026-04-25 15:11:51 +02:00
parent e0efb65efb
commit b5e967d8d3
15 changed files with 414 additions and 37 deletions

View file

@ -0,0 +1,88 @@
# Agent Instruction Audit
Datum: 2026-04-25
## Aanleiding
Tijdens de code review kwamen twee soorten fouten naar voren:
- Server Actions vertrouwden client-provided IDs te veel nadat alleen de parent-resource was gecontroleerd.
- Documentatie liep achter op dependency-, API- en deploymentwijzigingen.
Dit document legt vast welke wijzigingen zijn gecontroleerd, welke documentatie is bijgewerkt en welke instructies Claude Code en Codex voortaan expliciet moeten volgen.
## Gecontroleerde wijzigingen
| Wijziging | Code-locatie | Documentatie bijgewerkt |
|---|---|---|
| Reorder-acties valideren alle IDs binnen de juiste parent-scope | `actions/stories.ts`, `actions/sprints.ts` | `docs/scrum4me-architecture.md`, `docs/patterns/server-action.md`, `docs/patterns/sort-order.md`, `CLAUDE.md`, `AGENTS.md` |
| Sprint afronden accepteert alleen stories uit de actieve sprint | `actions/sprints.ts` | `docs/scrum4me-architecture.md`, `docs/patterns/server-action.md`, `AGENTS.md` |
| Todo-promotie gebruikt scoped todo lookup en `pbi.product_id` als bron van waarheid | `actions/todos.ts` | `docs/scrum4me-architecture.md`, `docs/patterns/server-action.md`, `CLAUDE.md`, `AGENTS.md` |
| `GET /api/products` retourneert ook gedeelde producten via `product_members` | `app/api/products/route.ts` | `README.md`, `docs/scrum4me-functional-spec.md`, `docs/scrum4me-backlog.md`, `docs/patterns/route-handler.md` |
| `sharp` is directe runtime dependency | `package.json`, `package-lock.json` | `README.md`, `docs/scrum4me-architecture.md`, `CLAUDE.md` |
| Vercel Analytics is toegevoegd aan de root layout | `app/layout.tsx`, `package.json`, `package-lock.json` | `README.md`, `docs/scrum4me-architecture.md`, `CLAUDE.md` |
| `.env.example` ontbrak ondanks verwijzingen in architectuurdocs | `.env.example` | `README.md`, `docs/scrum4me-architecture.md` |
## Preventieve regels
### Access-control regels
Agents mogen nooit aannemen dat een ID veilig is omdat de UI hem normaal gesproken correct doorgeeft. Elke Server Action en Route Handler moet bij writes de volledige resource-scope bewijzen.
Concrete regels:
- Gebruik `productAccessFilter(userId)` voor product-scoped resources waar eigenaar en teamlid toegang hebben.
- Gebruik owner-only filters alleen bij echte eigenaarsacties.
- Valideer bulk-ID's met `id in (...)` plus parent-scope voordat er wordt geschreven.
- Weiger dubbele IDs in reorder- en decision-payloads.
- Leid denormalized foreign keys af van de database-parent, niet van client-input.
- Valideer runtime waarden met Zod of expliciete runtime checks; TypeScript types alleen zijn onvoldoende.
- Gebruik scoped deletes/updates nadat ownership bewezen is.
### Documentatie-regels
Een codewijziging is niet klaar als de documentatie een oud contract beschrijft. Agents moeten bij elke wijziging nagaan of deze plekken geraakt worden:
- `README.md`: setup, scripts, dependencies, deployment, API-overzicht.
- `docs/scrum4me-functional-spec.md`: functionele eisen en API-contracten.
- `docs/scrum4me-architecture.md`: stack, datamodel, securitymodel, env vars, deployment.
- `docs/scrum4me-backlog.md`: "done when"-criteria en scope van backlog-items.
- `docs/patterns/`: herbruikbare implementatiepatronen.
- `CLAUDE.md` en `AGENTS.md`: agent-regels die de fout in de toekomst voorkomen.
### Dependency-regels
- Elke runtime import moet als directe dependency in `package.json` staan.
- Als een dependency aan deploymentgedrag raakt, moet README of architectuurdocs dat noemen.
- Na `npm install` moet `package-lock.json` meegaan in dezelfde change.
### Verification-regels
Voor oplevering:
```bash
npm run lint
npm test
npm run build
```
Bij securitywijzigingen hoort minimaal een test die laat zien dat cross-user of cross-scope input wordt geweigerd. Als die test nog niet bestaat, noteer dat expliciet in de oplevering.
## Claude Code
`CLAUDE.md` is aangescherpt met:
- een verwijzing naar dit auditdocument;
- de directe stackwijzigingen voor Sharp en Vercel Analytics;
- expliciete access-control regels voor `productAccessFilter`, bulk-ID's en foreign-key afleiding;
- een docs-sync regel voor gedrags-, API-, dependency- en deploymentwijzigingen.
## Codex
`AGENTS.md` is uitgebreid van alleen Next.js-waarschuwing naar projectregels voor:
- access control;
- documentatie-sync;
- verificatiecommands.
Deze regels zijn korter dan `CLAUDE.md`, maar bewust dwingend: ze moeten direct gelezen worden bij elke Codex-taak in deze repo.

View file

@ -36,6 +36,7 @@ export async function authenticateApiRequest(request: Request) {
// app/api/products/[id]/next-story/route.ts
import { authenticateApiRequest } from '@/lib/api-auth'
import { prisma } from '@/lib/prisma'
import { productAccessFilter } from '@/lib/product-access'
export async function GET(
request: Request,
@ -49,7 +50,7 @@ export async function GET(
const { id } = await params
const sprint = await prisma.sprint.findFirst({
where: { product_id: id, status: 'ACTIVE', product: { user_id: auth.userId } },
where: { product_id: id, status: 'ACTIVE', product: productAccessFilter(auth.userId) },
})
if (!sprint) {
return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 })
@ -88,3 +89,11 @@ export async function GET(
| POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen |
| PATCH | `/api/tasks/:id` | Taakstatus bijwerken |
| POST | `/api/todos` | Todo aanmaken |
## Security-invarianten
- Elk endpoint start met `authenticateApiRequest`.
- Schrijf-endpoints geven `403` voor demo-tokens.
- Product-scoped reads en writes gebruiken `productAccessFilter(auth.userId)`, zodat eigenaar en gekoppeld teamlid hetzelfde toegangsmodel volgen.
- Endpoints die geordende ID-lijsten ontvangen valideren dat elke ID bij de parent-resource hoort voordat er wordt geupdated.
- JSON bodies worden met Zod gevalideerd; TypeScript types zijn geen runtime-beveiliging.

View file

@ -11,6 +11,7 @@ import { cookies } from 'next/headers'
import { z } from 'zod'
import { prisma } from '@/lib/prisma'
import { SessionData, sessionOptions } from '@/lib/session'
import { productAccessFilter } from '@/lib/product-access'
const schema = z.object({
productId: z.string().cuid(),
@ -32,9 +33,9 @@ export async function createPbi(formData: FormData) {
})
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
// 3. Eigenaarschap controleren
// 3. Toegang controleren: eigenaar of gekoppeld product member
const product = await prisma.product.findFirst({
where: { id: parsed.data.productId, user_id: session.userId }
where: { id: parsed.data.productId, ...productAccessFilter(session.userId) }
})
if (!product) return { error: 'Product niet gevonden' }
@ -51,3 +52,13 @@ export async function createPbi(formData: FormData) {
return { success: true, pbi }
}
```
## Security-invarianten
- Controleer auth en `session.isDemo` voordat er geschreven wordt.
- Gebruik `productAccessFilter(userId)` voor resources waar eigenaar en gekoppelde Developer beide toegang hebben.
- Gebruik eigenaar-only filters (`user_id: session.userId`) alleen voor eigenaarsacties zoals product archiveren, teamleden beheren of persoonlijke todos.
- Vertrouw nooit losse client-ID's. Als een action meerdere IDs ontvangt, haal ze eerst op met `id in (...)` plus de parent-scope en weiger de operatie als het aantal gevonden records niet exact gelijk is.
- Weiger dubbele IDs in reorder-lijsten of beslissingsobjecten.
- Leid denormalized foreign keys af uit de database-parent. Voorbeeld: gebruik `pbi.product_id` bij story creation, niet `formData.get('productId')`.
- Delete pas nadat ownership/scoping bewezen is; gebruik scoped `deleteMany` als een directe unique `delete` anders een cross-user record kan raken.

View file

@ -27,3 +27,13 @@ async function reindexIfNeeded(items: { id: string; sort_order: number }[]) {
}
}
```
## Reorder Server Actions
Een drag-and-drop reorder stuurt altijd client-controlled ID-lijsten naar de server. Behandel die lijst als onbetrouwbaar.
- Weiger dubbele IDs.
- Haal alle IDs op met de parent-scope, bijvoorbeeld `product_id`, `pbi_id`, `sprint_id` of `story_id`.
- Weiger de operatie als het aantal gevonden records niet exact gelijk is aan het aantal aangeleverde IDs.
- Update pas daarna `sort_order` in een transactie.
- Gebruik bij priority changes dezelfde parent uit de database, niet een los meegegeven `productId`.

View file

@ -7,7 +7,7 @@
## Architectuursamenvatting
Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt gerenderd en gedeployed op Vercel. De database is PostgreSQL via Neon, aangestuurd via Prisma v7. Authenticatie is custom username/password via iron-session — geen externe auth-provider, geen e-mail. De REST API voor Claude Code-integratie loopt via Next.js Route Handlers, beveiligd met API-tokens. Drag-and-drop in de planningsschermen wordt afgehandeld door dnd-kit.
Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt gerenderd en gedeployed op Vercel. De database is PostgreSQL via Neon, aangestuurd via Prisma v7. Authenticatie is custom username/password via iron-session — geen externe auth-provider, geen e-mail. De REST API voor Claude Code-integratie loopt via Next.js Route Handlers, beveiligd met API-tokens. Drag-and-drop in de planningsschermen wordt afgehandeld door dnd-kit. Vercel Analytics meet pageviews via de root layout; profielfoto's worden server-side verwerkt met Sharp.
---
@ -25,6 +25,8 @@ Scrum4Me is een desktop-first Next.js 16 webapplicatie die server-side wordt ger
| Authenticatie | Custom — iron-session + bcrypt | Username/password zonder e-mail vereist geen externe auth-provider; iron-session beheert versleutelde cookies server-side |
| Drag-and-drop | dnd-kit | Actief onderhouden, React-native hooks, 60fps bij grote lijsten, ondersteuning voor meerdere containers |
| REST API | Next.js Route Handlers (`/app/api/`) | Naast Server Actions nodig voor Claude Code-integratie; Route Handlers zijn volledig HTTP-compatibel |
| Image processing | Sharp | Avataruploads worden gevalideerd, geschaald en als WebP opgeslagen in PostgreSQL |
| Analytics | Vercel Analytics (`@vercel/analytics/next`) | Pageviews zonder extra client-configuratie; component staat in `app/layout.tsx` |
| Hosting | Vercel | Zero-config Next.js deployment; preview-URLs per PR; gratis tier voldoende voor v1 |
| CI/CD | GitHub Actions | Lint + typecheck + build op elke PR; Vercel handelt de daadwerkelijke deploy af |
@ -239,6 +241,27 @@ Koppelt Developer-gebruikers aan een product backlog. De eigenaar (`products.use
---
## Toegangsmodel en schrijfbeveiliging
Producttoegang is centraal gedefinieerd als:
- eigenaar: `products.user_id === gebruiker.id`
- teamlid: `product_members` bevat `(product_id, user_id)`
Code gebruikt hiervoor `productAccessFilter(userId)` uit `lib/product-access.ts`. Route Handlers en Server Actions mogen geen eigenaar-only filter (`user_id`) gebruiken voor product-scoped resources tenzij het expliciet om eigenaarsbeheer gaat, zoals archiveren of teamleden beheren.
Schrijfoperaties volgen deze invarianten:
- Controleer eerst authenticatie en `session.isDemo`.
- Valideer input met Zod, maar behandel TypeScript types niet als runtime-beveiliging.
- Controleer de parent-resource met `productAccessFilter`.
- Vertrouw bulk-ID's nooit los: haal de records eerst op met `id in (...)` plus de parent-scope (`product_id`, `pbi_id`, `sprint_id` of `story_id`) en weiger de operatie als aantallen niet exact overeenkomen.
- Weiger dubbele IDs in reorder- en beslissingslijsten.
- Leid denormalized foreign keys af van de database-parent (`pbi.product_id`, `sprint.product_id`) en niet van form-data of JSON body.
- Delete of update alleen nadat de resource scoped is gevonden; gebruik scoped `deleteMany`/`updateMany` wanneer een unique `delete` anders onveilig zou zijn.
---
## Prisma Schema (excerpt)
```prisma
@ -474,8 +497,9 @@ Inloggen:
→ bij mismatch: generieke foutmelding (geen onderscheid)
Sessie per request:
middleware.ts → iron-session cookie uitlezen → user_id + is_demo in request
→ beschermde routes: redirect /login als geen geldige sessie
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
@ -554,7 +578,7 @@ scrum4me/
│ ├── schema.prisma
│ ├── migrations/
│ └── seed.ts # Testdata uit Product Backlog document
├── middleware.ts # Sessiecheck op beschermde routes
├── proxy.ts # Next.js 16 proxy voor route protection
├── prisma.config.ts # Prisma v7 config (DATABASE_URL)
└── .env.example
```
@ -755,6 +779,7 @@ NODE_ENV="development"
- [ ] `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

View file

@ -53,8 +53,8 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- Schrijf `lib/auth.ts` (registreer met bcrypt hash, verifieer bij inloggen); schrijf `lib/session.ts` (iron-session config); implementeer `/register` en `/login` pagina's met Server Actions; sla `{ userId, isDemo }` op in sessiecookie
- Done when: registreren → ingelogde sessie → redirect `/dashboard`; inloggen met verkeerde credentials geeft generieke foutmelding; sessie blijft actief na paginaverversing
- [ ] **ST-007** Route-beveiliging via `middleware.ts`
- Schrijf `middleware.ts` die iron-session cookie uitleest; redirect naar `/login` bij alle `/dashboard`, `/products/*`, `/todos`, `/settings/*` routes zonder geldige sessie; authenticated users worden van `/login` en `/register` doorgestuurd naar `/dashboard`
- [ ] **ST-007** Route-beveiliging via `proxy.ts`
- Schrijf `proxy.ts` die sessiecookie-aanwezigheid controleert; redirect naar `/login` bij alle `/dashboard`, `/products/*`, `/todos`, `/settings/*` routes zonder sessiecookie; authenticated users worden van `/login` en `/register` doorgestuurd naar `/dashboard`; volledige sessievalidatie gebeurt server-side in de app layout
- Done when: directe navigatie naar `/dashboard` zonder sessie redirect naar `/login`; ingelogde gebruiker op `/login` redirect naar `/dashboard`
- [ ] **ST-008** Navigatieshell + dashboard-layout
@ -214,8 +214,8 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- Done when: token aangemaakt en waarde zichtbaar; na sluiten dialoog niet meer te zien; intrekken maakt token onbruikbaar (getest via curl)
- [ ] **ST-403** `GET /api/products` — productenlijst
- Route Handler; authenticatie via `api-auth.ts`; retourneer actieve producten `[{ id, name, repo_url }]` als JSON
- Done when: `curl -H "Authorization: Bearer <token>" /api/products` retourneert correct JSON; 401 zonder token
- Route Handler; authenticatie via `api-auth.ts`; retourneer actieve producten `[{ id, name, repo_url }]` als JSON voor producten waar de tokengebruiker eigenaar of teamlid is
- Done when: `curl -H "Authorization: Bearer <token>" /api/products` retourneert correct JSON inclusief gedeelde product backlogs; 401 zonder token
- [ ] **ST-404** `GET /api/products/:id/next-story` — volgende story ophalen
- Route Handler; haal hoogst geprioriteerde OPEN story op van actieve Sprint van het product (priority ASC, sort_order ASC); retourneer `{ id, title, description, acceptance_criteria, tasks[] }`; 404 als geen open stories
@ -318,17 +318,17 @@ De MVP is klaar wanneer Lars — de primaire persona — de volledige cyclus kan
- Done when: 11 snelle inlogpogingen leiden tot 429-respons met duidelijke melding
- [ ] **ST-609** Beveiligingsreview API-endpoints
- Controleer alle Route Handlers: elke schrijfoperatie valideert dat de resource bij de geverifieerde gebruiker hoort; cross-user reads zijn onmogelijk; voeg integratietests toe die cross-user toegang testen
- Done when: poging om andere gebruikers's product op te halen via API geeft 403; getest met twee test-gebruikers
- Controleer alle Route Handlers: elke schrijfoperatie valideert dat de resource binnen de toegankelijke product-scope valt; cross-scope reads zijn onmogelijk tenzij de gebruiker via `product_members` gekoppeld is; voeg integratietests toe die cross-user toegang testen
- Done when: poging om een niet-gedeeld product van een andere gebruiker op te halen via API geeft 403 of 404; gedeelde producten zijn wel zichtbaar; getest met twee test-gebruikers
- [ ] **ST-610** CI/CD via GitHub Actions
- Workflow: `lint` (ESLint), `typecheck` (tsc --noEmit), `prisma validate`, `build` (next build) op elke PR en push naar main; Vercel auto-deploy op main
- Done when: een TypeScript-fout in een PR blokkeert merge; succesvolle merge triggert Vercel-deploy
- [ ] **ST-611** README en lokale setup-documentatie
- Schrijf `README.md` met: wat is Scrum4Me, quickstart lokaal (clone → env → prisma push → seed → dev), cloud deployment (Vercel + Neon stappenplan), API-documentatie (alle 7 endpoints met voorbeelden), Claude Code-integratie uitleg
- Schrijf `README.md` met: wat is Scrum4Me, quickstart lokaal (clone → env → prisma push → seed → dev), cloud deployment (Vercel + Neon stappenplan), API-documentatie (alle 7 endpoints met voorbeelden), Claude Code-integratie uitleg, Vercel Analytics status en directe dependencies zoals Sharp
- De in-app landingspagina (`/`) bevat al een gebruikershandleiding, Scrum-samenvatting en API-overzicht — de README richt zich op lokale setup en deployment
- Done when: iemand zonder context de app lokaal kan draaien op basis van alleen de README
- Done when: iemand zonder context de app lokaal kan draaien op basis van alleen de README en `.env.example`
- [ ] **ST-612** End-to-end acceptatietest
- Voer handmatig de volledige Lars-flow uit: product aanmaken → PBI's en stories aanmaken → Sprint starten → stories slepen → taken aanmaken → API-token aanmaken → curl `next-story` → curl `log` (plan, test, commit) → activiteitenlog controleren in UI

View file

@ -362,7 +362,7 @@ Een REST API waarmee Claude Code stories en taken kan ophalen, de taakvolgorde k
**Acceptatiecriteria:**
**Endpoints:**
- [ ] `GET /api/products` — lijst van actieve producten
- [ ] `GET /api/products` — lijst van actieve producten waarvoor de tokengebruiker eigenaar of teamlid is
- [ ] `GET /api/products/:id/next-story` — hoogst geprioriteerde open story van de actieve Sprint
- [ ] `GET /api/sprints/:id/tasks?limit=10` — eerste N taken in huidige volgorde
- [ ] `PATCH /api/stories/:id/tasks/reorder` — accepteert geordende lijst van taak-id's
@ -474,6 +474,7 @@ De app is deployable op Vercel + Neon PostgreSQL en lokaal draaibaar met een Neo
| Toegankelijkheid | Keyboard-navigatie voor alle primaire acties; WCAG AA voor de sprint planning flow |
| Database | Prisma ORM; PostgreSQL via Neon |
| Deployment | Vercel (cloud) of lokaal via `npm run dev` |
| Analytics | Vercel Analytics via `@vercel/analytics/next` in de root layout |
| Sessiebeheer | Server-side sessie of JWT; geen opslag van gevoelige data in localStorage |
---