Refine workspaces and event publishing; harden uploads and email delivery

This commit is contained in:
2026-09-09 15:44:00 -04:00
parent f5702caaea
commit 574f29a68e
93 changed files with 2885 additions and 535 deletions
+17 -7
View File
@@ -15,16 +15,18 @@ import { createTRPCRouter, publicProcedure } from "../trpc";
import { consumeRateLimit } from "@/server/rate-limit";
import { hashToken } from "@/server/tokens";
import { guests } from "@album/database";
import { effectiveEvent } from "@/lib/event-lifecycle";
export const photosRouter = createTRPCRouter({
create: publicProcedure
.input(createPhotoInputSchema)
.mutation(async ({ ctx, input }) => {
const [event] = await getDb()
const [stored] = await getDb()
.select()
.from(events)
.where(eq(events.slug, input.eventSlug))
.limit(1);
const event = stored ? effectiveEvent(stored) : null;
if (!event || event.status === "draft") {
throw new TRPCError({ code: "NOT_FOUND", message: "Event not found" });
}
@@ -37,7 +39,7 @@ export const photosRouter = createTRPCRouter({
const [submission] = await getDb()
.select()
.from(submissions)
.where(eq(submissions.id, input.submissionId))
.where(and(eq(submissions.id, input.submissionId), eq(submissions.eventId, event.id)))
.limit(1);
if (!submission || submission.eventId !== event.id) {
throw new TRPCError({
@@ -58,6 +60,7 @@ export const photosRouter = createTRPCRouter({
.where(
and(
eq(guests.id, submission.guestId),
eq(guests.eventId, event.id),
eq(guests.tokenHash, hashToken(token)),
),
)
@@ -86,7 +89,7 @@ export const photosRouter = createTRPCRouter({
eventId: event.id,
submissionId: submission.id,
processingStatus: "uploading",
visibility: "pending",
visibility: event.galleryPolicy === "automatic" ? "public" : "pending",
originalKey: "pending",
contentType: input.contentType,
byteSize: input.byteSize,
@@ -97,7 +100,7 @@ export const photosRouter = createTRPCRouter({
await getDb()
.update(photos)
.set({ originalKey: key, updatedAt: new Date() })
.where(eq(photos.id, photo.id));
.where(and(eq(photos.id, photo.id), eq(photos.eventId, event.id)));
const uploadUrl = await createPresignedPutUrl({
key,
contentType: input.contentType,
@@ -107,13 +110,18 @@ export const photosRouter = createTRPCRouter({
complete: publicProcedure
.input(completePhotoInputSchema)
.mutation(async ({ input }) => {
.mutation(async ({ ctx, input }) => {
const [photo] = await getDb()
.select()
.from(photos)
.where(eq(photos.id, input.photoId))
.limit(1);
if (!photo) throw new TRPCError({ code: "NOT_FOUND" });
const token = ctx.guestTokenForEvent(photo.eventId);
const [owner] = token ? await getDb().select({ id: guests.id }).from(guests)
.innerJoin(submissions, and(eq(submissions.guestId, guests.id), eq(submissions.eventId, photo.eventId)))
.where(and(eq(submissions.id, photo.submissionId), eq(guests.eventId, photo.eventId), eq(guests.tokenHash, hashToken(token)))).limit(1) : [];
if (!owner) throw new TRPCError({ code: "FORBIDDEN", message: "Guest session does not match this upload" });
if (photo.processingStatus !== "uploading") {
return {
photoId: photo.id,
@@ -135,14 +143,16 @@ export const photosRouter = createTRPCRouter({
});
}
await getDb().transaction(async (tx) => {
await tx
const changed = await tx
.update(photos)
.set({
processingStatus: "processing",
byteSize: size,
updatedAt: new Date(),
})
.where(eq(photos.id, photo.id));
.where(and(eq(photos.id, photo.id), eq(photos.eventId, photo.eventId), eq(photos.processingStatus, "uploading")))
.returning({ id: photos.id });
if (!changed.length) return;
await tx.insert(photoJobs).values({
photoId: photo.id,
kind: "transcode",