Brengt de docs gelijk met de werkelijkheid na PBI-46/47/50/58/59/61/63 en M12. Belangrijkste fixes: - data-model.md herschreven naar prisma/schema.prisma: nieuwe entiteiten (Idea, IdeaLog, IdeaProduct, UserQuestion, ClaudeQuestion, ClaudeJob, SprintRun, SprintTaskExecution, ClaudeWorker, LoginPairing, PushSubscription, ModelPrice, ProductMember), nieuwe enums (FAILED/EXCLUDED, OPEN/CLOSED/ARCHIVED, ADMIN, etc.) en codes (PBI/ST/T/SP-N) toegevoegd; verwijderde todos-tabel verwijderd. - glossary.md: Sprint zonder "max 1 actief" (PBI-63), Story/Task incl. FAILED/EXCLUDED, Todo verwijderd, Idea/SprintRun/ClaudeJob/ verify_result toegevoegd. - project-structure.md: app/(app)/todos vervangen door ideas/insights/jobs/manual/admin/solo; api-tree volledig. - overview.md: "geen realtime in v1" en Docker-rationale herschreven — Postgres LISTEN/NOTIFY + SSE, claude_jobs als queue, opt-in Docker-deploy-flow. - functional.md: F-08 Todo-lijst -> Ideeen-laag, F-09 multi-sprint, F-10 Task-status incl. FAILED/EXCLUDED, F-11 endpoint-lijst, navigatiestructuur, datamodel-schets en Flow 3 bijgewerkt. - README.md API-tabel: /api/todos weg, ideas/jobs/users/profile/health toegevoegd, kort over realtime/auth-pair/internal/cron. - patterns + mcp-integration runbook: Todo-/ACTIVE-references vervangen door Idea/OPEN; create_todo MCP-tool note over verwijdering. Linkcheck groen (105 files), INDEX hergegenereerd (98 docs). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
111 lines
3.7 KiB
Markdown
111 lines
3.7 KiB
Markdown
---
|
|
title: "Route Handler (REST API)"
|
|
status: active
|
|
audience: [ai-agent, contributor]
|
|
language: nl
|
|
last_updated: 2026-05-08
|
|
when_to_read: "When writing a new Next.js route handler (GET/POST/PATCH/DELETE)."
|
|
---
|
|
|
|
# Patroon: Route Handler (REST API)
|
|
|
|
Alle endpoints vereisen: `Authorization: Bearer <token>`
|
|
|
|
## lib/api-auth.ts
|
|
|
|
```ts
|
|
import { createHash } from 'crypto'
|
|
import { prisma } from '@/lib/prisma'
|
|
|
|
export async function authenticateApiRequest(request: Request) {
|
|
const authHeader = request.headers.get('Authorization')
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
return { error: 'Unauthorized', status: 401 }
|
|
}
|
|
|
|
const token = authHeader.slice(7)
|
|
const tokenHash = createHash('sha256').update(token).digest('hex')
|
|
|
|
const apiToken = await prisma.apiToken.findUnique({
|
|
where: { token_hash: tokenHash },
|
|
include: { user: true },
|
|
})
|
|
|
|
if (!apiToken || apiToken.revoked_at) {
|
|
return { error: 'Unauthorized', status: 401 }
|
|
}
|
|
|
|
return { userId: apiToken.user_id, isDemo: apiToken.user.is_demo }
|
|
}
|
|
```
|
|
|
|
## Route Handler
|
|
|
|
```ts
|
|
// 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,
|
|
{ params }: { params: Promise<{ id: string }> }
|
|
) {
|
|
const auth = await authenticateApiRequest(request)
|
|
if ('error' in auth) {
|
|
return Response.json({ error: auth.error }, { status: auth.status })
|
|
}
|
|
|
|
const { id } = await params
|
|
|
|
const sprint = await prisma.sprint.findFirst({
|
|
where: { product_id: id, status: 'OPEN', product: productAccessFilter(auth.userId) },
|
|
})
|
|
if (!sprint) {
|
|
return Response.json({ error: 'Geen actieve Sprint gevonden' }, { status: 404 })
|
|
}
|
|
|
|
const story = await prisma.story.findFirst({
|
|
where: { sprint_id: sprint.id, status: 'IN_SPRINT' },
|
|
orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }],
|
|
include: { tasks: { orderBy: [{ priority: 'asc' }, { sort_order: 'asc' }] } },
|
|
})
|
|
|
|
if (!story) {
|
|
return Response.json({ error: 'Geen open stories in de Sprint' }, { status: 404 })
|
|
}
|
|
|
|
return Response.json(story)
|
|
}
|
|
```
|
|
|
|
## POST /api/stories/:id/log — body schema
|
|
|
|
```json
|
|
{ "type": "IMPLEMENTATION_PLAN", "content": "string" }
|
|
{ "type": "TEST_RESULT", "content": "string", "status": "PASSED" | "FAILED" }
|
|
{ "type": "COMMIT", "content": "string", "commit_hash": "string", "commit_message": "string" }
|
|
```
|
|
|
|
## Alle endpoints
|
|
|
|
| Methode | Endpoint | Doel |
|
|
|---|---|---|
|
|
| GET | `/api/health` | Liveness; `?db=1` voor DB-ping (geen auth) |
|
|
| GET | `/api/products` | Actieve producten ophalen |
|
|
| GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story |
|
|
| GET | `/api/products/:id/claude-context` | Bundled MCP-context |
|
|
| GET | `/api/sprints/:id/tasks?limit=10` | Eerste N taken van de Sprint |
|
|
| PATCH | `/api/stories/:id/tasks/reorder` | Taakvolgorde aanpassen |
|
|
| POST | `/api/stories/:id/log` | Plan / testresultaat / commit vastleggen |
|
|
| PATCH | `/api/tasks/:id` | Taakstatus / `implementation_plan` bijwerken |
|
|
| GET / POST | `/api/ideas`, `GET / PATCH /api/ideas/:id` | Idea CRUD (vervangt voormalig `/api/todos`) |
|
|
| GET | `/api/jobs/:id/sub-tasks` | `sprint_task_executions` van een SPRINT_IMPLEMENTATION-job |
|
|
|
|
## 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.
|