From 59d2d98a8cea9194384f101c32b58760cd92d609 Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 2 Oct 2025 22:25:26 -0400 Subject: [PATCH] fix to populate results in my-settings --- app/api/my-settings/delete/route.ts | 48 +++++++++++++++++++++ app/portal/my-settings/page.tsx | 66 ++++++++++++++++------------- 2 files changed, 85 insertions(+), 29 deletions(-) create mode 100644 app/api/my-settings/delete/route.ts diff --git a/app/api/my-settings/delete/route.ts b/app/api/my-settings/delete/route.ts new file mode 100644 index 00000000..c9590c2a --- /dev/null +++ b/app/api/my-settings/delete/route.ts @@ -0,0 +1,48 @@ +import { NextResponse } from "next/server"; +import { requireBearer } from "@/app/api/_lib/auth"; +import { dxGET, directusAdminFetch } from "@/lib/directus"; + +export const runtime = "nodejs"; + +/** + * Deletes a settings row by submission_id, but only if the caller owns it. + * Body: { collection: "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv", submission_id: string|number } + */ +export async function POST(req: Request) { + try { + const { collection, submission_id } = await req.json(); + + if ( + !collection || + !["settings_co2gal", "settings_co2gan", "settings_fiber", "settings_uv"].includes(collection) || + (submission_id === undefined || submission_id === null || String(submission_id) === "") + ) { + return NextResponse.json({ error: "Invalid request" }, { status: 400 }); + } + + // Who is calling? + const bearer = requireBearer(req); + const me = await dxGET("/users/me?fields=id", bearer); + const meId = me?.data?.id ?? me?.id; + if (!meId) return NextResponse.json({ error: "Unable to resolve user" }, { status: 401 }); + + // Find the item by submission_id using admin token (we cannot read id with user perms) + const q = `/items/${collection}?filter[submission_id][_eq]=${encodeURIComponent( + String(submission_id) + )}&fields=id,owner.id&limit=1`; + const found = await directusAdminFetch(q); + const row = Array.isArray(found?.data) ? found.data[0] : null; + if (!row?.id) return NextResponse.json({ error: "Not found" }, { status: 404 }); + + const ownerId = row?.owner?.id ?? row?.owner; + if (String(ownerId) !== String(meId)) { + return NextResponse.json({ error: "Forbidden" }, { status: 403 }); + } + + // Delete via admin + await directusAdminFetch(`/items/${collection}/${row.id}`, { method: "DELETE" }); + return NextResponse.json({ ok: true }); + } catch (e: any) { + return NextResponse.json({ error: e?.message || "Unknown error" }, { status: 500 }); + } +} diff --git a/app/portal/my-settings/page.tsx b/app/portal/my-settings/page.tsx index a5629493..bc0cb72c 100644 --- a/app/portal/my-settings/page.tsx +++ b/app/portal/my-settings/page.tsx @@ -1,4 +1,3 @@ -// app/portal/my-settings/page.tsx "use client"; import { useEffect, useMemo, useState } from "react"; @@ -7,11 +6,10 @@ import Link from "next/link"; type Coll = "settings_co2gal" | "settings_co2gan" | "settings_fiber" | "settings_uv"; type Row = { - id: string | number; - submission_id?: string | number | null; + submission_id: string | number; setting_title?: string | null; - status?: string | null; last_modified_date?: string | null; + // NOTE: we intentionally do NOT request id or status due to permissions }; const COLLECTIONS: Coll[] = ["settings_co2gal", "settings_co2gan", "settings_fiber", "settings_uv"]; @@ -22,13 +20,13 @@ const LABEL: Record = { settings_uv: "UV", }; -function detailHref(coll: Coll, idOrSubmission: string | number | null | undefined) { - const subId = idOrSubmission ?? ""; +function detailHref(coll: Coll, submissionId: string | number | null | undefined) { + const sid = submissionId ?? ""; switch (coll) { - case "settings_co2gal": return `/settings/co2-galvo/${subId}?edit=1`; - case "settings_co2gan": return `/settings/co2-gantry/${subId}?edit=1`; - case "settings_fiber": return `/settings/fiber/${subId}?edit=1`; - case "settings_uv": return `/settings/uv/${subId}?edit=1`; + case "settings_co2gal": return `/settings/co2-galvo/${sid}?edit=1`; + case "settings_co2gan": return `/settings/co2-gantry/${sid}?edit=1`; + case "settings_fiber": return `/settings/fiber/${sid}?edit=1`; + case "settings_uv": return `/settings/uv/${sid}?edit=1`; } } @@ -43,15 +41,20 @@ export default function MySettingsPage() { settings_fiber: [], settings_uv: [], }); - const [errs, setErrs] = useState>({ me: null, settings_co2gal: null, settings_co2gan: null, settings_fiber: null, settings_uv: null }); + const [errs, setErrs] = useState>({ + me: null, + settings_co2gal: null, + settings_co2gan: null, + settings_fiber: null, + settings_uv: null, + }); - // Safe JSON reader so HTML/404s don't explode parsing async function readJson(res: Response) { const text = await res.text(); try { return text ? JSON.parse(text) : null; } catch { throw new Error(`Unexpected response (HTTP ${res.status})`); } } - // 1) Who am I (id + username) + // 1) Load current user id + username useEffect(() => { let dead = false; (async () => { @@ -76,7 +79,7 @@ export default function MySettingsPage() { return () => { dead = true; }; }, []); - // 2) Load my items per collection with OR(filters) + // 2) Load my items per collection (no id/status fields requested) useEffect(() => { if (!meId && !meUsername) return; let dead = false; @@ -90,12 +93,9 @@ export default function MySettingsPage() { const qs = new URLSearchParams(); qs.set("limit", "-1"); qs.set("sort", "-last_modified_date"); - qs.set("fields", "id,submission_id,setting_title,status,last_modified_date"); + qs.set("fields", "submission_id,setting_title,last_modified_date"); // ← only fields we’re allowed - // Robust OR filter: - // - owner == meId - // - owner.id == meId (some Directus setups require nested id filter) - // - uploader == meUsername (fallback for legacy rows with missing owner) + // OR filter by owner and legacy uploader mirror let orIdx = 0; if (meId) { qs.set(`filter[_or][${orIdx}][owner][_eq]`, meId); orIdx++; @@ -138,7 +138,7 @@ export default function MySettingsPage() { const out: Record = { settings_co2gal: [], settings_co2gan: [], settings_fiber: [], settings_uv: [] }; for (const coll of COLLECTIONS) { out[coll] = (byColl[coll] || []).filter(r => - [r.setting_title, r.status, r.last_modified_date] + [r.setting_title, r.last_modified_date] .filter(Boolean) .some(v => String(v).toLowerCase().includes(needle)) ); @@ -147,14 +147,24 @@ export default function MySettingsPage() { }, [byColl, q]); async function onDelete(coll: Coll, row: Row) { + if (!row.submission_id) return alert("Missing submission id."); if (!confirm(`Delete "${row.setting_title || "Untitled"}" from ${LABEL[coll]}?`)) return; + try { - const r = await fetch(`/api/dx/items/${coll}/${row.id}`, { method: "DELETE", credentials: "include" }); - if (!r.ok) { - const j = await readJson(r).catch(() => null); - throw new Error(j?.errors?.[0]?.message || `HTTP ${r.status}`); + const r = await fetch(`/api/my-settings/delete`, { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ collection: coll, submission_id: row.submission_id }), + }); + const j = await readJson(r).catch(() => null); + if (!r.ok || !j?.ok) { + throw new Error(j?.error || `HTTP ${r.status}`); } - setByColl(prev => ({ ...prev, [coll]: prev[coll].filter(x => String(x.id) !== String(row.id)) })); + setByColl(prev => ({ + ...prev, + [coll]: prev[coll].filter(x => String(x.submission_id) !== String(row.submission_id)) + })); } catch (e: any) { alert(`Delete failed: ${e?.message || e}`); } @@ -205,22 +215,20 @@ export default function MySettingsPage() { Title - Status Updated Actions {rows.map((r) => ( - + {r.setting_title || "Untitled"} - {r.status || "—"} {r.last_modified_date ? new Date(r.last_modified_date).toLocaleString() : "—"}
- Edit + Edit