diff --git a/Archive_Updated2.zip b/Archive_Updated2.zip new file mode 100644 index 00000000..2551fd9b Binary files /dev/null and b/Archive_Updated2.zip differ diff --git a/app/_app_settings.zip b/app/_app_settings.zip new file mode 100644 index 00000000..a2d5719f Binary files /dev/null and b/app/_app_settings.zip differ diff --git a/app/api/auth/logout/route.ts b/app/api/auth/logout/route.ts index fdcd8c44..519d695e 100644 --- a/app/api/auth/logout/route.ts +++ b/app/api/auth/logout/route.ts @@ -1,11 +1,28 @@ // app/api/auth/logout/route.ts import { NextRequest, NextResponse } from "next/server"; -import { clearAuthCookies } from "@/lib/auth-cookies"; export const runtime = "nodejs"; +const secure = process.env.NODE_ENV === "production"; + export async function POST(_req: NextRequest) { const res = NextResponse.json({ ok: true }); - clearAuthCookies(res); + + res.cookies.set({ + name: "ma_at", + value: "", + httpOnly: true, + sameSite: "lax", + secure, + path: "/", + expires: new Date(0), // expire immediately + maxAge: 0, + }); + return res; } + +// Optional: support GET if you ever link to /api/auth/logout directly +export async function GET(_req: NextRequest) { + return POST(_req); +} diff --git a/app/api/my/rigs/[id]/route.ts b/app/api/my/rigs/[id]/route.ts index 140c2368..3fa80dfb 100644 --- a/app/api/my/rigs/[id]/route.ts +++ b/app/api/my/rigs/[id]/route.ts @@ -1,3 +1,4 @@ +// app/api/my/rigs/[id]/route.ts import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; import { directusFetch } from "@/lib/directus"; @@ -5,17 +6,16 @@ import { directusFetch } from "@/lib/directus"; const BASE_COLLECTION = "user_rigs"; async function bearerFromCookies() { - // Some Next 15 type defs model cookies() as async—await to satisfy TS in all envs. const store = await cookies(); const at = store.get("ma_at")?.value; if (!at) throw new Error("Not authenticated"); return `Bearer ${at}`; } -export async function PATCH(req: NextRequest, { params }: any) { +export async function PATCH(req: NextRequest, { params }: { params: { id?: string } }) { try { const auth = await bearerFromCookies(); - const body = await req.json(); + const body = await req.json().catch(() => ({})); const id = params?.id; if (!id) return NextResponse.json({ error: "Missing id" }, { status: 400 }); @@ -25,7 +25,7 @@ export async function PATCH(req: NextRequest, { params }: any) { method: "PATCH", headers: { "Content-Type": "application/json", - Authorization: auth, // overrides helper's default token + Authorization: auth, // force user-token for this call }, body: JSON.stringify(body), } @@ -40,7 +40,7 @@ export async function PATCH(req: NextRequest, { params }: any) { } } -export async function DELETE(_req: NextRequest, { params }: any) { +export async function DELETE(_req: NextRequest, { params }: { params: { id?: string } }) { try { const auth = await bearerFromCookies(); const id = params?.id; @@ -48,7 +48,7 @@ export async function DELETE(_req: NextRequest, { params }: any) { await directusFetch(`/items/${BASE_COLLECTION}/${id}`, { method: "DELETE", - headers: { Authorization: auth }, + headers: { Authorization: auth }, // force user-token }); return NextResponse.json({ ok: true }); diff --git a/app/api/my/rigs/route.ts b/app/api/my/rigs/route.ts index 2d407002..34ce51b8 100644 --- a/app/api/my/rigs/route.ts +++ b/app/api/my/rigs/route.ts @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server"; import { cookies } from "next/headers"; -const BASE = process.env.DIRECTUS_URL!; +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); async function bearerFromCookies() { const store = await cookies(); @@ -25,6 +25,8 @@ async function getMyUserId(bearer: string) { export async function GET(_req: NextRequest) { try { const bearer = await bearerFromCookies(); + const myId = await getMyUserId(bearer); + const fields = [ "id", "name", @@ -42,10 +44,17 @@ export async function GET(_req: NextRequest) { "date_updated", ].join(","); - const res = await fetch( - `${BASE}/items/user_rigs?fields=${encodeURIComponent(fields)}&sort=-date_created`, - { headers: { Authorization: bearer, Accept: "application/json" }, cache: "no-store" } - ); + const url = new URL(`${BASE}/items/user_rigs`); + url.searchParams.set("fields", fields); + url.searchParams.set("sort", "-date_created"); + // If you use a custom owner field, switch this to filter[owner][_eq] + url.searchParams.set("filter[user_created][_eq]", myId); + + const res = await fetch(String(url), { + headers: { Authorization: bearer, Accept: "application/json" }, + cache: "no-store", + }); + const txt = await res.text(); if (!res.ok) return NextResponse.json({ error: txt || res.statusText }, { status: res.status }); const j = txt ? JSON.parse(txt) : { data: [] }; @@ -65,9 +74,11 @@ export async function POST(req: NextRequest) { try { const bearer = await bearerFromCookies(); const body = await req.json().catch(() => ({})); - const owner = await getMyUserId(bearer); - const payload = { ...body, owner }; + // If your collection requires a custom 'owner' field, uncomment: + // const owner = await getMyUserId(bearer); + // const payload = { ...body, owner }; + const payload = body; const res = await fetch(`${BASE}/items/user_rigs`, { method: "POST", diff --git a/app/api/options/[collection]/route.ts b/app/api/options/[collection]/route.ts index b3841bdf..83d931ef 100644 --- a/app/api/options/[collection]/route.ts +++ b/app/api/options/[collection]/route.ts @@ -44,13 +44,12 @@ export async function GET(req: NextRequest, ctx: any) { const mapped = items.map((it) => ({ id: String(it.id ?? it.submission_id ?? ""), - label: pickLabel(it, cfg.labelFields), + name: pickLabel(it, cfg.labelFields), _search: `${Object.values(it).join(" ")}`.toLowerCase(), })).filter((m) => !!m.id); const filtered = q ? mapped.filter((m) => m._search.includes(q)) : mapped; - filtered.sort((a, b) => a.label.localeCompare(b.label)); - + filtered.sort((a, b) => (a.name ?? "").localeCompare(b.name ?? "")); return NextResponse.json({ data: filtered.map(({ _search, ...r }) => r) }); } catch (err: any) { return NextResponse.json( diff --git a/app/api/options/laser_focus_lens/route.ts b/app/api/options/laser_focus_lens/route.ts new file mode 100644 index 00000000..514dcf96 --- /dev/null +++ b/app/api/options/laser_focus_lens/route.ts @@ -0,0 +1,26 @@ +export const dynamic = "force-dynamic"; +import { NextRequest, NextResponse } from "next/server"; + +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +const PATH = `/items/laser_focus_lens?fields=id,name&sort=name`; + +export async function GET(req: NextRequest) { + try { + const userAt = req.cookies.get("ma_at")?.value; + if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + + const res = await fetch(`${BASE}${PATH}`, { + headers: { Accept: "application/json", Authorization: `Bearer ${userAt}` }, + cache: "no-store", + }); + + const txt = await res.text(); + if (!res.ok) return NextResponse.json({ error: txt || res.statusText }, { status: res.status }); + + const j = txt ? JSON.parse(txt) : { data: [] }; + const data = (j.data ?? []).map(({ id, name }: any) => ({ id, name })); + return NextResponse.json({ data }); + } catch (e: any) { + return NextResponse.json({ error: e?.message || "Failed to load focus lenses" }, { status: 500 }); + } +} diff --git a/app/api/options/laser_software/route.ts b/app/api/options/laser_software/route.ts new file mode 100644 index 00000000..2a41c7b4 --- /dev/null +++ b/app/api/options/laser_software/route.ts @@ -0,0 +1,39 @@ +// app/api/options/laser_software/route.ts +export const dynamic = "force-dynamic"; + +import { NextRequest, NextResponse } from "next/server"; + +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +const PATH = `/items/laser_software?fields=id,name&sort=name`; + +async function dFetch(bearer: string) { + const res = await fetch(`${BASE}${PATH}`, { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + cache: "no-store", + }); + const text = await res.text().catch(() => ""); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + return { res, json, text }; +} + +export async function GET(req: NextRequest) { + try { + const userAt = req.cookies.get("ma_at")?.value; + if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + + const r = await dFetch(userAt); + if (!r.res.ok) { + return NextResponse.json( + { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, + { status: r.res.status } + ); + } + + const rows: Array<{ id: number | string; name: string }> = r.json?.data ?? []; + const data = rows.map(({ id, name }) => ({ id, name })); + return NextResponse.json({ data }); + } catch (e: any) { + return NextResponse.json({ error: e?.message || "Failed to load software" }, { status: 500 }); + } +} diff --git a/app/api/options/laser_source/route.ts b/app/api/options/laser_source/route.ts index cc95be80..645bede6 100644 --- a/app/api/options/laser_source/route.ts +++ b/app/api/options/laser_source/route.ts @@ -1,65 +1,49 @@ -// app/app/api/options/laser_source/route.ts -import { NextResponse } from "next/server"; -import { directusFetch } from "@/lib/directus"; +// app/api/options/laser_source/route.ts +export const dynamic = "force-dynamic"; -// Parse "nm" that may be stored as a string (e.g., "1064", "1064nm", "1,064") -function parseNm(v: any): number | null { - const s = String(v ?? "").replace(/[^0-9.]/g, ""); - if (!s) return null; - const n = Number(s); - return Number.isFinite(n) ? n : null; +import { NextRequest, NextResponse } from "next/server"; + +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); + +function buildPath(target?: string | null) { + // If your schema supports target filtering, add it here. Otherwise we return all. + const url = new URL(`${BASE}/items/laser_source`); + url.searchParams.set("fields", "id,name"); + url.searchParams.set("sort", "name"); + // Example (uncomment/adjust if you actually have a `target` field or relation): + // if (target) url.searchParams.set("filter[target][_eq]", target); + return String(url); } -// target → wavelength range (nm) -function nmRangeForTarget(t?: string): [number, number] | null { - switch (t) { - case "settings_fiber": return [1000, 1100]; - case "settings_uv": return [300, 400]; - case "settings_co2gan": - case "settings_co2gal": return [10000, 11000]; - default: return null; - } +async function dFetch(bearer: string, target?: string | null) { + const res = await fetch(buildPath(target), { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + cache: "no-store", + }); + const text = await res.text().catch(() => ""); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + return { res, json, text }; } -export async function GET(req: Request) { +export async function GET(req: NextRequest) { try { - const { searchParams } = new URL(req.url); - const target = searchParams.get("target") || undefined; - const q = (searchParams.get("q") || "").trim().toLowerCase(); - const limit = Number(searchParams.get("limit") || "500"); + const userAt = req.cookies.get("ma_at")?.value; + if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - const range = nmRangeForTarget(target); - if (!range) { - return NextResponse.json({ error: "missing/invalid target" }, { status: 400 }); + const target = req.nextUrl.searchParams.get("target"); + const r = await dFetch(userAt, target); + if (!r.res.ok) { + return NextResponse.json( + { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, + { status: r.res.status } + ); } - const [lo, hi] = range; - // Only request fields we can read. laser_source uses submission_id as PK. - const url = `/items/laser_source?fields=submission_id,make,model,nm&limit=${limit}`; - const { data } = await directusFetch<{ data: any[] }>(url); - const list = Array.isArray(data) ? data : []; - - // Filter by nm and optional text query - const filtered = list.filter((x) => { - const nm = parseNm(x.nm); - if (nm === null || nm < lo || nm > hi) return false; - if (!q) return true; - const label = [x.make, x.model].filter(Boolean).join(" ").toLowerCase(); - return label.includes(q); - }); - - // Build labels and sort by make, then model - const out = filtered - .map((x) => ({ - id: String(x.submission_id), // critical: use submission_id, not id - label: [x.make, x.model].filter(Boolean).join(" ") || String(x.submission_id), - sortKey: [x.make ?? "", x.model ?? ""].join(" ").toLowerCase(), - })) - .sort((a, b) => a.sortKey.localeCompare(b.sortKey)) - .map(({ id, label }) => ({ id, label })); - - return NextResponse.json({ data: out }); + const rows: Array<{ id: number | string; name: string }> = r.json?.data ?? []; + const data = rows.map(({ id, name }) => ({ id, name })); + return NextResponse.json({ data }); } catch (e: any) { - return NextResponse.json({ error: e?.message || "Failed to load laser_source" }, { status: 500 }); + return NextResponse.json({ error: e?.message || "Failed to load laser sources" }, { status: 500 }); } } diff --git a/app/api/options/lens/route.ts b/app/api/options/lens/route.ts index 6a3c339e..dff09311 100644 --- a/app/api/options/lens/route.ts +++ b/app/api/options/lens/route.ts @@ -1,101 +1,49 @@ // app/api/options/lens/route.ts +export const dynamic = "force-dynamic"; + import { NextRequest, NextResponse } from "next/server"; -import { directusFetch } from "@/lib/directus"; -/** - * Parse "110x110", "110×110", "110 X 110", etc. -> [110, 110] - * Returns null if we can't find two numbers. - */ -function parseFieldSize(s: unknown): [number, number] | null { - if (!s) return null; - const nums = String(s).match(/\d+(\.\d+)?/g)?.map((n) => Number(n)) ?? []; - if (nums.length >= 2 && Number.isFinite(nums[0]) && Number.isFinite(nums[1])) { - return [nums[0], nums[1]]; - } - return null; +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); + +function buildPath(target?: string | null) { + // Adjust the collection name if yours differs (e.g., laser_scan_lens) + const url = new URL(`${BASE}/items/laser_scan_lens`); + url.searchParams.set("fields", "id,name"); + url.searchParams.set("sort", "name"); + // Example if you model per-target lenses: + // if (target) url.searchParams.set("filter[target][_eq]", target); + return String(url); } -/** Pull a clean integer from a value like "F160", "160", "160mm" */ -function parseFocalLength(v: unknown): number | null { - if (v == null) return null; - const m = String(v).match(/\d+(\.\d+)?/); - if (!m) return null; - const n = Number(m[0]); - return Number.isFinite(n) ? Math.round(n) : null; -} - -/** Natural-ish compare for scan lens sizes: sort by width then height */ -function sizeCompare(a: string, b: string): number { - const as = parseFieldSize(a); - const bs = parseFieldSize(b); - if (!as && !bs) return String(a).localeCompare(String(b)); - if (!as) return 1; - if (!bs) return -1; - // width, then height - if (as[0] !== bs[0]) return as[0] - bs[0]; - if (as[1] !== bs[1]) return as[1] - bs[1]; - return 0; +async function dFetch(bearer: string, target?: string | null) { + const res = await fetch(buildPath(target), { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + cache: "no-store", + }); + const text = await res.text().catch(() => ""); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + return { res, json, text }; } export async function GET(req: NextRequest) { try { - const { searchParams } = new URL(req.url); - const target = searchParams.get("target") || ""; - const q = (searchParams.get("q") || "").trim().toLowerCase(); - const limit = Number(searchParams.get("limit") || "500"); + const userAt = req.cookies.get("ma_at")?.value; + if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); - const isGantry = target === "settings_co2gan"; - - if (isGantry) { - // CO2 Gantry -> FOCUS lenses - const url = `/items/laser_focus_lens?fields=id,name&limit=${limit}`; - const { data } = await directusFetch<{ data: Array<{ id: string | number; name?: string }> }>(url); - let rows = (data ?? []).map((r) => ({ - id: String(r.id), - label: r.name?.trim() || String(r.id), - _key: r.name?.toLowerCase() ?? "", - })); - - if (q) rows = rows.filter((r) => r.label.toLowerCase().includes(q)); - rows.sort((a, b) => a.label.localeCompare(b.label)); - - return NextResponse.json({ data: rows.map(({ id, label }) => ({ id, label })) }); + const target = req.nextUrl.searchParams.get("target"); + const r = await dFetch(userAt, target); + if (!r.res.ok) { + return NextResponse.json( + { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, + { status: r.res.status } + ); } - // Galvo/UV/Fiber -> SCAN lenses with field_size + focal_length - const url = `/items/laser_scan_lens?fields=id,field_size,focal_length&limit=${limit}`; - const { data } = await directusFetch<{ - data: Array<{ id: string | number; field_size?: string; focal_length?: string | number }>; - }>(url); - - let rows = (data ?? []).map((r) => { - const sizeTxt = (r.field_size ?? "").toString().trim(); // e.g. "110x110" - const fmm = parseFocalLength(r.focal_length); // e.g. 160 - // Build label: "110x110mm (F160)" or "110x110mm" if focal length missing - const label = - sizeTxt - ? `${sizeTxt}mm${fmm != null ? ` (F${fmm})` : ""}` - : `${r.id}`; - return { - id: String(r.id), - label, - _size: sizeTxt, - }; - }); - - if (q) { - const qq = q.toLowerCase(); - rows = rows.filter((r) => r.label.toLowerCase().includes(qq)); - } - - rows.sort((a, b) => { - const c = sizeCompare(a._size, b._size); - return c !== 0 ? c : a.label.localeCompare(b.label); - }); - - return NextResponse.json({ data: rows.map(({ id, label }) => ({ id, label })) }); + const rows: Array<{ id: number | string; name: string }> = r.json?.data ?? []; + const data = rows.map(({ id, name }) => ({ id, name })); + return NextResponse.json({ data }); } catch (e: any) { - console.error("[options/lens] error:", e?.message || e); - return NextResponse.json({ error: e?.message || "Internal error" }, { status: 500 }); + return NextResponse.json({ error: e?.message || "Failed to load lenses" }, { status: 500 }); } } diff --git a/app/api/options/rig_type/route.ts b/app/api/options/rig_type/route.ts deleted file mode 100644 index 43cdc06b..00000000 --- a/app/api/options/rig_type/route.ts +++ /dev/null @@ -1,57 +0,0 @@ -// app/api/options/rig_type/route.ts -export const dynamic = "force-dynamic"; - -import { NextRequest, NextResponse } from "next/server"; - -const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); -const SUBMIT = process.env.DIRECTUS_TOKEN_SUBMIT || ""; // fallback for anon contexts -const ADMIN = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; // last-resort fallback - -const PATH = `/items/user_rig_type?fields=id,name&sort=sort`; - -async function dFetch(auth?: string) { - const headers: HeadersInit = { Accept: "application/json" }; - if (auth) headers.Authorization = `Bearer ${auth}`; - const res = await fetch(`${BASE}${PATH}`, { headers, cache: "no-store" }); - const text = await res.text().catch(() => ""); - let json: any = null; - try { json = text ? JSON.parse(text) : null; } catch {} - return { res, json, text }; -} - -export async function GET(req: NextRequest) { - try { - // 1) Prefer the *user's* token set by login - const userAt = req.cookies.get("ma_at")?.value; - - let r = await dFetch(userAt); - - // 2) If that’s forbidden/unauthorized (or they’re logged out), fall back to SUBMIT - if ((r.res.status === 401 || r.res.status === 403) && SUBMIT) { - r = await dFetch(SUBMIT); - } - - // 3) As a final fallback, try ADMIN (useful during migrations/hardening) - if ((r.res.status === 401 || r.res.status === 403) && ADMIN) { - r = await dFetch(ADMIN); - } - - if (!r.res.ok) { - return NextResponse.json( - { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, - { status: 500 } - ); - } - - const rows: Array<{ id: number | string; name: string }> = - r.json?.data ?? r.json ?? []; - - const data = rows.map(({ id, name }) => ({ id, label: name })); - return NextResponse.json({ data }, { status: 200 }); - } catch (e: any) { - return NextResponse.json( - { error: e?.message || "Failed to load rig types" }, - { status: 500 } - ); - } -} diff --git a/app/api/options/user_rig_type/route.ts b/app/api/options/user_rig_type/route.ts new file mode 100644 index 00000000..1bca7221 --- /dev/null +++ b/app/api/options/user_rig_type/route.ts @@ -0,0 +1,39 @@ +// app/api/options/user_rig_type/route.ts +export const dynamic = "force-dynamic"; + +import { NextRequest, NextResponse } from "next/server"; + +const BASE = (process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +const PATH = `/items/user_rig_type?fields=id,name&sort=sort`; + +async function dFetch(bearer: string) { + const res = await fetch(`${BASE}${PATH}`, { + headers: { Accept: "application/json", Authorization: `Bearer ${bearer}` }, + cache: "no-store", + }); + const text = await res.text().catch(() => ""); + let json: any = null; + try { json = text ? JSON.parse(text) : null; } catch {} + return { res, json, text }; +} + +export async function GET(req: NextRequest) { + try { + const userAt = req.cookies.get("ma_at")?.value; + if (!userAt) return NextResponse.json({ error: "Not authenticated" }, { status: 401 }); + + const r = await dFetch(userAt); + if (!r.res.ok) { + return NextResponse.json( + { error: `Directus ${r.res.status}: ${r.text || r.res.statusText}` }, + { status: r.res.status === 401 || r.res.status === 403 ? r.res.status : 500 } + ); + } + + const rows: Array<{ id: number | string; name: string }> = r.json?.data ?? []; + const data = rows.map(({ id, name }) => ({ id, name })); + return NextResponse.json({ data }, { status: 200 }); + } catch (e: any) { + return NextResponse.json({ error: e?.message || "Failed to load rig types" }, { status: 500 }); + } +} diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 8da0a7f5..fc337cac 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -1,116 +1,23 @@ -"use client"; +// app/auth/sign-in/page.tsx +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import SignIn from "./sign-in"; -import { Suspense, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; - -function SignInInner() { - const router = useRouter(); - const sp = useSearchParams(); - - const [idVal, setIdVal] = useState(""); - const [pwVal, setPwVal] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [err, setErr] = useState(null); - - const next = sp.get("next") || "/my/rigs"; - - async function onSubmit(e: React.FormEvent) { - e.preventDefault(); - setErr(null); - setSubmitting(true); - try { - const res = await fetch("/api/auth/login", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - identity: idVal, - email: idVal, - username: idVal, - password: pwVal, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(data?.error || "Login failed"); - - router.replace(next); - } catch (e: any) { - setErr(e?.message || "Login failed"); - } finally { - setSubmitting(false); - } +export default async function SignInPage({ + searchParams, +}: { + searchParams?: Record; +}) { + const at = (await cookies()).get("ma_at")?.value; + if (at) { + redirect("/portal"); } - return ( -
-
-
-

