update:char limits
This commit is contained in:
@@ -1,6 +1,12 @@
|
||||
import type { FastifyPluginAsync, FastifyReply, FastifyRequest } from "fastify";
|
||||
import type { Ticket, TicketType } from "../types.ts";
|
||||
import { TICKET_LIMIT, REPLY_LIMIT } from "../types.ts";
|
||||
import {
|
||||
TICKET_LIMIT,
|
||||
REPLY_LIMIT,
|
||||
SUBJECT_MAX_LENGTH,
|
||||
DESCRIPTION_MAX_LENGTH,
|
||||
REPLY_MAX_LENGTH,
|
||||
} from "../types.ts";
|
||||
import { filterContent, filterBody } from "../middleware/contentFilter.ts";
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
@@ -70,6 +76,22 @@ export const ticketsRouter: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(400).send({ error: "subject is required" });
|
||||
}
|
||||
|
||||
if (subject.trim().length > SUBJECT_MAX_LENGTH) {
|
||||
return reply.status(400).send({
|
||||
error: "subject_too_long",
|
||||
message: `Subject must be ${SUBJECT_MAX_LENGTH} characters or fewer.`,
|
||||
max: SUBJECT_MAX_LENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
if (description.length > DESCRIPTION_MAX_LENGTH) {
|
||||
return reply.status(400).send({
|
||||
error: "description_too_long",
|
||||
message: `Description must be ${DESCRIPTION_MAX_LENGTH} characters or fewer.`,
|
||||
max: DESCRIPTION_MAX_LENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
// Enforce per-user ticket limit
|
||||
if (req.user?.id) {
|
||||
const userTicketCount = await req.storage.countTicketsByUser(req.user.id);
|
||||
@@ -138,6 +160,14 @@ export const ticketsRouter: FastifyPluginAsync = async (app) => {
|
||||
return reply.status(400).send({ error: "body is required" });
|
||||
}
|
||||
|
||||
if (body.trim().length > REPLY_MAX_LENGTH) {
|
||||
return reply.status(400).send({
|
||||
error: "reply_too_long",
|
||||
message: `Reply must be ${REPLY_MAX_LENGTH} characters or fewer.`,
|
||||
max: REPLY_MAX_LENGTH,
|
||||
});
|
||||
}
|
||||
|
||||
const ticket = await req.storage.getTicket(req.params.id);
|
||||
if (!ticket) return reply.status(404).send({ error: "Not found" });
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
export interface User {
|
||||
id: string
|
||||
googleId: string
|
||||
username: string
|
||||
avatarUrl: string | null
|
||||
createdAt: string
|
||||
id: string;
|
||||
googleId: string;
|
||||
username: string;
|
||||
avatarUrl: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type TicketType =
|
||||
@@ -15,50 +15,67 @@ export type TicketType =
|
||||
| "other";
|
||||
|
||||
export interface Ticket {
|
||||
id: string
|
||||
userId: string | null
|
||||
username: string | null
|
||||
subject: string
|
||||
description: string
|
||||
type: TicketType
|
||||
status: 'open' | 'in-progress' | 'resolved' | 'closed'
|
||||
createdAt: string
|
||||
id: string;
|
||||
userId: string | null;
|
||||
username: string | null;
|
||||
subject: string;
|
||||
description: string;
|
||||
type: TicketType;
|
||||
status: "open" | "in-progress" | "resolved" | "closed";
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export const TICKET_LIMIT = 10
|
||||
export const REPLY_LIMIT = 20
|
||||
export const TICKET_LIMIT = 10;
|
||||
export const REPLY_LIMIT = 20;
|
||||
|
||||
export const SUBJECT_MAX_LENGTH = 128;
|
||||
export const DESCRIPTION_MAX_LENGTH = 2000;
|
||||
export const REPLY_MAX_LENGTH = 1000;
|
||||
|
||||
export interface TicketFilters {
|
||||
status?: Ticket['status']
|
||||
type?: TicketType
|
||||
userId?: string
|
||||
status?: Ticket["status"];
|
||||
type?: TicketType;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
export interface PaginatedTickets {
|
||||
data: Ticket[]
|
||||
total: number
|
||||
data: Ticket[];
|
||||
total: number;
|
||||
}
|
||||
|
||||
export interface Reply {
|
||||
id: string
|
||||
ticketId: string
|
||||
userId: string | null
|
||||
username: string | null
|
||||
body: string
|
||||
authorRole: 'user' | 'support'
|
||||
createdAt: string
|
||||
id: string;
|
||||
ticketId: string;
|
||||
userId: string | null;
|
||||
username: string | null;
|
||||
body: string;
|
||||
authorRole: "user" | "support";
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface StorageAdapter {
|
||||
getTickets(): Promise<Ticket[]>
|
||||
getTicketsByUser(userId: string): Promise<Ticket[]>
|
||||
getTicketsPaginated(limit: number, offset: number, filters?: TicketFilters): Promise<PaginatedTickets>
|
||||
getTicket(id: string): Promise<Ticket | null>
|
||||
countTicketsByUser(userId: string): Promise<number>
|
||||
createTicket(data: Pick<Ticket, 'subject' | 'description' | 'type'> & { userId?: string }): Promise<Ticket>
|
||||
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>
|
||||
getTickets(): Promise<Ticket[]>;
|
||||
getTicketsByUser(userId: string): Promise<Ticket[]>;
|
||||
getTicketsPaginated(
|
||||
limit: number,
|
||||
offset: number,
|
||||
filters?: TicketFilters,
|
||||
): Promise<PaginatedTickets>;
|
||||
getTicket(id: string): Promise<Ticket | null>;
|
||||
countTicketsByUser(userId: string): Promise<number>;
|
||||
createTicket(
|
||||
data: Pick<Ticket, "subject" | "description" | "type"> & {
|
||||
userId?: string;
|
||||
},
|
||||
): Promise<Ticket>;
|
||||
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