import { NextResponse } from "next/server" import { Prisma } from "@/lib/prisma" import { hashPassword } from "better-auth/crypto" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { prisma } from "@/lib/prisma" import { computeInviteStatus, normalizeInvite, normalizeRoleOption, type NormalizedInvite, } from "@/server/invite-utils" import { requireConvexUrl } from "@/server/convex-client" type AcceptInvitePayload = { name?: string password: string } const JSON_NULL = Prisma.JsonNull as Prisma.NullableJsonNullValueInput function validatePassword(password: string) { return password.length >= 8 } async function syncInvite(invite: NormalizedInvite) { const url = requireConvexUrl() const client = new ConvexHttpClient(url) 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, }) } export async function GET(_request: Request, context: { params: Promise<{ token: string }> }) { const { token } = await context.params const invite = await prisma.authInvite.findUnique({ where: { token }, include: { events: { orderBy: { createdAt: "asc" } } }, }) if (!invite) { return NextResponse.json({ error: "Convite não encontrado" }, { status: 404 }) } const now = new Date() const status = computeInviteStatus(invite, now) if (status !== invite.status) { await prisma.authInvite.update({ where: { id: invite.id }, data: { status } }) const event = await prisma.authInviteEvent.create({ data: { inviteId: invite.id, type: status, payload: JSON_NULL, actorId: null, }, }) invite.status = status invite.events.push(event) } const normalized = normalizeInvite(invite, now) await syncInvite(normalized) return NextResponse.json({ invite: normalized }) } export async function POST(request: Request, context: { params: Promise<{ token: string }> }) { const { token } = await context.params const payload = (await request.json().catch(() => null)) as Partial | null if (!payload || typeof payload.password !== "string") { return NextResponse.json({ error: "Senha inválida" }, { status: 400 }) } if (!validatePassword(payload.password)) { return NextResponse.json({ error: "Senha deve conter pelo menos 8 caracteres" }, { status: 400 }) } const invite = await prisma.authInvite.findUnique({ where: { token }, include: { events: { orderBy: { createdAt: "asc" } } }, }) if (!invite) { return NextResponse.json({ error: "Convite não encontrado" }, { status: 404 }) } const now = new Date() const status = computeInviteStatus(invite, now) if (status === "expired") { await prisma.authInvite.update({ where: { id: invite.id }, data: { status: "expired" } }) const event = await prisma.authInviteEvent.create({ data: { inviteId: invite.id, type: "expired", payload: JSON_NULL, actorId: null, }, }) invite.status = "expired" invite.events.push(event) const normalizedExpired = normalizeInvite(invite, now) await syncInvite(normalizedExpired) return NextResponse.json({ error: "Convite expirado" }, { status: 410 }) } if (status === "revoked") { return NextResponse.json({ error: "Convite revogado" }, { status: 410 }) } if (status === "accepted") { return NextResponse.json({ error: "Convite já utilizado" }, { status: 409 }) } const existingUser = await prisma.authUser.findUnique({ where: { email: invite.email } }) if (existingUser) { return NextResponse.json({ error: "Usuário já registrado" }, { status: 409 }) } const name = typeof payload.name === "string" && payload.name.trim() ? payload.name.trim() : invite.name || invite.email const tenantId = invite.tenantId || DEFAULT_TENANT_ID const role = normalizeRoleOption(invite.role) const hashedPassword = await hashPassword(payload.password) const user = await prisma.authUser.create({ data: { email: invite.email, name, role, tenantId, accounts: { create: { providerId: "credential", accountId: invite.email, password: hashedPassword, }, }, }, }) const updatedInvite = await prisma.authInvite.update({ where: { id: invite.id }, data: { status: "accepted", acceptedAt: now, acceptedById: user.id, name, }, }) const event = await prisma.authInviteEvent.create({ data: { inviteId: invite.id, type: "accepted", payload: { userId: user.id }, actorId: user.id, }, }) const normalized = normalizeInvite({ ...updatedInvite, events: [...invite.events, event] }, now) await syncInvite(normalized) try { const convex = new ConvexHttpClient(requireConvexUrl()) await convex.mutation(api.users.ensureUser, { tenantId, email: invite.email, name, avatarUrl: undefined, role: role.toUpperCase(), }) } catch (error) { console.warn("Falha ao sincronizar usuário no Convex", error) } return NextResponse.json({ success: true }) }