security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
91
packages/api/src/routes/admin/changelog.ts
Normal file
91
packages/api/src/routes/admin/changelog.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
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<typeof createBody> }>(
|
||||
"/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<typeof updateBody> }>(
|
||||
"/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();
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user