shivao-projeto/API.md
PontualTech / Karlão 5b02feae50 chore: initial commit + security hardening (4 runs squad shivao-melhoria)
Importação inicial do projeto Shivão (Diário de Bordo do veleiro) em estado
pronto pra produção, já incluindo as 5 mudanças de hardening implementadas
pela squad shivao-melhoria em 2026-04-27.

Mudanças de hardening (HANDOFF.md seção " Pronto"):

1. **Rate limiting nos 3 endpoints públicos de share**
   - express-rate-limit ^8.4.1, 60 req/min/IP
   - server/src/index.js linhas 38, 262, 271, 279

2. **Tamanho de upload reduzido pra 50MB** (era 200MB)
   - multer linha 84

3. **Validação Zod nos endpoints autenticados** (POST /api/data e /zones)
   - novo arquivo server/src/schemas/index.js
   - middleware validate() retorna 400 com top 5 issues

4. **Audit log de ações sensíveis**
   - nova tabela audit_log + funções db.audit() e db.recentAudit()
   - 6 endpoints instrumentados (state_set, media_insert/delete, share_create/revoke/zones_update)
   - novo endpoint GET /api/audit (autenticado)

5. **Catch silencioso de webhook em zona PROIBIDA tratado**
   - app/diario-bordo.html + server/public/index.html linha 3280
   - agora loga erro + exibe toast ao usuário

Status final do HANDOFF:
- 🔴 Críticos restantes: 1 (CORS — decisão consciente single-tenant, não-acionável)
- 🟡 Importantes: 4 itens (testes, vigia reconnect, refator frontend, demais catches)
- 🟢 Bom-ter: 5 itens

Stack: Node 20 ESM + Express + better-sqlite3 + Docker (Coolify)
Single-tenant pessoal · single-file frontend HTML · offline-first

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 13:24:08 -03:00

6.1 KiB

API Reference

Servidor Express em Node.js. Todas as rotas autenticadas exigem header:

Authorization: Bearer <BOAT_TOKEN>

Salvo onde indicado como público (sem auth).

Convenções

  • Content-Type: application/json (exceto /api/media que é multipart)
  • Timestamps: Unix ms (números inteiros)
  • Coordenadas: lat, lng (decimais)
  • Velocidade: m/s no servidor, knots no frontend (conversão x1.94384)
  • Distância: metros no servidor, milhas náuticas no frontend (m / 1852)

Endpoints públicos

GET /api/health

Health check. Útil para monitoramento.

Response:

{ "ok": true, "ts": 1730000000000 }

GET /share/:token

Página HTML pública com mapa em tempo real do barco.

  • Renderiza Leaflet com marcador do barco e trilha das últimas posições
  • Inclui zonas de geofencing (proibidas/atenção) se foram salvas no share
  • Auto-refresh a cada 15 segundos
  • Retorna 404 se token inválido, 410 se expirado

GET /api/share/:token/info

Metadata do share (sem expor BOAT_TOKEN).

Response:

{
  "boatName": "Shivao",
  "expiresAt": 1730086400000,
  "zones": [
    { "id": "...", "name": "Pedras", "type": "forbidden",
      "center": { "lat": -23.0, "lng": -43.0 }, "radius": 100 }
  ]
}

GET /api/share/:token/positions

Posições históricas do share (até 500 últimas).

Response:

[
  { "lat": -23.0, "lng": -43.0, "speed": 3.2, "ts": 1730000000000 },
  ...
]

Server info

GET /api/info

Quais canais de notificação estão configurados.

Response:

{
  "channels": ["telegram", "email", "ntfy"],
  "heartbeatTimeoutSec": 300,
  "version": "1.0"
}

Sync de dados

GET /api/data

Retorna todo o estado armazenado.

Response:

{
  "data": { ... estado completo ... },
  "updated_at": 1730000000000
}

POST /api/data

Substitui todo o estado.

Body:

{ "data": { ... estado completo ... } }

Nota: o servidor não merge nem versiona — substitui inteiro. Para multi-dispositivo, é responsabilidade do cliente puxar antes de modificar ou implementar uma estratégia de merge.


Mídia

POST /api/media

