feat: file/screenshot attachments on support tickets (v0.49.1)
Support form now accepts up to 5 attachments (images, PDF, text, JSON, zip; 10MB each) via the existing S3/MinIO presigned-upload flow. New support_ticket_attachments table; attachment links get folded into the Gitea issue body and shown as thumbnails in both the user's ticket history and the admin support view. New presign endpoint (/api/v1/support/attachments/presign, rate-limited 20/hr) scoped to the uploading user rather than a ticket id, since the ticket doesn't exist yet at upload time. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "node:crypto";
|
||||
import { z } from "zod";
|
||||
import { requireSession } from "@/lib/api-auth";
|
||||
import { createPresignedUploadPost } from "@/lib/storage";
|
||||
import { applyRateLimit } from "@/lib/rate-limit";
|
||||
|
||||
const ALLOWED_TYPES = [
|
||||
"image/jpeg",
|
||||
"image/png",
|
||||
"image/webp",
|
||||
"image/avif",
|
||||
"image/gif",
|
||||
"application/pdf",
|
||||
"text/plain",
|
||||
"application/json",
|
||||
"application/zip",
|
||||
] as const;
|
||||
type AllowedType = (typeof ALLOWED_TYPES)[number];
|
||||
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
||||
|
||||
const Schema = z.object({
|
||||
contentType: z.string().refine((t): t is AllowedType => (ALLOWED_TYPES as readonly string[]).includes(t), {
|
||||
message: "Unsupported file type",
|
||||
}),
|
||||
fileSize: z.number().int().positive().max(MAX_FILE_SIZE, "File exceeds 10MB limit"),
|
||||
});
|
||||
|
||||
export async function POST(req: NextRequest) {
|
||||
const { session, response } = await requireSession();
|
||||
if (response) return response;
|
||||
|
||||
const rateLimitResponse = await applyRateLimit(`rl:support-attachment:${session!.user.id}`, 20, 3600);
|
||||
if (rateLimitResponse) return rateLimitResponse;
|
||||
|
||||
const body = (await req.json()) as unknown;
|
||||
const parsed = Schema.safeParse(body);
|
||||
if (!parsed.success) {
|
||||
return NextResponse.json({ error: "Validation error", issues: parsed.error.issues }, { status: 400 });
|
||||
}
|
||||
|
||||
const { contentType, fileSize } = parsed.data;
|
||||
const ext = contentType.split("/")[1] ?? "bin";
|
||||
const key = `support/${session!.user.id}/${crypto.randomUUID()}.${ext}`;
|
||||
const { url, fields } = await createPresignedUploadPost(key, contentType, MAX_FILE_SIZE);
|
||||
|
||||
return NextResponse.json({ url, fields, key, contentType, fileSize });
|
||||
}
|
||||
Reference in New Issue
Block a user