Fix upload: route through server to avoid CORS

This commit is contained in:
Manohar Gupta 2026-05-10 14:13:17 +05:30
parent e3c33bb0dc
commit b39f344426
2 changed files with 72 additions and 49 deletions

View file

@ -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 { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
@ -9,7 +9,7 @@ function getR2() {
const bucket = process.env.R2_BUCKET_NAME; const bucket = process.env.R2_BUCKET_NAME;
if (!accountId || !accessKeyId || !secretKey || !bucket) { 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 { 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) { export async function POST(req: NextRequest) {
let body; let body;
try { try {
@ -42,13 +67,17 @@ export async function POST(req: NextRequest) {
const ext = filename.split(".").pop() || "jpg"; const ext = filename.split(".").pop() || "jpg";
const key = `memories/${childId || "default"}/${Date.now()}-${Math.random().toString(36).slice(2)}.${ext}`; 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({ const command = new PutObjectCommand({
Bucket: bucket, Bucket: bucket,
Key: key, Key: key,
ContentType: contentType, 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({ return NextResponse.json({
uploadUrl: url, uploadUrl: url,
@ -56,34 +85,42 @@ export async function POST(req: NextRequest) {
publicUrl: `${baseUrl}/${key}`, publicUrl: `${baseUrl}/${key}`,
}); });
} catch (error) { } catch (error) {
console.error("R2 upload error:", error); console.error("R2 error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 }); 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 { try {
const { client, bucket, baseUrl } = getR2(); const { client, bucket, baseUrl } = getR2();
const { searchParams } = new URL(req.url); const { searchParams } = new URL(req.url);
const childId = searchParams.get("childId") || "default"; const key = searchParams.get("key");
const prefix = `memories/${childId}`; 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, Bucket: bucket,
Prefix: prefix, Key: key,
Body: buffer,
ContentType: contentType,
}); });
const res = await client.send(command); 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 }); return NextResponse.json({
success: true,
key,
url: `${baseUrl}/${key}`,
});
} catch (error) { } catch (error) {
console.error("R2 list error:", error); console.error("R2 upload error:", error);
return NextResponse.json({ error: String(error) }, { status: 500 }); return NextResponse.json({ error: String(error) }, { status: 500 });
} }
} }

View file

@ -40,7 +40,7 @@ export default function MemoriesPage() {
setUploadProgress(0); setUploadProgress(0);
try { try {
// Get presigned URL // Get upload key
const res = await fetch("/api/upload", { const res = await fetch("/api/upload", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@ -49,47 +49,33 @@ export default function MemoriesPage() {
const data = await res.json(); const data = await res.json();
if (data.error) { if (data.error) {
console.error("API error:", data.error);
alert("Error: " + data.error); alert("Error: " + data.error);
setUploading(false); setUploading(false);
return; return;
} }
const { uploadUrl, key, publicUrl } = data; const { key, publicUrl } = data;
console.log("Got presigned URL:", uploadUrl);
// Upload to R2 // Upload through our server (no CORS issue)
const xhr = new XMLHttpRequest(); const uploadRes = await fetch(`/api/upload?key=${encodeURIComponent(key)}&contentType=${encodeURIComponent(file.type)}`, {
xhr.open("PUT", uploadUrl); method: "PUT",
xhr.setRequestHeader("Content-Type", file.type); body: file,
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);
xhr.onload = () => { const uploadData = await uploadRes.json();
if (xhr.status === 200) {
setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]); if (uploadData.success) {
} else { setMemories([{ key, url: publicUrl, size: file.size, lastModified: new Date().toISOString() }, ...memories]);
console.error("Upload failed status:", xhr.status, xhr.statusText); } else {
alert("Upload failed: " + xhr.statusText); alert("Upload failed: " + uploadData.error);
} }
setUploading(false);
setUploadProgress(0);
if (fileRef.current) fileRef.current.value = "";
};
} catch (err) { } catch (err) {
console.error("Upload failed:", err); console.error("Upload failed:", err);
alert("Error: " + err); alert("Error: " + err);
setUploading(false);
} }
setUploading(false);
setUploadProgress(0);
if (fileRef.current) fileRef.current.value = "";
}; };
return ( return (