107 lines
3.9 KiB
TypeScript
107 lines
3.9 KiB
TypeScript
import { NextResponse } from "next/server"
|
|
import convert from "heic-convert"
|
|
|
|
import { getInvitationByToken } from "@/lib/db/queries/invitations"
|
|
import { getPhotoUploadByInvitationId, upsertPhotoUpload } from "@/lib/db/queries/uploads"
|
|
import { isRsvpClosed } from "@/lib/rsvp-deadline"
|
|
import { getUploadBucket } from "@/lib/storage/s3"
|
|
import {
|
|
createInvitationUploadKey,
|
|
createPresignedDownloadUrl,
|
|
deleteUploadObject,
|
|
downloadUploadObject,
|
|
getUploadedObjectMetadata,
|
|
isObjectKeyForInvitation,
|
|
putUploadObject,
|
|
} from "@/lib/storage/uploads"
|
|
import { completeUploadSchema, maxUploadSizeBytes } from "@/lib/validation/upload"
|
|
|
|
export const runtime = "nodejs"
|
|
|
|
export async function POST(request: Request) {
|
|
if (isRsvpClosed()) {
|
|
return NextResponse.json({ message: "Upload deadline has passed" }, { status: 403 })
|
|
}
|
|
|
|
let body: unknown
|
|
try {
|
|
body = await request.json()
|
|
} catch {
|
|
return NextResponse.json({ message: "Invalid upload completion" }, { status: 400 })
|
|
}
|
|
const parsed = completeUploadSchema.safeParse(body)
|
|
|
|
if (!parsed.success) {
|
|
return NextResponse.json({ message: "Invalid upload completion" }, { status: 400 })
|
|
}
|
|
|
|
const invitation = await getInvitationByToken(parsed.data.token)
|
|
|
|
if (!invitation) {
|
|
return NextResponse.json({ message: "Invitation not found" }, { status: 404 })
|
|
}
|
|
|
|
if (!isObjectKeyForInvitation(parsed.data.objectKey, invitation.invitation.id)) {
|
|
return NextResponse.json({ message: "Invalid object key" }, { status: 400 })
|
|
}
|
|
|
|
let metadata: Awaited<ReturnType<typeof getUploadedObjectMetadata>>
|
|
try {
|
|
metadata = await getUploadedObjectMetadata(parsed.data.objectKey)
|
|
} catch {
|
|
return NextResponse.json({ message: "Uploaded object not found" }, { status: 400 })
|
|
}
|
|
if (metadata.contentType !== parsed.data.contentType || metadata.sizeBytes !== parsed.data.sizeBytes) {
|
|
return NextResponse.json({ message: "Uploaded object does not match" }, { status: 400 })
|
|
}
|
|
|
|
const isHeic = parsed.data.contentType === "image/heic" || parsed.data.contentType === "image/heif"
|
|
let objectKey = parsed.data.objectKey
|
|
let filename = parsed.data.filename
|
|
let contentType = parsed.data.contentType
|
|
let sizeBytes = parsed.data.sizeBytes
|
|
|
|
if (isHeic) {
|
|
try {
|
|
const input = await downloadUploadObject(parsed.data.objectKey, maxUploadSizeBytes)
|
|
const jpeg = await convert({ buffer: input, format: "JPEG", quality: 0.92 })
|
|
if (jpeg.length === 0 || jpeg.length > maxUploadSizeBytes) {
|
|
return NextResponse.json({ message: "Converted image has an invalid size" }, { status: 400 })
|
|
}
|
|
filename = `${parsed.data.filename.replace(/\.[^.]*$/, "") || "upload"}.jpg`
|
|
objectKey = createInvitationUploadKey(invitation.invitation.id, filename)
|
|
await putUploadObject({ objectKey, contentType: "image/jpeg", body: jpeg })
|
|
const finalMetadata = await getUploadedObjectMetadata(objectKey)
|
|
if (finalMetadata.contentType !== "image/jpeg" || finalMetadata.sizeBytes !== jpeg.length) {
|
|
throw new Error("Converted object does not match")
|
|
}
|
|
contentType = "image/jpeg"
|
|
sizeBytes = jpeg.length
|
|
} catch {
|
|
return NextResponse.json({ message: "Unable to convert uploaded image" }, { status: 400 })
|
|
}
|
|
}
|
|
|
|
const previousUpload = await getPhotoUploadByInvitationId(invitation.invitation.id)
|
|
const upload = await upsertPhotoUpload({
|
|
invitationId: invitation.invitation.id,
|
|
bucket: getUploadBucket(),
|
|
objectKey,
|
|
originalFilename: filename,
|
|
contentType,
|
|
sizeBytes,
|
|
})
|
|
|
|
if (isHeic) {
|
|
await deleteUploadObject(parsed.data.objectKey).catch(() => undefined)
|
|
}
|
|
if (previousUpload && previousUpload.object_key !== objectKey) {
|
|
await deleteUploadObject(previousUpload.object_key).catch(() => undefined)
|
|
}
|
|
|
|
return NextResponse.json({
|
|
upload,
|
|
photo: { filename: upload.original_filename, url: await createPresignedDownloadUrl(upload.object_key) },
|
|
})
|
|
}
|