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

@ -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`.