feat: add garden module infrastructure (task 70)
- Add MinIO service to compose.yaml with named volume - Add generic /api/uploads POST+GET route with auth, 5 MB, image/* guards - Add src/lib/minio.ts singleton client with ensureBucket helper - Add garden Drizzle schema: containers, plants, care_logs, care_schedules, species_cache - Register garden module in src/modules/index.ts and src/lib/db.ts - Apply migration 0015_garden_schema.sql - Add MINIO_* and PERENUAL_API_KEY to .env.example - Add task briefs 70-75 for the full garden feature arc - Add uploads.spec.ts E2E test (RED → GREEN on route auth guard)
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getCurrentSession } from "@/lib/session";
|
||||
import { ensureBucket, minioClient, MINIO_BUCKET } from "@/lib/minio";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
|
||||
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
let session: Awaited<ReturnType<typeof getCurrentSession>>;
|
||||
try {
|
||||
session = await getCurrentSession();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
let formData: FormData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid multipart body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const file = formData.get("file");
|
||||
if (!(file instanceof File)) {
|
||||
return NextResponse.json({ error: "No file field in request" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!file.type.startsWith("image/")) {
|
||||
return NextResponse.json({ error: "Only image files are allowed" }, { status: 415 });
|
||||
}
|
||||
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
return NextResponse.json({ error: "File exceeds 5 MB limit" }, { status: 413 });
|
||||
}
|
||||
|
||||
const rawExt = file.name.split(".").pop() ?? "bin";
|
||||
const safeExt = rawExt
|
||||
.replace(/[^a-z0-9]/gi, "")
|
||||
.toLowerCase()
|
||||
.slice(0, 8);
|
||||
const key = `garden/${session.household.id}/${randomUUID()}.${safeExt}`;
|
||||
|
||||
try {
|
||||
await ensureBucket();
|
||||
const buffer = Buffer.from(await file.arrayBuffer());
|
||||
await minioClient.putObject(MINIO_BUCKET, key, buffer, buffer.length, {
|
||||
"Content-Type": file.type,
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Upload failed", err);
|
||||
return NextResponse.json({ error: "Upload service unavailable" }, { status: 503 });
|
||||
}
|
||||
|
||||
return NextResponse.json({ url: `/api/uploads/${key}` });
|
||||
}
|
||||
Reference in New Issue
Block a user