Upload de arquivo. Multipart form-data:

  • file: arquivo binário
  • id: ID único (cliente gera)
  • parent_id: ID da viagem/manutenção pai (opcional)
  • parent_type: trip, maint, ou pending
  • kind: photo, video, audio
  • created_at: timestamp ms

Limite: 200 MB por arquivo (revisar antes de produção — ver HANDOFF.md).

Response:

{ "ok": true, "id": "abc123", "url": "/api/media/abc123" }

GET /api/media/list

Lista todas as mídias (metadata, sem o conteúdo).

Response:

[
  { "id": "abc", "parent_id": "trip_1", "kind": "photo",
    "mime": "image/jpeg", "size": 123456, "created_at": 1730000000000 }
]

GET /api/media/:id

Stream do arquivo. Retorna o blob com Content-Type original.

DELETE /api/media/:id

Remove a mídia (DB + filesystem).


Vigia de fundeio + dead-man switch

POST /api/anchor/start

Registra início de uma vigia no servidor.

Body:

{
  "boat_name": "Shivao",
  "anchor_lat": -23.0, "anchor_lng": -43.0,
  "radius": 50
}

POST /api/anchor/heartbeat

App envia a cada 30s enquanto vigia ativa. Reseta o timer do dead-man.

Body:

{ "lat": -23.001, "lng": -43.001, "distance": 12.5 }

POST /api/anchor/alarm

Disparar alarme manualmente. Aciona TODOS os canais configurados em paralelo.

Body:

{
  "boat_name": "Shivao",
  "lat": -23.005, "lng": -43.005,
  "distance": 87.3,
  "radius": 50,
  "reason": "drift"
}

Response:

{
  "sent": ["telegram", "email", "ntfy"],
  "failed": [{ "channel": "sms", "error": "Twilio rate limit" }]
}

POST /api/anchor/stop

Encerra vigia. Limpa estado no servidor.

GET /api/anchor/status

Estado atual da vigia.

Response:

{
  "active": 1,
  "boat_name": "Shivao",
  "anchor_lat": -23.0, "anchor_lng": -43.0,
  "radius": 50,
  "started_at": 1730000000000,
  "last_heartbeat": 1730000300000,
  "last_lat": -23.001, "last_lng": -43.001,
  "last_distance": 12.5,
  "alarm_fired": 0
}

Dead-man switch

Roda em background a cada 30s. Se last_heartbeat está há mais de HEARTBEAT_TIMEOUT_SEC (padrão 5 min) e active=1, dispara alarme com reason: "heartbeat_lost".

Não envia spam — só dispara uma vez por episódio (até receber novo heartbeat ou stop).


Compartilhamento ao vivo

POST /api/share/create

Cria link público temporário.

Body:

{
  "durationMinutes": 360,
  "boatName": "Shivao",
  "zones": [ ... opcional, snapshot das zonas atuais ... ]
}

Response:

{
  "token": "Aljg29x71kqp...",
  "expiresAt": 1730086400000,
  "url": "https://shivao.exemplo.com/share/Aljg29x71kqp..."
}

GET /api/share/list

Lista shares ativos (não expirados, não revogados).

DELETE /api/share/:token

Revoga share (marca como inativo). Página pública passa a retornar 404.

POST /api/share/:token/zones

Atualiza zonas do share (caso o dono adicione/remova zonas após criar o share).

Body:

{ "zones": [ ... ] }

POST /api/share/position

Posta posição atual. App chama a cada 30s enquanto há share ativo. A posição vai para todos os shares ativos do barco (filtrados por boat_name).

Body:

{ "lat": -23.0, "lng": -43.0, "speed": 3.2, "boatName": "Shivao" }

Response:

{ "ok": true, "posted": 2 }

Outros

POST /api/test

Dispara mensagem de teste em todos os canais configurados (sem urgência).

Response:

{ "sent": ["telegram"], "failed": [] }

GET /api/alarms

Histórico dos últimos 50 alarmes (incluindo testes).

Response:

[
  {
    "id": 1, "ts": 1730000000000, "type": "drift",
    "payload": "{...JSON...}",
    "sent": "[\"telegram\",\"ntfy\"]",
    "failed": "[]"
  }
]

Cleanup automático

Job rodando 1x/dia:

  • Remove share_positions de shares cujo expires_at passou há mais de 7 dias
  • Remove os próprios shares expirados