Persist event sign designs and harden invitation permissions

This commit is contained in:
2026-09-10 09:52:02 -04:00
parent 3a115f1161
commit 76ef573b3a
15 changed files with 289 additions and 32 deletions
+1
View File
@@ -1,4 +1,5 @@
import { z } from "zod";
export { SIGN_PAPERS, savedSignSchema, type SavedSign } from "./sign";
export const eventStatusSchema = z.enum(["draft", "published", "closed"]);
export const groupRoleSchema = z.enum(["owner", "member"]);
+10
View File
@@ -0,0 +1,10 @@
import { expect, test } from "bun:test";
import { savedSignSchema } from "./sign";
test("saved designs reject unsafe assets, invalid settings, and embedded runtime fonts", () => {
const design = { title: "Our event", headline: "Share your moments", message: "Photos welcome", paper: "card6x4", ink: "indigo" };
expect(savedSignSchema.safeParse(design).success).toBe(true);
for (const patch of [{ paper: "constructor" }, { title: "" }, { headline: "x".repeat(45) }, { customWidth: Infinity }, { background: "red" }, { backgroundImage: "https://example.com/photo.png" }, { logoImage: "data:image/svg+xml;base64,AA==" }, { fontData: "data:font/woff2;base64,AA==" }]) {
expect(savedSignSchema.safeParse({ ...design, ...patch }).success).toBe(false);
}
});
+32
View File
@@ -0,0 +1,32 @@
import { z } from "zod";
export const SIGN_PAPERS = {
letter: { label: "US Letter · 8.5 × 11 in", width: "8.5in", height: "11in", viewHeight: 1100 },
a4: { label: "A4 · 210 × 297 mm", width: "210mm", height: "297mm", viewHeight: 1202 },
letterLandscape: { label: "US Letter landscape · 11 × 8.5 in", width: "11in", height: "8.5in", viewHeight: 850 * 8.5 / 11 },
a4Landscape: { label: "A4 landscape · 297 × 210 mm", width: "297mm", height: "210mm", viewHeight: 850 * 210 / 297 },
a5: { label: "A5 · 148 × 210 mm", width: "148mm", height: "210mm", viewHeight: 850 * 210 / 148 },
a5Landscape: { label: "A5 landscape · 210 × 148 mm", width: "210mm", height: "148mm", viewHeight: 850 * 148 / 210 },
card3x5: { label: "Small card · 3 × 5 in", width: "3in", height: "5in", viewHeight: 850 * 5 / 3 },
card5x3: { label: "Small landscape card · 5 × 3 in", width: "5in", height: "3in", viewHeight: 510 },
card4x6: { label: "Postcard · 4 × 6 in", width: "4in", height: "6in", viewHeight: 1275 },
card6x4: { label: "Landscape postcard · 6 × 4 in", width: "6in", height: "4in", viewHeight: 850 * 4 / 6 },
card5x7: { label: "Table card · 5 × 7 in", width: "5in", height: "7in", viewHeight: 1190 },
card7x5: { label: "Landscape table card · 7 × 5 in", width: "7in", height: "5in", viewHeight: 850 * 5 / 7 },
square: { label: "Square card · 5 × 5 in", width: "5in", height: "5in", viewHeight: 850 },
} as const;
const image = z.string().max(12 * 1024 * 1024).regex(/^data:image\/(png|jpeg|webp);base64,[A-Za-z0-9+/=]+$/).optional();
const papers: ["custom", ...(keyof typeof SIGN_PAPERS)[]] = ["custom", ...Object.keys(SIGN_PAPERS) as (keyof typeof SIGN_PAPERS)[]];
export const savedSignSchema = z.object({
title: z.string().trim().min(1).max(120), headline: z.string().trim().min(1).max(44),
message: z.string().trim().min(1).max(120),
paper: z.enum(papers),
ink: z.enum(["indigo", "black"]), layout: z.enum(["photo", "typography", "compact"]).optional(),
customWidth: z.number().finite().positive().optional(), customHeight: z.number().finite().positive().optional(),
unit: z.enum(["in", "mm"]).optional(), background: z.string().regex(/^#[a-f0-9]{6}$/i).optional(),
accent: z.string().regex(/^#[a-f0-9]{6}$/i).optional(), font: z.enum(["funnel", "inter", "geologica"]).optional(),
decoration: z.enum(["arch", "frame", "minimal"]).optional(), showBrand: z.boolean().optional(),
backgroundImage: image, logoImage: image,
}).strict();
export type SavedSign = z.infer<typeof savedSignSchema>;
@@ -0,0 +1,6 @@
CREATE TABLE "event_signs" (
"event_id" uuid PRIMARY KEY REFERENCES "events"("id") ON DELETE CASCADE,
"revision" uuid NOT NULL,
"object_key" text NOT NULL,
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
);
+2 -1
View File
@@ -78,6 +78,7 @@
"when": 1788996000000,
"tag": "0010_banner_crop",
"breakpoints": true
}
},
{ "idx": 11, "version": "7", "when": 1789086000000, "tag": "0011_event_signs", "breakpoints": true }
]
}
+7
View File
@@ -211,6 +211,13 @@ export const photoExports = pgTable("photo_exports", {
...timestamps,
}, (table) => [index("photo_exports_queue_idx").on(table.status, table.createdAt), uniqueIndex("photo_exports_active_idx").on(table.eventId,table.requestedBy).where(sql`${table.status} IN ('pending', 'processing')`), check("photo_exports_filter_check",sql`${table.filter} IN ('approved', 'all')`)]);
export const eventSigns = pgTable("event_signs", {
eventId: uuid("event_id").primaryKey().references(() => events.id, { onDelete: "cascade" }),
revision: uuid("revision").notNull(),
objectKey: text("object_key").notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
});
export const eventBanners = pgTable("event_banners", {
id: uuid("id").defaultRandom().primaryKey(),
eventId: uuid("event_id").notNull().references(() => events.id, { onDelete: "cascade" }),