security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
@@ -1,17 +1,15 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { PrismaClient } from "@prisma/client";
|
||||
import { z } from "zod";
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
import prisma from "../lib/prisma.js";
|
||||
|
||||
const reactionBody = z.object({
|
||||
emoji: z.string().min(1).max(8),
|
||||
emoji: z.string().min(1).max(8).regex(/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+$/u, "Invalid emoji"),
|
||||
});
|
||||
|
||||
export default async function reactionRoutes(app: FastifyInstance) {
|
||||
app.post<{ Params: { id: string }; Body: z.infer<typeof reactionBody> }>(
|
||||
"/comments/:id/reactions",
|
||||
{ preHandler: [app.requireUser] },
|
||||
{ preHandler: [app.requireUser], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
|
||||
async (req, reply) => {
|
||||
const comment = await prisma.comment.findUnique({ where: { id: req.params.id } });
|
||||
if (!comment) {
|
||||
@@ -19,6 +17,19 @@ export default async function reactionRoutes(app: FastifyInstance) {
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await prisma.post.findUnique({
|
||||
where: { id: comment.postId },
|
||||
select: { isThreadLocked: true, board: { select: { isArchived: true } } },
|
||||
});
|
||||
if (post?.board?.isArchived) {
|
||||
reply.status(403).send({ error: "Board is archived" });
|
||||
return;
|
||||
}
|
||||
if (post?.isThreadLocked) {
|
||||
reply.status(403).send({ error: "Thread is locked" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { emoji } = reactionBody.parse(req.body);
|
||||
|
||||
const existing = await prisma.reaction.findUnique({
|
||||
@@ -35,6 +46,14 @@ export default async function reactionRoutes(app: FastifyInstance) {
|
||||
await prisma.reaction.delete({ where: { id: existing.id } });
|
||||
reply.send({ toggled: false });
|
||||
} else {
|
||||
const distinctCount = await prisma.reaction.count({
|
||||
where: { commentId: comment.id, userId: req.user!.id },
|
||||
});
|
||||
if (distinctCount >= 10) {
|
||||
reply.status(400).send({ error: "Too many reactions" });
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.reaction.create({
|
||||
data: {
|
||||
emoji,
|
||||
@@ -49,8 +68,33 @@ export default async function reactionRoutes(app: FastifyInstance) {
|
||||
|
||||
app.delete<{ Params: { id: string; emoji: string } }>(
|
||||
"/comments/:id/reactions/:emoji",
|
||||
{ preHandler: [app.requireUser] },
|
||||
{ preHandler: [app.requireUser], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
|
||||
async (req, reply) => {
|
||||
const emoji = req.params.emoji;
|
||||
if (!emoji || emoji.length > 8 || !/^[\p{Extended_Pictographic}\u{FE0F}\u{200D}]+$/u.test(emoji)) {
|
||||
reply.status(400).send({ error: "Invalid emoji" });
|
||||
return;
|
||||
}
|
||||
|
||||
const comment = await prisma.comment.findUnique({ where: { id: req.params.id }, select: { id: true, postId: true } });
|
||||
if (!comment) {
|
||||
reply.status(404).send({ error: "Comment not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const post = await prisma.post.findUnique({
|
||||
where: { id: comment.postId },
|
||||
select: { isThreadLocked: true, board: { select: { isArchived: true } } },
|
||||
});
|
||||
if (post?.board?.isArchived) {
|
||||
reply.status(403).send({ error: "Board is archived" });
|
||||
return;
|
||||
}
|
||||
if (post?.isThreadLocked) {
|
||||
reply.status(403).send({ error: "Thread is locked" });
|
||||
return;
|
||||
}
|
||||
|
||||
const deleted = await prisma.reaction.deleteMany({
|
||||
where: {
|
||||
commentId: req.params.id,
|
||||
|
||||
Reference in New Issue
Block a user