116 lines
4.6 KiB
TypeScript
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);
|
|
}
|
|
}
|