import { NextResponse } from "next/server" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { env } from "@/lib/env" import { assertAuthenticatedSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { buildXlsxWorkbook } from "@/lib/xlsx" export const runtime = "nodejs" type MachineListEntry = { id: Id<"machines"> tenantId: string hostname: string companyId: Id<"companies"> | null companySlug: string | null companyName: string | null status: string | null isActive: boolean lastHeartbeatAt: number | null persona: string | null assignedUserName: string | null assignedUserEmail: string | null authEmail: string | null osName: string osVersion: string | null architecture: string | null macAddresses: string[] serialNumbers: string[] registeredBy: string | null createdAt: number updatedAt: number token: { expiresAt: number; usageCount: number; lastUsedAt: number | null } | null inventory: Record | null linkedUsers?: Array<{ id: string; email: string; name: string }> } function formatIso(value: number | null | undefined): string | null { if (typeof value !== "number") return null try { return new Date(value).toISOString() } catch { return null } } function formatMemory(bytes: unknown): number | null { if (typeof bytes !== "number" || !Number.isFinite(bytes) || bytes <= 0) return null const gib = bytes / (1024 ** 3) return Number(gib.toFixed(2)) } function extractPrimaryIp(inventory: Record | null): string | null { if (!inventory) return null const network = inventory.network if (!network) return null if (Array.isArray(network)) { for (const entry of network) { if (entry && typeof entry === "object") { const candidate = (entry as { ip?: unknown }).ip if (typeof candidate === "string" && candidate.trim().length > 0) return candidate.trim() } } } else if (typeof network === "object") { const record = network as Record const ip = typeof record.primaryIp === "string" ? record.primaryIp : typeof record.publicIp === "string" ? record.publicIp : null if (ip && ip.trim().length > 0) return ip.trim() } return null } function extractHardware(inventory: Record | null) { if (!inventory) return {} const hardware = inventory.hardware if (!hardware || typeof hardware !== "object") return {} const hw = hardware as Record return { vendor: typeof hw.vendor === "string" ? hw.vendor : null, model: typeof hw.model === "string" ? hw.model : null, serial: typeof hw.serial === "string" ? hw.serial : null, cpuType: typeof hw.cpuType === "string" ? hw.cpuType : null, physicalCores: typeof hw.physicalCores === "number" ? hw.physicalCores : null, logicalCores: typeof hw.logicalCores === "number" ? hw.logicalCores : null, memoryBytes: typeof hw.memoryBytes === "number" ? hw.memoryBytes : null, } } export async function GET(request: Request) { const session = await assertAuthenticatedSession() if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) const convexUrl = env.NEXT_PUBLIC_CONVEX_URL if (!convexUrl) { return NextResponse.json({ error: "Convex não configurado" }, { status: 500 }) } const { searchParams } = new URL(request.url) const companyId = searchParams.get("companyId") ?? undefined const client = new ConvexHttpClient(convexUrl) const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID let viewerId: string | null = null try { const ensuredUser = await client.mutation(api.users.ensureUser, { tenantId, name: session.user.name ?? session.user.email, email: session.user.email, avatarUrl: session.user.avatarUrl ?? undefined, role: session.user.role.toUpperCase(), }) viewerId = ensuredUser?._id ?? null } catch (error) { console.error("Failed to synchronize user with Convex for machines export", error) return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 }) } if (!viewerId) { return NextResponse.json({ error: "Usuário não encontrado no Convex" }, { status: 403 }) } try { const machines = (await client.query(api.machines.listByTenant, { tenantId, includeMetadata: true, })) as MachineListEntry[] const filtered = machines.filter((machine) => { if (!companyId) return true return String(machine.companyId ?? "") === companyId || machine.companySlug === companyId }) const statusCounts = filtered.reduce>((acc, machine) => { const key = machine.status ?? "unknown" acc[key] = (acc[key] ?? 0) + 1 return acc }, {}) const summaryRows: Array> = [ ["Tenant", tenantId], ["Total de máquinas", filtered.length], ] if (companyId) summaryRows.push(["Filtro de empresa", companyId]) Object.entries(statusCounts).forEach(([status, total]) => { summaryRows.push([`Status: ${status}`, total]) }) const inventorySheetRows = filtered.map((machine) => { const inventory = machine.inventory && typeof machine.inventory === "object" ? (machine.inventory as Record) : null const hardware = extractHardware(inventory) const primaryIp = extractPrimaryIp(inventory) const memoryGiB = formatMemory(hardware.memoryBytes) return [ machine.hostname, machine.companyName ?? "—", machine.status ?? "unknown", machine.isActive ? "Sim" : "Não", formatIso(machine.lastHeartbeatAt), machine.persona ?? null, machine.assignedUserName ?? null, machine.assignedUserEmail ?? null, machine.authEmail ?? null, machine.osName, machine.osVersion ?? null, machine.architecture ?? null, machine.macAddresses.join(", "), machine.serialNumbers.join(", "), machine.registeredBy ?? null, formatIso(machine.createdAt), formatIso(machine.updatedAt), hardware.vendor, hardware.model, hardware.serial, hardware.cpuType, hardware.physicalCores, hardware.logicalCores, memoryGiB, primaryIp, machine.token?.expiresAt ? formatIso(machine.token.expiresAt) : null, machine.token?.usageCount ?? null, ] }) const linksSheetRows: Array> = [] filtered.forEach((machine) => { if (!machine.linkedUsers || machine.linkedUsers.length === 0) return machine.linkedUsers.forEach((user) => { linksSheetRows.push([ machine.hostname, machine.companyName ?? "—", user.name ?? user.email ?? "—", user.email ?? "—", ]) }) }) const workbook = buildXlsxWorkbook([ { name: "Resumo", headers: ["Item", "Valor"], rows: summaryRows, }, { name: "Máquinas", headers: [ "Hostname", "Empresa", "Status", "Ativa", "Último heartbeat", "Persona", "Responsável", "E-mail responsável", "E-mail autenticado", "Sistema operacional", "Versão SO", "Arquitetura", "Endereços MAC", "Seriais", "Registrada via", "Criada em", "Atualizada em", "Fabricante", "Modelo", "Serial hardware", "Processador", "Cores físicas", "Cores lógicas", "Memória (GiB)", "IP principal", "Token expira em", "Uso do token", ], rows: inventorySheetRows.length > 0 ? inventorySheetRows : [["—"]], }, { name: "Vínculos", headers: ["Hostname", "Empresa", "Usuário", "E-mail"], rows: linksSheetRows.length > 0 ? linksSheetRows : [["—", "—", "—", "—"]], }, ]) const body = new Uint8Array(workbook) return new NextResponse(body, { headers: { "Content-Type": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "Content-Disposition": `attachment; filename="machines-inventory-${tenantId}${companyId ? `-${companyId}` : ""}.xlsx"`, "Cache-Control": "no-store", }, }) } catch (error) { console.error("Failed to generate machines inventory export", error) return NextResponse.json({ error: "Falha ao gerar planilha de inventário" }, { status: 500 }) } }