Sign in

-

- Welcome back. Enter your credentials to continue. -

-
- -
- {err && ( -
- {err} -
- )} - -
- - setIdVal(e.target.value)} - required - /> -
- -
- - setPwVal(e.target.value)} - required - /> -
- - - - -
- -

- Don’t have an account?{" "} - - Create one - -

-
-
- ); + const nextParam = toStr(searchParams?.next) || "/portal"; + return ; } -export default function SignInPage() { - return ( - -
Loading…
- - } - > - -
- ); +function toStr(v: string | string[] | undefined): string | undefined { + if (!v) return undefined; + return Array.isArray(v) ? v[0] : v; } diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx new file mode 100644 index 00000000..34e0c91b --- /dev/null +++ b/app/auth/sign-in/sign-in.tsx @@ -0,0 +1,101 @@ +// app/auth/sign-in/sign-in.tsx +"use client"; + +import * as React from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +type Props = { nextPath?: string }; + +export default function SignIn({ nextPath = "/portal" }: Props) { + const router = useRouter(); + const sp = useSearchParams(); + const [email, setEmail] = React.useState(""); + const [password, setPassword] = React.useState(""); + const [loading, setLoading] = React.useState(false); + const [error, setError] = React.useState(null); + + const next = sp.get("next") || nextPath; + + async function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + setLoading(true); + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + credentials: "include", // ensure cookie (ma_at) is set + body: JSON.stringify({ email, password }), + }); + const txt = await res.text(); + if (!res.ok) { + let msg = res.statusText || "Sign-in failed"; + try { + const j = txt ? JSON.parse(txt) : null; + msg = j?.error || j?.message || msg; + } catch {} + throw new Error(msg); + } + // success → land on next (or /portal) + router.replace(next || "/portal"); + } catch (err: any) { + setError(err?.message || "Sign-in failed"); + } finally { + setLoading(false); + } + } + + return ( +
+

