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>
2.9 KiB
2.9 KiB
| title | status | audience | language | last_updated | when_to_read | ||
|---|---|---|---|---|---|---|---|
| Server Action | active |
|
nl | 2026-05-08 | When writing a new server action with auth and Zod validation. |
Patroon: Server Action
Altijd in actions/[domein].ts. Nooit inline in page.tsx.
'use server'
import { revalidatePath } from 'next/cache'
import { getIronSession } from 'iron-session'
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(),
title: z.string().min(1).max(200),
priority: z.number().int().min(1).max(4),
})
export async function createPbi(formData: FormData) {
// 1. Auth
const session = await getIronSession<SessionData>(await cookies(), sessionOptions)
if (!session.userId) return { error: 'Niet ingelogd' }
if (session.isDemo) return { error: 'Niet beschikbaar in demo-modus' }
// 2. Validatie
const parsed = schema.safeParse({
productId: formData.get('productId'),
title: formData.get('title'),
priority: Number(formData.get('priority')),
})
if (!parsed.success) return { error: parsed.error.flatten().fieldErrors }
// 3. Toegang controleren: eigenaar of gekoppeld product member
const product = await prisma.product.findFirst({
where: { id: parsed.data.productId, ...productAccessFilter(session.userId) }
})
if (!product) return { error: 'Product niet gevonden' }
// 4. Schrijven
const last = await prisma.pbi.findFirst({
where: { product_id: parsed.data.productId, priority: parsed.data.priority },
orderBy: { sort_order: 'desc' },
})
const pbi = await prisma.pbi.create({
data: { ...parsed.data, product_id: parsed.data.productId, sort_order: (last?.sort_order ?? 0) + 1.0 },
})
revalidatePath(`/products/${parsed.data.productId}`)
return { success: true, pbi }
}
Security-invarianten
- Controleer auth en
session.isDemovoordat 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 ideas. - 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_idbij story creation, nietformData.get('productId'). - Delete pas nadat ownership/scoping bewezen is; gebruik scoped
deleteManyals een directe uniquedeleteanders een cross-user record kan raken.