- /api/img: add garments/ to ALLOWED_PREFIXES so garment images proxy correctly - garments upload: resolve Android empty MIME type from file extension; return /api/img proxy URLs instead of raw pub-*.r2.dev (blocked by Cloudflare Bot Mgmt) - garments route + [id] route: toDto() now builds /api/img?key= proxy URLs - date-ist.ts: add toUTCDate() helper -- strings without Z/offset treated as UTC, preventing browser local-time misinterpretation; used in fmtTime, fmtDate, dateIST - LogModal: add editTime to SmartDefault; pre-fill time picker (custom preset) when editing an existing log instead of defaulting to now - activity page: pass editTime: log.loggedAt in handleEdit so LogModal pre-fills - wardrobe/add: explicit Camera and Gallery buttons via separate hidden inputs (one with capture=environment for direct camera, one without for media picker) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
50 lines
1.9 KiB
TypeScript
50 lines
1.9 KiB
TypeScript
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
|
|
import { NextRequest, NextResponse } from "next/server";
|
|
|
|
const ALLOWED_PREFIXES = ["avatars/", "profiles/", "memories/", "thumbnails/", "families/", "garments/"];
|
|
|
|
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 });
|
|
}
|
|
}
|