From c030a3ac097f8a70f1d143940cadd34021e61d65 Mon Sep 17 00:00:00 2001 From: rever-tecnologia Date: Thu, 18 Dec 2025 18:20:35 -0300 Subject: [PATCH] fix: tratar tokens de maquinas e alinhar stack/docs --- agents.md | 22 ++++---- docs/DEPLOY-MANUAL.md | 4 +- docs/DEV.md | 10 ++-- docs/RETENTION-HEALTH.md | 4 +- docs/SETUP.md | 6 +-- docs/alteracoes-producao-2025-12-18.md | 54 +++++++++++++++++++ docs/convex-export-worker-loop.md | 36 ++++++++++++- .../machines/chat/attachments/url/route.ts | 11 ++++ src/app/api/machines/chat/messages/route.ts | 19 +++++++ src/app/api/machines/chat/poll/route.ts | 11 ++++ src/app/api/machines/chat/read/route.ts | 12 ++++- src/app/api/machines/chat/sessions/route.ts | 11 ++++ src/app/api/machines/chat/stream/route.ts | 15 +++++- src/app/api/machines/chat/upload/route.ts | 10 ++++ src/app/api/machines/heartbeat/route.ts | 10 ++++ src/app/api/machines/inventory/route.ts | 15 +++++- src/app/api/machines/remote-access/route.ts | 10 ++++ src/app/api/machines/sessions/route.ts | 11 ++++ src/app/api/machines/usb-policy/route.ts | 19 +++++++ src/server/machines/token-errors.ts | 43 +++++++++++++++ stack.yml | 12 ++--- 21 files changed, 309 insertions(+), 36 deletions(-) create mode 100644 docs/alteracoes-producao-2025-12-18.md create mode 100644 src/server/machines/token-errors.ts diff --git a/agents.md b/agents.md index b27a2d5..fcac12d 100644 --- a/agents.md +++ b/agents.md @@ -19,10 +19,10 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas - Seeds de usuários/tickets demo: `convex/seed.ts`. - Para DEV: rode `bun run convex:dev:bun` e acesse `/dev/seed` uma vez para popular dados realistas. -## Stack atual (06/11/2025) -- **Next.js**: `16.0.8` (Turbopack por padrão; webpack fica como fallback). +## Stack atual (18/12/2025) +- **Next.js**: `16.0.10` (Turbopack por padrão; webpack fica como fallback). - Whitelist de domínios em `src/config/allowed-hosts.ts` é aplicada pelo `middleware.ts`. -- **React / React DOM**: `19.2.0`. +- **React / React DOM**: `19.2.1`. - **Trilha de testes**: Vitest (`bun test`) sem modo watch por padrão (`--run --passWithNoTests`). - **CI**: workflow `Quality Checks` (`.github/workflows/quality-checks.yml`) roda `bun install`, `bun run prisma:generate`, `bun run lint`, `bun test`, `bun run build:bun`. Variáveis críticas (`BETTER_AUTH_SECRET`, `NEXT_PUBLIC_APP_URL`, etc.) são definidas apenas no runner — não afetam a VPS. - **Disciplina pós-mudanças**: sempre que fizer alterações locais, rode **obrigatoriamente** `bun run lint`, `bun run build:bun` e `bun test` antes de entregar ou abrir PR. Esses comandos são mandatórios também para os agentes/automations, garantindo que o projeto continua íntegro. @@ -38,7 +38,7 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas BETTER_AUTH_URL=http://localhost:3000 BETTER_AUTH_SECRET=dev-only-long-random-string NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 - DATABASE_URL=file:./prisma/db.dev.sqlite + DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados ``` 3. `bun run auth:seed` 4. (Opcional) `bun run queues:ensure` @@ -47,8 +47,8 @@ Os demais colaboradores reais são provisionados via **Convites & acessos**. Cas 7. Acesse `http://localhost:3000` e valide login com os usuários padrão. ### Banco de dados -- Local (DEV): `DATABASE_URL=file:./prisma/db.dev.sqlite` (guardado em `prisma/prisma/`). -- Produção: SQLite persistido no volume Swarm `sistema_sistema_db`. Migrations em PROD devem apontar para esse volume (ver `docs/DEPLOY-RUNBOOK.md`). +- Local (DEV): PostgreSQL local (ex.: `postgres:18`) com `DATABASE_URL=postgresql://postgres:dev@localhost:5432/sistema_chamados`. +- Produção: PostgreSQL no Swarm (serviço `postgres` em uso hoje; `postgres18` provisionado para migração). Migrations em PROD devem apontar para o `DATABASE_URL` ativo (ver `docs/OPERATIONS.md`). - Limpeza de legados: `node scripts/remove-legacy-demo-users.mjs` remove contas demo antigas (Cliente Demo, gestores fictícios etc.). ### Verificações antes de PR/deploy @@ -104,12 +104,12 @@ bun run build:bun ln -sfn /home/renan/apps/sistema.build. /home/renan/apps/sistema.current docker service update --force sistema_web ``` -- Resolver `P3009` (migration falhou) sempre no volume `sistema_sistema_db`: +- Resolver `P3009` (migration falhou) no PostgreSQL ativo: ```bash docker service scale sistema_web=0 - docker run --rm -it -e DATABASE_URL=file:/app/data/db.sqlite \ + docker run --rm -it --network traefik_public \ + --env-file /home/renan/apps/sistema.current/.env \ -v /home/renan/apps/sistema.current:/app \ - -v sistema_sistema_db:/app/data -w /app \ oven/bun:1 bash -lc "bun install --frozen-lockfile && bun x prisma migrate resolve --rolled-back && bun x prisma migrate deploy" docker service scale sistema_web=1 ``` @@ -164,7 +164,7 @@ bun run build:bun - **Docs complementares**: - `docs/DEV.md` — guia diário atualizado. - `docs/STATUS-2025-10-16.md` — snapshot do estado atual e backlog. - - `docs/DEPLOY-RUNBOOK.md` — runbook do Swarm. + - `docs/OPERATIONS.md` — runbook do Swarm. - `docs/admin-inventory-ui.md`, `docs/plano-app-desktop-maquinas.md` — detalhes do inventário/agente. ## Regras de Codigo @@ -211,4 +211,4 @@ Para manter acessibilidade em botoes apenas com icone, prefira usar `aria-label` ``` --- -_Última atualização: 15/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._ +_Última atualização: 18/12/2025 (Next.js 16, build padrão com Turbopack e fallback webpack documentado)._ diff --git a/docs/DEPLOY-MANUAL.md b/docs/DEPLOY-MANUAL.md index 4487d5c..bb9aa59 100644 --- a/docs/DEPLOY-MANUAL.md +++ b/docs/DEPLOY-MANUAL.md @@ -1,11 +1,11 @@ # Deploy Manual via VPS ## Acesso rápido -- Host: 31.220.78.20 +- Host: 154.12.253.40 - Usuário: root - Caminho do projeto: /srv/apps/sistema - Chave SSH (local): ./codex_ed25519 (chmod 600) -- Login: `ssh -i ./codex_ed25519 root@31.220.78.20` +- Login: `ssh -i ./codex_ed25519 root@154.12.253.40` ## Passo a passo resumido 1. Conectar na VPS usando o comando acima. diff --git a/docs/DEV.md b/docs/DEV.md index 2ca05f7..e0da9da 100644 --- a/docs/DEV.md +++ b/docs/DEV.md @@ -1,4 +1,4 @@ -# Guia de Desenvolvimento — 18/10/2025 +# Guia de Desenvolvimento — 18/12/2025 Este documento consolida o estado atual do ambiente de desenvolvimento, descreve como rodar lint/test/build localmente (e no CI) e registra erros recorrentes com as respectivas soluções. @@ -6,7 +6,7 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve - **Bun (runtime padrão)**: 1.3+ já instalado no runner e VPS (`bun --version`). Após instalar localmente, exporte `PATH="$HOME/.bun/bin:$PATH"` para tornar o binário disponível. Use `bun install`, `bun run dev:bun`, `bun run convex:dev:bun`, `bun run build:bun` e `bun test` como fluxo principal (scripts Node continuam disponíveis como fallback). - **Node.js**: mantenha a versão 20.9+ instalada para ferramentas auxiliares (Prisma CLI, scripts legados em Node) quando não estiver usando o runtime do Bun. -- **Next.js 16**: Projeto roda em `next@16.0.8` com Turbopack como bundler padrão (dev e build); webpack continua disponível como fallback. +- **Next.js 16**: Projeto roda em `next@16.0.10` com Turbopack como bundler padrão (dev e build); webpack continua disponível como fallback. - **Lint/Test/Build**: `bun run lint`, `bun test`, `bun run build:bun`. O test runner do Bun já roda em modo não interativo; utilize `bunx vitest --watch` apenas quando precisar do modo watch manualmente. - **Banco DEV**: PostgreSQL local (Docker recomendado). Defina `DATABASE_URL` apontando para seu PostgreSQL. - **Desktop (Tauri)**: fonte em `apps/desktop`. Usa Radix tabs + componentes shadcn-like, integra com os endpoints `/api/machines/*` e suporta atualização automática via GitHub Releases. @@ -47,7 +47,7 @@ Este documento consolida o estado atual do ambiente de desenvolvimento, descreve ## Next.js 16 (estável) -- Mantemos o projeto em `next@16.0.8`, com React 19 e o App Router completo. +- Mantemos o projeto em `next@16.0.10`, com React 19 e o App Router completo. - **Bundlers**: Turbopack permanece habilitado no `next dev`/`bun run dev:bun` e agora também no `next build --turbopack`. Use `next build --webpack` somente para reproduzir bugs ou comparar saídas. - **Whitelist de hosts**: o release estável continua sem aceitar `server.allowedHosts` (vide [`invalid-next-config`](https://nextjs.org/docs/messages/invalid-next-config)), portanto bloqueamos domínios exclusivamente via `middleware.ts`. @@ -200,8 +200,8 @@ PY ## Referências úteis -- **Deploy (Swarm)**: veja `docs/DEPLOY-RUNBOOK.md`. -- **Plano do agente desktop / heartbeat**: `docs/plano-app-desktop-maquinas.md`. +- **Deploy (Swarm)**: veja `docs/OPERATIONS.md`. +- **Plano do agente desktop / heartbeat**: `docs/archive/plano-app-desktop-dispositivos.md`. - **Histórico de incidentes**: `docs/historico-agente-desktop-2025-10-10.md`. > Última revisão: 18/10/2025. Atualize este guia sempre que o fluxo de DEV ou automações mudarem. diff --git a/docs/RETENTION-HEALTH.md b/docs/RETENTION-HEALTH.md index 4bcd947..4f8d198 100644 --- a/docs/RETENTION-HEALTH.md +++ b/docs/RETENTION-HEALTH.md @@ -18,7 +18,7 @@ Estrategia: nenhuma limpeza automatica ligada. Usamos apenas monitoramento e, se - Export/backup local de tickets: endpoint `POST /api/admin/tickets/archive-local` (staff) grava tickets resolvidos mais antigos que N dias em JSONL dentro de `ARCHIVE_DIR` (padrão `./archives`). Usa `exportResolvedTicketsToDisk` com segredo interno (`INTERNAL_HEALTH_TOKEN`/`REPORTS_CRON_SECRET`). ## Como acessar tickets antigos sem perda -- Base quente: Prisma (SQLite) guarda todos os tickets; nenhuma rotina remove ou trunca tickets. +- Base quente: Prisma (PostgreSQL) guarda todos os tickets; nenhuma rotina remove ou trunca tickets. - Se um dia for preciso offload (ex.: >50k tickets): - Exportar em lotes (ex.: JSONL mensais) para storage frio (S3/compat). - Gravar um marcador de offload no DB quente (ex.: `ticket_archived_at`, `archive_key`). @@ -33,7 +33,7 @@ Estrategia: nenhuma limpeza automatica ligada. Usamos apenas monitoramento e, se ## Checks operacionais sugeridos (manuais) - Tamanho do banco do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "ls -lh /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3"` - Memoria do Convex: `ssh -i ~/.ssh/codex_ed25519 root@154.12.253.40 "docker stats --no-stream | grep convex"` -- Alvos: <100-200 MB para o SQLite e <5 GB de RAM. Acima disso, abrir janela curta, fazer backup e avaliar limpeza ou arquivamento pontual. +- Alvos: <100-200 MB para o SQLite do Convex e <5 GB de RAM. Acima disso, abrir janela curta, fazer backup e avaliar limpeza ou arquivamento pontual. ## Estado atual e proximos passos - Cron de limpeza segue desativado. Prioridade: monitorar 2-4 semanas para validar estabilidade pos-correcoes. diff --git a/docs/SETUP.md b/docs/SETUP.md index c11db0b..02452c9 100644 --- a/docs/SETUP.md +++ b/docs/SETUP.md @@ -121,7 +121,7 @@ docker run -d \ -p 5432:5432 \ -e POSTGRES_PASSWORD=dev \ -e POSTGRES_DB=sistema_chamados \ - postgres:16 + postgres:18 # Criar arquivo .env cp .env.example .env @@ -230,7 +230,7 @@ docker start postgres-dev # Ou recriar docker rm -f postgres-dev -docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:16 +docker run -d --name postgres-dev -p 5432:5432 -e POSTGRES_PASSWORD=dev -e POSTGRES_DB=sistema_chamados postgres:18 ``` ## Convex (Backend de Tempo Real) @@ -248,5 +248,5 @@ bun run dev:bun ## Mais Informacoes - **Desenvolvimento detalhado:** `docs/DEV.md` -- **Deploy e operacoes:** `docs/DEPLOY-RUNBOOK.md` +- **Deploy e operacoes:** `docs/OPERATIONS.md` - **CI/CD Forgejo:** `docs/FORGEJO-CI-CD.md` diff --git a/docs/alteracoes-producao-2025-12-18.md b/docs/alteracoes-producao-2025-12-18.md new file mode 100644 index 0000000..68a0371 --- /dev/null +++ b/docs/alteracoes-producao-2025-12-18.md @@ -0,0 +1,54 @@ +# Alteracoes de producao - 2025-12-18 + +Este documento registra as mudancas aplicadas na VPS para estabilizar o ambiente e padronizar o uso do PostgreSQL 18. + +## Resumo +- Migracao do banco principal do sistema para o servico `postgres18`. +- Desativacao do servico `postgres` (pg16) no Swarm. +- Convex backend fixado na tag `ghcr.io/get-convex/convex-backend:6690a911bced1e5e516eafc0409a7239fb6541bb`. +- `CONVEX_INTERNAL_URL` ajustado para o endpoint publico, evitando falhas de DNS interno (`ENOTFOUND sistema_convex_backend`). +- Tratamento explicito para tokens revogados/expirados/invalidos nas rotas `/api/machines/*` e chat. +- Limpeza de documento legado no Convex (`liveChatSessions` id `pd71bvfbxx7th3npdj519hcf3s7xbe2j`). + +## Backups gerados +- `/root/pg-backups/sistema_chamados_pg16_20251218215925.dump` +- `/root/pg-backups/sistema_chamados_pg18_20251218215925.dump` +- Convex: `/var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3.backup-20251218165717` +- Observacao: foi gerado um arquivo extra `db.sqlite3.backup-` (sem timestamp) por comando incorreto. + +## Procedimento (principais comandos) +``` +# 1) Backup dos bancos +docker exec -u postgres pg_dump -Fc -d sistema_chamados -f /tmp/sistema_chamados_pg16_20251218215925.dump +docker exec -u postgres pg_dump -Fc -d sistema_chamados -f /tmp/sistema_chamados_pg18_20251218215925.dump + +# 2) Parar o web durante a migracao +docker service scale sistema_web=0 + +# 3) Restaurar dump do pg16 no pg18 +docker exec -u postgres psql -c "DROP DATABASE IF EXISTS sistema_chamados;" +docker exec -u postgres psql -c "CREATE DATABASE sistema_chamados OWNER sistema;" +docker cp /root/pg-backups/sistema_chamados_pg16_20251218215925.dump :/tmp/sistema_chamados_restore.dump +docker exec -u postgres pg_restore -d sistema_chamados -Fc /tmp/sistema_chamados_restore.dump + +# 4) Atualizar stack (com variaveis exportadas) +set -a; . /srv/apps/sistema/.env; set +a +docker stack deploy --with-registry-auth -c /srv/apps/sistema/stack.yml sistema + +# 5) Desativar pg16 +docker service scale postgres=0 +``` + +## Ajustes em stack.yml +- `DATABASE_URL` apontando para `postgres18:5432`. +- `CONVEX_INTERNAL_URL` apontando para `https://convex.esdrasrenan.com.br`. +- Imagem do Convex ajustada para a tag acima. + +## Resultado +- `sistema_web` voltou com 2 replicas saudaveis. +- `sistema_convex_backend` rodando na tag informada. +- `postgres` (pg16) desativado no Swarm. +- Healthcheck OK: `GET /api/health` e `GET /version`. + +## Observacoes operacionais +- O deploy do stack precisa de variaveis exportadas do `.env`. Sem isso, `NEXT_PUBLIC_*` fica vazio e o `POSTGRES_PASSWORD` nao e propagado, causando `P1000` no Prisma. diff --git a/docs/convex-export-worker-loop.md b/docs/convex-export-worker-loop.md index a9f3f5b..5812685 100644 --- a/docs/convex-export-worker-loop.md +++ b/docs/convex-export-worker-loop.md @@ -112,7 +112,39 @@ Critérios de sucesso: --- -## 6. Referências rápidas +## 6. Registro de alterações manuais + +### 2025-12-18 — liveChatSessions com versão legada (shape_inference) + +Motivo: logs do Convex mostravam `shape_inference` recorrente apontando para o documento +`pd71bvfbxx7th3npdj519hcf3s7xbe2j` (sessão de chat antiga com status `ACTIVE` em versão histórica). + +Comandos executados: + +```bash +# 1) Parar Convex +docker service scale sistema_convex_backend=0 + +# 2) Backup +cp /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3 \ + /var/lib/docker/volumes/sistema_convex_data/_data/db.sqlite3.backup-20251218165717 + +# 3) Remover versões antigas do documento (mantendo a mais recente) +docker run --rm -v sistema_convex_data:/convex/data nouchka/sqlite3 /convex/data/db.sqlite3 \ + "DELETE FROM documents \ + WHERE json_extract(json_value, '$._id') = 'pd71bvfbxx7th3npdj519hcf3s7xbe2j' \ + AND ts < (SELECT MAX(ts) FROM documents \ + WHERE json_extract(json_value, '$._id') = 'pd71bvfbxx7th3npdj519hcf3s7xbe2j');" + +# 4) Subir Convex +docker service scale sistema_convex_backend=1 +``` + +Resultado: versões antigas do documento foram removidas e os erros de `shape_inference` pararam após o restart. + +--- + +## 7. Referências rápidas - Volume Convex: `sistema_convex_data` - Banco: `/convex/data/db.sqlite3` @@ -122,4 +154,4 @@ Critérios de sucesso: --- -Última revisão: **18/11/2025** — sanado por remoção dos registros incompatíveis e rerun bem-sucedido do export `gg20vw5b479d9a2jprjpe3pxg57vk9wa`. +Última revisão: **18/12/2025** — limpeza da versão legada de `liveChatSessions` (`pd71bvfbxx7th3npdj519hcf3s7xbe2j`) e restart do Convex. diff --git a/src/app/api/machines/chat/attachments/url/route.ts b/src/app/api/machines/chat/attachments/url/route.ts index d94d458..ed7feb3 100644 --- a/src/app/api/machines/chat/attachments/url/route.ts +++ b/src/app/api/machines/chat/attachments/url/route.ts @@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" const attachmentUrlSchema = z.object({ @@ -87,6 +88,16 @@ export async function POST(request: Request) { return jsonWithCors({ url }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS, + rateLimitHeaders(rateLimit) + ) + } console.error("[machines.chat.attachments.url] Falha ao obter URL de anexo", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao obter URL de anexo", details }, 500, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) diff --git a/src/app/api/machines/chat/messages/route.ts b/src/app/api/machines/chat/messages/route.ts index e431795..74019d5 100644 --- a/src/app/api/machines/chat/messages/route.ts +++ b/src/app/api/machines/chat/messages/route.ts @@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" import { withRetry } from "@/server/retry" @@ -115,6 +116,15 @@ export async function POST(request: Request) { }) return jsonWithCors(result, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.chat.messages] Falha ao listar mensagens", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao listar mensagens", details }, 500, origin, CORS_METHODS) @@ -159,6 +169,15 @@ export async function POST(request: Request) { ) return jsonWithCors(result, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.chat.messages] Falha ao enviar mensagem", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao enviar mensagem", details }, 500, origin, CORS_METHODS) diff --git a/src/app/api/machines/chat/poll/route.ts b/src/app/api/machines/chat/poll/route.ts index c3e009c..f84bfde 100644 --- a/src/app/api/machines/chat/poll/route.ts +++ b/src/app/api/machines/chat/poll/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" const pollSchema = z.object({ @@ -68,6 +69,16 @@ export async function POST(request: Request) { }) return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS, + rateLimitHeaders(rateLimit) + ) + } console.error("[machines.chat.poll] Falha ao verificar atualizacoes", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao verificar atualizacoes", details }, 500, origin, CORS_METHODS) diff --git a/src/app/api/machines/chat/read/route.ts b/src/app/api/machines/chat/read/route.ts index d31ba72..fdc6fa0 100644 --- a/src/app/api/machines/chat/read/route.ts +++ b/src/app/api/machines/chat/read/route.ts @@ -4,6 +4,7 @@ import { api } from "@/convex/_generated/api" import type { Id } from "@/convex/_generated/dataModel" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" const readSchema = z.object({ @@ -69,9 +70,18 @@ export async function POST(request: Request) { }) return jsonWithCors(result, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS, + rateLimitHeaders(rateLimit) + ) + } console.error("[machines.chat.read] Falha ao marcar mensagens como lidas", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao marcar mensagens como lidas", details }, 500, origin, CORS_METHODS) } } - diff --git a/src/app/api/machines/chat/sessions/route.ts b/src/app/api/machines/chat/sessions/route.ts index 431bcc2..0ab0474 100644 --- a/src/app/api/machines/chat/sessions/route.ts +++ b/src/app/api/machines/chat/sessions/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { checkRateLimit, RATE_LIMITS, rateLimitHeaders } from "@/server/rate-limit" const sessionsSchema = z.object({ @@ -66,6 +67,16 @@ export async function POST(request: Request) { }) return jsonWithCors({ sessions }, 200, origin, CORS_METHODS, rateLimitHeaders(rateLimit)) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS, + rateLimitHeaders(rateLimit) + ) + } console.error("[machines.chat.sessions] Falha ao listar sessoes", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao listar sessoes", details }, 500, origin, CORS_METHODS) diff --git a/src/app/api/machines/chat/stream/route.ts b/src/app/api/machines/chat/stream/route.ts index e86a01d..183528a 100644 --- a/src/app/api/machines/chat/stream/route.ts +++ b/src/app/api/machines/chat/stream/route.ts @@ -1,6 +1,7 @@ import { api } from "@/convex/_generated/api" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" import { resolveCorsOrigin } from "@/server/cors" +import { resolveMachineTokenError } from "@/server/machines/token-errors" export const runtime = "nodejs" export const dynamic = "force-dynamic" @@ -45,9 +46,10 @@ export async function GET(request: Request) { try { await client.query(api.liveChat.checkMachineUpdates, { machineToken: token }) } catch (error) { - const message = error instanceof Error ? error.message : "Token invalido" + const tokenError = resolveMachineTokenError(error) + const message = tokenError?.message ?? (error instanceof Error ? error.message : "Token invalido") return new Response(message, { - status: 401, + status: tokenError?.status ?? 401, headers: { "Access-Control-Allow-Origin": resolvedOrigin, "Access-Control-Allow-Credentials": resolvedOrigin !== "*" ? "true" : "false", @@ -110,6 +112,15 @@ export async function GET(request: Request) { previousState = currentState } } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + sendEvent("error", { code: tokenError.code, message: tokenError.message }) + isAborted = true + clearInterval(pollInterval) + clearInterval(heartbeatInterval) + controller.close() + return + } console.error("[SSE] Poll error:", error) // Enviar erro e fechar conexao sendEvent("error", { message: "Poll failed" }) diff --git a/src/app/api/machines/chat/upload/route.ts b/src/app/api/machines/chat/upload/route.ts index 8a231be..1055c5a 100644 --- a/src/app/api/machines/chat/upload/route.ts +++ b/src/app/api/machines/chat/upload/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" const uploadUrlSchema = z.object({ machineToken: z.string().min(1), @@ -60,6 +61,15 @@ export async function POST(request: Request) { }) return jsonWithCors(result, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.chat.upload] Falha ao gerar URL de upload", error) const details = error instanceof Error ? error.message : String(error) diff --git a/src/app/api/machines/heartbeat/route.ts b/src/app/api/machines/heartbeat/route.ts index cb520ee..068f37b 100644 --- a/src/app/api/machines/heartbeat/route.ts +++ b/src/app/api/machines/heartbeat/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" const heartbeatSchema = z.object({ machineToken: z.string().min(1), @@ -59,6 +60,15 @@ export async function POST(request: Request) { const response = await client.mutation(api.devices.heartbeat, payload) return jsonWithCors(response, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.heartbeat] Falha ao registrar heartbeat", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao registrar heartbeat", details }, 500, origin, CORS_METHODS) diff --git a/src/app/api/machines/inventory/route.ts b/src/app/api/machines/inventory/route.ts index 2ae6fb1..11ac5f9 100644 --- a/src/app/api/machines/inventory/route.ts +++ b/src/app/api/machines/inventory/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" const tokenModeSchema = z.object({ machineToken: z.string().min(1), @@ -77,6 +78,15 @@ export async function POST(request: Request) { }) return jsonWithCors({ ok: true, machineId: result.machineId, expiresAt: result.expiresAt }, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.inventory:token] Falha ao atualizar inventário", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao atualizar inventário", details }, 500, origin, CORS_METHODS) @@ -94,8 +104,8 @@ export async function POST(request: Request) { macAddresses: provParsed.data.macAddresses, serialNumbers: provParsed.data.serialNumbers, inventory: provParsed.data.inventory, - metrics: provParsed.data.metrics, - registeredBy: provParsed.data.registeredBy ?? "agent:inventory", + metrics: provParsed.data.metrics, + registeredBy: provParsed.data.registeredBy ?? "agent:inventory", }) return jsonWithCors({ ok: true, machineId: result.machineId, status: result.status }, 200, origin, CORS_METHODS) } catch (error) { @@ -107,3 +117,4 @@ export async function POST(request: Request) { return jsonWithCors({ error: "Formato de payload não suportado" }, 400, origin, CORS_METHODS) } + diff --git a/src/app/api/machines/remote-access/route.ts b/src/app/api/machines/remote-access/route.ts index c5718c1..a0027af 100644 --- a/src/app/api/machines/remote-access/route.ts +++ b/src/app/api/machines/remote-access/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" export const runtime = "nodejs" @@ -54,6 +55,15 @@ export async function POST(request: Request) { const response = await client.mutation(api.devices.upsertRemoteAccessViaToken, payload) return jsonWithCors({ ok: true, remoteAccess: response?.remoteAccess ?? null }, 200, origin, METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + METHODS + ) + } console.error("[machines.remote-access:token] Falha ao registrar acesso remoto", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao registrar acesso remoto", details }, 500, origin, METHODS) diff --git a/src/app/api/machines/sessions/route.ts b/src/app/api/machines/sessions/route.ts index ce30382..d97bc18 100644 --- a/src/app/api/machines/sessions/route.ts +++ b/src/app/api/machines/sessions/route.ts @@ -2,6 +2,7 @@ import { NextResponse } from "next/server" import { z } from "zod" import { createMachineSession, MachineInactiveError } from "@/server/machines-session" import { applyCorsHeaders, createCorsPreflight, jsonWithCors } from "@/server/cors" +import { resolveMachineTokenError } from "@/server/machines/token-errors" import { MACHINE_CTX_COOKIE, serializeMachineCookie, @@ -133,7 +134,17 @@ export async function POST(request: Request) { CORS_METHODS ) } + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.sessions] Falha ao criar sessão", error) return jsonWithCors({ error: "Falha ao autenticar dispositivo" }, 500, origin, CORS_METHODS) } } + diff --git a/src/app/api/machines/usb-policy/route.ts b/src/app/api/machines/usb-policy/route.ts index 2f9b7e6..9d8be18 100644 --- a/src/app/api/machines/usb-policy/route.ts +++ b/src/app/api/machines/usb-policy/route.ts @@ -3,6 +3,7 @@ import { z } from "zod" import { api } from "@/convex/_generated/api" import { createCorsPreflight, jsonWithCors } from "@/server/cors" import { createConvexClient, ConvexConfigurationError } from "@/server/convex-client" +import { resolveMachineTokenError } from "@/server/machines/token-errors" const getPolicySchema = z.object({ machineToken: z.string().min(1), @@ -54,6 +55,15 @@ export async function GET(request: Request) { appliedAt: pendingPolicy.appliedAt, }, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.usb-policy] Falha ao buscar politica USB", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao buscar politica USB", details }, 500, origin, CORS_METHODS) @@ -90,6 +100,15 @@ export async function POST(request: Request) { const response = await client.mutation(api.usbPolicy.reportUsbPolicyStatus, payload) return jsonWithCors(response, 200, origin, CORS_METHODS) } catch (error) { + const tokenError = resolveMachineTokenError(error) + if (tokenError) { + return jsonWithCors( + { error: tokenError.message, code: tokenError.code }, + tokenError.status, + origin, + CORS_METHODS + ) + } console.error("[machines.usb-policy] Falha ao reportar status de politica USB", error) const details = error instanceof Error ? error.message : String(error) return jsonWithCors({ error: "Falha ao reportar status", details }, 500, origin, CORS_METHODS) diff --git a/src/server/machines/token-errors.ts b/src/server/machines/token-errors.ts new file mode 100644 index 0000000..a7d966b --- /dev/null +++ b/src/server/machines/token-errors.ts @@ -0,0 +1,43 @@ +type MachineTokenErrorKind = "invalid" | "expired" | "revoked" + +type MachineTokenErrorMatch = { + match: string + kind: MachineTokenErrorKind +} + +const MACHINE_TOKEN_ERROR_MATCHES: MachineTokenErrorMatch[] = [ + { match: "Token de dispositivo inválido", kind: "invalid" }, + { match: "Token de dispositivo invalido", kind: "invalid" }, + { match: "Token de dispositivo expirado", kind: "expired" }, + { match: "Token de dispositivo revogado", kind: "revoked" }, + { match: "Token de maquina invalido ou revogado", kind: "revoked" }, + { match: "Token de máquina inválido", kind: "invalid" }, + { match: "Token de maquina invalido", kind: "invalid" }, + { match: "Token de máquina expirado", kind: "expired" }, + { match: "Token de maquina expirado", kind: "expired" }, + { match: "Token de máquina revogado", kind: "revoked" }, + { match: "Token de maquina revogado", kind: "revoked" }, +] + +export type MachineTokenErrorInfo = { + kind: MachineTokenErrorKind + message: string + status: number + code: string +} + +export function resolveMachineTokenError(error: unknown): MachineTokenErrorInfo | null { + const message = error instanceof Error ? error.message : String(error) + const match = MACHINE_TOKEN_ERROR_MATCHES.find((entry) => message.includes(entry.match)) + if (!match) { + return null + } + + const status = match.kind === "revoked" ? 403 : 401 + return { + kind: match.kind, + message: match.match, + status, + code: `machine_token_${match.kind}`, + } +} diff --git a/stack.yml b/stack.yml index f965133..019b141 100644 --- a/stack.yml +++ b/stack.yml @@ -21,18 +21,18 @@ 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://sistema_convex_backend:3210" + # URLs consumidas apenas pelo backend/SSR usam o endpoint publico para evitar falhas de DNS interno + CONVEX_INTERNAL_URL: "https://convex.esdrasrenan.com.br" # URLs públicas do app (evita fallback para localhost) NEXT_PUBLIC_APP_URL: "${NEXT_PUBLIC_APP_URL}" BETTER_AUTH_URL: "${BETTER_AUTH_URL}" BETTER_AUTH_SECRET: "${BETTER_AUTH_SECRET}" REPORTS_CRON_SECRET: "${REPORTS_CRON_SECRET}" REPORTS_CRON_BASE_URL: "${REPORTS_CRON_BASE_URL}" - # PostgreSQL connection string (usa o servico 'postgres' existente na rede traefik_public) + # PostgreSQL connection string (usa o servico 'postgres18' existente na rede traefik_public) # connection_limit: maximo de conexoes por replica (2 replicas x 10 = 20 conexoes) # pool_timeout: tempo maximo para aguardar conexao disponivel - DATABASE_URL: "postgresql://${POSTGRES_USER:-sistema}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-sistema_chamados}?connection_limit=10&pool_timeout=10" + DATABASE_URL: "postgresql://${POSTGRES_USER:-sistema}:${POSTGRES_PASSWORD}@postgres18:5432/${POSTGRES_DB:-sistema_chamados}?connection_limit=10&pool_timeout=10" # Evita apt-get na inicialização porque a imagem já vem com toolchain pronta SKIP_APT_BOOTSTRAP: "true" # Usado para forçar novo rollout a cada deploy (setado pelo CI) @@ -87,12 +87,12 @@ services: # O novo container só entra em serviço APÓS passar no healthcheck start_period: 180s - # PostgreSQL: usando o servico 'postgres' existente na rede traefik_public + # PostgreSQL: usando o servico 'postgres18' existente na rede traefik_public # Nao e necessario definir aqui pois ja existe um servico global convex_backend: # Versao estavel - crons movidos para /api/cron/* chamados via crontab do Linux - image: ghcr.io/get-convex/convex-backend:precompiled-2025-12-04-cc6af4c + image: ghcr.io/get-convex/convex-backend:6690a911bced1e5e516eafc0409a7239fb6541bb stop_grace_period: 10s stop_signal: SIGINT volumes: