fix: tratar tokens de maquinas e alinhar stack/docs
All checks were successful
All checks were successful
This commit is contained in:
parent
b7e2c4cc98
commit
c030a3ac09
21 changed files with 309 additions and 36 deletions
22
agents.md
22
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.<novo> /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 <migration> && 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)._
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
10
docs/DEV.md
10
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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
54
docs/alteracoes-producao-2025-12-18.md
Normal file
54
docs/alteracoes-producao-2025-12-18.md
Normal file
|
|
@ -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 <pg16> pg_dump -Fc -d sistema_chamados -f /tmp/sistema_chamados_pg16_20251218215925.dump
|
||||
docker exec -u postgres <pg18> 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 <pg18> psql -c "DROP DATABASE IF EXISTS sistema_chamados;"
|
||||
docker exec -u postgres <pg18> psql -c "CREATE DATABASE sistema_chamados OWNER sistema;"
|
||||
docker cp /root/pg-backups/sistema_chamados_pg16_20251218215925.dump <pg18>:/tmp/sistema_chamados_restore.dump
|
||||
docker exec -u postgres <pg18> 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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" })
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -107,3 +117,4 @@ export async function POST(request: Request) {
|
|||
|
||||
return jsonWithCors({ error: "Formato de payload não suportado" }, 400, origin, CORS_METHODS)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
43
src/server/machines/token-errors.ts
Normal file
43
src/server/machines/token-errors.ts
Normal file
|
|
@ -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}`,
|
||||
}
|
||||
}
|
||||
12
stack.yml
12
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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue