security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states

This commit is contained in:
2026-03-21 17:37:01 +02:00
parent f07eddf29e
commit 5ba25fb956
142 changed files with 30397 additions and 2287 deletions

View 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();
}
);
}