Scrum4Me/app/api/ideas/[id]/route.ts
Madhura68 4b234dc300 api: REST endpoints for ideas (M12 T-500)
- app/api/ideas/route.ts: GET (list with archived/product_id/status filters,
  user_id-scope), POST (creates DRAFT with auto IDEA-NNN code, 201)
- app/api/ideas/[id]/route.ts: GET (idea + recent logs), PATCH
  (ideaUpdateSchema, isIdeaEditable guard)
- lib/idea-dto.ts: API projection — converts Prisma row → DTO with
  lowercase status + has_grill_md/has_plan_md flags (md content excluded
  from list payloads, fetch via dedicated download action)

Auth: session OR API-token via authenticateApiRequest. Strict user_id
scope (no productAccessFilter — Idee is privé per Q8). 404 (not 403) for
foreign-user reads to prevent enumeration.

Tests: 13 cases (auth-401, demo-403, validation-422, malformed-400,
not-found-404, status-mismatch-422, filter param round-trip, DTO shape).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:55:49 +02:00

91 lines
2.9 KiB
TypeScript

// Per-idea REST endpoints (M12). user_id-strict scope, 404 (niet 403) bij
// foreign user om enumeratie te vermijden.
import { authenticateApiRequest } from '@/lib/api-auth'
import { prisma } from '@/lib/prisma'
import { ideaUpdateSchema } from '@/lib/schemas/idea'
import { isIdeaEditable } from '@/lib/idea-status'
import { ideaToDto } from '@/lib/idea-dto'
interface RouteContext {
params: Promise<{ id: string }>
}
export async function GET(request: Request, ctx: RouteContext) {
const auth = await authenticateApiRequest(request)
if ('error' in auth) {
return Response.json({ error: auth.error }, { status: auth.status })
}
const { id } = await ctx.params
const idea = await prisma.idea.findFirst({
where: { id, user_id: auth.userId },
include: {
product: { select: { id: true, name: true, repo_url: true } },
pbi: { select: { id: true, code: true, title: true } },
},
})
if (!idea) {
return Response.json({ error: 'Idee niet gevonden' }, { status: 404 })
}
// Recente logs (max 50) — handig voor MCP tools die context willen ophalen.
const logs = await prisma.ideaLog.findMany({
where: { idea_id: id },
orderBy: { created_at: 'desc' },
take: 50,
select: { id: true, type: true, content: true, metadata: true, created_at: true },
})
return Response.json({ idea: ideaToDto(idea), logs })
}
export async function PATCH(request: Request, ctx: RouteContext) {
const auth = await authenticateApiRequest(request)
if ('error' in auth) {
return Response.json({ error: auth.error }, { status: auth.status })
}
if (auth.isDemo) {
return Response.json({ error: 'Niet beschikbaar in demo-modus' }, { status: 403 })
}
const { id } = await ctx.params
let body: unknown
try {
body = await request.json()
} catch {
return Response.json({ error: 'Malformed JSON' }, { status: 400 })
}
const parsed = ideaUpdateSchema.safeParse(body)
if (!parsed.success) {
return Response.json({ error: parsed.error.flatten() }, { status: 422 })
}
const idea = await prisma.idea.findFirst({
where: { id, user_id: auth.userId },
select: { id: true, status: true },
})
if (!idea) {
return Response.json({ error: 'Idee niet gevonden' }, { status: 404 })
}
if (!isIdeaEditable(idea.status)) {
return Response.json(
{ error: `Idee niet bewerkbaar in status ${idea.status}` },
{ status: 422 },
)
}
const updated = await prisma.idea.update({
where: { id },
data: {
...(parsed.data.title !== undefined ? { title: parsed.data.title } : {}),
...(parsed.data.description !== undefined ? { description: parsed.data.description } : {}),
...(parsed.data.product_id !== undefined ? { product_id: parsed.data.product_id } : {}),
},
include: { product: { select: { id: true, name: true, repo_url: true } } },
})
return Response.json({ idea: ideaToDto(updated) })
}