Sign In

+
+
+ + setEmail(e.currentTarget.value)} + autoComplete="email" + required + /> +
+
+ + setPassword(e.currentTarget.value)} + autoComplete="current-password" + required + /> +
+ + {error && ( +
+ {error} +
+ )} + + +
+ +

+ Don’t have an account?{" "} + + Sign up + +

+
+ ); +} diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 63e3b9f2..dd8c12a6 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -1,131 +1,23 @@ -"use client"; +// app/auth/sign-up/page.tsx +import { cookies } from "next/headers"; +import { redirect } from "next/navigation"; +import SignUp from "./sign-up"; -import { Suspense, useState } from "react"; -import { useRouter, useSearchParams } from "next/navigation"; -import Link from "next/link"; -import { Input } from "@/components/ui/input"; -import { Button } from "@/components/ui/button"; - -function SignUpInner() { - const router = useRouter(); - const sp = useSearchParams(); - - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); // optional - const [password, setPassword] = useState(""); - const [submitting, setSubmitting] = useState(false); - const [err, setErr] = useState(null); - - const next = sp.get("next") || "/my/rigs"; - - async function onSubmit(e: React.FormEvent) { - e.preventDefault(); - setErr(null); - setSubmitting(true); - try { - const res = await fetch("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - username, - email: email || undefined, - password, - }), - }); - const data = await res.json().catch(() => ({})); - if (!res.ok) throw new Error(data?.error || "Registration failed"); - - router.replace(next); - } catch (e: any) { - setErr(e?.message || "Registration failed"); - } finally { - setSubmitting(false); - } +export default async function SignUpPage({ + searchParams, +}: { + searchParams?: Record; +}) { + const at = (await cookies()).get("ma_at")?.value; + if (at) { + redirect("/portal"); } - return ( -
-
-
-

