import { NextResponse } from "next/server"; import { getSessionUser, canMutate } from "@/lib/session"; import { inviteOrgMember, isValidInviteRole } from "@/lib/team"; import { z } from "zod"; import { safeError } from "@/lib/errors"; const inviteSchema = z.object({ email: z.string().email(), givenName: z.string().min(1).max(100), familyName: z.string().min(1).max(100), role: z.enum(["owner", "user"]), preferredLanguage: z.enum(["en", "de", "fr", "it"]).optional(), }); /** * POST /api/team/invite * * Invite a new member into the caller's org. Body shape: * { email, givenName, familyName, role: "owner" | "user" } * * Allowed roles are explicitly only the customer-side ones — * `isValidInviteRole` enforces this server-side too as a belt * alongside the Zod enum (the Zod enum is the primary check; the * helper exists because future callers in admin tooling may want the * same predicate). * * Platform users can also call this — they'd be inviting members * into their own platform org, which is uncommon but legal. */ export async function POST(req: Request) { const user = await getSessionUser(); if (!user) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!canMutate(user)) { return NextResponse.json({ error: "Forbidden" }, { status: 403 }); } const body = await req.json().catch(() => null); const parsed = inviteSchema.safeParse(body); if (!parsed.success) { return NextResponse.json( { error: "Invalid input", details: parsed.error.flatten() }, { status: 400 } ); } const input = parsed.data; // Defensive recheck — the Zod enum already guarantees this, but it // makes the intent explicit at the call site. if (!isValidInviteRole(input.role)) { return NextResponse.json( { error: "Role must be 'owner' or 'user'." }, { status: 400 } ); } try { const result = await inviteOrgMember({ orgId: user.orgId, email: input.email, givenName: input.givenName, familyName: input.familyName, role: input.role, preferredLanguage: input.preferredLanguage, }); return NextResponse.json( { userId: result.userId, message: "Invitation sent. The user will receive an email with a link to set their password.", }, { status: 201 } ); } catch (e: any) { console.error("Invite failed:", e); // ZITADEL "user already exists" surfaces as a 4xx error; pass it // through with a clean message so the client can render localized // text. const msg = e?.message ?? ""; if (msg.includes("already exists") || msg.includes("9.User.AlreadyExisting")) { return NextResponse.json( { error: "A user with this email already exists.", code: "user_already_exists", }, { status: 409 } ); } return NextResponse.json( { error: safeError(e, "Failed to invite user") }, { status: e.statusCode || 500 } ); } }