tia/src/app/api/img/route.ts
Mannu f953963b3b Fix memories disappearing + always-processing state
Three bugs fixed:

1. Vision failure was marking memories as 'failed', triggering orphan
   cleanup that permanently deleted them. Vision is optional — on error,
   now marks 'ready' so the image stays visible without AI captions.

2. Thumbnail failure was blocking vision from running (rejected promise
   swallowed the chain). Fixed by catching thumbnail error separately so
   processMemoryVision always executes and always sets a final status.

3. /api/img proxy was rejecting thumbnail keys (families/{id}/thumbnails/…)
   because 'families/' was not in ALLOWED_PREFIXES. Added it.

Also: replaced the full-bleed 'Processing…' overlay with a small corner
badge so the uploaded photo is visible immediately after upload.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-28 22:37:46 +05:30

50 lines
1.8 KiB
TypeScript

import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { NextRequest, NextResponse } from "next/server";
const ALLOWED_PREFIXES = ["avatars/", "profiles/", "memories/", "thumbnails/", "families/"];
export async function GET(req: NextRequest) {
const key = req.nextUrl.searchParams.get("key");
if (!key) return NextResponse.json({ error: "key required" }, { status: 400 });
// Only proxy our own R2 objects
if (!ALLOWED_PREFIXES.some(p => key.startsWith(p))) {
return NextResponse.json({ error: "Invalid key" }, { status: 403 });
}
const accountId = process.env.R2_ACCOUNT_ID;
const accessKeyId = process.env.R2_ACCESS_KEY_ID;
const secretKey = process.env.R2_SECRET_ACCESS_KEY;
const bucket = process.env.R2_BUCKET_NAME;
if (!accountId || !accessKeyId || !secretKey || !bucket) {
return NextResponse.json({ error: "Storage not configured" }, { status: 500 });
}
const client = new S3Client({
region: "auto",
endpoint: `https://${accountId}.r2.cloudflarestorage.com`,
credentials: { accessKeyId, secretAccessKey: secretKey },
});
try {
const obj = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
if (!obj.Body) return new NextResponse(null, { status: 404 });
const bytes = await (obj.Body as any).transformToByteArray();
return new NextResponse(bytes, {
status: 200,
headers: {
"Content-Type": obj.ContentType || "image/jpeg",
"Cache-Control": "public, max-age=604800, immutable",
...(obj.ContentLength ? { "Content-Length": String(obj.ContentLength) } : {}),
},
});
} catch (e: any) {
if (e?.name === "NoSuchKey" || e?.$metadata?.httpStatusCode === 404) {
return new NextResponse(null, { status: 404 });
}
console.error("R2 img proxy error:", e);
return new NextResponse(null, { status: 502 });
}
}