106 lines
2.9 KiB
TypeScript
106 lines
2.9 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { registerCustomer } from "@/lib/zitadel";
|
|
import { rateLimit } from "@/lib/rate-limit";
|
|
import type { RegistrationInput } from "@/types";
|
|
import { z } from "zod";
|
|
|
|
const registrationSchema = z.object({
|
|
companyName: z.string().min(2).max(100),
|
|
givenName: z.string().min(1).max(100),
|
|
familyName: z.string().min(1).max(100),
|
|
email: z.string().email(),
|
|
preferredLanguage: z.enum(["en", "de", "fr", "it"]).optional(),
|
|
});
|
|
|
|
/** 3 registrations per IP per hour */
|
|
const RATE_LIMIT = 3;
|
|
const RATE_WINDOW_MS = 3_600_000; // 1 hour
|
|
|
|
export async function POST(request: NextRequest) {
|
|
// --- Rate limiting ---
|
|
const ip =
|
|
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
|
|
request.headers.get("x-real-ip") ??
|
|
"unknown";
|
|
|
|
const rl = rateLimit(`register:${ip}`, RATE_LIMIT, RATE_WINDOW_MS);
|
|
|
|
if (!rl.allowed) {
|
|
return NextResponse.json(
|
|
{ error: "Too many registration attempts. Please try again later." },
|
|
{
|
|
status: 429,
|
|
headers: {
|
|
"Retry-After": String(Math.ceil(rl.resetMs / 1000)),
|
|
"X-RateLimit-Limit": String(RATE_LIMIT),
|
|
"X-RateLimit-Remaining": "0",
|
|
},
|
|
},
|
|
);
|
|
}
|
|
|
|
// --- Validation ---
|
|
try {
|
|
const body = await request.json();
|
|
const parsed = registrationSchema.safeParse(body);
|
|
|
|
if (!parsed.success) {
|
|
return NextResponse.json(
|
|
{ error: "Validation failed", details: parsed.error.flatten() },
|
|
{ status: 400 },
|
|
);
|
|
}
|
|
|
|
const input: RegistrationInput = parsed.data;
|
|
|
|
const result = await registerCustomer({
|
|
companyName: input.companyName,
|
|
email: input.email,
|
|
givenName: input.givenName,
|
|
familyName: input.familyName,
|
|
preferredLanguage: input.preferredLanguage,
|
|
});
|
|
|
|
return NextResponse.json(
|
|
{
|
|
orgId: result.orgId,
|
|
userId: result.userId,
|
|
message:
|
|
"Registration successful. You will receive an invitation email to set your password.",
|
|
},
|
|
{
|
|
status: 201,
|
|
headers: {
|
|
"X-RateLimit-Limit": String(RATE_LIMIT),
|
|
"X-RateLimit-Remaining": String(rl.remaining),
|
|
},
|
|
},
|
|
);
|
|
} catch (e: any) {
|
|
console.error("Registration failed:", e);
|
|
|
|
const zitadelMessage = extractZitadelMessage(e.message);
|
|
|
|
return NextResponse.json(
|
|
{ error: zitadelMessage || "Registration failed. Please try again." },
|
|
{ status: e.statusCode || 500 },
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ZITADEL errors come as:
|
|
* "ZITADEL POST /path: 400 {"code":3, "message":"..."}"
|
|
* Extract the human-readable "message" field.
|
|
*/
|
|
function extractZitadelMessage(errorMsg: string): string | null {
|
|
try {
|
|
const jsonStart = errorMsg.indexOf("{");
|
|
if (jsonStart === -1) return null;
|
|
const json = JSON.parse(errorMsg.slice(jsonStart));
|
|
return json.message || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|