import { FastifyInstance } from "fastify"; import { z } from "zod"; import { prisma } from "../../lib/prisma.js"; const createBody = z.object({ title: z.string().min(1).max(200).trim(), body: z.string().min(1).max(10000).trim(), boardId: z.string().optional().nullable(), publishedAt: z.coerce.date().optional(), }); const updateBody = z.object({ title: z.string().min(1).max(200).trim().optional(), body: z.string().min(1).max(10000).trim().optional(), boardId: z.string().optional().nullable(), publishedAt: z.coerce.date().optional(), }); export default async function adminChangelogRoutes(app: FastifyInstance) { app.get( "/admin/changelog", { preHandler: [app.requireAdmin, app.requireRole("SUPER_ADMIN", "ADMIN")], config: { rateLimit: { max: 60, timeWindow: "1 minute" } } }, async (_req, reply) => { const entries = await prisma.changelogEntry.findMany({ include: { board: { select: { id: true, slug: true, name: true } } }, orderBy: { publishedAt: "desc" }, take: 200, }); reply.send({ entries }); } ); app.post<{ Body: z.infer }>( "/admin/changelog", { preHandler: [app.requireAdmin, app.requireRole("SUPER_ADMIN", "ADMIN")], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, async (req, reply) => { const body = createBody.parse(req.body); const entry = await prisma.changelogEntry.create({ data: { title: body.title, body: body.body, boardId: body.boardId || null, publishedAt: body.publishedAt ?? new Date(), }, }); req.log.info({ adminId: req.adminId, entryId: entry.id }, "changelog entry created"); reply.status(201).send(entry); } ); app.put<{ Params: { id: string }; Body: z.infer }>( "/admin/changelog/:id", { preHandler: [app.requireAdmin, app.requireRole("SUPER_ADMIN", "ADMIN")], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, async (req, reply) => { const entry = await prisma.changelogEntry.findUnique({ where: { id: req.params.id } }); if (!entry) { reply.status(404).send({ error: "Entry not found" }); return; } const body = updateBody.parse(req.body); const updated = await prisma.changelogEntry.update({ where: { id: entry.id }, data: { ...(body.title !== undefined && { title: body.title }), ...(body.body !== undefined && { body: body.body }), ...(body.boardId !== undefined && { boardId: body.boardId || null }), ...(body.publishedAt !== undefined && { publishedAt: body.publishedAt }), }, }); req.log.info({ adminId: req.adminId, entryId: entry.id }, "changelog entry updated"); reply.send(updated); } ); app.delete<{ Params: { id: string } }>( "/admin/changelog/:id", { preHandler: [app.requireAdmin, app.requireRole("SUPER_ADMIN", "ADMIN")], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } }, async (req, reply) => { const entry = await prisma.changelogEntry.findUnique({ where: { id: req.params.id } }); if (!entry) { reply.status(404).send({ error: "Entry not found" }); return; } await prisma.changelogEntry.delete({ where: { id: entry.id } }); req.log.info({ adminId: req.adminId, entryId: entry.id }, "changelog entry deleted"); reply.status(204).send(); } ); }