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:
Arnaud
2026-07-18 12:09:40 +02:00
parent 811d4cad42
commit 12c2ec213a
19 changed files with 5855 additions and 64 deletions
@@ -0,0 +1,11 @@
CREATE TABLE "support_ticket_attachments" (
"id" text PRIMARY KEY NOT NULL,
"ticket_id" text NOT NULL,
"storage_key" text NOT NULL,
"content_type" text NOT NULL,
"size_bytes" integer NOT NULL,
"created_at" timestamp DEFAULT now() NOT NULL
);
--> statement-breakpoint
ALTER TABLE "support_ticket_attachments" ADD CONSTRAINT "support_ticket_attachments_ticket_id_support_tickets_id_fk" FOREIGN KEY ("ticket_id") REFERENCES "public"."support_tickets"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
CREATE INDEX "support_ticket_attachments_ticket_idx" ON "support_ticket_attachments" USING btree ("ticket_id");
File diff suppressed because it is too large Load Diff
@@ -323,6 +323,13 @@
"when": 1784366396485,
"tag": "0045_overjoyed_moondragon",
"breakpoints": true
},
{
"idx": 46,
"version": "7",
"when": 1784369252969,
"tag": "0046_curly_sabretooth",
"breakpoints": true
}
]
}
+18 -2
View File
@@ -1,4 +1,4 @@
import { pgTable, text, timestamp, index, pgEnum } from "drizzle-orm/pg-core";
import { pgTable, text, timestamp, integer, index, pgEnum } from "drizzle-orm/pg-core";
import { relations } from "drizzle-orm";
import { users } from "./users";
@@ -21,6 +21,22 @@ export const supportTickets = pgTable("support_tickets", {
index("support_tickets_status_idx").on(t.status, t.createdAt),
]);
export const supportTicketsRelations = relations(supportTickets, ({ one }) => ({
export const supportTicketAttachments = pgTable("support_ticket_attachments", {
id: text("id").primaryKey(),
ticketId: text("ticket_id").notNull().references(() => supportTickets.id, { onDelete: "cascade" }),
storageKey: text("storage_key").notNull(),
contentType: text("content_type").notNull(),
sizeBytes: integer("size_bytes").notNull(),
createdAt: timestamp("created_at").notNull().defaultNow(),
}, (t) => [
index("support_ticket_attachments_ticket_idx").on(t.ticketId),
]);
export const supportTicketsRelations = relations(supportTickets, ({ one, many }) => ({
user: one(users, { fields: [supportTickets.userId], references: [users.id] }),
attachments: many(supportTicketAttachments),
}));
export const supportTicketAttachmentsRelations = relations(supportTicketAttachments, ({ one }) => ({
ticket: one(supportTickets, { fields: [supportTicketAttachments.ticketId], references: [supportTickets.id] }),
}));