feat(PBI-96/T-1061): add rate-limit keys + server-only helpers

- lib/rate-limit.ts: 'create-product-doc' (30/min) + 'edit-product-doc'
  (60/min) in eigen PBI-96-blok na M12-Ideas-keys.
- lib/product-docs-server.ts: loadAccessibleProduct + folderApiToDbOrThrow
  als 'server-only' helpers. Wordt door create/update/list-actions
  hergebruikt; folder-toggle gebruikt direct user_id-scope (owner-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Janpeter Visser 2026-05-16 11:44:21 +02:00
parent afafbca855
commit 5ae78ff872
2 changed files with 50 additions and 0 deletions

View file

@ -0,0 +1,46 @@
// Server-only helpers voor de Product Docs server-actions. Bevat
// prisma-toegang en mag NIET in client-componenten worden geïmporteerd
// (zie CLAUDE.md hardstop "Server/client grens").
//
// Plan: docs/plans/PBI-96-product-docs.md §B.2.
import 'server-only'
import { prisma } from '@/lib/prisma'
import { productAccessFilter } from '@/lib/product-access'
import { productDocFolderFromApi } from '@/lib/product-doc-folder'
import type { ProductDocFolder } from '@prisma/client'
/**
* Laadt het Product gescoped op `productAccessFilter(userId)` voor een
* gegeven product_id. Returnt alleen de velden die de write-actions
* nodig hebben (`id`, eigenaar, folder-config). Voor folder-toggle
* gebruikt de owner-only-check direct `where: { id, user_id }` zodat
* ProductMember de folder-config niet kan wijzigen.
*
* Returnt `null` als de user geen access heeft caller stuurt dan
* 404 (anti-enum, géén 403).
*/
export async function loadAccessibleProduct(productId: string, userId: string) {
return prisma.product.findFirst({
where: { id: productId, ...productAccessFilter(userId) },
select: { id: true, user_id: true, enabled_doc_folders: true },
})
}
/**
* Converteert een API-folder-string (`'runbooks'`) naar het Prisma-enum
* (`'RUNBOOKS'`). Throwed bij onbekende waarden server-actions hebben
* de input al via zod gevalideerd, dus dit dient alleen als type-narrowing
* vangnet.
*/
export function folderApiToDbOrThrow(folderApi: string): ProductDocFolder {
const db = productDocFolderFromApi(folderApi)
if (!db) {
throw new Error(
`Internal: folderApiToDbOrThrow ontving onbekende folder "${folderApi}" — zod-validatie zou dit niet door moeten laten`,
)
}
return db
}

View file

@ -33,6 +33,10 @@ const CONFIGS: Record<string, RateLimitConfig> = {
'start-idea-job': { windowMs: 60_000, max: 10 }, // Grill / Make Plan triggers
'materialize-idea': { windowMs: 60_000, max: 5 },
'create-user-question': { windowMs: 60_000, max: 20 }, // PLAN_CHAT vragen
// PBI-96 — Per-product Product Docs (zie docs/plans/PBI-96-product-docs.md)
'create-product-doc': { windowMs: 60_000, max: 30 },
'edit-product-doc': { windowMs: 60_000, max: 60 },
}
const DEFAULT_CONFIG: RateLimitConfig = { windowMs: 60_000, max: 10 }