user auth bearer updates to multiple apis and scripts

This commit is contained in:
makearmy 2025-09-29 12:39:59 -04:00
parent eb1a97541e
commit b54120d88e
4 changed files with 182 additions and 160 deletions

View file

@ -5,7 +5,9 @@ import {
createProjectRow,
patchProject,
bytesFromMB,
dxGET,
} from "@/lib/directus";
import { requireBearer } from "@/app/api/_lib/auth";
// Optional: tweak via env
const MAX_MB = Number(process.env.FILE_MAX_MB || 25);
@ -32,127 +34,105 @@ export const runtime = "nodejs";
export async function POST(req: NextRequest) {
try {
const ip =
req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ||
"0.0.0.0";
const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "0.0.0.0";
if (!rateLimitOk(ip)) {
return NextResponse.json({ error: "Rate limited" }, { status: 429 });
}
// NEW: enforce user auth
const bearer = requireBearer(req);
const ct = req.headers.get("content-type") || "";
if (!ct.includes("multipart/form-data")) {
return NextResponse.json(
{ error: "Expected multipart/form-data" },
{ status: 400 }
);
return NextResponse.json({ error: "Expected multipart/form-data" }, { status: 400 });
}
const form = await req.formData();
// Required read-side field names from your repo:
// title, body (markdown), uploader, category, tags[], p_image (file), p_files (M2M to files)
const title = String(form.get("title") || "").trim();
const uploader = String(form.get("uploader") || "").trim();
const category = String(form.get("category") || "").trim();
const body = String(form.get("body") || form.get("description") || "").trim();
if (!title || !uploader || !body) {
return NextResponse.json(
{ error: "Missing required fields: title, uploader, body" },
{ status: 400 }
);
if (!title || !body) {
return NextResponse.json({ error: "Missing required fields: title, body" }, { status: 400 });
}
// tags: allow comma-separated string or JSON array
let tags: string[] = [];
const rawTags = form.get("tags");
if (typeof rawTags === "string" && rawTags.trim()) {
try {
// Accept JSON array
const maybeArray = JSON.parse(rawTags);
if (Array.isArray(maybeArray)) {
tags = maybeArray.map((t) => String(t).trim()).filter(Boolean);
} else {
// Fallback: comma list
tags = rawTags
.split(",")
.map((t) => t.trim())
.filter(Boolean);
}
} catch {
// Comma-separated
tags = rawTags
.split(",")
.map((t) => t.trim())
.filter(Boolean);
}
// Derive uploader from the authenticated user (ignore any spoofed form value)
const me = await dxGET<any>("/users/me?fields=username,display_name,first_name,last_name,email", bearer);
const uploader =
me?.display_name ||
me?.username ||
[me?.first_name, me?.last_name].filter(Boolean).join(" ") ||
me?.email ||
"user";
// tags: allow comma-separated string or JSON array
let tags: string[] = [];
const rawTags = form.get("tags");
if (typeof rawTags === "string" && rawTags.trim()) {
try {
const maybeArray = JSON.parse(rawTags);
if (Array.isArray(maybeArray)) {
tags = maybeArray.map((t) => String(t).trim()).filter(Boolean);
} else {
tags = rawTags.split(",").map((t) => t.trim()).filter(Boolean);
}
// Optional license (not shown on read pages, but harmless to store)
const license =
(form.get("license") && String(form.get("license")).trim()) ||
undefined;
// Upload hero image (single)
const hero = form.get("image") as File | null; // input name="image"
let p_image_id: string | undefined;
if (hero && typeof hero === "object" && "size" in hero) {
if (hero.size > MAX_BYTES) {
return NextResponse.json(
{ error: `Hero image exceeds ${MAX_MB} MB` },
{ status: 400 }
);
}
const up = await uploadFile(hero, (hero as File).name || "project-image");
p_image_id = up.id;
}
// Upload attachments (multiple)
const fileBlobs = form.getAll("files").filter(Boolean) as File[];
const attachIds: string[] = [];
for (const f of fileBlobs.slice(0, 20)) {
if (f.size > MAX_BYTES) {
return NextResponse.json(
{ error: `One of the files exceeds ${MAX_MB} MB` },
{ status: 400 }
);
}
const up = await uploadFile(f, (f as File).name || "attachment");
attachIds.push(up.id);
}
// 1) Create the project row
const { data: created } = await createProjectRow({
title,
body, // you render `project.body` in detail page
uploader, // exact key used by your list/detail
category,
tags, // stored as array
...(license ? { license } : {}),
status: "pending",
submitted_via: "makearmy-app",
submitted_at: new Date().toISOString(),
});
// 2) Patch hero image + M2M attachments in one go
// For M2M (p_files), Directus accepts nested objects to create junction rows
// e.g. [{ directus_files_id: "<file-id>" }, ...]
const patch: Record<string, any> = {};
if (p_image_id) patch.p_image = p_image_id;
if (attachIds.length) {
patch.p_files = attachIds.map((id) => ({ directus_files_id: id }));
}
if (Object.keys(patch).length) {
await patchProject(created.id, patch);
}
return NextResponse.json({ ok: true, id: created.id });
} catch (err: any) {
return NextResponse.json(
{ error: err?.message || "Unknown error" },
{ status: 500 }
);
} catch {
tags = rawTags.split(",").map((t) => t.trim()).filter(Boolean);
}
}
const license = (form.get("license") && String(form.get("license")).trim()) || undefined;
// Upload hero image (single)
const hero = form.get("image") as File | null;
let p_image_id: string | undefined;
if (hero && typeof hero === "object" && "size" in hero) {
if (hero.size > MAX_BYTES) {
return NextResponse.json({ error: `Hero image exceeds ${MAX_MB} MB` }, { status: 400 });
}
const up = await uploadFile(hero, (hero as File).name || "project-image", bearer);
p_image_id = up.id;
}
// Upload attachments (multiple)
const fileBlobs = form.getAll("files").filter(Boolean) as File[];
const attachIds: string[] = [];
for (const f of fileBlobs.slice(0, 20)) {
if (f.size > MAX_BYTES) {
return NextResponse.json({ error: `One of the files exceeds ${MAX_MB} MB` }, { status: 400 });
}
const up = await uploadFile(f, (f as File).name || "attachment", bearer);
attachIds.push(up.id);
}
// 1) Create the project row (as current user)
const { data: created } = await createProjectRow(
{
title,
body,
uploader, // server-derived
category,
tags,
...(license ? { license } : {}),
status: "pending",
submitted_via: "makearmy-app",
submitted_at: new Date().toISOString(),
},
bearer
);
// 2) Patch hero image + M2M attachments in one go
const patch: Record<string, any> = {};
if (p_image_id) patch.p_image = p_image_id;
if (attachIds.length) patch.p_files = attachIds.map((id) => ({ directus_files_id: id }));
if (Object.keys(patch).length) {
await patchProject(created.id, patch, bearer);
}
return NextResponse.json({ ok: true, id: created.id });
} catch (err: any) {
return NextResponse.json({ error: err?.message || "Unknown error" }, { status: err?.status ?? 500 });
}
}