feat: adicionar painel de máquinas e autenticação por agente

This commit is contained in:
Esdras Renan 2025-10-07 21:37:41 -03:00
parent e2a5b560b1
commit ee18619519
52 changed files with 7598 additions and 1 deletions

View file

@ -0,0 +1,51 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api"
import { env } from "@/lib/env"
const heartbeatSchema = z.object({
machineToken: z.string().min(1),
status: z.string().optional(),
hostname: z.string().optional(),
os: z
.object({
name: z.string(),
version: z.string().optional(),
architecture: z.string().optional(),
})
.optional(),
metrics: z.record(z.string(), z.unknown()).optional(),
inventory: z.record(z.string(), z.unknown()).optional(),
metadata: z.record(z.string(), z.unknown()).optional(),
})
export async function POST(request: Request) {
if (request.method !== "POST") {
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
}
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
let payload
try {
const raw = await request.json()
payload = heartbeatSchema.parse(raw)
} catch (error) {
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
}
const client = new ConvexHttpClient(convexUrl)
try {
const response = await client.mutation(api.machines.heartbeat, payload)
return NextResponse.json(response)
} catch (error) {
console.error("[machines.heartbeat] Falha ao registrar heartbeat", error)
return NextResponse.json({ error: "Falha ao registrar heartbeat" }, { status: 500 })
}
}

View file

@ -0,0 +1,94 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { ensureMachineAccount } from "@/server/machines-auth"
const registerSchema = z
.object({
provisioningSecret: z.string().min(1),
tenantId: z.string().optional(),
companySlug: z.string().optional(),
hostname: z.string().min(1),
os: z.object({
name: z.string().min(1),
version: z.string().optional(),
architecture: z.string().optional(),
}),
macAddresses: z.array(z.string()).default([]),
serialNumbers: z.array(z.string()).default([]),
metadata: z.record(z.string(), z.unknown()).optional(),
registeredBy: z.string().optional(),
})
.refine(
(data) => (data.macAddresses && data.macAddresses.length > 0) || (data.serialNumbers && data.serialNumbers.length > 0),
{ message: "Informe ao menos um MAC address ou número de série" }
)
export async function POST(request: Request) {
if (request.method !== "POST") {
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
}
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
let payload
try {
const raw = await request.json()
payload = registerSchema.parse(raw)
} catch (error) {
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
}
const client = new ConvexHttpClient(convexUrl)
try {
const registration = await client.mutation(api.machines.register, {
provisioningSecret: payload.provisioningSecret,
tenantId: payload.tenantId ?? DEFAULT_TENANT_ID,
companySlug: payload.companySlug ?? undefined,
hostname: payload.hostname,
os: payload.os,
macAddresses: payload.macAddresses,
serialNumbers: payload.serialNumbers,
metadata: payload.metadata,
registeredBy: payload.registeredBy,
})
const account = await ensureMachineAccount({
machineId: registration.machineId,
tenantId: registration.tenantId ?? DEFAULT_TENANT_ID,
hostname: payload.hostname,
machineToken: registration.machineToken,
})
await client.mutation(api.machines.linkAuthAccount, {
machineId: registration.machineId as Id<"machines">,
authUserId: account.authUserId,
authEmail: account.authEmail,
})
return NextResponse.json(
{
machineId: registration.machineId,
tenantId: registration.tenantId,
companyId: registration.companyId,
companySlug: registration.companySlug,
machineToken: registration.machineToken,
machineEmail: account.authEmail,
expiresAt: registration.expiresAt,
},
{ status: 201 }
)
} catch (error) {
console.error("[machines.register] Falha no provisionamento", error)
return NextResponse.json({ error: "Falha ao provisionar máquina" }, { status: 500 })
}
}

View file

@ -0,0 +1,96 @@
import { NextResponse } from "next/server"
import { z } from "zod"
import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { ensureMachineAccount } from "@/server/machines-auth"
import { auth } from "@/lib/auth"
const sessionSchema = z.object({
machineToken: z.string().min(1),
rememberMe: z.boolean().optional(),
})
export async function POST(request: Request) {
if (request.method !== "POST") {
return NextResponse.json({ error: "Método não permitido" }, { status: 405 })
}
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL
if (!convexUrl) {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
let payload
try {
const raw = await request.json()
payload = sessionSchema.parse(raw)
} catch (error) {
return NextResponse.json({ error: "Payload inválido", details: error instanceof Error ? error.message : String(error) }, { status: 400 })
}
const client = new ConvexHttpClient(convexUrl)
try {
const resolved = await client.mutation(api.machines.resolveToken, { machineToken: payload.machineToken })
let machineEmail = resolved.machine.authEmail ?? null
if (!machineEmail) {
const account = await ensureMachineAccount({
machineId: resolved.machine._id,
tenantId: resolved.machine.tenantId ?? DEFAULT_TENANT_ID,
hostname: resolved.machine.hostname,
machineToken: payload.machineToken,
})
await client.mutation(api.machines.linkAuthAccount, {
machineId: resolved.machine._id as Id<"machines">,
authUserId: account.authUserId,
authEmail: account.authEmail,
})
machineEmail = account.authEmail
}
const signIn = await auth.api.signInEmail({
body: {
email: machineEmail,
password: payload.machineToken,
rememberMe: payload.rememberMe ?? true,
},
returnHeaders: true,
})
const response = NextResponse.json(
{
ok: true,
machine: {
id: resolved.machine._id,
hostname: resolved.machine.hostname,
osName: resolved.machine.osName,
osVersion: resolved.machine.osVersion,
architecture: resolved.machine.architecture,
status: resolved.machine.status,
lastHeartbeatAt: resolved.machine.lastHeartbeatAt,
companyId: resolved.machine.companyId,
companySlug: resolved.machine.companySlug,
metadata: resolved.machine.metadata,
},
session: signIn.response,
},
{ status: 200 }
)
signIn.headers.forEach((value, key) => {
response.headers.set(key, value)
})
return response
} catch (error) {
console.error("[machines.sessions] Falha ao criar sessão", error)
return NextResponse.json({ error: "Falha ao autenticar máquina" }, { status: 500 })
}
}