Create account

-

- Pick a username and password. Email is optional (recommended for password reset). -

-
- -
- {err && ( -
- {err} -
- )} - -
- - setUsername(e.target.value)} - required - /> -
- -
- - setEmail(e.target.value)} - /> -

- Without an email, we can’t reset your password if you lose it. -

-
- -
- - setPassword(e.target.value)} - required - /> -
- - - - -
- -

- Already have an account?{" "} - - Sign in - -

-
-
- ); + const nextParam = toStr(searchParams?.next) || "/portal"; + return ; } -export default function SignUpPage() { - return ( - -
Loading…
- - } - > - -
- ); +function toStr(v: string | string[] | undefined): string | undefined { + if (!v) return undefined; + return Array.isArray(v) ? v[0] : v; } diff --git a/app/auth/sign-up/sign-up.tsx b/app/auth/sign-up/sign-up.tsx new file mode 100644 index 00000000..e8f54590 --- /dev/null +++ b/app/auth/sign-up/sign-up.tsx @@ -0,0 +1,138 @@ +// app/auth/sign-up/sign-up.tsx +"use client"; + +import { useState, useCallback } from "react"; +import { useRouter } from "next/navigation"; + +type Props = { + nextPath?: string; // where to go after successful sign-up +}; + +export default function SignUp({ nextPath = "/portal" }: Props) { + const router = useRouter(); + + const [username, setUsername] = useState(""); + const [email, setEmail] = useState(""); // optional per your backend flow + const [password, setPassword] = useState(""); + const [showPassword, setShowPassword] = useState(false); + const [loading, setLoading] = useState(false); + const [err, setErr] = useState(null); + + const onSubmit = useCallback(async (e: React.FormEvent) => { + e.preventDefault(); + setErr(null); + setLoading(true); + + try { + const res = await fetch("/api/auth/register", { + method: "POST", + credentials: "include", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ + username, + email: email || undefined, + password, + }), + }); + + const txt = await res.text(); + let j: any = null; + try { j = txt ? JSON.parse(txt) : null; } catch {} + + if (!res.ok) { + const message = + j?.error || + j?.message || + (typeof j === "string" ? j : "") || + `Sign-up failed (${res.status})`; + throw new Error(message); + } + + // Expect server to create user + set cookies (ma_at, etc.) + router.replace(nextPath || "/portal"); + router.refresh(); + } catch (e: any) { + setErr(e?.message || "Unable to sign up."); + } finally { + setLoading(false); + } + }, [username, email, password, nextPath, router]); + + return ( +
+

