feat(convex): add internal url and remote access fixes

This commit is contained in:
Esdras Renan 2025-11-11 16:06:11 -03:00
parent feb31d48c1
commit da46fa448b
17 changed files with 73 additions and 92 deletions

View file

@ -9,6 +9,7 @@ BETTER_AUTH_SECRET=change-me-in-prod
# Convex (dev server URL) # Convex (dev server URL)
NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210
CONVEX_INTERNAL_URL=http://127.0.0.1:3210
# SQLite database (local dev) # SQLite database (local dev)
DATABASE_URL=file:./prisma/db.dev.sqlite DATABASE_URL=file:./prisma/db.dev.sqlite

View file

@ -966,6 +966,7 @@ export const listByTenant = query({
inventory, inventory,
postureAlerts, postureAlerts,
lastPostureAt, lastPostureAt,
remoteAccess: machine.remoteAccess ?? null,
customFields: machine.customFields ?? [], customFields: machine.customFields ?? [],
} }
}) })

View file

@ -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 NEXT_PUBLIC_APP_URL=https://tickets.esdrasrenan.com.br
BETTER_AUTH_URL=https://tickets.esdrasrenan.com.br BETTER_AUTH_URL=https://tickets.esdrasrenan.com.br
NEXT_PUBLIC_CONVEX_URL=https://convex.esdrasrenan.com.br NEXT_PUBLIC_CONVEX_URL=https://convex.esdrasrenan.com.br
CONVEX_INTERNAL_URL=http://convex_backend:3210
BETTER_AUTH_SECRET=<hex forte gerado por `openssl rand -hex 32`> BETTER_AUTH_SECRET=<hex forte gerado por `openssl rand -hex 32`>
DATABASE_URL=file:./prisma/db.sqlite DATABASE_URL=file:./prisma/db.sqlite
@ -77,6 +78,9 @@ MACHINE_PROVISIONING_SECRET=<hex forte>
MACHINE_TOKEN_TTL_MS=2592000000 MACHINE_TOKEN_TTL_MS=2592000000
FLEET_SYNC_SECRET=<hex forte ou igual ao de provisionamento> FLEET_SYNC_SECRET=<hex forte ou igual ao de provisionamento>
# Conexões internas (Next.js -> Convex)
# CONVEX_INTERNAL_URL deve apontar para o hostname/porta do serviço no Swarm.
# Outros # Outros
CONVEX_SYNC_SECRET=dev-sync-secret CONVEX_SYNC_SECRET=dev-sync-secret
ALERTS_LOCAL_HOUR=8 ALERTS_LOCAL_HOUR=8

View file

