diff --git a/patterns/route-handler.md b/patterns/route-handler.md new file mode 100644 index 0000000..35c2b32 --- /dev/null +++ b/patterns/route-handler.md @@ -0,0 +1,90 @@ +# Patroon: Route Handler (REST API) + +Alle endpoints vereisen: `Authorization: Bearer ` + +## 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' + +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: 'ACTIVE', product: { user_id: 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/products` | Actieve producten ophalen | +| GET | `/api/products/:id/next-story` | Hoogst geprioriteerde open story | +| 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 bijwerken | +| POST | `/api/todos` | Todo aanmaken |