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:
2026-03-19 18:05:16 +02:00
commit f07eddf29e
77 changed files with 7031 additions and 0 deletions

View File

@@ -0,0 +1,173 @@
import { FastifyInstance } from "fastify";
import { PrismaClient, PostStatus, Prisma } from "@prisma/client";
import { z } from "zod";
import { notifyPostSubscribers } from "../../services/push.js";
const prisma = new PrismaClient();
const statusBody = z.object({
status: z.nativeEnum(PostStatus),
});
const respondBody = z.object({
body: z.string().min(1).max(5000),
});
export default async function adminPostRoutes(app: FastifyInstance) {
app.get<{ Querystring: Record<string, string> }>(
"/admin/posts",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const page = Math.max(1, parseInt(req.query.page ?? "1", 10));
const limit = Math.min(100, Math.max(1, parseInt(req.query.limit ?? "50", 10)));
const status = req.query.status as PostStatus | undefined;
const boardId = req.query.boardId;
const where: Prisma.PostWhereInput = {};
if (status) where.status = status;
if (boardId) where.boardId = boardId;
const [posts, total] = await Promise.all([
prisma.post.findMany({
where,
orderBy: { createdAt: "desc" },
skip: (page - 1) * limit,
take: limit,
include: {
board: { select: { slug: true, name: true } },
author: { select: { id: true, displayName: true } },
_count: { select: { comments: true, votes: true } },
},
}),
prisma.post.count({ where }),
]);
reply.send({ posts, total, page, pages: Math.ceil(total / limit) });
}
);
app.put<{ Params: { id: string }; Body: z.infer<typeof statusBody> }>(
"/admin/posts/:id/status",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
if (!post) {
reply.status(404).send({ error: "Post not found" });
return;
}
const { status } = statusBody.parse(req.body);
const oldStatus = post.status;
const [updated] = await Promise.all([
prisma.post.update({ where: { id: post.id }, data: { status } }),
prisma.statusChange.create({
data: {
postId: post.id,
fromStatus: oldStatus,
toStatus: status,
changedBy: req.adminId!,
},
}),
prisma.activityEvent.create({
data: {
type: "status_changed",
boardId: post.boardId,
postId: post.id,
metadata: { from: oldStatus, to: status },
},
}),
]);
await notifyPostSubscribers(post.id, {
title: "Status updated",
body: `"${post.title}" moved to ${status}`,
url: `/post/${post.id}`,
tag: `status-${post.id}`,
});
reply.send(updated);
}
);
app.put<{ Params: { id: string } }>(
"/admin/posts/:id/pin",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
if (!post) {
reply.status(404).send({ error: "Post not found" });
return;
}
const updated = await prisma.post.update({
where: { id: post.id },
data: { isPinned: !post.isPinned },
});
reply.send(updated);
}
);
app.post<{ Params: { id: string }; Body: z.infer<typeof respondBody> }>(
"/admin/posts/:id/respond",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
if (!post) {
reply.status(404).send({ error: "Post not found" });
return;
}
const { body } = respondBody.parse(req.body);
const response = await prisma.adminResponse.create({
data: {
body,
postId: post.id,
adminId: req.adminId!,
},
include: { admin: { select: { id: true, email: true } } },
});
await notifyPostSubscribers(post.id, {
title: "Official response",
body: body.slice(0, 100),
url: `/post/${post.id}`,
tag: `response-${post.id}`,
});
reply.status(201).send(response);
}
);
app.delete<{ Params: { id: string } }>(
"/admin/posts/:id",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
if (!post) {
reply.status(404).send({ error: "Post not found" });
return;
}
await prisma.post.delete({ where: { id: post.id } });
reply.status(204).send();
}
);
app.delete<{ Params: { id: string } }>(
"/admin/comments/:id",
{ preHandler: [app.requireAdmin] },
async (req, reply) => {
const comment = await prisma.comment.findUnique({ where: { id: req.params.id } });
if (!comment) {
reply.status(404).send({ error: "Comment not found" });
return;
}
await prisma.comment.delete({ where: { id: comment.id } });
reply.status(204).send();
}
);
}