Arquitetura

O TermoPortal segue o mesmo padrão dos outros sistemas da Termotubos: backend em Rust organizado por feature, com as mesmas 7 operações em toda feature, e frontend em SvelteKit. A regra de negócio, a autorização, o escopo por empresa, a sessão (cookie httpOnly) e os arquivos enviados ficam no Rust; o frontend é um site estático, sem servidor, que chama a API direto do navegador.

Stack

CamadaTecnologia
BackendRust, Axum 0.8 e Tokio
BancoPostgreSQL com SQLx 0.8 (macros checadas em tempo de compilação e migrations), no schema termoportal
FrontendSvelteKit com Svelte 5, TypeScript, bits-ui e Tailwind CSS 4, site estático (adapter-static, SPA) no Cloudflare Pages
AutenticaçãoTermoAuth, o login central da Termotubos (app portal-b2b), com JWT validado offline pelo JWKS
Catálogo e cadastros externosAPI Termotubos (produtos, clientes do Tiny e colaboradores do RH)
Atendimento humanoChatwoot, pela Application API

Como uma requisição anda

navegador (site estático, portal.termotubos.com.br) ──► API Rust (api-portal.termotubos.com.br) ──► PostgreSQL (schema termoportal)
                                                        cookie httpOnly "sessao" + CORS
text
  • O navegador chama a API direto (endereço em PUBLIC_API_URL). A sessão é o cookie httpOnly sessao, gravado pela API no login (POST /sessao) e mandado em toda chamada; o JavaScript nunca vê o token.
  • Site e API no mesmo site (*.termotubos.com.br): o cookie é SameSite=Lax, a API só responde via CORS às origens de FRONTEND_URL e recusa escrita pelo cookie sem Origin do frontend (CSRF).
  • A API continua ouvindo em 127.0.0.1; a exposição para o navegador (proxy ou túnel) é da infra.
  • Arquivos enviados (imagens de produto, PDFs de boletim, anexos de demanda) ficam no disco da API, na pasta UPLOAD_DIR, e são servidos em /arquivo/... (com sessão). Os boletins técnicos também abrem por /boletins/<slug> no portal, que redireciona para a API.

Organização do repositório

├── backend/
│   ├── main.rs
│   ├── router/
│   │   └── cotacao.rs
│   ├── controller/
│   │   ├── cotacao/
│   │   │   ├── criar.rs
│   │   │   ├── buscarPeloId.rs
│   │   │   ├── buscarPeloFiltro.rs
│   │   │   ├── buscarPaginador.rs
│   │   │   ├── buscarAutoComplete.rs
│   │   │   ├── atualizarPeloId.rs
│   │   │   └── deletarPeloId.rs
│   │   └── sessao/
│   │       └── exigirSessao.rs
│   ├── model/
│   │   ├── cotacao/
│   │   └── sessao/
│   ├── integrations/
│   │   ├── termoauth/
│   │   ├── termotubos/
│   │   └── chatwoot/
│   ├── migrations/
│   └── seeds/
├── frontend/
│   ├── src/hooks.client.ts
│   ├── src/lib/
│   │   ├── api/client.ts
│   │   └── auth/session.ts
│   └── src/routes/
│       ├── (portal)/
│       ├── login/
│       ├── app/
│       └── convite/
├── docs/
│   └── projeto/
└── .github/workflows/
txt
Mostrar código completo

Backend: 7 operações por feature

Cada feature tem um arquivo em router/, uma pasta em controller/ com um handler por operação e uma pasta em model/, que é a única camada que fala com o banco.

OperaçãoArquivoHTTP
CriarcriarPOST
Buscar por idbuscarPeloIdGET /{id}
Buscar por filtrobuscarPeloFiltroGET /filtro
Listar paginadobuscarPaginadorGET
AutocompletebuscarAutoCompleteGET /autocomplete
Atualizar por idatualizarPeloIdPATCH /{id}
Deletar por iddeletarPeloIdDELETE /{id}

Features que não são cadastros expõem só o que faz sentido. Por exemplo, sessao tem POST /sessao (entrar) e GET/DELETE /sessao/atual; produto_termotubos tem só a busca na API e a importação; chatwoot_entrega_webhook tem só o POST que o Chatwoot chama.

