From ca5082b2f7093af83f9d70c3fbaf956d37c6478f Mon Sep 17 00:00:00 2001 From: makearmy Date: Tue, 30 Sep 2025 22:24:17 -0400 Subject: [PATCH] registration bug fixes --- app/api/auth/register/route.ts | 208 ++++++++++++++++++--------------- 1 file changed, 113 insertions(+), 95 deletions(-) diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index b597b237..6b996fd1 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,114 +1,132 @@ -// app/app/api/auth/register/route.ts -import { NextRequest, NextResponse } from "next/server"; +// app/api/auth/register/route.ts +import { NextResponse } from "next/server"; +import { loginDirectus } from "@/lib/directus"; -const BASE = process.env.DIRECTUS_URL!; -const ADMIN_TOKEN = process.env.DIRECTUS_TOKEN_ADMIN_REGISTER!; -const MEMBER_ROLE_ID = process.env.DIRECTUS_ROLE_MEMBER_ID || ""; // optional override +export const runtime = "nodejs"; -if (!BASE) console.warn("[auth/register] Missing DIRECTUS_URL"); -if (!ADMIN_TOKEN) console.warn("[auth/register] Missing DIRECTUS_TOKEN_ADMIN_REGISTER"); +// Base URL (no trailing slash) +const API = (process.env.NEXT_PUBLIC_API_BASE_URL || process.env.DIRECTUS_URL || "").replace(/\/$/, ""); +// Service token to create users / read roles +const SERVICE_TOKEN = process.env.DIRECTUS_SERVICE_TOKEN || process.env.DIRECTUS_STATIC_TOKEN || ""; +// Auto login right after signup (default: true) +const AUTO_LOGIN = (process.env.SIGNUP_AUTO_LOGIN ?? "1") !== "0"; +const secure = process.env.NODE_ENV === "production"; -type DirectusList = { data: T[] }; -type DirectusItem = { data: T }; +function bad(message: string, status = 400, extra: Record = {}) { + return NextResponse.json({ error: message, ...extra }, { status }); +} -async function dFetch(path: string, init?: RequestInit): Promise { - const res = await fetch(`${BASE}${path}`, { - ...init, - headers: { - "Accept": "application/json", - "Content-Type": "application/json", - "Authorization": `Bearer ${ADMIN_TOKEN}`, - ...(init?.headers || {}), - }, +// 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 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", }); - const text = await res.text(); - const json = text ? JSON.parse(text) : null; - if (!res.ok) { - throw new Error(`Directus ${res.status}: ${text || res.statusText}`); + 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}`); } - return (json ?? {}) as T; + const id = j?.data?.[0]?.id ?? j?.[0]?.id; + if (!id) { + throw new Error('Role "Users" not found. Create it in Directus or set DIRECTUS_SERVICE_TOKEN correctly.'); + } + return String(id); } -async function getMemberRoleId(): Promise { - if (MEMBER_ROLE_ID) return MEMBER_ROLE_ID; - const q = `/roles?limit=1&filter[name][_in]=Member,member&fields=id,name`; - const out = await dFetch>(q); - const hit = out.data?.[0]; - if (!hit?.id) throw new Error("Member role not found. Set DIRECTUS_ROLE_MEMBER_ID."); - return String(hit.id); -} - -async function usernameExists(username: string): Promise { - const q = `/users?limit=1&filter[username][_eq]=${encodeURIComponent(username)}&fields=id`; - const out = await dFetch>(q); - return !!out.data?.length; -} - -async function emailExists(email: string): Promise { - const q = `/users?limit=1&filter[email][_eq]=${encodeURIComponent(email)}&fields=id`; - const out = await dFetch>(q); - return !!out.data?.length; -} - -function isEmailLike(s: string) { - return /^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(s); -} - -export async function POST(req: NextRequest) { +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 is not set", + 500, + { hint: "Create a service/static token in Directus Admin and set DIRECTUS_SERVICE_TOKEN." } + ); + } + const body = await req.json().catch(() => ({})); - const rawUsername: string = (body?.username ?? "").trim(); - const rawPassword: string = (body?.password ?? "").trim(); - const rawEmail: string = String(body?.email ?? "").trim(); + 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 (!rawUsername || rawUsername.length < 3) { - return NextResponse.json({ error: "Username must be at least 3 characters." }, { status: 400 }); - } - if (!rawPassword || rawPassword.length < 8) { - return NextResponse.json({ error: "Password must be at least 8 characters." }, { status: 400 }); - } - if (rawEmail && !isEmailLike(rawEmail)) { - return NextResponse.json({ error: "Email format is invalid." }, { status: 400 }); - } + if (!username) return bad("Username is required"); + if (!password || password.length < 8) return bad("Password must be at least 8 characters"); - if (await usernameExists(rawUsername)) { - return NextResponse.json({ error: "Username is already taken." }, { status: 409 }); - } + // Only accept the "Users" role + const roleId = await getUsersRoleId(); - // If no email provided, synthesize a placeholder that’s unique. - let email = rawEmail; - if (!email) { - const base = `${rawUsername.toLowerCase()}+noemail@users.makearmy.local`; - let candidate = base; - let i = 1; - while (await emailExists(candidate)) { - i += 1; - candidate = `${rawUsername.toLowerCase()}+noemail${i}@users.makearmy.local`; - } - email = candidate; - } else { - if (await emailExists(email)) { - return NextResponse.json({ error: "Email already in use." }, { status: 409 }); - } - } + // Create the user in Directus using service token + const createPayload: Record = { + status: "active", // change to "pending" if you want a verification flow + role: roleId, + username, + password, + }; + if (email) createPayload.email = email; + if (first_name) createPayload.first_name = first_name; + if (last_name) createPayload.last_name = last_name; - const role = await getMemberRoleId(); - - // Create the user (status active to allow immediate login) - const create = await dFetch>(`/users`, { + const createRes = await fetch(`${API}/users`, { method: "POST", - body: JSON.stringify({ - email, - password: rawPassword, - role, - status: "active", - // Add custom field username on directus_users (unique) - username: rawUsername, - }), + headers: { + Authorization: `Bearer ${SERVICE_TOKEN}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(createPayload), + }); + const createJson = 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 user = createJson?.data ?? createJson; + const res = NextResponse.json({ + ok: true, + user: { id: user?.id, email: user?.email, username: user?.username }, }); - return NextResponse.json({ ok: true, id: create.data.id, warning: rawEmail ? null : "No email provided. You won't be able to reset your password until you add one." }); - } catch (err: any) { - return NextResponse.json({ error: err?.message || "Register failed" }, { status: 500 }); + // Optional auto-login after signup + if (AUTO_LOGIN && (email || username)) { + try { + // Prefer email when available; otherwise attempt username if your Directus login allows it + 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. + } + } + + return res; + } catch (e: any) { + return bad(e?.message || "Registration error", e?.status || 500); } }