initial project setup
Fastify + Prisma backend, React + Vite frontend, Docker deployment. Multi-board feedback platform with anonymous cookie auth, passkey upgrade path, ALTCHA spam protection, plugin system, and full privacy-first architecture.
This commit is contained in:
112
packages/api/src/routes/admin/boards.ts
Normal file
112
packages/api/src/routes/admin/boards.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const createBoardBody = z.object({
|
||||
slug: z.string().min(2).max(50).regex(/^[a-z0-9-]+$/),
|
||||
name: z.string().min(1).max(100),
|
||||
description: z.string().max(500).optional(),
|
||||
externalUrl: z.string().url().optional(),
|
||||
voteBudget: z.number().int().min(0).default(10),
|
||||
voteBudgetReset: z.enum(["weekly", "monthly", "quarterly", "yearly", "never"]).default("monthly"),
|
||||
allowMultiVote: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const updateBoardBody = z.object({
|
||||
name: z.string().min(1).max(100).optional(),
|
||||
description: z.string().max(500).optional().nullable(),
|
||||
externalUrl: z.string().url().optional().nullable(),
|
||||
isArchived: z.boolean().optional(),
|
||||
voteBudget: z.number().int().min(0).optional(),
|
||||
voteBudgetReset: z.enum(["weekly", "monthly", "quarterly", "yearly", "never"]).optional(),
|
||||
allowMultiVote: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export default async function adminBoardRoutes(app: FastifyInstance) {
|
||||
app.get(
|
||||
"/admin/boards",
|
||||
{ preHandler: [app.requireAdmin] },
|
||||
async (_req, reply) => {
|
||||
const boards = await prisma.board.findMany({
|
||||
orderBy: { createdAt: "asc" },
|
||||
include: {
|
||||
_count: { select: { posts: true } },
|
||||
},
|
||||
});
|
||||
reply.send(boards);
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Body: z.infer<typeof createBoardBody> }>(
|
||||
"/admin/boards",
|
||||
{ preHandler: [app.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const body = createBoardBody.parse(req.body);
|
||||
|
||||
const existing = await prisma.board.findUnique({ where: { slug: body.slug } });
|
||||
if (existing) {
|
||||
reply.status(409).send({ error: "Slug already taken" });
|
||||
return;
|
||||
}
|
||||
|
||||
const board = await prisma.board.create({ data: body });
|
||||
reply.status(201).send(board);
|
||||
}
|
||||
);
|
||||
|
||||
app.put<{ Params: { id: string }; Body: z.infer<typeof updateBoardBody> }>(
|
||||
"/admin/boards/:id",
|
||||
{ preHandler: [app.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const board = await prisma.board.findUnique({ where: { id: req.params.id } });
|
||||
if (!board) {
|
||||
reply.status(404).send({ error: "Board not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const body = updateBoardBody.parse(req.body);
|
||||
const updated = await prisma.board.update({
|
||||
where: { id: board.id },
|
||||
data: body,
|
||||
});
|
||||
|
||||
reply.send(updated);
|
||||
}
|
||||
);
|
||||
|
||||
app.post<{ Params: { id: string } }>(
|
||||
"/admin/boards/:id/reset-budget",
|
||||
{ preHandler: [app.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const board = await prisma.board.findUnique({ where: { id: req.params.id } });
|
||||
if (!board) {
|
||||
reply.status(404).send({ error: "Board not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const updated = await prisma.board.update({
|
||||
where: { id: board.id },
|
||||
data: { lastBudgetReset: new Date() },
|
||||
});
|
||||
|
||||
reply.send(updated);
|
||||
}
|
||||
);
|
||||
|
||||
app.delete<{ Params: { id: string } }>(
|
||||
"/admin/boards/:id",
|
||||
{ preHandler: [app.requireAdmin] },
|
||||
async (req, reply) => {
|
||||
const board = await prisma.board.findUnique({ where: { id: req.params.id } });
|
||||
if (!board) {
|
||||
reply.status(404).send({ error: "Board not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.board.delete({ where: { id: board.id } });
|
||||
reply.status(204).send();
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user