As 43 features do backend
GrupoFeatures
Acessosessao, convite_acesso, perfil, pessoa, pessoa_email, empresa, grupo_cliente, endereco, contato
Integração de cadastrosempresa_tiny, colaborador_rh, colaborador_termotubos
Catálogoproduto, produto_termotubos, produto_conflito_sincronizacao, marca, departamento, categoria, catalogo_arvore_no, catalogo_atributo, boletim_tecnico, grupo_atacado, kit, especificacao, compre_junto, preco_oculto, estoque_forcado, alerta_estoque, avaliacao, favorito
Vendacotacao, frete, cupom, cupom_uso, configuracao
Pós-vendapedido, fatura
Atendimentodemanda, atendente_duvida, chatwoot_caixa_entrada, chatwoot_conversa, chatwoot_mensagem, chatwoot_entrega_webhook

Banco de dados

  • PostgreSQL com SQLx, sem ORM: a fonte da verdade é o banco, e o histórico dele são as migrations em backend/migrations/. O backend aplica as migrations sozinho ao subir.
  • Schema próprio. Tudo do portal, inclusive a tabela _sqlx_migrations, fica no schema termoportal (constante ESQUEMA_BANCO em main.rs). O backend cria o schema se ele não existir e conecta com search_path só nele, porque os apps da Termotubos podem dividir o mesmo banco, como acontece na sandbox.
  • Exclusão lógica. Toda tabela tem deletado boolean NOT NULL DEFAULT false, os índices únicos são parciais (WHERE deletado = false) e as leituras filtram os deletados.
  • Dinheiro e quantidades fracionárias são numeric. Enumerações viram text com CHECK. Dados aninhados, como os itens de uma cotação, são jsonb.
  • Snapshots. Cotação, pedido e fatura guardam uma cópia dos dados do momento (itens com preço, frete aplicado, cupom), para que mudanças posteriores no catálogo não reescrevam o passado.

Autenticação e autorização

O portal não guarda senha. O login é do TermoAuth; o portal só troca o JWT dele por uma sessão local.

  1. Validação do JWT offline em integrations/termoauth/: algoritmo EdDSA, chave pública buscada pelo kid no JWKS de TERMOAUTH_API_URL (/.well-known/jwks.json), iss e aud iguais a TERMOAUTH_URL, e exp. As chaves ficam em cache; um kid desconhecido só dispara nova busca depois de 30 segundos da anterior.
  2. Sessão local. POST /sessao acha a pessoa pelo sub do token (pessoa.identificador_termoauth) e grava na tabela sessao só o resumo SHA-256 de um token opaco, válido por 5 dias.
  3. Middleware único. controller/sessao/exigirSessao.rs roda em toda rota. Ele consulta o mapa requisito() de model/sessao/mod.rs, que decide por feature e método HTTP o que a rota exige, e injeta o Contexto da sessão (pessoa, se é interna, se é administradora, permissões por módulo, permissões administrativas e empresas).
RequisitoQuem passa
PublicoQualquer um. Só POST /sessao e POST /chatwoot_entrega_webhook
SessaoQualquer pessoa logada
Modulo(modulo, acao)Quem tem a ação (ver, criar, editar, excluir) no módulo, pelo perfil
Administrativa(permissao)Quem tem a permissão administrativa, como usuarios.gerenciar
InternoSó colaborador Termotubos
InternoComPermissao(permissao)Colaborador Termotubos com a permissão administrativa
AdministradorSó perfil master ou administrador
NegadoNinguém. É o padrão de toda rota sem entrada no mapa

O escopo por empresa e por pessoa fica nos controllers: listagem de cliente exige uma empresa das dele, registro de outra empresa responde 404, e o autor de uma cotação ou demanda criada por cliente é sempre quem está logado. O controle por página no frontend só mostra e esconde telas; nunca é a barreira real. O fluxo completo está em Entrada e acesso .

Integrações

IntegraçãoPastaO que o portal usa
TermoAuthintegrations/termoauth/buscarChaves (JWKS) e validarToken. O frontend leva a pessoa para /handoff, /sign-out e /settings do TermoAuth, sempre com ?app=portal-b2b
API Termotubosintegrations/termotubos/GET /product/search para buscar e importar produtos (buscarProdutos, buscarProdutosPorPrefixo, buscarProdutoPeloSku); GET /customer/contact/search para verificar um CPF ou CNPJ no Tiny (buscarClienteTiny); GET /hr/user-profile/search e GET /hr/user-profile/{id} para colaboradores do RH (buscarColaboradores, buscarColaboradorPeloId)
Chatwootintegrations/chatwoot/criarContato, criarConversa, enviarMensagem (com anexos), resolverConversa e buscarCaixaEntrada. O retorno do Chatwoot chega por webhook assinado
O portal não fala com o Tiny diretamente

