sistema-de-chamados/src/app/api/invites/[token]/route.ts
2025-11-19 13:24:08 -03:00

196 lines
5.7 KiB
TypeScript

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<AcceptInvitePayload> | 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 })
}