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>
331 lines
6.1 KiB
Markdown
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
|