A verificação de cliente no Tiny passa pela API Termotubos (/customer/contact/search). Pedidos e faturas não têm integração automática hoje: são registros gravados no próprio portal.

Variáveis de ambiente

OndeVariávelPara quê
BackendDATABASE_URLObrigatória. Conexão com o PostgreSQL
BackendPORT, HOSTPorta e interface da API (padrão 51011 e 127.0.0.1)
BackendTERMOAUTH_URL, TERMOAUTH_API_URLEmissor do JWT e endereço do JWKS do TermoAuth
BackendTERMOTUBOS_API_URL, TERMOTUBOS_API_AUTHEndereço e cabeçalho Authorization da API Termotubos
BackendCHATWOOT_BASE_URL, CHATWOOT_ACCOUNT_ID, CHATWOOT_INBOX_ID, CHATWOOT_API_ACCESS_TOKENConta e caixa de entrada do Chatwoot; sem elas o atendimento humano fica indisponível
BackendCHATWOOT_WEBHOOK_SECRETSegredo que confere a assinatura do webhook do Chatwoot
BackendFRONTEND_URLOrigens do frontend (CORS e CSRF); com todas em https, o cookie sai com Secure
BackendUPLOAD_DIRPasta dos arquivos enviados
FrontendPUBLIC_API_URLEndereço da API Rust, chamada pelo navegador
FrontendPUBLIC_TERMOAUTH_URLEndereço do TermoAuth para os links de entrada e saída
FrontendPUBLIC_SITE_URL, PUBLIC_WHATSAPP_URLEndereço do site institucional e link do WhatsApp
FrontendPUBLIC_CHATWOOT_*Só exibição na tela de integração do Chatwoot

No frontend só existem variáveis públicas: todas vão para o navegador, então nunca guardam segredo.

Portas de desenvolvimento

O TermoPortal usa a faixa 51010–51019 do padrão de portas da Termotubos. O TermoAuth, que devolve a pessoa ao portal, roda sempre em http://localhost:51000 e tem o portal-b2b registrado com o frontend em 51010.

PortaServiço
51010frontend: o portal em SvelteKit
51011backend: a API em Rust
51012docs-projeto: esta documentação
51013database-erd: diagrama do banco
51014database-studio: Drizzle Studio
51019PostgreSQL do Docker (backend/docker-compose.yml, banco termo_portal)

Rodando localmente

1. Banco

Em backend/, docker compose up --detach sobe o PostgreSQL em localhost:51019. O DATABASE_URL do backend/.env.local termina em ?options=-c%20search_path%3Dtermoportal para o sqlx CLI enxergar o mesmo schema do backend.

2. Backend

cargo run aplica as migrations e sobe a API. cargo run --bin seeds cria os dados padrão (perfis, configurações, fretes, departamentos, categorias, alertas de estoque e a pessoa master ti.04@termotubos.com.br); com -- --teste, cria também grupos, empresas e pessoas de teste.

3. Primeiro acesso

A pessoa master entra pelo TermoAuth com o Google, que confirma o e-mail e liga a conta sozinho, ou por convite: cargo run --bin seeds -- --convite ti.04@termotubos.com.br imprime o caminho /convite?token=... para abrir em http://localhost:51010.

4. Frontend

Em frontend/, npm run dev sobe o portal em http://localhost:51010, com PUBLIC_API_URL apontando para a API (e o FRONTEND_URL do backend com http://localhost:51010).

Deploy da sandbox

O repositório oficial é github.com/OrgTermotubos/TermoPortal. Um push na branch sandbox dispara os workflows deploy-sandbox-backend.yml e deploy-sandbox-frontend.yml do GitHub Actions, conforme a pasta alterada.

  • Backend: compila em release, monta o .env com os segredos SANDBOX_*, envia por SSH para /opt/termoportal/backend e reinicia o serviço termoportal-backend, que escuta em 127.0.0.1:51011.
  • Frontend: site estático no Cloudflare Pages (npm run build, pasta build, variáveis PUBLIC_* no ambiente do build). O workflow antigo deploy-sandbox-frontend.yml (SSH + serviço Node) ficou obsoleto com o frontend estático.
  • Fila única. Os dois workflows usam o grupo de concorrência deploy-sandbox sem cancelar o anterior: quando um push mexe nos dois lados, os deploys rodam um depois do outro.
  • TermoAuth da sandbox: https://sandbox-auth.navsoftware.com.br, com a API em https://api-sandbox-auth.navsoftware.com.br.
Atualizado em 2026/10/02 11:32