From fda015531c9c59faadf53faf9b67ee97de14f37e Mon Sep 17 00:00:00 2001 From: makearmy Date: Mon, 29 Sep 2025 10:43:53 -0400 Subject: [PATCH] switching fields and users calls to API proxy --- app/api/auth/me/route.ts | 65 +++++++++++++++++++++++++ app/api/directus/choices/route.ts | 39 +++++++++++++++ app/api/directus/fields/route.ts | 46 ++++++----------- app/components/forms/SettingsSubmit.tsx | 44 +++++------------ 4 files changed, 132 insertions(+), 62 deletions(-) create mode 100644 app/api/auth/me/route.ts create mode 100644 app/api/directus/choices/route.ts diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts new file mode 100644 index 00000000..f187b283 --- /dev/null +++ b/app/api/auth/me/route.ts @@ -0,0 +1,65 @@ +// app/api/auth/me/route.ts +import { NextRequest, NextResponse } from "next/server"; + +const DIRECTUS_URL = process.env.DIRECTUS_URL!; +const ACCESS_COOKIE = "ma_at"; + +export const runtime = "nodejs"; + +/** + * GET /api/auth/me + * Returns the current Directus user using the access token in "ma_at". + * Mirrors the shape you’re already expecting on the client: + * { id, username, display_name, first_name, last_name, email, ... } + */ +export async function GET(_req: NextRequest) { + try { + if (!DIRECTUS_URL) { + return NextResponse.json({ error: "Missing DIRECTUS_URL" }, { status: 500 }); + } + + // Prefer cookie; allow Authorization header for flexibility + const cookie = _req.cookies.get(ACCESS_COOKIE)?.value; + const authHeader = _req.headers.get("authorization") || ""; + const bearer = + authHeader?.toLowerCase().startsWith("bearer ") + ? authHeader.slice(7).trim() + : cookie; + + if (!bearer) { + // No token: treat as not signed in (same semantics as your client) + return NextResponse.json({ error: "not-signed-in" }, { status: 401 }); + } + + const url = `${DIRECTUS_URL}/users/me?fields=id,username,display_name,first_name,last_name,email`; + + const res = await fetch(url, { + headers: { + Accept: "application/json", + Authorization: `Bearer ${bearer}`, + }, + cache: "no-store", + }); + + const text = await res.text(); + let json: any = null; + try { + json = text ? JSON.parse(text) : null; + } catch { + // non-JSON from Directus; keep raw text for error messages + } + + if (!res.ok) { + const msg = json?.errors?.[0]?.message || json?.error || text || "Directus error"; + const status = res.status === 401 || res.status === 403 ? res.status : 500; + return NextResponse.json({ error: msg }, { status }); + } + + // Directus often wraps in { data: {...} } + const data = json?.data ?? json ?? null; + return NextResponse.json(data ?? {}, { status: 200 }); + } catch (err: any) { + const msg = err?.message || "Failed to fetch current user"; + return NextResponse.json({ error: msg }, { status: 500 }); + } +} diff --git a/app/api/directus/choices/route.ts b/app/api/directus/choices/route.ts new file mode 100644 index 00000000..1b66287d --- /dev/null +++ b/app/api/directus/choices/route.ts @@ -0,0 +1,39 @@ +// app/api/directus/choices/route.ts +import { NextRequest } from "next/server"; +import { directusAdminFetch } from "@/lib/directus"; + +export const dynamic = "force-dynamic"; + +export async function GET(req: NextRequest) { + const { searchParams } = new URL(req.url); + const collection = String(searchParams.get("collection") || ""); + const group = String(searchParams.get("group") || ""); + const field = String(searchParams.get("field") || ""); + if (!collection || !group || !field) { + return Response.json({ error: "collection, group, and field are required" }, { status: 400 }); + } + + // Pull field metadata server-side (admin token), then extract repeater child choices + const meta = await directusAdminFetch<{ data: any[] }>( + `/fields?filter[collection][_eq]=${encodeURIComponent(collection)}&limit=500` + ); + const rows: any[] = Array.isArray(meta?.data) ? meta.data : []; + + const parent = rows.find((r: any) => r?.field === group); + const nestedChildren: any[] = parent?.meta?.options?.fields || []; + const child = + nestedChildren.find((f: any) => f?.field === field) || + rows.find((r: any) => r?.field === `${group}.${field}`); + + const choices: any[] = + (child?.options?.choices as any[]) ?? + (child?.meta?.options?.choices as any[]) ?? + []; + + const data = choices.map((c: any) => ({ + id: String(c.value ?? c.key ?? c.id), + label: String(c.text ?? c.label ?? c.name ?? c.value ?? c.id), + })); + + return Response.json({ data }); +} diff --git a/app/api/directus/fields/route.ts b/app/api/directus/fields/route.ts index 0d0f5a83..e9a5f21c 100644 --- a/app/api/directus/fields/route.ts +++ b/app/api/directus/fields/route.ts @@ -1,40 +1,26 @@ // app/api/directus/fields/route.ts -import { NextRequest, NextResponse } from "next/server"; +import { NextRequest } from "next/server"; +import { directusAdminFetch } from "@/lib/directus"; -const DIRECTUS_URL = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); -const AUTH_HEADER = process.env.DIRECTUS_TOKEN_SUBMIT -? { Authorization: `Bearer ${process.env.DIRECTUS_TOKEN_SUBMIT}` } -: {}; - -export const dynamic = "force-dynamic"; // no caching -export const revalidate = 0; +export const dynamic = "force-dynamic"; export async function GET(req: NextRequest) { const { searchParams } = new URL(req.url); - const collection = searchParams.get("collection"); - + const collection = searchParams.get("collection")?.trim(); if (!collection) { - return NextResponse.json({ error: "Missing `collection`" }, { status: 400 }); + return Response.json({ error: "Missing ?collection" }, { status: 400 }); } - // 1) Preferred: /fields/{collection} - const url1 = `${DIRECTUS_URL}/fields/${encodeURIComponent(collection)}`; - let res = await fetch(url1, { headers: AUTH_HEADER, cache: "no-store" }); - - // 2) Fallback: /fields?filter[collection][_eq]=... - if (!res.ok) { - const url2 = `${DIRECTUS_URL}/fields?filter[collection][_eq]=${encodeURIComponent(collection)}`; - res = await fetch(url2, { headers: AUTH_HEADER, cache: "no-store" }); + try { + // Preferred endpoint + const res = await directusAdminFetch(`/fields/${encodeURIComponent(collection)}`); + const data = Array.isArray(res?.data) ? res.data : Array.isArray(res) ? res : []; + return Response.json({ data }); + } catch { + // Fallback (some Directus setups restrict the path variant) + const qs = new URLSearchParams({ "filter[collection][_eq]": collection }); + const fb = await directusAdminFetch(`/fields?${qs.toString()}`); + const data = Array.isArray(fb?.data) ? fb.data : Array.isArray(fb) ? fb : []; + return Response.json({ data }); } - - if (!res.ok) { - const body = await res.text().catch(() => ""); - return NextResponse.json( - { error: "Directus request failed", status: res.status, body }, - { status: 502 }, - ); - } - - const json = await res.json().catch(() => ({})); - return NextResponse.json(json); } diff --git a/app/components/forms/SettingsSubmit.tsx b/app/components/forms/SettingsSubmit.tsx index 1f8397a4..194755c6 100644 --- a/app/components/forms/SettingsSubmit.tsx +++ b/app/components/forms/SettingsSubmit.tsx @@ -109,36 +109,15 @@ function useOptions(path: string) { }; } } else if (rawPath === "repeater-choices") { - // target=, group=, field= const group = params.get("group") || ""; const field = params.get("field") || ""; const collection = params.get("target") || ""; - // Always go through our server proxy to read Directus field meta - const proxyUrl = `/api/directus/fields?collection=${encodeURIComponent(collection)}`; - const metaRes = await fetch(proxyUrl, { cache: "no-store" }); - if (!metaRes.ok) throw new Error(`Proxy ${metaRes.status} fetching ${proxyUrl}`); - - const metaJson = await metaRes.json().catch(() => ({})); - const rows: any[] = Array.isArray(metaJson?.data) ? metaJson.data : Array.isArray(metaJson) ? metaJson : []; - - // Nested repeater children live under parent.meta.options.fields - const parent = rows.find((r: any) => r?.field === group); - const nestedChildren = parent?.meta?.options?.fields || []; - let child = - nestedChildren.find((f: any) => f?.field === field) || - rows.find((r: any) => r?.field === `${group}.${field}`); // flat fallback - - // Choices may be on child.options.choices or child.meta.options.choices - const choices: any[] = - (child?.options?.choices as any[]) ?? - (child?.meta?.options?.choices as any[]) ?? - []; - - const mapped: Opt[] = choices.map((c: any) => ({ - id: String(c.value ?? c.key ?? c.id), - label: String(c.text ?? c.label ?? c.name ?? c.value), - })); + const proxy = `/api/directus/choices?collection=${encodeURIComponent(collection)}&group=${encodeURIComponent(group)}&field=${encodeURIComponent(field)}`; + const r = await fetch(proxy, { cache: "no-store" }); + if (!r.ok) throw new Error(`Proxy ${r.status} fetching ${proxy}`); + const j = await r.json().catch(() => ({})); + const mapped: Opt[] = Array.isArray(j?.data) ? j.data : []; if (alive) { const needle = (q || "").trim().toLowerCase(); @@ -146,7 +125,7 @@ function useOptions(path: string) { setOpts(filtered); setLoading(false); } - return; // short-circuit + return; } else { // unknown path → empty @@ -279,17 +258,18 @@ export default function SettingsSubmit({ initialTarget }: { initialTarget?: Targ useEffect(() => { let alive = true; - fetch(`${API}/users/me?fields=id,username,display_name,first_name,last_name,email`, { - cache: "no-store", - credentials: "include", - }) + + fetch(`/api/auth/me`, { cache: "no-store", credentials: "include" }) .then((r) => (r.ok ? r.json() : Promise.reject(r))) .then((j) => { - if (alive) setMe(j?.data || j || null); + if (!alive) return; + // j is the user object directly (not wrapped in { data }) + setMe(j || null); }) .catch(() => { if (alive) setMeErr("not-signed-in"); }); + return () => { alive = false; };