From da46fa448ba6eb7516e87cfb5f240fe3c98ebc33 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Tue, 11 Nov 2025 16:06:11 -0300 Subject: [PATCH] feat(convex): add internal url and remote access fixes --- .env.example | 1 + convex/machines.ts | 1 + docs/OPERACAO-PRODUCAO.md | 4 +++ src/app/api/admin/users/[id]/route.ts | 27 +++++++--------- .../api/admin/users/assign-company/route.ts | 10 ++++-- src/app/api/integrations/fleet/hosts/route.ts | 8 ++--- src/app/api/invites/[token]/route.ts | 32 ++++++++----------- src/app/api/portal/profile/route.ts | 24 +++++++------- .../reports/machines-inventory.xlsx/route.ts | 9 ++---- src/app/api/tickets/[id]/export/pdf/route.ts | 9 ++---- src/app/api/tickets/mentions/route.ts | 9 ++---- .../admin/devices/admin-devices-overview.tsx | 2 +- src/lib/env.ts | 2 ++ src/server/convex-client.ts | 9 ++++-- src/server/machines-session.ts | 8 ++--- src/server/report-exporters.ts | 8 ++--- stack.yml | 2 ++ 17 files changed, 73 insertions(+), 92 deletions(-) diff --git a/.env.example b/.env.example index 2fbeb51..814fa87 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,7 @@ BETTER_AUTH_SECRET=change-me-in-prod # Convex (dev server URL) NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 +CONVEX_INTERNAL_URL=http://127.0.0.1:3210 # SQLite database (local dev) DATABASE_URL=file:./prisma/db.dev.sqlite diff --git a/convex/machines.ts b/convex/machines.ts index b03ef0f..8409011 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -966,6 +966,7 @@ export const listByTenant = query({ inventory, postureAlerts, lastPostureAt, + remoteAccess: machine.remoteAccess ?? null, customFields: machine.customFields ?? [], } }) diff --git a/docs/OPERACAO-PRODUCAO.md b/docs/OPERACAO-PRODUCAO.md index 6eeeef1..f667cca 100644 --- a/docs/OPERACAO-PRODUCAO.md +++ b/docs/OPERACAO-PRODUCAO.md @@ -54,6 +54,7 @@ Arquivo base: `.env` na raiz do projeto. Exemplo mínimo (substitua domínios/se NEXT_PUBLIC_APP_URL=https://tickets.esdrasrenan.com.br BETTER_AUTH_URL=https://tickets.esdrasrenan.com.br NEXT_PUBLIC_CONVEX_URL=https://convex.esdrasrenan.com.br +CONVEX_INTERNAL_URL=http://convex_backend:3210 BETTER_AUTH_SECRET= DATABASE_URL=file:./prisma/db.sqlite @@ -77,6 +78,9 @@ MACHINE_PROVISIONING_SECRET= MACHINE_TOKEN_TTL_MS=2592000000 FLEET_SYNC_SECRET= +# Conexões internas (Next.js -> Convex) +# CONVEX_INTERNAL_URL deve apontar para o hostname/porta do serviço no Swarm. + # Outros CONVEX_SYNC_SECRET=dev-sync-secret ALERTS_LOCAL_HOUR=8 diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index 1ebceaf..a849ac3 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -7,6 +7,7 @@ import { prisma } from "@/lib/prisma" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { assertStaffSession } from "@/lib/auth-server" import { ROLE_OPTIONS, type RoleOption, isAdmin } from "@/lib/authz" +import { requireConvexUrl } from "@/server/convex-client" function normalizeRole(input: string | null | undefined): RoleOption { const candidate = (input ?? "agent").toLowerCase() as RoleOption @@ -256,10 +257,8 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id }) } - const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL - if (convexUrl) { - try { - const convex = new ConvexHttpClient(convexUrl) + try { + const convex = new ConvexHttpClient(requireConvexUrl()) let managerConvexId: Id<"users"> | undefined if (hasManagerField && managerRecord?.email) { try { @@ -299,10 +298,9 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id if (hasManagerField) { ensurePayload.managerId = managerConvexId } - await convex.mutation(api.users.ensureUser, ensurePayload) - } catch (error) { - console.warn("Falha ao sincronizar usuário no Convex", error) - } + await convex.mutation(api.users.ensureUser, ensurePayload) + } catch (error) { + console.warn("Falha ao sincronizar usuário no Convex", error) } const updatedDomain = await prisma.user.findUnique({ @@ -363,12 +361,10 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str return NextResponse.json({ error: "Você não pode remover o usuário atualmente autenticado." }, { status: 400 }) } - const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL const tenantId = target.tenantId ?? session.user.tenantId ?? DEFAULT_TENANT_ID - if (convexUrl) { - try { - const convex = new ConvexHttpClient(convexUrl) + try { + const convex = new ConvexHttpClient(requireConvexUrl()) const ensured = await convex.mutation(api.users.ensureUser, { tenantId, email: session.user.email, @@ -393,10 +389,9 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str actorId, }) } - } catch (error) { - const message = error instanceof Error ? error.message : "Falha ao remover usuário na base de dados" - return NextResponse.json({ error: message }, { status: 400 }) - } + } catch (error) { + const message = error instanceof Error ? error.message : "Falha ao remover usuário na base de dados" + return NextResponse.json({ error: message }, { status: 400 }) } await prisma.authUser.delete({ where: { id: target.id } }) diff --git a/src/app/api/admin/users/assign-company/route.ts b/src/app/api/admin/users/assign-company/route.ts index 47f1a65..8a772b3 100644 --- a/src/app/api/admin/users/assign-company/route.ts +++ b/src/app/api/admin/users/assign-company/route.ts @@ -7,6 +7,7 @@ import { prisma } from "@/lib/prisma" import { DEFAULT_TENANT_ID } from "@/lib/constants" import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" +import { requireConvexUrl } from "@/server/convex-client" export const runtime = "nodejs" @@ -21,9 +22,12 @@ export async function POST(request: Request) { return NextResponse.json({ error: "Informe e-mail e empresa" }, { status: 400 }) } - const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) return NextResponse.json({ error: "Convex não configurado" }, { status: 500 }) - const client = new ConvexHttpClient(convexUrl) + let client: ConvexHttpClient + try { + client = new ConvexHttpClient(requireConvexUrl()) + } catch { + return NextResponse.json({ error: "Convex não configurado" }, { status: 500 }) + } try { const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID diff --git a/src/app/api/integrations/fleet/hosts/route.ts b/src/app/api/integrations/fleet/hosts/route.ts index 732cbeb..7d190f8 100644 --- a/src/app/api/integrations/fleet/hosts/route.ts +++ b/src/app/api/integrations/fleet/hosts/route.ts @@ -4,6 +4,7 @@ import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import { env } from "@/lib/env" +import { requireConvexUrl } from "@/server/convex-client" const fleetHostSchema = z.object({ host: z @@ -92,11 +93,6 @@ export async function POST(request: Request) { 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 }) - } - let parsed try { const raw = await request.json() @@ -159,7 +155,7 @@ export async function POST(request: Request) { cpuLogicalCores: host.cpu_logical_cores, } - const client = new ConvexHttpClient(convexUrl) + const client = new ConvexHttpClient(requireConvexUrl()) try { const result = await client.mutation(api.devices.upsertInventory, { diff --git a/src/app/api/invites/[token]/route.ts b/src/app/api/invites/[token]/route.ts index 89209b7..a781a3c 100644 --- a/src/app/api/invites/[token]/route.ts +++ b/src/app/api/invites/[token]/route.ts @@ -6,7 +6,6 @@ import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" -import { env } from "@/lib/env" import { prisma } from "@/lib/prisma" import { computeInviteStatus, @@ -14,6 +13,7 @@ import { normalizeRoleOption, type NormalizedInvite, } from "@/server/invite-utils" +import { requireConvexUrl } from "@/server/convex-client" type AcceptInvitePayload = { name?: string @@ -25,9 +25,8 @@ function validatePassword(password: string) { } async function syncInvite(invite: NormalizedInvite) { - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) return - const client = new ConvexHttpClient(convexUrl) + const url = requireConvexUrl() + const client = new ConvexHttpClient(url) await client.mutation(api.invites.sync, { tenantId: invite.tenantId, inviteId: invite.id, @@ -178,20 +177,17 @@ export async function POST(request: Request, context: { params: Promise<{ token: const normalized = normalizeInvite({ ...updatedInvite, events: [...invite.events, event] }, now) await syncInvite(normalized) - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL - if (convexUrl) { - try { - const convex = new ConvexHttpClient(convexUrl) - 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) - } + 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 }) diff --git a/src/app/api/portal/profile/route.ts b/src/app/api/portal/profile/route.ts index 3e554a9..b1f3009 100644 --- a/src/app/api/portal/profile/route.ts +++ b/src/app/api/portal/profile/route.ts @@ -5,10 +5,10 @@ import { ConvexHttpClient } from "convex/browser" import { prisma } from "@/lib/prisma" import { requireAuthenticatedSession } from "@/lib/auth-server" -import { env } from "@/lib/env" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { api } from "@/convex/_generated/api" import { ensureCollaboratorAccount } from "@/server/machines-auth" +import { requireConvexUrl } from "@/server/convex-client" const updateSchema = z.object({ email: z.string().email().optional(), @@ -167,18 +167,16 @@ export async function PATCH(request: Request) { role: effectiveRole, }) - if (env.NEXT_PUBLIC_CONVEX_URL) { - try { - const client = new ConvexHttpClient(env.NEXT_PUBLIC_CONVEX_URL) - await client.mutation(api.users.ensureUser, { - tenantId, - email: effectiveEmail, - name, - role: effectiveRole, - }) - } catch (error) { - console.warn("[portal.profile] Falha ao sincronizar usuário no Convex", error) - } + try { + const client = new ConvexHttpClient(requireConvexUrl()) + await client.mutation(api.users.ensureUser, { + tenantId, + email: effectiveEmail, + name, + role: effectiveRole, + }) + } catch (error) { + console.warn("[portal.profile] Falha ao sincronizar usuário no Convex", error) } return NextResponse.json({ ok: true, email: effectiveEmail }) diff --git a/src/app/api/reports/machines-inventory.xlsx/route.ts b/src/app/api/reports/machines-inventory.xlsx/route.ts index 1feda15..0343a1a 100644 --- a/src/app/api/reports/machines-inventory.xlsx/route.ts +++ b/src/app/api/reports/machines-inventory.xlsx/route.ts @@ -2,11 +2,11 @@ import { NextResponse } from "next/server" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" -import { env } from "@/lib/env" import { assertAuthenticatedSession } from "@/lib/auth-server" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export" import type { DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns" +import { requireConvexUrl } from "@/server/convex-client" export const runtime = "nodejs" @@ -14,11 +14,6 @@ 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 machineIdParams = searchParams.getAll("machineId").filter(Boolean) @@ -49,7 +44,7 @@ export async function GET(request: Request) { } } - const client = new ConvexHttpClient(convexUrl) + const client = new ConvexHttpClient(requireConvexUrl()) const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID let viewerId: string | null = null diff --git a/src/app/api/tickets/[id]/export/pdf/route.ts b/src/app/api/tickets/[id]/export/pdf/route.ts index a774af0..5933e4b 100644 --- a/src/app/api/tickets/[id]/export/pdf/route.ts +++ b/src/app/api/tickets/[id]/export/pdf/route.ts @@ -5,11 +5,11 @@ 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 { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { renderTicketPdfBuffer } from "@/server/pdf/ticket-pdf-template" +import { requireConvexUrl } from "@/server/convex-client" export const runtime = "nodejs" @@ -32,12 +32,7 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st 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 client = new ConvexHttpClient(convexUrl) + const client = new ConvexHttpClient(requireConvexUrl()) const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID let viewerId: string | null = null diff --git a/src/app/api/tickets/mentions/route.ts b/src/app/api/tickets/mentions/route.ts index 3285a92..24e4095 100644 --- a/src/app/api/tickets/mentions/route.ts +++ b/src/app/api/tickets/mentions/route.ts @@ -2,9 +2,9 @@ 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 { requireConvexUrl } from "@/server/convex-client" export const runtime = "nodejs" @@ -42,11 +42,6 @@ export async function GET(request: Request) { return NextResponse.json({ items: [] }, { status: 401 }) } - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) { - return NextResponse.json({ items: [] }, { status: 500 }) - } - const normalizedRole = normalizeRole(session.user.role) const isAgentOrAdmin = normalizedRole === "admin" || normalizedRole === "agent" const canLinkOwnTickets = normalizedRole === "collaborator" @@ -59,7 +54,7 @@ export async function GET(request: Request) { const rawQuery = url.searchParams.get("q") ?? "" const query = rawQuery.trim() - const client = new ConvexHttpClient(convexUrl) + const client = new ConvexHttpClient(requireConvexUrl()) // Garantir que o usuário exista no Convex para obter viewerId let viewerId: string | null = null diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index fc42ea7..167a918 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -3922,7 +3922,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) : null} ) : null} - {entry.url ? ( + {entry.url && !isRustDesk ? ( { - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) { - throw new Error("Convex não configurado") - } - + const convexUrl = requireConvexUrl() const client = new ConvexHttpClient(convexUrl) const resolved = await client.mutation(api.devices.resolveToken, { machineToken }) diff --git a/src/server/report-exporters.ts b/src/server/report-exporters.ts index 7dbef74..ba70fa6 100644 --- a/src/server/report-exporters.ts +++ b/src/server/report-exporters.ts @@ -4,9 +4,9 @@ import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" -import { env } from "@/lib/env" import { buildXlsxWorkbook } from "@/lib/xlsx" import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions" +import { requireConvexUrl } from "@/server/convex-client" export type { ReportExportKey } type ViewerIdentity = { @@ -24,11 +24,7 @@ export type ConvexReportContext = { } export async function createConvexContext(identity: ViewerIdentity): Promise { - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL - if (!convexUrl) { - throw new Error("Convex URL não configurada para exportações") - } - const client = new ConvexHttpClient(convexUrl) + const client = new ConvexHttpClient(requireConvexUrl()) const ensuredUser = await client.mutation(api.users.ensureUser, { tenantId: identity.tenantId, name: identity.name, diff --git a/stack.yml b/stack.yml index 7c86fb7..adfaed0 100644 --- a/stack.yml +++ b/stack.yml @@ -20,6 +20,8 @@ services: # IMPORTANTE: "NEXT_PUBLIC_*" é consumida pelo navegador (cliente). Use a URL pública do Convex. # Não use o hostname interno do Swarm aqui, pois o browser não consegue resolvê-lo. NEXT_PUBLIC_CONVEX_URL: "${NEXT_PUBLIC_CONVEX_URL}" + # URLs consumidas apenas pelo backend/SSR podem usar o hostname interno + CONVEX_INTERNAL_URL: "http://convex_backend:3210" # URLs públicas do app (evita fallback para localhost) NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}" BETTER_AUTH_URL: "${BETTER_AUTH_URL}"