add:reply count enforcer
This commit is contained in:
@@ -131,6 +131,14 @@ export class SQLiteAdapter implements StorageAdapter {
|
||||
await db.delete(tickets).where(eq(tickets.id, id));
|
||||
}
|
||||
|
||||
async countRepliesByTicket(ticketId: string): Promise<number> {
|
||||
const result = await db
|
||||
.select({ count: count() })
|
||||
.from(ticketReplies)
|
||||
.where(eq(ticketReplies.ticketId, ticketId));
|
||||
return result[0]?.count ?? 0;
|
||||
}
|
||||
|
||||
async getReplies(ticketId: string): Promise<Reply[]> {
|
||||
const rows = await db
|
||||
.select({
|
||||
@@ -146,9 +154,9 @@ export class SQLiteAdapter implements StorageAdapter {
|
||||
.leftJoin(users, eq(ticketReplies.userId, users.id))
|
||||
.where(eq(ticketReplies.ticketId, ticketId))
|
||||
.orderBy(ticketReplies.createdAt);
|
||||
return rows.map(r => ({
|
||||
return rows.map((r) => ({
|
||||
...r,
|
||||
authorRole: r.authorRole as Reply['authorRole'],
|
||||
authorRole: r.authorRole as Reply["authorRole"],
|
||||
username: r.username ?? null,
|
||||
}));
|
||||
}
|
||||
@@ -157,7 +165,7 @@ export class SQLiteAdapter implements StorageAdapter {
|
||||
ticketId: string;
|
||||
body: string;
|
||||
userId?: string;
|
||||
authorRole: Reply['authorRole'];
|
||||
authorRole: Reply["authorRole"];
|
||||
}): Promise<Reply> {
|
||||
const id = crypto.randomUUID();
|
||||
const now = new Date().toISOString();
|
||||
@@ -183,7 +191,11 @@ export class SQLiteAdapter implements StorageAdapter {
|
||||
.leftJoin(users, eq(ticketReplies.userId, users.id))
|
||||
.where(eq(ticketReplies.id, id));
|
||||
const row = rows[0]!;
|
||||
return { ...row, authorRole: row.authorRole as Reply['authorRole'], username: row.username ?? null };
|
||||
return {
|
||||
...row,
|
||||
authorRole: row.authorRole as Reply["authorRole"],
|
||||
username: row.username ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
||||
import type { Ticket, TicketType } from "../types.ts";
|
||||
import { TICKET_LIMIT } from "../types.ts";
|
||||
import { TICKET_LIMIT, REPLY_LIMIT } from "../types.ts";
|
||||
import { filterContent, filterBody } from "../middleware/contentFilter.ts";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -142,12 +142,10 @@ export const ticketsRouter: FastifyPluginAsync = async (app) => {
|
||||
if (!ticket) return reply.status(404).send({ error: "Not found" });
|
||||
|
||||
if (ticket.status === "closed") {
|
||||
return reply
|
||||
.status(409)
|
||||
.send({
|
||||
error: "ticket_closed",
|
||||
message: "Cannot reply to a closed ticket.",
|
||||
});
|
||||
return reply.status(409).send({
|
||||
error: "ticket_closed",
|
||||
message: "Cannot reply to a closed ticket.",
|
||||
});
|
||||
}
|
||||
|
||||
// Determine role:
|
||||
@@ -159,6 +157,18 @@ export const ticketsRouter: FastifyPluginAsync = async (app) => {
|
||||
if (!isOwner && ticket.userId !== null) {
|
||||
return reply.status(403).send({ error: "Forbidden" });
|
||||
}
|
||||
|
||||
// Enforce per-ticket reply limit
|
||||
const replyCount = await req.storage.countRepliesByTicket(req.params.id);
|
||||
if (replyCount >= REPLY_LIMIT) {
|
||||
return reply.status(429).send({
|
||||
error: "reply_limit_reached",
|
||||
message: `This ticket has reached the maximum of ${REPLY_LIMIT} replies. Delete the ticket from the Admin tab and open a new one.`,
|
||||
limit: REPLY_LIMIT,
|
||||
current: replyCount,
|
||||
});
|
||||
}
|
||||
|
||||
const authorRole: "user" | "support" =
|
||||
ticket.userId === null || asSupport ? "support" : "user";
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface Ticket {
|
||||
}
|
||||
|
||||
export const TICKET_LIMIT = 10
|
||||
export const REPLY_LIMIT = 20
|
||||
|
||||
export interface TicketFilters {
|
||||
status?: Ticket['status']
|
||||
@@ -58,5 +59,6 @@ export interface StorageAdapter {
|
||||
updateTicket(id: string, patch: Partial<Ticket>): Promise<Ticket | null>
|
||||
deleteTicket(id: string): Promise<void>
|
||||
getReplies(ticketId: string): Promise<Reply[]>
|
||||
countRepliesByTicket(ticketId: string): Promise<number>
|
||||
createReply(data: { ticketId: string; body: string; userId?: string; authorRole: Reply['authorRole'] }): Promise<Reply>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user