Create Account

+

Join MakerDash to manage rigs, settings, and projects.

+ +
+
+ + setUsername(e.currentTarget.value)} + required + /> +
+ +
+ + setEmail(e.currentTarget.value)} + /> +
+ +
+
+ + +
+ setPassword(e.currentTarget.value)} + required + minLength={8} + /> +
+ + {err && ( +
+ {err} +
+ )} + + +
+ +
+ Already have an account?{" "} + + Sign in + +
+
+ ); +} diff --git a/app/my/rigs/RigBuilderClient.tsx b/app/my/rigs/RigBuilderClient.tsx index ed4221a3..03f074ad 100644 --- a/app/my/rigs/RigBuilderClient.tsx +++ b/app/my/rigs/RigBuilderClient.tsx @@ -1,3 +1,4 @@ +// app/my/rigs/RigBuilderClient.tsx "use client"; import { useEffect, useMemo, useState } from "react"; @@ -5,12 +6,18 @@ import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; +let _redirectingDueToAuth = false; function handleAuthError(err: any): boolean { const status = (err as any)?.status; const code = (err as any)?.code; if (status === 401 || code === "TOKEN_EXPIRED") { - const next = encodeURIComponent(window.location.pathname + window.location.search); - window.location.assign(`/auth/sign-in?next=${next}`); + if (_redirectingDueToAuth) return true; + _redirectingDueToAuth = true; + + const here = window.location.pathname + window.location.search; + const onSignIn = window.location.pathname.startsWith("/auth"); + const next = encodeURIComponent(here); + window.location.replace(onSignIn ? `/auth/sign-in` : `/auth/sign-in?next=${next}`); return true; } return false; @@ -40,7 +47,7 @@ import { Badge } from "@/components/ui/badge"; // Types // ───────────────────────────────────────────────────────────── -type Option = { id: string | number; label: string }; +type Option = { id: string | number; name: string }; type RigType = { id: number | string; name: "fiber" | "uv" | "co2_galvo" | "co2_gantry" | string }; type RigRow = { @@ -54,7 +61,6 @@ type RigRow = { // Helpers // ───────────────────────────────────────────────────────────── -// Builder rig_type -> settings form target expected by options API const SETTINGS_TARGET_MAP: Record = { fiber: "settings_fiber", co2_gantry: "settings_co2gan", @@ -73,7 +79,6 @@ async function apiJson(url: string, init?: RequestInit): Promise { if (res.ok) { try { return JSON.parse(txt) as T; } catch { return undefined as T; } } - // try to unwrap nested error format let body: any = undefined; try { body = JSON.parse(txt); } catch {} if (body && typeof body.error === "string") { @@ -121,10 +126,8 @@ export default function RigBuilderClient() { useEffect(() => { (async () => { try { - const swJson = await apiJson(`/api/options/laser_software`); - const sw = - Array.isArray(swJson?.data) ? swJson.data : - Array.isArray(swJson) ? swJson : []; + const swJson = await apiJson<{ data: Option[] }>(`/api/options/laser_software`); + const sw = Array.isArray(swJson?.data) ? swJson.data : []; setSoftwareOpts(sw); } catch (e: any) { if (!handleAuthError(e)) { @@ -168,13 +171,13 @@ export default function RigBuilderClient() { (async () => { try { const [typesRes, rigsRes] = await Promise.all([ - apiJson<{ data: { id: number | string; label?: string; name?: string }[] }>(`/api/options/user_rig_type`), + apiJson<{ data: { id: number | string; name: string }[] }>(`/api/options/user_rig_type`), apiJson<{ data: RigRow[] }>(`/api/my/rigs`), ]); const mappedTypes: RigType[] = (typesRes?.data ?? []).map((t) => ({ id: t.id, - name: (t.label ?? t.name ?? String(t.id)) as any, + name: t.name as any, })); setRigTypes(mappedTypes); setRigs(rigsRes.data ?? []); @@ -255,7 +258,10 @@ export default function RigBuilderClient() { try { const payload = { name: values.name, - rig_type: rigTypes.find((t) => String(t.name) === String(values.rig_type))?.id ?? values.rig_type, + // Your select uses the slug (name) as value; map it back to id for save: + rig_type: + rigTypes.find((t) => String(t.name) === String(values.rig_type))?.id ?? + values.rig_type, laser_source: values.laser_source || null, laser_software: values.laser_software || null, laser_focus_lens: isGantry ? values.laser_focus_lens || null : null, @@ -325,16 +331,12 @@ export default function RigBuilderClient() { const rigTypeItems = useMemo( () => rigTypes.map((t) => ({ - value: String(t.name), + value: String(t.name), // using slug as value per your current pattern label: String(t.name).replaceAll("_", " "), })), [rigTypes] ); - // ───────────────────────────────────────────────────────────── - // UI - // ───────────────────────────────────────────────────────────── - return (
@@ -380,7 +382,7 @@ export default function RigBuilderClient() { {sourceOpts.map((o) => ( - {o.label} + {o.name} ))} @@ -401,14 +403,14 @@ export default function RigBuilderClient() { {softwareOpts.map((o) => ( - {o.label} + {o.name} ))}
- {/* Notes spans 2 cols */} + {/* Notes */}