From 6743c0b83bdcc9adba0188a93f4828e74ce353bf Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 2 Oct 2025 14:21:12 -0400 Subject: [PATCH 01/10] sign in and register fixes for looped and forwarded pages --- app/auth/sign-in/page.tsx | 3 ++- app/auth/sign-in/sign-in.tsx | 29 +++++++++++++++++++---------- app/auth/sign-up/page.tsx | 3 ++- app/page.tsx | 29 +++++++++++++++++++++++------ lib/jwt.ts | 15 +++++++++++++++ middleware.ts | 9 +++++++-- 6 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 lib/jwt.ts diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 59709b12..2938212c 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -2,6 +2,7 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import SignIn from "./sign-in"; +import { isJwtValid } from "@/lib/jwt"; export default async function SignInPage({ searchParams, @@ -22,7 +23,7 @@ export default async function SignInPage({ if (!reauth) { const ck = await cookies(); const at = ck.get("ma_at")?.value; - if (at) redirect("/portal"); + if (isJwtValid(at)) redirect("/portal"); } return ; diff --git a/app/auth/sign-in/sign-in.tsx b/app/auth/sign-in/sign-in.tsx index 55b24cdc..ce4c6868 100644 --- a/app/auth/sign-in/sign-in.tsx +++ b/app/auth/sign-in/sign-in.tsx @@ -20,11 +20,16 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props) setErr(null); setLoading(true); try { + const body = { + identifier: identifier.trim(), + password, + }; + const res = await fetch("/api/auth/login", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", Accept: "application/json" }, - body: JSON.stringify({ identifier, password }), + body: JSON.stringify(body), }); const txt = await res.text(); @@ -34,7 +39,10 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props) } catch {} if (!res.ok) { - throw new Error(j?.error || j?.message || `Sign-in failed (${res.status})`); + // surface server-provided message when available + const msg = + j?.error || j?.message || (res.status === 401 ? "Invalid credentials." : `Sign-in failed (${res.status})`); + throw new Error(msg); } // If this sign-in is being used as a re-auth step, set a short-lived 'recent auth' marker. @@ -56,6 +64,8 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props) [identifier, password, nextPath, router, reauth] ); + const createHref = `/auth/sign-up?next=${encodeURIComponent(nextPath)}`; + return (
@@ -116,14 +126,13 @@ export default function SignIn({ nextPath = "/portal", reauth = false }: Props) - {!reauth && ( -
- New here?{" "} - - Create an account - -
- )} + {/* Always show a sign-up path, even on reauth, to avoid dead-ends for first-time visitors */} +
+ {reauth ? "Don't have an account?" : "New here?"}{" "} + + Create an account + +
); } diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 569ae57c..96ce741d 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -2,6 +2,7 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import SignUp from "./sign-up"; +import { isJwtValid } from "@/lib/jwt"; export default async function SignUpPage({ searchParams, @@ -10,7 +11,7 @@ export default async function SignUpPage({ }) { const ck = await cookies(); const at = ck.get("ma_at")?.value; - if (at) redirect("/portal"); + if (isJwtValid(at)) redirect("/portal"); const sp = searchParams ?? {}; const nextParam = Array.isArray(sp.next) ? sp.next[0] : sp.next; diff --git a/app/page.tsx b/app/page.tsx index 245481bc..29fd3114 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -3,19 +3,35 @@ import { cookies } from "next/headers"; import { redirect } from "next/navigation"; import SignIn from "@/app/auth/sign-in/sign-in"; import SignUp from "@/app/auth/sign-up/sign-up"; +import { isJwtValid } from "@/lib/jwt"; -export default async function HomePage() { - // If already signed in, go straight to the app +type SearchParams = { [key: string]: string | string[] | undefined }; + +export default async function HomePage({ + searchParams, +}: { + searchParams?: SearchParams; +}) { + // If already signed in with a VALID token, go straight to the app const ck = await cookies(); const at = ck.get("ma_at")?.value; - if (at) redirect("/portal"); + if (isJwtValid(at)) redirect("/portal"); + + const reauth = searchParams?.reauth === "1"; return (
+ {reauth && ( +

+ Your session expired. Please sign in again. +

+ )} +

MakeArmy

- Free to use. Manage laser rigs, settings, and projects—all in one place. + Free to use. Manage laser rigs, settings, and projects—all in one + place.

@@ -29,12 +45,13 @@ export default async function HomePage() {

Sign in

{/* Uses your existing sign-in component */} - +
- We only use cookies strictly necessary to operate the site (e.g., your sign-in session). + We only use cookies strictly necessary to operate the site (e.g., your + sign-in session).
); diff --git a/lib/jwt.ts b/lib/jwt.ts new file mode 100644 index 00000000..287a9ff2 --- /dev/null +++ b/lib/jwt.ts @@ -0,0 +1,15 @@ +// lib/jwt.ts +export function jwtExp(token?: string | null): number | null { + if (!token) return null; + try { + const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString("utf8")); + return typeof payload?.exp === "number" ? payload.exp : null; + } catch { + return null; + } +} + +export function isJwtValid(token?: string | null): boolean { + const exp = jwtExp(token); + return !!exp && exp * 1000 > Date.now(); +} diff --git a/middleware.ts b/middleware.ts index ac77008f..a535742e 100644 --- a/middleware.ts +++ b/middleware.ts @@ -83,6 +83,11 @@ import { NextResponse, NextRequest } from "next/server"; const url = req.nextUrl.clone(); const { pathname } = url; + // ── 0) Absolute rule: the homepage must never redirect (no mapping, no gating). + if (pathname === "/") { + return NextResponse.next(); + } + // ── 1) Legacy → Portal / Canonical mapping (runs before auth gating) const mapped = legacyMap(pathname); if (mapped && !isSameUrl(req, mapped)) { @@ -174,8 +179,8 @@ import { NextResponse, NextRequest } from "next/server"; type MapResult = { pathname: string; query?: Record }; function legacyMap(pathname: string): MapResult | null { - // If we’re already inside the portal, don’t try to remap again. - if (pathname.startsWith("/portal")) return null; + // Never map the homepage, and if we’re already inside the portal, don’t remap again. + if (pathname === "/" || pathname.startsWith("/portal")) return null; // 1) DETAIL PAGES: map legacy detail URLs straight into the portal with ?id= // NOTE: We intentionally DO NOT remap `/lasers/:id` and `/projects/:id` From a77db7e7811a737d1cb8f3a713139e280189a8f6 Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 2 Oct 2025 18:36:50 -0400 Subject: [PATCH 02/10] registration requires email --- app/api/auth/login/route.ts | 87 ++++++++-------- app/api/auth/register/route.ts | 180 ++++++++++++++------------------- app/auth/sign-up/sign-up.tsx | 90 ++++++++++------- 3 files changed, 173 insertions(+), 184 deletions(-) diff --git a/app/api/auth/login/route.ts b/app/api/auth/login/route.ts index f3d95725..4f357ed2 100644 --- a/app/api/auth/login/route.ts +++ b/app/api/auth/login/route.ts @@ -3,71 +3,72 @@ import { NextRequest, NextResponse } from "next/server"; import { emailForUsername, loginDirectus } from "@/lib/directus"; export const runtime = "nodejs"; - const secure = process.env.NODE_ENV === "production"; -/** - * Accepts any of: - * - { identifier: string, password: string } // email OR username in `identifier` - * - { email: string, password: string } - * - { username: string, password: string } - * - * On success: sets HttpOnly "ma_at" cookie and returns { ok: true }. - */ export async function POST(req: NextRequest) { try { const body = await req.json().catch(() => ({} as any)); + const identifier = + String(body?.identifier ?? body?.email ?? body?.username ?? "").trim(); const password = String(body?.password ?? "").trim(); - let identifier = String( - body?.identifier ?? body?.email ?? body?.username ?? "" - ).trim(); if (!identifier || !password) { return NextResponse.json({ error: "Missing credentials" }, { status: 400 }); } - // Resolve to an email for Directus login: - // - If identifier looks like an email, use it directly. - // - Otherwise treat it as a username and look up the email. - let email = identifier.includes("@") ? identifier : null; - if (!email) { - email = await emailForUsername(identifier); - if (!email) { - return NextResponse.json({ error: "User not found" }, { status: 404 }); + // 1) Try Directus directly with the identifier (email OR username) + // Directus expects the field name "email" for both. + const tryIds: string[] = [identifier]; + + // 2) Fallback: if it doesn’t look like an email, try the canonical email (if any) + if (!identifier.includes("@")) { + try { + const em = await emailForUsername(identifier); // returns string|null + if (em && em !== identifier) tryIds.push(em); + } catch { + // ignore lookup errors, we'll just rely on the first attempt } } - // Login against Directus; helper returns { access_token, expires } (root or .data) - const data = await loginDirectus(email, password); - - const access = - data?.access_token ?? data?.data?.access_token ?? null; - const expiresSec = - data?.expires ?? data?.data?.expires ?? null; - - if (!access) { - return NextResponse.json( - { error: "Invalid response from auth provider" }, - { status: 502 } - ); + let tokens: any = null; + let lastErr: any = null; + for (const id of tryIds) { + try { + tokens = await loginDirectus(id, password); // { access_token, refresh_token, expires? } + if (tokens) break; + } catch (e) { + lastErr = e; + } } + if (!tokens?.access_token) { + const msg = + lastErr?.response?.data?.errors?.[0]?.message || + lastErr?.response?.data?.error || + lastErr?.message || + "Invalid credentials."; +return NextResponse.json({ error: msg }, { status: 401 }); + } + + // Set HttpOnly cookies for your middleware + const maxAge = 60 * 60; // 1h const res = NextResponse.json({ ok: true }); - - // Use provider TTL if present, else fallback to 8h - const maxAge = - typeof expiresSec === "number" ? Math.max(0, Math.floor(expiresSec)) : 60 * 60 * 8; - - res.cookies.set({ - name: "ma_at", - value: access, + res.cookies.set("ma_at", tokens.access_token, { + path: "/", httpOnly: true, sameSite: "lax", secure, - path: "/", maxAge, }); - + if (tokens.refresh_token) { + res.cookies.set("ma_rt", tokens.refresh_token, { + path: "/", + httpOnly: true, + sameSite: "lax", + secure, + maxAge: 60 * 60 * 24 * 30, // 30d + }); + } return res; } catch (err: any) { const message = diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 037cce4e..9cc98e5b 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,89 +1,76 @@ // app/api/auth/register/route.ts import { NextResponse } from "next/server"; -import { loginDirectus } from "@/lib/directus"; -export const runtime = "nodejs"; - -// Base URL (no trailing slash) -const API = (process.env.NEXT_PUBLIC_API_BASE_URL || process.env.DIRECTUS_URL || "").replace(/\/$/, ""); - -/** - * Accept either: - * - DIRECTUS_SERVICE_TOKEN (generic name), or - * - DIRECTUS_TOKEN_ADMIN_REGISTER (your current env) - */ +const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); const SERVICE_TOKEN = process.env.DIRECTUS_SERVICE_TOKEN || -process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || -""; +process.env.DIRECTUS_ADMIN_TOKEN || +process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; +const DEFAULT_ROLE = process.env.DIRECTUS_DEFAULT_ROLE || undefined; +const SECURE = process.env.NODE_ENV === "production"; -// Auto login right after signup (default: true) -const AUTO_LOGIN = (process.env.SIGNUP_AUTO_LOGIN ?? "1") !== "0"; -const secure = process.env.NODE_ENV === "production"; - -function bad(message: string, status = 400, extra: Record = {}) { - return NextResponse.json({ error: message, ...extra }, { status }); +function bad(message: string, status = 400) { + return NextResponse.json({ error: message }, { status }); } +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; -// Resolve the role id for the role named **Users**. No fallbacks. -async function getUsersRoleId(): Promise { - if (!API) throw new Error("DIRECTUS_URL / NEXT_PUBLIC_API_BASE_URL is not set"); - if (!SERVICE_TOKEN) throw new Error("DIRECTUS_SERVICE_TOKEN / DIRECTUS_TOKEN_ADMIN_REGISTER is not set"); - - const r = await fetch(`${API}/roles?filter[name][_eq]=Users&fields=id,name&limit=1`, { - headers: { Authorization: `Bearer ${SERVICE_TOKEN}`, Accept: "application/json" }, - cache: "no-store", +async function directusLogin(email: string, password: string) { + const r = await fetch(`${DIRECTUS}/auth/login`, { + method: "POST", + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ email, password }), + cache: "no-store", }); const j = await r.json().catch(() => ({})); - if (!r.ok) { - const reason = j?.errors?.[0]?.message || r.statusText; - throw new Error(`Failed to query role "Users": ${reason}`); - } - const id = j?.data?.[0]?.id ?? j?.[0]?.id; - if (!id) { - throw new Error('Role "Users" not found. Create it in Directus or check the service token permissions.'); - } - return String(id); + if (!r.ok) throw new Error(j?.errors?.[0]?.message || j?.message || `Login failed (${r.status})`); + return j?.data || j; } export async function POST(req: Request) { try { - if (!API) { - return bad("Server misconfiguration: DIRECTUS_URL / NEXT_PUBLIC_API_BASE_URL is not set", 500); - } - if (!SERVICE_TOKEN) { - return bad( - "Server misconfiguration: DIRECTUS_SERVICE_TOKEN / DIRECTUS_TOKEN_ADMIN_REGISTER is not set", - 500, - { hint: "Set DIRECTUS_TOKEN_ADMIN_REGISTER= (or DIRECTUS_SERVICE_TOKEN) and restart the server." } - ); + if (!DIRECTUS) return bad("Missing DIRECTUS_URL/NEXT_PUBLIC_API_BASE_URL", 500); + if (!SERVICE_TOKEN) return bad("Missing DIRECTUS_SERVICE_TOKEN / admin token", 500); + + const body = await req.json().catch(() => ({} as any)); + const email = String(body?.email ?? "").trim().toLowerCase(); + const username = String(body?.username ?? "").trim(); + const password = String(body?.password ?? "").trim(); + const confirm = String(body?.confirmPassword ?? body?.confirm ?? "").trim(); + + if (!email || !username || !password || !confirm) return bad("All fields are required"); + if (!EMAIL_RE.test(email)) return bad("Enter a valid email address"); + if (password.length < 8) return bad("Password must be at least 8 characters"); + if (password !== confirm) return bad("Passwords do not match"); + + // Optional pre-check to return a friendly 409 instead of a generic Directus error + const existsRes = await fetch( + `${DIRECTUS}/users?filter[_or][0][email][_eq]=${encodeURIComponent(email)}` + + `&filter[_or][1][username][_eq]=${encodeURIComponent(username)}` + + `&fields=id,email,username&limit=1`, + { + headers: { + Authorization: `Bearer ${SERVICE_TOKEN}`, + Accept: "application/json", + }, + cache: "no-store", + } + ); + const existsJson = await existsRes.json().catch(() => ({})); + if (Array.isArray(existsJson?.data) && existsJson.data.length > 0) { + return bad("Email or username already in use", 409); } - const body = await req.json().catch(() => ({})); - const username = String(body?.username || "").trim(); - const email = String(body?.email || "").trim().toLowerCase(); // optional - const password = String(body?.password || "").trim(); - const first_name = String(body?.first_name || "").trim() || undefined; - const last_name = String(body?.last_name || "").trim() || undefined; - - if (!username) return bad("Username is required"); - if (!password || password.length < 8) return bad("Password must be at least 8 characters"); - - // Only accept the "Users" role - const roleId = await getUsersRoleId(); - - // Create the user in Directus using service token - const createPayload: Record = { - status: "active", // change to "pending" to require email verification - role: roleId, + // Create user with sane defaults + const createPayload: any = { + email, username, password, + status: "active", + provider: "default", }; - if (email) createPayload.email = email; - if (first_name) createPayload.first_name = first_name; - if (last_name) createPayload.last_name = last_name; + if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE; - const createRes = await fetch(`${API}/users`, { + const createRes = await fetch(`${DIRECTUS}/users`, { method: "POST", headers: { Authorization: `Bearer ${SERVICE_TOKEN}`, @@ -91,50 +78,37 @@ export async function POST(req: Request) { Accept: "application/json", }, body: JSON.stringify(createPayload), + cache: "no-store", }); - const createJson = await createRes.json().catch(() => ({})); + const cj = await createRes.json().catch(() => ({})); if (!createRes.ok) { - const reason = - createJson?.errors?.[0]?.message || - createJson?.error || - createRes.statusText || - "Registration failed"; - return bad("Registration failed", createRes.status, { debug: reason }); + const msg = cj?.errors?.[0]?.message || cj?.message || `User create failed (${createRes.status})`; + return bad(msg, createRes.status || 500); } - const user = createJson?.data ?? createJson; - const res = NextResponse.json({ - ok: true, - user: { id: user?.id, email: user?.email, username: user?.username }, - }); + // Auto-login (email-based; directus expects "email" even though it's an identifier) + const tokens = await directusLogin(email, password); - // Optional auto-login after signup - if (AUTO_LOGIN && (email || username)) { - try { - const identifier = email || username; - const auth = await loginDirectus(identifier, password); - const access = auth?.access_token ?? auth?.data?.access_token; - const expiresSec = auth?.expires ?? auth?.data?.expires; - - if (access) { - const maxAge = - typeof expiresSec === "number" ? Math.max(0, Math.floor(expiresSec)) : 60 * 60 * 8; - res.cookies.set({ - name: "ma_at", - value: access, - httpOnly: true, - sameSite: "lax", - secure, - path: "/", - maxAge, - }); - } - } catch { - // Ignore auto-login failure; user creation succeeded. - } + const res = NextResponse.json({ ok: true, id: cj?.data?.id || null }, { status: 201 }); + if (tokens?.access_token) { + res.cookies.set("ma_at", tokens.access_token, { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: SECURE, + maxAge: 60 * 60, // 1h + }); + } + if (tokens?.refresh_token) { + res.cookies.set("ma_rt", tokens.refresh_token, { + path: "/", + httpOnly: true, + sameSite: "lax", + secure: SECURE, + maxAge: 60 * 60 * 24 * 30, // 30d + }); } - return res; } catch (e: any) { return bad(e?.message || "Registration error", e?.status || 500); diff --git a/app/auth/sign-up/sign-up.tsx b/app/auth/sign-up/sign-up.tsx index a753bab5..b85e3921 100644 --- a/app/auth/sign-up/sign-up.tsx +++ b/app/auth/sign-up/sign-up.tsx @@ -6,11 +6,14 @@ import { useRouter } from "next/navigation"; type Props = { nextPath?: string }; +const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + export default function SignUp({ nextPath = "/portal" }: Props) { const router = useRouter(); - const [username, setUsername] = useState(""); const [email, setEmail] = useState(""); + const [username, setUsername] = useState(""); const [password, setPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [loading, setLoading] = useState(false); const [err, setErr] = useState(null); @@ -20,33 +23,33 @@ export default function SignUp({ nextPath = "/portal" }: Props) { e.preventDefault(); setErr(null); - const u = username.trim(); const em = email.trim().toLowerCase(); - const pw = password; + const un = username.trim(); - if (!u) { - setErr("Username is required."); + if (!em || !un || !password || !confirmPassword) { + setErr("All fields are required."); return; } - if (!pw || pw.length < 8) { + if (!EMAIL_RE.test(em)) { + setErr("Please enter a valid email address."); + return; + } + if (password.length < 8) { setErr("Password must be at least 8 characters."); return; } + if (password !== confirmPassword) { + setErr("Passwords do not match."); + return; + } 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: u, - email: em || undefined, - password: pw, - }), + headers: { "Content-Type": "application/json", Accept: "application/json" }, + body: JSON.stringify({ email: em, username: un, password, confirmPassword }), }); const txt = await res.text(); @@ -59,13 +62,12 @@ export default function SignUp({ nextPath = "/portal" }: Props) { if (!res.ok) { const message = - (j?.error && j?.debug ? `${j.error} (${j.debug})` : j?.error) || + j?.error || j?.message || - `Sign-up failed (${res.status})`; + (res.status === 409 ? "Email or username already in use." : `Sign-up failed (${res.status})`); throw new Error(message); } - // If AUTO_LOGIN is enabled on the server, the user is already signed in. router.replace(nextPath); router.refresh(); } catch (e: any) { @@ -74,17 +76,28 @@ export default function SignUp({ nextPath = "/portal" }: Props) { setLoading(false); } }, - [username, email, password, nextPath, router] - ); // <-- make sure this closing ); is present + [email, username, password, confirmPassword, nextPath, router] + ); return (

Create Account

-

- Join MakerDash to manage rigs, settings, and projects. -

+

Join MakeArmy to manage rigs, settings, and projects.

+
+ + setEmail(e.currentTarget.value)} + required + /> +
+
setUsername(e.currentTarget.value)} required - /> -
- -
- - setEmail(e.currentTarget.value)} + minLength={3} />
@@ -127,7 +127,7 @@ export default function SignUp({ nextPath = "/portal" }: Props) { type={showPassword ? "text" : "password"} autoComplete="new-password" className="w-full rounded-md border px-3 py-2" - placeholder="Choose a strong password" + placeholder="At least 8 characters" value={password} onChange={(e) => setPassword(e.currentTarget.value)} required @@ -135,6 +135,20 @@ export default function SignUp({ nextPath = "/portal" }: Props) { />
+
+ + setConfirmPassword(e.currentTarget.value)} + required + minLength={8} + /> +
+ {err && (
{err} From 886059f5e2d195399686f69c095b1b146ccdf437 Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 2 Oct 2025 20:01:57 -0400 Subject: [PATCH 03/10] debug route --- app/api/debug/meta/route.ts | 17 +++++++++++++++++ app/settings/co2-galvo/page.tsx | 3 +-- 2 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 app/api/debug/meta/route.ts diff --git a/app/api/debug/meta/route.ts b/app/api/debug/meta/route.ts new file mode 100644 index 00000000..b9ddf63a --- /dev/null +++ b/app/api/debug/meta/route.ts @@ -0,0 +1,17 @@ +// app/api/debug/meta/route.ts +import { NextResponse } from "next/server"; +import { promises as fs } from "fs"; +export async function GET() { + let buildId = null; + try { + buildId = (await fs.readFile(process.cwd() + "/.next/BUILD_ID", "utf8")).trim(); + } catch {} + return NextResponse.json({ + env: process.env.NEXT_PUBLIC_ENV || null, + nodeEnv: process.env.NODE_ENV || null, + buildId, + image: process.env.IMAGE_TAG || null, + commit: process.env.COMMIT_SHA || null, + time: new Date().toISOString(), + }); +} diff --git a/app/settings/co2-galvo/page.tsx b/app/settings/co2-galvo/page.tsx index adb3cd9f..6b16602f 100644 --- a/app/settings/co2-galvo/page.tsx +++ b/app/settings/co2-galvo/page.tsx @@ -38,10 +38,9 @@ export default function CO2GalvoSettingsPage() { "submission_id", "setting_title", "uploader", - // owner (M2O) – ensure username is requested + "owner" "owner.id", "owner.username", - // assets / denorms "photo.id", "photo.title", "mat.name", From fd0fb78699124e52e81fe0aaa25e127c4442c36a Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 2 Oct 2025 21:02:15 -0400 Subject: [PATCH 04/10] a comma --- app/settings/co2-galvo/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/settings/co2-galvo/page.tsx b/app/settings/co2-galvo/page.tsx index 6b16602f..2a7415b3 100644 --- a/app/settings/co2-galvo/page.tsx +++ b/app/settings/co2-galvo/page.tsx @@ -38,7 +38,7 @@ export default function CO2GalvoSettingsPage() { "submission_id", "setting_title", "uploader", - "owner" + "owner", "owner.id", "owner.username", "photo.id", From 7502c3e4c1d95cb7e0ba8683954ae94e7e7d45dc Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 3 Oct 2025 07:11:42 -0400 Subject: [PATCH 05/10] registration emergency fix --- app/api/auth/register/route.ts | 2 -- app/page.tsx | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 9cc98e5b..217ba53b 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -3,8 +3,6 @@ import { NextResponse } from "next/server"; const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); const SERVICE_TOKEN = -process.env.DIRECTUS_SERVICE_TOKEN || -process.env.DIRECTUS_ADMIN_TOKEN || process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; const DEFAULT_ROLE = process.env.DIRECTUS_DEFAULT_ROLE || undefined; const SECURE = process.env.NODE_ENV === "production"; diff --git a/app/page.tsx b/app/page.tsx index 29fd3114..3e52fc92 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -50,8 +50,7 @@ export default async function HomePage({
- We only use cookies strictly necessary to operate the site (e.g., your - sign-in session). + This is the production build v0.0.1 - this site is an active BETA. Some features may not be available or work as intended. If you're experiencing issues please report them here: https://forge.makearmy.io/makearmy/makearmy-app/issues PRIVACY: We only use cookies strictly necessary to operate the site (e.g., your sign-in session). We do not store user data or telemetry other than what you provide and never share or sell data to third parties. Ever.
); From fba693f7617351f0632517aef6eb68fdf5bcaac5 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 3 Oct 2025 07:15:00 -0400 Subject: [PATCH 06/10] removed provider from payload --- app/api/auth/register/route.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 217ba53b..d64074b0 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -63,8 +63,7 @@ export async function POST(req: Request) { email, username, password, - status: "active", - provider: "default", + status: "active",, }; if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE; From d6103793374bbfd82dc0c1c2efbe961fe5d424e0 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 3 Oct 2025 07:21:49 -0400 Subject: [PATCH 07/10] registration reprint --- app/api/auth/register/route.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index d64074b0..8eedc20b 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -2,14 +2,17 @@ import { NextResponse } from "next/server"; const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, ""); -const SERVICE_TOKEN = -process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; + +// Registration MUST use only the dedicated admin-register token. No fallbacks. +const SERVICE_TOKEN = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER || ""; + const DEFAULT_ROLE = process.env.DIRECTUS_DEFAULT_ROLE || undefined; const SECURE = process.env.NODE_ENV === "production"; function bad(message: string, status = 400) { return NextResponse.json({ error: message }, { status }); } + const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; async function directusLogin(email: string, password: string) { @@ -27,7 +30,7 @@ async function directusLogin(email: string, password: string) { export async function POST(req: Request) { try { if (!DIRECTUS) return bad("Missing DIRECTUS_URL/NEXT_PUBLIC_API_BASE_URL", 500); - if (!SERVICE_TOKEN) return bad("Missing DIRECTUS_SERVICE_TOKEN / admin token", 500); + if (!SERVICE_TOKEN) return bad("Missing DIRECTUS_TOKEN_ADMIN_REGISTER", 500); const body = await req.json().catch(() => ({} as any)); const email = String(body?.email ?? "").trim().toLowerCase(); @@ -58,12 +61,12 @@ export async function POST(req: Request) { return bad("Email or username already in use", 409); } - // Create user with sane defaults + // Create user with sane defaults (no provider — Directus defaults to "default") const createPayload: any = { email, username, password, - status: "active",, + status: "active", }; if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE; @@ -74,7 +77,7 @@ export async function POST(req: Request) { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(createPayload), + body: JSON.stringify({ data: createPayload }), cache: "no-store", }); @@ -84,7 +87,7 @@ export async function POST(req: Request) { return bad(msg, createRes.status || 500); } - // Auto-login (email-based; directus expects "email" even though it's an identifier) + // Auto-login (Directus expects "email" even though it's the identifier) const tokens = await directusLogin(email, password); const res = NextResponse.json({ ok: true, id: cj?.data?.id || null }, { status: 201 }); From c18ca5f558401b8c9aa9924660a6128ef5ca3512 Mon Sep 17 00:00:00 2001 From: makearmy Date: Fri, 3 Oct 2025 07:32:08 -0400 Subject: [PATCH 08/10] data restructure for registration --- app/api/auth/register/route.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 8eedc20b..f0c29324 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -77,8 +77,8 @@ export async function POST(req: Request) { "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify({ data: createPayload }), - cache: "no-store", + body: JSON.stringify(createPayload), // <-- raw payload (system endpoint) + cache: "no-store", }); const cj = await createRes.json().catch(() => ({})); From 647c58b3e35e99e0a781ce3ae35772fbf5190f59 Mon Sep 17 00:00:00 2001 From: makearmy Date: Sat, 4 Oct 2025 08:45:49 -0400 Subject: [PATCH 09/10] env update for user registration roles --- .env.local | 39 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/.env.local b/.env.local index d28b125a..3de32c5c 100644 --- a/.env.local +++ b/.env.local @@ -1,9 +1,42 @@ -# Client-side (used by the dropdown fetches) +# ───────────────────────────────────────────── +# Public (used by client-side dropdown fetches) +# ───────────────────────────────────────────── NEXT_PUBLIC_API_BASE_URL=https://forms.lasereverything.net -# Server-side (used by API routes) +# ───────────────────────────────────────────── +# Server-side Directus +# ───────────────────────────────────────────── DIRECTUS_URL=https://forms.lasereverything.net DIRECTUS_TOKEN_ADMIN_REGISTER=l_QqNXKpi--Dt-hHDncHyBX0eiHNYZr7 +DIRECTUS_DEFAULT_ROLE=296a28bc-60ab-4251-8bef-27f6dfb67948 +DIRECTUS_ROLE_MEMBER_NAME=Users -# Image Folders +# ───────────────────────────────────────────── +# Files / Folders (IDs only; no folder browsing) +# ───────────────────────────────────────────── DIRECTUS_AVATAR_FOLDER_ID=b8ddddf8-3ee3-4380-b27e-c7a5f01deef1 + +# Settings — CO₂ Galvo +DX_FOLDER_GALVO_NOTES=7b04a706-754d-4302-a9a0-6c88cd8faddf +DX_FOLDER_GALVO_PHOTOS=e5535371-828a-498b-80fc-3891b6220fd4 +DX_FOLDER_GALVO_SCREENS=8201e4c0-c39c-456a-bd55-1beb96642bcb + +# Settings — CO₂ Gantry +DX_FOLDER_GANTRY_NOTES=926e2c1a-7907-4ef2-b778-859c6f40ba82 +DX_FOLDER_GANTRY_PHOTOS=d19c4f8d-a42f-422d-b113-b89b736c34e6 +DX_FOLDER_GANTRY_SCREENS=9b7d0b47-c1f4-4749-8876-2e4b52ccded0 + +# Settings — Fiber +DX_FOLDER_FIBER_NOTES=00eed759-480e-43cc-9de3-854dc59cca79 +DX_FOLDER_FIBER_PHOTOS=54f6a9d2-bc57-41fc-8c7d-7c7d7cb9cadc +DX_FOLDER_FIBER_SCREENS=5c830975-7926-4e01-911c-2443b62d7f88 + +# Settings — UV +DX_FOLDER_UV_NOTES=8ca37379-7178-48b2-8670-6b8d8a880677 +DX_FOLDER_UV_PHOTOS=c639360b-3116-4b5d-98da-f8b502089486 +DX_FOLDER_UV_SCREENS=a84f54b1-0e92-4ea6-8fbe-37a3a74bd49c + +# Projects +DX_FOLDER_PROJECTS_FILES=f264f066-5b38-4335-bb10-5b014bfa62cb +DX_FOLDER_PROJECTS_IMAGES=da11b876-2ede-4e19-ad3a-76fc9db449a8 +DX_FOLDER_PROJECTS_INSTRUCTIONS=905a4259-0c8e-489b-b810-c27186a2f266 From ba77186161a6a32bad9cc4aa4fd9f8f5542905c5 Mon Sep 17 00:00:00 2001 From: makearmy Date: Thu, 16 Oct 2025 09:32:02 -0400 Subject: [PATCH 10/10] chore(release): 0.0.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0aa3cec4..40c0448f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ledb", - "version": "1.0.0", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ledb", - "version": "1.0.0", + "version": "0.0.1", "dependencies": { "@hookform/resolvers": "^5.2.2", "@radix-ui/react-accordion": "^1.2.10", diff --git a/package.json b/package.json index 2b54bfe2..478823b8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ledb", - "version": "1.0.0", + "version": "0.0.1", "scripts": { "dev": "next dev", "build": "next build",