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,70 @@
import { FastifyInstance } from "fastify";
import { PrismaClient } from "@prisma/client";
import { z } from "zod";
const prisma = new PrismaClient();
const reactionBody = z.object({
emoji: z.string().min(1).max(8),
});
export default async function reactionRoutes(app: FastifyInstance) {
app.post<{ Params: { id: string }; Body: z.infer<typeof reactionBody> }>(
"/comments/:id/reactions",
{ preHandler: [app.requireUser] },
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;
}
const { emoji } = reactionBody.parse(req.body);
const existing = await prisma.reaction.findUnique({
where: {
commentId_userId_emoji: {
commentId: comment.id,
userId: req.user!.id,
emoji,
},
},
});
if (existing) {
await prisma.reaction.delete({ where: { id: existing.id } });
reply.send({ toggled: false });
} else {
await prisma.reaction.create({
data: {
emoji,
commentId: comment.id,
userId: req.user!.id,
},
});
reply.send({ toggled: true });
}
}
);
app.delete<{ Params: { id: string; emoji: string } }>(
"/comments/:id/reactions/:emoji",
{ preHandler: [app.requireUser] },
async (req, reply) => {
const deleted = await prisma.reaction.deleteMany({
where: {
commentId: req.params.id,
userId: req.user!.id,
emoji: req.params.emoji,
},
});
if (deleted.count === 0) {
reply.status(404).send({ error: "Reaction not found" });
return;
}
reply.send({ ok: true });
}
);
}