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

331 lines
6.1 KiB
Markdown

# 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:**
```json
{ "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:**
```json
{
"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:**
```json
[
{ "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:**
```json
{
"channels": ["telegram", "email", "ntfy"],
"heartbeatTimeoutSec": 300,
"version": "1.0"
}
```
---
## Sync de dados
### `GET /api/data`
Retorna todo o estado armazenado.
**Response:**
```json
{
"data": { ... estado completo ... },
"updated_at": 1730000000000
}
```
### `POST /api/data`
Substitui todo o estado.
**Body:**
```json
{ "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:**
```json
{ "ok": true, "id": "abc123", "url": "/api/media/abc123" }
```
### `GET /api/media/list`
Lista todas as mídias (metadata, sem o conteúdo).
**Response:**
```json
[
{ "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:**
```json
{
"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:**
```json
{ "lat": -23.001, "lng": -43.001, "distance": 12.5 }
```
### `POST /api/anchor/alarm`
Disparar alarme manualmente. Aciona TODOS os canais configurados em paralelo.
**Body:**
```json
{
"boat_name": "Shivao",
"lat": -23.005, "lng": -43.005,
"distance": 87.3,
"radius": 50,
"reason": "drift"
}
```
**Response:**
```json
{
"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:**
```json
{
"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:**
```json
{
"durationMinutes": 360,
"boatName": "Shivao",
"zones": [ ... opcional, snapshot das zonas atuais ... ]
}
```
**Response:**
```json
{
"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:**
```json
{ "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:**
```json
{ "lat": -23.0, "lng": -43.0, "speed": 3.2, "boatName": "Shivao" }
```
**Response:**
```json
{ "ok": true, "posted": 2 }
```
---
## Outros
### `POST /api/test`
Dispara mensagem de teste em todos os canais configurados (sem urgência).
**Response:**
```json
{ "sent": ["telegram"], "failed": [] }
```
### `GET /api/alarms`
Histórico dos últimos 50 alarmes (incluindo testes).
**Response:**
```json
[
{
"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