# Patroon: Server Action Altijd in `actions/[domein].ts`. Nooit inline in page.tsx. ```ts '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(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.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.