update:char limits

This commit is contained in:
2026-03-10 18:08:22 +09:00
parent 774a79eef3
commit 0f602a48c6
6 changed files with 161 additions and 63 deletions

View File

@@ -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" });

View File

@@ -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>;
}