Reestrutura para API multi-cliente com payload cifrado

A API deixa de ser especifica da Scardua e passa a atender varios
clientes pela rota /v1/holmes/{cliente}/...

Camada de clientes:
- app/clientes/base.py: ClienteBase (comportamento) + ClienteConfig (dado),
  separados de proposito — config sai do configs.json hoje e pode sair do
  banco amanha sem o resto mudar
- app/clientes/registry.py: resolve o {cliente} da rota pra instancia
- cada cliente traz a propria conta do Holmes; o cache de token virou um
  arquivo por cliente (antes era constante de modulo e dois clientes
  sobrescreveriam o token um do outro)

Holmes:
- funcoes de modulo viraram a classe HolmesAPI, amarrada a uma conta
- exposta pronta em cliente.holmes (cached_property)
- as funcoes de parsing puro (extrair_*) ficaram fora da classe

Config:
- app/schemas.py: modulo folha, so pydantic. Config e services usam os
  mesmos modelos sem um importar o outro
- app/config.py: so o carregamento do configs.json
- configs.json passa a ter "clientes": {"<nome>": {...}}

Compras:
- schemas aceitam o payload real do Holmes (PEDIDO_LINX vem como numero,
  NUMERO NF ENTRADA so existe depois da fase da nota)
- parcelas viram dict[int, Parcela], pareando parcelaN com o vencimento
  correspondente e pulando os slots vazios
- payload de saida carrega id_processo como chave de deduplicacao: o
  Holmes reentrega webhook e sem isso duplica linha no destino

Criptografia:
- app/services/cripto.py: envelope AES-256-GCM + RSA-OAEP-SHA256. RSA
  sozinho nao serve (cifra ~190 bytes; o payload passa disso)
- ciframos com a publica do conector e nao conseguimos desfazer

Deploy:
- Dockerfile e .dockerignore vieram da VPS pro repo
- docs/deploy.md: runbook, incluindo que --format docker e obrigatorio
  (em OCI o podman ignora o HEALTHCHECK silenciosamente)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ricardo 2026-08-18 06:01:01 -03:00
parent 0f47953453
commit 4d47887c00
15 changed files with 1004 additions and 683 deletions

115
docs/deploy.md Normal file
View file

@ -0,0 +1,115 @@
# Deploy — API (VPS da FLS)
Ambiente: **VPS Hostinger** (Debian 13), **Podman rootless + Quadlet**, **Caddy**
(proxy + TLS), user `admin`.
No ar: **https://api-scardua.flstecnologia.tech**
> O nome `api-scardua` é **legado** — a API hoje atende múltiplos clientes
> (`/v1/holmes/{cliente}/...`). Renomear implica mexer junto em: nome do quadlet,
> `Image=`, `ContainerName=`, pasta do `configs.json`, build dir, bloco do
> Caddyfile e DNS. O `reverse_proxy` do Caddy resolve o container pelo
> `ContainerName` dentro da rede `fls` — se um mudar sem o outro, dá **502**.
## Acesso à VPS
```bash
ssh vps-fls # alias -> 179.197.230.154, porta 3115, user admin
```
- Porta 22 é **fechada** no firewall. Sempre `-p 3115` (ou o alias).
- `systemctl --user` sempre (rootless) — **nunca** `sudo systemctl` pros containers.
- ⚠️ O `~/.ssh/config` tem um **BOM** na primeira linha e o OpenSSH do Git Bash
recusa (`Bad configuration option: \357\273\277host`). Contorno sem editar o
arquivo:
```bash
ssh -F none -i ~/.ssh/id_ed25519 -p 3115 admin@179.197.230.154
```
## Arquitetura
- Imagem: buildada **local na VPS** = `localhost/api-scardua:latest` (sem registry).
- Container na rede **`fls`** — só o Caddy alcança; a porta 8000 **não** é publicada.
- Config: `configs.json` montado read-only de `~/.config/api-scardua/configs.json`.
- Quadlet: `~/.config/containers/systemd/api-scardua.container`.
- Caddy: `api-scardua.flstecnologia.tech``api-scardua:8000`, TLS automático.
Caddyfile em `/srv/containers/stacks/caddy/Caddyfile`.
## Atualizar a app
```bash
# 1. marcar rollback ANTES de qualquer coisa
ssh vps-fls 'podman tag localhost/api-scardua:latest localhost/api-scardua:rollback-$(date +%F)'
# 2. mandar o código (não tem rsync no Git Bash do Windows; tar resolve)
tar czf - --exclude='__pycache__' --exclude='*.pyc' --exclude='.ruff_cache' \
main.py app requirements.txt Dockerfile .dockerignore pyproject.toml \
| ssh vps-fls 'rm -rf ~/build/api-scardua && mkdir -p ~/build/api-scardua && tar xzf - -C ~/build/api-scardua'
# 3. buildar e reiniciar
ssh vps-fls 'cd ~/build/api-scardua && podman build --format docker -t api-scardua:latest . \
&& systemctl --user restart api-scardua'
```
> ⚠️ **`--format docker` é obrigatório.** No formato OCI (padrão do Podman) o
> `HEALTHCHECK` do Dockerfile é **silenciosamente ignorado** — e é ele que o
> `podman auto-update` usa pra decidir rollback. Conferir depois do build:
> ```bash
> podman inspect localhost/api-scardua:latest --format '{{json .HealthCheck}}'
> ```
> ⚠️ Diretórios da build antiga podem estar **sem bit de escrita** (`dr-x------`)
> e travar o `rm -rf` no meio. Se acontecer: `chmod -R u+rwX ~/build/api-scardua`.
## O configs.json da VPS é separado
Ele **não** vem do repo (gitignored). Vive em `~/.config/api-scardua/configs.json`
e tem que ser atualizado à mão quando o formato do `Settings` mudar — senão o
container entra em **crash-loop** na validação do Pydantic, que roda no import.
```bash
cat configs.json | ssh vps-fls 'cat > ~/.config/api-scardua/configs.json && chmod 600 $_'
```
## Rollback
```bash
ssh vps-fls '
podman tag localhost/api-scardua:rollback-<data> localhost/api-scardua:latest
systemctl --user restart api-scardua
'
```
## Mexer no Caddy (com segurança)
```bash
cd /srv/containers/stacks/caddy
cp Caddyfile Caddyfile.bak
# editar...
podman exec caddy caddy validate --config /etc/caddy/Caddyfile # valida ANTES
podman exec caddy caddy reload --config /etc/caddy/Caddyfile # sem downtime
```
## Comandos úteis
```bash
systemctl --user status api-scardua
podman logs -f api-scardua
podman ps --filter name=api-scardua # estado + health
podman healthcheck run api-scardua
```
## Verificar depois de subir
```bash
curl -s -o /dev/null -w '%{http_code}\n' https://api-scardua.flstecnologia.tech/health
curl -X POST https://api-scardua.flstecnologia.tech/v1/holmes/scardua/compras/dados \
-H 'Content-Type: application/json' -d @payload-do-holmes.json
```
Cliente desconhecido na rota deve dar **404** (`cliente 'x' nao atendido`).
## Evolução planejada
- **Registry + CI** (Forgejo Actions): publicar em `git.flstecnologia.tech`, trocar
o Quadlet pro alvo com `AutoUpdate=registry` → deploy pull-based.
- Backup do Postgres da VPS (pendência geral do servidor).