feat: improve requester combobox and admin cleanup flows

This commit is contained in:
Esdras Renan 2025-10-24 00:45:41 -03:00
parent 788f6928a1
commit 37c32149a6
13 changed files with 923 additions and 180 deletions

View file

@ -0,0 +1,160 @@
import { NextResponse } from "next/server"
import type { Id } from "@/convex/_generated/dataModel"
import { api } from "@/convex/_generated/api"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { assertStaffSession } from "@/lib/auth-server"
import { isAdmin } from "@/lib/authz"
import { prisma } from "@/lib/prisma"
import { createConvexClient } from "@/server/convex-client"
const DEFAULT_KEEP_EMAILS = ["renan.pac@paulicon.com.br"]
type CleanupSummary = {
removedPortalUserIds: string[]
removedPortalEmails: string[]
removedAuthUserIds: string[]
removedConvexUserIds: string[]
removedTicketIds: string[]
convictTicketsDeleted: number
keepEmails: string[]
}
export const runtime = "nodejs"
export async function POST(request: Request) {
const session = await assertStaffSession()
if (!session) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
}
if (!isAdmin(session.user.role)) {
return NextResponse.json({ error: "Apenas administradores podem remover dados." }, { status: 403 })
}
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
const sessionEmail = session.user.email?.toLowerCase()
if (!sessionEmail) {
return NextResponse.json({ error: "Administrador sem e-mail associado." }, { status: 400 })
}
const body = await request.json().catch(() => ({})) as { keepEmails?: string[] }
const keepEmailsInput = Array.isArray(body.keepEmails) ? body.keepEmails : []
const keepEmailsSet = new Set<string>()
DEFAULT_KEEP_EMAILS.forEach((email) => keepEmailsSet.add(email.toLowerCase()))
keepEmailsInput
.map((email) => (typeof email === "string" ? email.trim().toLowerCase() : ""))
.filter((email) => email.length > 0)
.forEach((email) => keepEmailsSet.add(email))
keepEmailsSet.add(sessionEmail)
const viewerEmail = sessionEmail
const portalUsers = await prisma.user.findMany({
where: { tenantId },
select: { id: true, email: true },
})
const portalToRemove = portalUsers.filter((user) => !keepEmailsSet.has(user.email.toLowerCase()))
const portalIdsToRemove = portalToRemove.map((user) => user.id)
const portalEmailsToRemove = portalToRemove.map((user) => user.email.toLowerCase())
const responseSummary: CleanupSummary = {
removedPortalUserIds: portalIdsToRemove,
removedPortalEmails: portalEmailsToRemove,
removedAuthUserIds: [],
removedConvexUserIds: [],
removedTicketIds: [],
convictTicketsDeleted: 0,
keepEmails: Array.from(keepEmailsSet),
}
if (portalIdsToRemove.length > 0) {
const ticketsToRemove = await prisma.ticket.findMany({
where: {
tenantId,
OR: [{ requesterId: { in: portalIdsToRemove } }, { assigneeId: { in: portalIdsToRemove } }],
},
select: { id: true },
})
responseSummary.removedTicketIds = ticketsToRemove.map((ticket) => ticket.id)
await prisma.$transaction(async (tx) => {
if (ticketsToRemove.length > 0) {
await tx.ticket.deleteMany({
where: { id: { in: ticketsToRemove.map((ticket) => ticket.id) } },
})
}
if (portalEmailsToRemove.length > 0) {
const authUsers = await tx.authUser.findMany({
where: { email: { in: portalEmailsToRemove } },
select: { id: true },
})
if (authUsers.length > 0) {
const authIds = authUsers.map((item) => item.id)
responseSummary.removedAuthUserIds = authIds
await tx.authSession.deleteMany({ where: { userId: { in: authIds } } })
await tx.authAccount.deleteMany({ where: { userId: { in: authIds } } })
await tx.authUser.deleteMany({ where: { id: { in: authIds } } })
}
}
await tx.user.deleteMany({
where: { id: { in: portalIdsToRemove } },
})
})
}
try {
const client = createConvexClient()
const actor =
(await client.query(api.users.findByEmail, { tenantId, email: sessionEmail })) ??
(await client.mutation(api.users.ensureUser, {
tenantId,
email: sessionEmail,
name: session.user.name ?? sessionEmail,
}))
const actorId = actor?._id as Id<"users"> | undefined
if (actorId) {
const convexCustomers = await client.query(api.users.listCustomers, {
tenantId,
viewerId: actorId,
})
const convexToRemove = convexCustomers.filter(
(customer) => !keepEmailsSet.has(customer.email.toLowerCase()),
)
const removedConvexIds: Id<"users">[] = []
for (const customer of convexToRemove) {
const userId = customer.id as Id<"users">
try {
await client.mutation(api.users.deleteUser, { userId, actorId })
removedConvexIds.push(userId)
} catch (error) {
console.error("[users.cleanup] Falha ao remover usuário do Convex", customer.email, error)
}
}
responseSummary.removedConvexUserIds = removedConvexIds.map((id) => id as unknown as string)
if (removedConvexIds.length > 0) {
try {
const result = await client.mutation(api.tickets.purgeTicketsForUsers, {
tenantId,
actorId,
userIds: removedConvexIds,
})
responseSummary.convictTicketsDeleted = typeof result?.deleted === "number" ? result.deleted : 0
} catch (error) {
console.error("[users.cleanup] Falha ao remover tickets no Convex", error)
}
}
}
} catch (error) {
console.error("[users.cleanup] Convex indisponível", error)
}
return NextResponse.json(responseSummary)
}