import { NextResponse } from "next/server" import { randomBytes } from "crypto" import { Prisma } from "@/lib/prisma" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import { assertStaffSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { ROLE_OPTIONS, type RoleOption, isAdmin } from "@/lib/authz" import { env } from "@/lib/env" import { prisma } from "@/lib/prisma" import { buildInviteUrl, computeInviteStatus, normalizeInvite, type InviteWithEvents, type NormalizedInvite } from "@/server/invite-utils" import { notifyUserInvite } from "@/server/notification/notification-service" const DEFAULT_EXPIRATION_DAYS = 7 const JSON_NULL = Prisma.JsonNull as Prisma.NullableJsonNullValueInput function toJsonPayload(payload: unknown): Prisma.InputJsonValue | Prisma.NullableJsonNullValueInput { if (payload === null || payload === undefined) { return JSON_NULL } return payload as Prisma.InputJsonValue } function normalizeRole(input: string | null | undefined): RoleOption { const role = (input ?? "agent").toLowerCase() as RoleOption return (ROLE_OPTIONS as readonly string[]).includes(role) ? role : "agent" } const ROLE_LABELS: Record = { admin: "Administrador", manager: "Gestor", agent: "Agente", collaborator: "Colaborador", } function formatRoleName(role: string): string { return ROLE_LABELS[role.toLowerCase()] ?? role } function generateToken() { return randomBytes(32).toString("hex") } async function syncInviteWithConvex(invite: NormalizedInvite) { const convexUrl = env.NEXT_PUBLIC_CONVEX_URL if (!convexUrl) return const client = new ConvexHttpClient(convexUrl) await client.mutation(api.invites.sync, { tenantId: invite.tenantId, inviteId: invite.id, email: invite.email, name: invite.name ?? undefined, role: invite.role.toUpperCase(), status: invite.status, token: invite.token, expiresAt: Date.parse(invite.expiresAt), createdAt: Date.parse(invite.createdAt), createdById: invite.createdById ?? undefined, acceptedAt: invite.acceptedAt ? Date.parse(invite.acceptedAt) : undefined, acceptedById: invite.acceptedById ?? undefined, revokedAt: invite.revokedAt ? Date.parse(invite.revokedAt) : undefined, revokedById: invite.revokedById ?? undefined, revokedReason: invite.revokedReason ?? undefined, }) } async function appendEvent(inviteId: string, type: string, actorId: string | null, payload: unknown = null) { return prisma.authInviteEvent.create({ data: { inviteId, type, payload: toJsonPayload(payload), actorId, }, }) } async function refreshInviteStatus(invite: InviteWithEvents, now: Date) { const computedStatus = computeInviteStatus(invite, now) if (computedStatus === invite.status) { return invite } const updated = await prisma.authInvite.update({ where: { id: invite.id }, data: { status: computedStatus }, }) const event = await appendEvent(invite.id, computedStatus, null) const inviteWithEvents: InviteWithEvents = { ...updated, events: [...invite.events, event], } return inviteWithEvents } function buildInvitePayload(invite: InviteWithEvents, now: Date) { const normalized = normalizeInvite(invite, now) return normalized } export async function GET() { const session = await assertStaffSession() if (!session) { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) } const invites = await prisma.authInvite.findMany({ orderBy: { createdAt: "desc" }, include: { events: { orderBy: { createdAt: "asc" }, }, }, }) const now = new Date() const results: NormalizedInvite[] = [] for (const invite of invites) { const updatedInvite = await refreshInviteStatus(invite, now) const normalized = buildInvitePayload(updatedInvite, now) await syncInviteWithConvex(normalized) results.push(normalized) } return NextResponse.json({ invites: results }) } type CreateInvitePayload = { email: string name?: string role?: RoleOption tenantId?: string expiresInDays?: number } export async function POST(request: Request) { const session = await assertStaffSession() if (!session) { return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) } const body = (await request.json().catch(() => null)) as Partial | null if (!body || typeof body !== "object") { return NextResponse.json({ error: "Payload inválido" }, { status: 400 }) } const email = typeof body.email === "string" ? body.email.trim().toLowerCase() : "" if (!email || !email.includes("@")) { return NextResponse.json({ error: "Informe um e-mail válido" }, { status: 400 }) } const name = typeof body.name === "string" ? body.name.trim() : undefined const role = normalizeRole(body.role) const isSessionAdmin = isAdmin(session.user.role) if (!isSessionAdmin && !["manager", "collaborator"].includes(role)) { return NextResponse.json({ error: "Agentes só podem convidar gestores ou colaboradores" }, { status: 403 }) } const tenantId = typeof body.tenantId === "string" && body.tenantId.trim() ? body.tenantId.trim() : session.user.tenantId || DEFAULT_TENANT_ID const expiresInDays = Number.isFinite(body.expiresInDays) ? Math.max(1, Number(body.expiresInDays)) : DEFAULT_EXPIRATION_DAYS const existing = await prisma.authInvite.findFirst({ where: { email, status: { in: ["pending", "accepted"] }, }, include: { events: true }, }) const now = new Date() if (existing) { const computed = computeInviteStatus(existing, now) if (computed === "pending") { return NextResponse.json({ error: "Já existe um convite pendente para este e-mail" }, { status: 409 }) } if (computed === "accepted") { return NextResponse.json({ error: "Este e-mail já possui acesso ativo" }, { status: 409 }) } if (existing.status !== computed) { await prisma.authInvite.update({ where: { id: existing.id }, data: { status: computed } }) await appendEvent(existing.id, computed, session.user.id ?? null) const refreshed = await prisma.authInvite.findUnique({ where: { id: existing.id }, include: { events: true }, }) if (refreshed) { const normalizedExisting = buildInvitePayload(refreshed, now) await syncInviteWithConvex(normalizedExisting) } } } const token = generateToken() const expiresAt = new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1000) const invite = await prisma.authInvite.create({ data: { email, name, role, tenantId, token, status: "pending", expiresAt, createdById: session.user.id ?? null, }, }) const event = await appendEvent(invite.id, "created", session.user.id ?? null, { role, tenantId, expiresAt: expiresAt.toISOString(), }) const inviteWithEvents: InviteWithEvents = { ...invite, events: [event], } const normalized = buildInvitePayload(inviteWithEvents, now) await syncInviteWithConvex(normalized) // Envia email de convite const inviteUrl = buildInviteUrl(token) const inviterName = session.user.name ?? session.user.email const roleName = formatRoleName(role) try { await notifyUserInvite( email, name ?? null, inviterName, roleName, null, // companyName - não temos essa informação no convite inviteUrl ) } catch (error) { // Log do erro mas não falha a criação do convite console.error("[invites] Falha ao enviar email de convite:", error) } return NextResponse.json({ invite: normalized }) }