From b39f344426a37c8f195957ec4b27d2f2e92a8d3f Mon Sep 17 00:00:00 2001 From: Mannu Date: Sun, 10 May 2026 14:13:17 +0530 Subject: [PATCH] Fix upload: route through server to avoid CORS --- src/app/api/upload/route.ts | 73 ++++++++++++++++++++++++++++--------- src/app/memories/page.tsx | 48 +++++++++--------------- 2 files changed, 72 insertions(+), 49 deletions(-) diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index 305b29f..8bdd534 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -1,4 +1,4 @@ -import { S3Client, PutObjectCommand, ListObjectsV2Command } from "@aws-sdk/client-s3"; +import { S3Client, PutObjectCommand, ListObjectsV2Command, GetObjectCommand } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { NextRequest, NextResponse } from "next/server"; @@ -9,7 +9,7 @@ function getR2() { const bucket = process.env.R2_BUCKET_NAME; if (!accountId || !accessKeyId || !secretKey || !bucket) { - throw new Error(`Missing R2 config: accountId=${!!accountId}, accessKeyId=${!!accessKeyId}, secretKey=${!!secretKey}, bucket=${!!bucket}`); + throw new Error(`Missing R2 config`); } return { @@ -23,6 +23,31 @@ function getR2() { }; } +// GET: List memories +export async function GET(req: NextRequest) { + try { + const { client, bucket, baseUrl } = getR2(); + const { searchParams } = new URL(req.url); + const childId = searchParams.get("childId") || "default"; + const prefix = `memories/${childId}`; + + const command = new ListObjectsV2Command({ Bucket: bucket, Prefix: prefix }); + const res = await client.send(command); + const objects = (res.Contents || []).map((obj) => ({ + key: obj.Key, + url: `${baseUrl}/${obj.Key}`, + size: obj.Size, + lastModified: obj.LastModified?.toISOString(), + })).sort((a, b) => new Date(b.lastModified!).getTime() - new Date(a.lastModified!).getTime()); + + return NextResponse.json({ memories: objects }); + } catch (error) { + console.error("R2 list error:", error); + return NextResponse.json({ error: String(error) }, { status: 500 }); + } +} + +// POST: Upload (proxy through server to avoid CORS) export async function POST(req: NextRequest) { let body; try { @@ -42,13 +67,17 @@ export async function POST(req: NextRequest) { const ext = filename.split(".").pop() || "jpg"; const key = `memories/${childId || "default"}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`; + // Instead of presigned URL, we're using PUT to our own server + // The frontend willPOST the file here directly const command = new PutObjectCommand({ Bucket: bucket, Key: key, ContentType: contentType, }); - const url = await getSignedUrl(client, command, { expiresIn: 60 }); + // Generate short-lived presigned URL that bypasses CORS issues + // R2 needs proper signing - let's use direct put with our server as proxy + const url = await getSignedUrl(client, command, { expiresIn: 3600 }); // 1 hour return NextResponse.json({ uploadUrl: url, @@ -56,34 +85,42 @@ export async function POST(req: NextRequest) { publicUrl: `${baseUrl}/${key}`, }); } catch (error) { - console.error("R2 upload error:", error); + console.error("R2 error:", error); return NextResponse.json({ error: String(error) }, { status: 500 }); } } -export async function GET(req: NextRequest) { +// PUT: Direct upload from file content +export async function PUT(req: NextRequest) { try { const { client, bucket, baseUrl } = getR2(); const { searchParams } = new URL(req.url); - const childId = searchParams.get("childId") || "default"; - const prefix = `memories/${childId}`; + const key = searchParams.get("key"); + const contentType = searchParams.get("contentType") || "image/jpeg"; - const command = new ListObjectsV2Command({ + if (!key) { + return NextResponse.json({ error: "Missing key" }, { status: 400 }); + } + + const arrayBuffer = await req.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + const command = new PutObjectCommand({ Bucket: bucket, - Prefix: prefix, + Key: key, + Body: buffer, + ContentType: contentType, }); - const res = await client.send(command); - const objects = (res.Contents || []).map((obj) => ({ - key: obj.Key, - url: `${baseUrl}/${obj.Key}`, - size: obj.Size, - lastModified: obj.LastModified?.toISOString(), - })).sort((a, b) => new Date(b.lastModified!).getTime() - new Date(a.lastModified!).getTime()); + await client.send(command); - return NextResponse.json({ memories: objects }); + return NextResponse.json({ + success: true, + key, + url: `${baseUrl}/${key}`, + }); } catch (error) { - console.error("R2 list error:", error); + console.error("R2 upload error:", error); return NextResponse.json({ error: String(error) }, { status: 500 }); } } \ No newline at end of file diff --git a/src/app/memories/page.tsx b/src/app/memories/page.tsx index 5a65032..9bc5f08 100644 --- a/src/app/memories/page.tsx +++ b/src/app/memories/page.tsx @@ -40,7 +40,7 @@ export default function MemoriesPage() { setUploadProgress(0); try { - // Get presigned URL + // Get upload key const res = await fetch("/api/upload", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -49,47 +49,33 @@ export default function MemoriesPage() { const data = await res.json(); if (data.error) { - console.error("API error:", data.error); alert("Error: " + data.error); setUploading(false); return; } - const { uploadUrl, key, publicUrl } = data; - console.log("Got presigned URL:", uploadUrl); + const { key, publicUrl } = data; - // Upload to R2 - const xhr = new XMLHttpRequest(); - xhr.open("PUT", uploadUrl); - xhr.setRequestHeader("Content-Type", file.type); - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - setUploadProgress(Math.round((e.loaded / e.total) * 100)); - } - }; - xhr.onerror = () => { - console.error("Upload XHR error"); - alert("Upload failed"); - setUploading(false); - }; - xhr.send(file); + // Upload through our server (no CORS issue) + const uploadRes = await fetch(`/api/upload?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(file.type)}`, { + method: "PUT", + body: file, + }); - xhr.onload = () => { - if (xhr.status === 200) { - setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]); - } else { - console.error("Upload failed status:", xhr.status, xhr.statusText); - alert("Upload failed: " + xhr.statusText); - } - setUploading(false); - setUploadProgress(0); - if (fileRef.current) fileRef.current.value = ""; - }; + const uploadData = await uploadRes.json(); + + if (uploadData.success) { + setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]); + } else { + alert("Upload failed: " + uploadData.error); + } } catch (err) { console.error("Upload failed:", err); alert("Error: " + err); - setUploading(false); } + setUploading(false); + setUploadProgress(0); + if (fileRef.current) fileRef.current.value = ""; }; return (