makearmy-app/app/api/auth/register/route.ts
2025-10-03 07:21:49 -04:00

116 lines
4.6 KiB
TypeScript

// app/api/auth/register/route.ts
import { NextResponse } from "next/server";
const DIRECTUS = (process.env.DIRECTUS_URL || process.env.NEXT_PUBLIC_API_BASE_URL || "").replace(/\/$/, "");
// 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) {
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) 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 (!DIRECTUS) return bad("Missing DIRECTUS_URL/NEXT_PUBLIC_API_BASE_URL", 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();
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);
}
// Create user with sane defaults (no provider — Directus defaults to "default")
const createPayload: any = {
email,
username,
password,
status: "active",
};
if (DEFAULT_ROLE) createPayload.role = DEFAULT_ROLE;
const createRes = await fetch(`${DIRECTUS}/users`, {
method: "POST",
headers: {
Authorization: `Bearer ${SERVICE_TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ data: createPayload }),
cache: "no-store",
});
const cj = await createRes.json().catch(() => ({}));
if (!createRes.ok) {
const msg = cj?.errors?.[0]?.message || cj?.message || `User create failed (${createRes.status})`;
return bad(msg, createRes.status || 500);
}
// 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 });
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);
}
}