@ -7,6 +7,7 @@ import { prisma } from "@/lib/prisma"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { assertStaffSession } from "@/lib/auth-server" import { assertStaffSession } from "@/lib/auth-server"
import { ROLE_OPTIONS, type RoleOption, isAdmin } from "@/lib/authz" import { ROLE_OPTIONS, type RoleOption, isAdmin } from "@/lib/authz"
import { requireConvexUrl } from "@/server/convex-client"
function normalizeRole(input: string | null | undefined): RoleOption { function normalizeRole(input: string | null | undefined): RoleOption {
const candidate = (input ?? "agent").toLowerCase() as 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 try {
if (convexUrl) { const convex = new ConvexHttpClient(requireConvexUrl())
try {
const convex = new ConvexHttpClient(convexUrl)
let managerConvexId: Id<"users"> | undefined let managerConvexId: Id<"users"> | undefined
if (hasManagerField && managerRecord?.email) { if (hasManagerField && managerRecord?.email) {
try { try {
@ -299,10 +298,9 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
if (hasManagerField) { if (hasManagerField) {
ensurePayload.managerId = managerConvexId ensurePayload.managerId = managerConvexId
} }
await convex.mutation(api.users.ensureUser, ensurePayload) await convex.mutation(api.users.ensureUser, ensurePayload)
} catch (error) { } catch (error) {
console.warn("Falha ao sincronizar usuário no Convex", error) console.warn("Falha ao sincronizar usuário no Convex", error)
}
} }
const updatedDomain = await prisma.user.findUnique({ 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 }) 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 const tenantId = target.tenantId ?? session.user.tenantId ?? DEFAULT_TENANT_ID
if (convexUrl) { try {
try { const convex = new ConvexHttpClient(requireConvexUrl())
const convex = new ConvexHttpClient(convexUrl)
const ensured = await convex.mutation(api.users.ensureUser, { const ensured = await convex.mutation(api.users.ensureUser, {
tenantId, tenantId,
email: session.user.email, email: session.user.email,
@ -393,10 +389,9 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str
actorId, actorId,
}) })
} }
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : "Falha ao remover usuário na base de dados" const message = error instanceof Error ? error.message : "Falha ao remover usuário na base de dados"
return NextResponse.json({ error: message }, { status: 400 }) return NextResponse.json({ error: message }, { status: 400 })
}
} }
await prisma.authUser.delete({ where: { id: target.id } }) await prisma.authUser.delete({ where: { id: target.id } })

View file

@ -7,6 +7,7 @@ import { prisma } from "@/lib/prisma"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { requireConvexUrl } from "@/server/convex-client"
export const runtime = "nodejs" 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 }) return NextResponse.json({ error: "Informe e-mail e empresa" }, { status: 400 })
} }
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL let client: ConvexHttpClient
if (!convexUrl) return NextResponse.json({ error: "Convex não configurado" }, { status: 500 }) try {
const client = new ConvexHttpClient(convexUrl) client = new ConvexHttpClient(requireConvexUrl())
} catch {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
try { try {
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID

View file

@ -4,6 +4,7 @@ import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { env } from "@/lib/env" import { env } from "@/lib/env"
import { requireConvexUrl } from "@/server/convex-client"
const fleetHostSchema = z.object({ const fleetHostSchema = z.object({
host: z host: z
@ -92,11 +93,6 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) 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 let parsed
try { try {
const raw = await request.json() const raw = await request.json()
@ -159,7 +155,7 @@ export async function POST(request: Request) {
cpuLogicalCores: host.cpu_logical_cores, cpuLogicalCores: host.cpu_logical_cores,
} }
const client = new ConvexHttpClient(convexUrl) const client = new ConvexHttpClient(requireConvexUrl())
try { try {
const result = await client.mutation(api.devices.upsertInventory, { const result = await client.mutation(api.devices.upsertInventory, {

View file

@ -6,7 +6,6 @@ import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { env } from "@/lib/env"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { import {
computeInviteStatus, computeInviteStatus,
@ -14,6 +13,7 @@ import {
normalizeRoleOption, normalizeRoleOption,
type NormalizedInvite, type NormalizedInvite,
} from "@/server/invite-utils" } from "@/server/invite-utils"
import { requireConvexUrl } from "@/server/convex-client"
type AcceptInvitePayload = { type AcceptInvitePayload = {
name?: string name?: string
@ -25,9 +25,8 @@ function validatePassword(password: string) {
} }
async function syncInvite(invite: NormalizedInvite) { async function syncInvite(invite: NormalizedInvite) {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL const url = requireConvexUrl()
if (!convexUrl) return const client = new ConvexHttpClient(url)
const client = new ConvexHttpClient(convexUrl)
await client.mutation(api.invites.sync, { await client.mutation(api.invites.sync, {
tenantId: invite.tenantId, tenantId: invite.tenantId,
inviteId: invite.id, 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) const normalized = normalizeInvite({ ...updatedInvite, events: [...invite.events, event] }, now)
await syncInvite(normalized) await syncInvite(normalized)
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL try {
if (convexUrl) { const convex = new ConvexHttpClient(requireConvexUrl())
try { await convex.mutation(api.users.ensureUser, {
const convex = new ConvexHttpClient(convexUrl) tenantId,
await convex.mutation(api.users.ensureUser, { email: invite.email,
tenantId, name,
email: invite.email, avatarUrl: undefined,
name, role: role.toUpperCase(),
avatarUrl: undefined, })
role: role.toUpperCase(), } catch (error) {
}) console.warn("Falha ao sincronizar usuário no Convex", error)
} catch (error) {
console.warn("Falha ao sincronizar usuário no Convex", error)
}
} }
return NextResponse.json({ success: true }) return NextResponse.json({ success: true })

View file

@ -5,10 +5,10 @@ import { ConvexHttpClient } from "convex/browser"
import { prisma } from "@/lib/prisma" import { prisma } from "@/lib/prisma"
import { requireAuthenticatedSession } from "@/lib/auth-server" import { requireAuthenticatedSession } from "@/lib/auth-server"
import { env } from "@/lib/env"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { ensureCollaboratorAccount } from "@/server/machines-auth" import { ensureCollaboratorAccount } from "@/server/machines-auth"
import { requireConvexUrl } from "@/server/convex-client"
const updateSchema = z.object({ const updateSchema = z.object({
email: z.string().email().optional(), email: z.string().email().optional(),
@ -167,18 +167,16 @@ export async function PATCH(request: Request) {
role: effectiveRole, role: effectiveRole,
}) })
if (env.NEXT_PUBLIC_CONVEX_URL) { try {
try { const client = new ConvexHttpClient(requireConvexUrl())
const client = new ConvexHttpClient(env.NEXT_PUBLIC_CONVEX_URL) await client.mutation(api.users.ensureUser, {
await client.mutation(api.users.ensureUser, { tenantId,
tenantId, email: effectiveEmail,
email: effectiveEmail, name,
name, role: effectiveRole,
role: effectiveRole, })
}) } catch (error) {
} catch (error) { console.warn("[portal.profile] Falha ao sincronizar usuário no Convex", error)
console.warn("[portal.profile] Falha ao sincronizar usuário no Convex", error)
}
} }
return NextResponse.json({ ok: true, email: effectiveEmail }) return NextResponse.json({ ok: true, email: effectiveEmail })

View file

@ -2,11 +2,11 @@ import { NextResponse } from "next/server"
import { ConvexHttpClient } from "convex/browser" import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import { env } from "@/lib/env"
import { assertAuthenticatedSession } from "@/lib/auth-server" import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export" import { buildMachinesInventoryWorkbook, type MachineInventoryRecord } from "@/server/machines/inventory-export"
import type { DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns" import type { DeviceInventoryColumnConfig } from "@/lib/device-inventory-columns"
import { requireConvexUrl } from "@/server/convex-client"
export const runtime = "nodejs" export const runtime = "nodejs"
@ -14,11 +14,6 @@ export async function GET(request: Request) {
const session = await assertAuthenticatedSession() const session = await assertAuthenticatedSession()
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 }) 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 { searchParams } = new URL(request.url)
const companyId = searchParams.get("companyId") ?? undefined const companyId = searchParams.get("companyId") ?? undefined
const machineIdParams = searchParams.getAll("machineId").filter(Boolean) 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 const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
let viewerId: string | null = null let viewerId: string | null = null

View file

@ -5,11 +5,11 @@ import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { assertAuthenticatedSession } from "@/lib/auth-server" import { assertAuthenticatedSession } from "@/lib/auth-server"
import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket" import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { renderTicketPdfBuffer } from "@/server/pdf/ticket-pdf-template" import { renderTicketPdfBuffer } from "@/server/pdf/ticket-pdf-template"
import { requireConvexUrl } from "@/server/convex-client"
export const runtime = "nodejs" 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 }) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
} }
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL const client = new ConvexHttpClient(requireConvexUrl())
if (!convexUrl) {
return NextResponse.json({ error: "Convex não configurado" }, { status: 500 })
}
const client = new ConvexHttpClient(convexUrl)
const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID
let viewerId: string | null = null let viewerId: string | null = null

View file

@ -2,9 +2,9 @@ import { NextResponse } from "next/server"
import { ConvexHttpClient } from "convex/browser" import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { assertAuthenticatedSession } from "@/lib/auth-server" import { assertAuthenticatedSession } from "@/lib/auth-server"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { requireConvexUrl } from "@/server/convex-client"
export const runtime = "nodejs" export const runtime = "nodejs"
@ -42,11 +42,6 @@ export async function GET(request: Request) {
return NextResponse.json({ items: [] }, { status: 401 }) 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 normalizedRole = normalizeRole(session.user.role)
const isAgentOrAdmin = normalizedRole === "admin" || normalizedRole === "agent" const isAgentOrAdmin = normalizedRole === "admin" || normalizedRole === "agent"
const canLinkOwnTickets = normalizedRole === "collaborator" const canLinkOwnTickets = normalizedRole === "collaborator"
@ -59,7 +54,7 @@ export async function GET(request: Request) {
const rawQuery = url.searchParams.get("q") ?? "" const rawQuery = url.searchParams.get("q") ?? ""
const query = rawQuery.trim() 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 // Garantir que o usuário exista no Convex para obter viewerId
let viewerId: string | null = null let viewerId: string | null = null

View file

@ -3922,7 +3922,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
) : null} ) : null}
</div> </div>
) : null} ) : null}
{entry.url ? ( {entry.url && !isRustDesk ? (
<a <a
href={entry.url} href={entry.url}
target="_blank" target="_blank"

View file

@ -4,6 +4,7 @@ const envSchema = z.object({
BETTER_AUTH_SECRET: z.string().min(1, "Missing BETTER_AUTH_SECRET"), BETTER_AUTH_SECRET: z.string().min(1, "Missing BETTER_AUTH_SECRET"),
BETTER_AUTH_URL: z.string().url().optional(), BETTER_AUTH_URL: z.string().url().optional(),
NEXT_PUBLIC_CONVEX_URL: z.string().url().optional(), NEXT_PUBLIC_CONVEX_URL: z.string().url().optional(),
CONVEX_INTERNAL_URL: z.string().url().optional(),
DATABASE_URL: z.string().min(1).optional(), DATABASE_URL: z.string().min(1).optional(),
NEXT_PUBLIC_APP_URL: z.string().url().optional(), NEXT_PUBLIC_APP_URL: z.string().url().optional(),
MACHINE_PROVISIONING_SECRET: z.string().optional(), MACHINE_PROVISIONING_SECRET: z.string().optional(),
@ -33,6 +34,7 @@ export const env = {
BETTER_AUTH_SECRET: parsed.data.BETTER_AUTH_SECRET, BETTER_AUTH_SECRET: parsed.data.BETTER_AUTH_SECRET,
BETTER_AUTH_URL: parsed.data.BETTER_AUTH_URL ?? parsed.data.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000", BETTER_AUTH_URL: parsed.data.BETTER_AUTH_URL ?? parsed.data.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000",
NEXT_PUBLIC_CONVEX_URL: parsed.data.NEXT_PUBLIC_CONVEX_URL, NEXT_PUBLIC_CONVEX_URL: parsed.data.NEXT_PUBLIC_CONVEX_URL,
CONVEX_INTERNAL_URL: parsed.data.CONVEX_INTERNAL_URL,
DATABASE_URL: parsed.data.DATABASE_URL, DATABASE_URL: parsed.data.DATABASE_URL,
NEXT_PUBLIC_APP_URL: parsed.data.NEXT_PUBLIC_APP_URL, NEXT_PUBLIC_APP_URL: parsed.data.NEXT_PUBLIC_APP_URL,
MACHINE_PROVISIONING_SECRET: parsed.data.MACHINE_PROVISIONING_SECRET, MACHINE_PROVISIONING_SECRET: parsed.data.MACHINE_PROVISIONING_SECRET,

View file

@ -9,8 +9,14 @@ export class ConvexConfigurationError extends Error {
} }
} }
function isServerSide() {
return typeof window === "undefined"
}
export function requireConvexUrl(): string { export function requireConvexUrl(): string {
const url = env.NEXT_PUBLIC_CONVEX_URL const url = isServerSide()
? env.CONVEX_INTERNAL_URL ?? env.NEXT_PUBLIC_CONVEX_URL
: env.NEXT_PUBLIC_CONVEX_URL
if (!url) { if (!url) {
throw new ConvexConfigurationError() throw new ConvexConfigurationError()
} }
@ -21,4 +27,3 @@ export function createConvexClient(): ConvexHttpClient {
const url = requireConvexUrl() const url = requireConvexUrl()
return new ConvexHttpClient(url) return new ConvexHttpClient(url)
} }

View file

@ -3,9 +3,9 @@ import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { DEFAULT_TENANT_ID } from "@/lib/constants" import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { env } from "@/lib/env"
import { ensureMachineAccount } from "@/server/machines-auth" import { ensureMachineAccount } from "@/server/machines-auth"
import { auth } from "@/lib/auth" import { auth } from "@/lib/auth"
import { requireConvexUrl } from "@/server/convex-client"
export type MachineSessionContext = { export type MachineSessionContext = {
machine: { machine: {
@ -38,11 +38,7 @@ export class MachineInactiveError extends Error {
} }
export async function createMachineSession(machineToken: string, rememberMe = true): Promise<MachineSessionContext> { export async function createMachineSession(machineToken: string, rememberMe = true): Promise<MachineSessionContext> {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL const convexUrl = requireConvexUrl()
if (!convexUrl) {
throw new Error("Convex não configurado")
}
const client = new ConvexHttpClient(convexUrl) const client = new ConvexHttpClient(convexUrl)
const resolved = await client.mutation(api.devices.resolveToken, { machineToken }) const resolved = await client.mutation(api.devices.resolveToken, { machineToken })

View file

@ -4,9 +4,9 @@ import { ConvexHttpClient } from "convex/browser"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
import { env } from "@/lib/env"
import { buildXlsxWorkbook } from "@/lib/xlsx" import { buildXlsxWorkbook } from "@/lib/xlsx"
import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions" import { REPORT_EXPORT_DEFINITIONS, type ReportExportKey } from "@/lib/report-definitions"
import { requireConvexUrl } from "@/server/convex-client"
export type { ReportExportKey } export type { ReportExportKey }
type ViewerIdentity = { type ViewerIdentity = {
@ -24,11 +24,7 @@ export type ConvexReportContext = {
} }
export async function createConvexContext(identity: ViewerIdentity): Promise<ConvexReportContext> { export async function createConvexContext(identity: ViewerIdentity): Promise<ConvexReportContext> {
const convexUrl = env.NEXT_PUBLIC_CONVEX_URL const client = new ConvexHttpClient(requireConvexUrl())
if (!convexUrl) {
throw new Error("Convex URL não configurada para exportações")
}
const client = new ConvexHttpClient(convexUrl)
const ensuredUser = await client.mutation(api.users.ensureUser, { const ensuredUser = await client.mutation(api.users.ensureUser, {
tenantId: identity.tenantId, tenantId: identity.tenantId,
name: identity.name, name: identity.name,

View file

@ -20,6 +20,8 @@ services:
# IMPORTANTE: "NEXT_PUBLIC_*" é consumida pelo navegador (cliente). Use a URL pública do Convex. # 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. # 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}" 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) # URLs públicas do app (evita fallback para localhost)
NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}" NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}"
BETTER_AUTH_URL: "${BETTER_AUTH_URL}" BETTER_AUTH_URL: "${BETTER_AUTH_URL}"