108 lines
5.3 KiB
Markdown
108 lines
5.3 KiB
Markdown
# FASE 2 — API do backend
|
|
|
|
## Objetivo
|
|
|
|
Todos os endpoints REST, WebSocket de sinalização, armazenamento de mídia e o ciclo de vida da visita funcionando ponta a ponta — verificável por teste de integração, ainda sem interface.
|
|
|
|
## Pré-requisitos
|
|
|
|
FASES 0 e 1 concluídas. Ler `docs/03-FLUXOS-E-CONTRATOS.md` inteiro e `docs/06-LGPD-E-SEGURANCA.md` §5.
|
|
|
|
## Tarefas
|
|
|
|
### 1. Validação de QR e sessão de visitante
|
|
|
|
`GET /api/v1/visitor/gates/{gateId}/preview?s={sig}` — valida assinatura HMAC com `gates.qr_secret` e confere `qr_version`. Devolve nome do condomínio e o aviso de tratamento vigente. Sem autenticação.
|
|
|
|
`POST /api/v1/visitor/sessions` — recebe coordenadas, valida geofence com `ST_DWithin(gates.location, ponto, geofence_meters)`, registra `privacy_notices` (versão do aviso, IP, user agent) e emite o JWT efêmero.
|
|
|
|
Localização ausente ou negada → `403 LOCATION_REQUIRED`. Fora do raio → `403 OUTSIDE_GEOFENCE` com `detail` em português pronto para exibir.
|
|
|
|
### 2. Criação de visita — busca cega
|
|
|
|
`POST /api/v1/visitor/visits`, multipart (JSON + mídia).
|
|
|
|
**Esta é a implementação mais sensível do sistema.** Requisitos, todos obrigatórios:
|
|
|
|
- Resolve `unit_id` a partir de bloco + unidade; **se não existir, cria a visita com `unit_id = NULL`** e preserva `unit_input`
|
|
- Resposta **idêntica** — mesmo corpo, mesmo status, mesmos headers — nos dois casos
|
|
- **Tempo de resposta constante:** meça o caminho mais lento e aplique delay artificial no mais rápido. Sem isso o ataque vira timing attack e a busca cega não serve para nada
|
|
- Nome de morador nunca aparece na resposta
|
|
- Rate limit por IP e por dispositivo, com bloqueio progressivo
|
|
|
|
Verifica `quiet_hours` do condomínio: dentro da janela, a visita pula o toque ao morador e vai direto para `FILA_OPERADOR` ou `RECADO_EM_VIDEO`.
|
|
|
|
Enfileira `VisitaCriada` no outbox e agenda `VISIT_RING_TIMEOUT` (20s) e `VISIT_EXPIRE` (10 min).
|
|
|
|
### 3. Ciclo de vida da visita
|
|
|
|
Serviço de aplicação com as transições de `03-FLUXOS-E-CONTRATOS.md` §1, sempre validadas por `VisitStateMachine` e sempre com optimistic locking em `visits.version`.
|
|
|
|
Handlers de job:
|
|
|
|
| Job | Ação |
|
|
|---|---|
|
|
| `VISIT_RING_TIMEOUT` | → `ESCALONADA`; toca para os demais `unit_members` por `ring_order`; agenda `VISIT_ESCALATE` |
|
|
| `VISIT_ESCALATE` | → `FILA_OPERADOR` se `operator_queue` ativo, senão `RECADO_EM_VIDEO` |
|
|
| `VISIT_QUEUE_TIMEOUT` | → `RECADO_EM_VIDEO` |
|
|
| `VISIT_EXPIRE` | → `EXPIRADA` |
|
|
| `DELIVERY_DEFAULT_RULE` | aplica `units.delivery_rule` |
|
|
|
|
`POST /api/v1/app/visits/{id}/resolve` exige `Idempotency-Key`. Conflito de versão devolve `409 VISIT_ALREADY_RESOLVED` com `resolvedBy` e `resolvedAt` — o app precisa mostrar "Maria já autorizou", não um erro técnico.
|
|
|
|
### 4. Armazenamento de mídia
|
|
|
|
Porta `StoragePort` com adaptador MinIO/S3. Ao receber mídia:
|
|
|
|
- Valida `Content-Type` por **magic bytes**, nunca por extensão
|
|
- Limites: 8MB foto, 30MB vídeo
|
|
- **Remove EXIF** — carrega GPS próprio, que não é o dado que coletamos e não passou pelo aviso
|
|
- Calcula `sha256` e grava em `media_assets`
|
|
- Nome de objeto gerado pelo servidor
|
|
- Define `expires_at` conforme a retenção do tipo (`06-LGPD-E-SEGURANCA.md` §4)
|
|
|
|
URL assinada de 5 min para leitura, **sempre** registrando acesso em `audit_log`. Nenhuma URL pública, em nenhuma hipótese.
|
|
|
|
### 5. WebSocket
|
|
|
|
`/ws/visitor/{visitId}` e `/ws/app`, com o envelope e os tipos de `03-FLUXOS-E-CONTRATOS.md` §7. Heartbeat de 20s.
|
|
|
|
**O WebSocket nunca é fonte de verdade.** Toda mudança de estado é confirmada por REST ou push. Se o WS cair, o cliente faz polling — e a portaria continua funcionando.
|
|
|
|
Com múltiplas réplicas do backend, a difusão de eventos usa Redis pub/sub (só transporte; a durabilidade fica no outbox).
|
|
|
|
### 6. Endpoints de app e admin
|
|
|
|
Todos os de `03-FLUXOS-E-CONTRATOS.md` §6, com autorização por método:
|
|
|
|
```kotlin
|
|
@PreAuthorize("@access.canViewVisit(#visitId, authentication)")
|
|
```
|
|
|
|
Morador vê apenas visitas das suas unidades; operador, apenas as da fila; admin, apenas as do seu tenant.
|
|
|
|
Importação CSV de unidades com **dry-run obrigatório** — devolve o que seria criado, alterado e rejeitado antes de gravar.
|
|
|
|
### 7. Tratamento de erros
|
|
|
|
`application/problem+json` (RFC 7807) com os `code` da tabela de `03-FLUXOS-E-CONTRATOS.md` §9. **`detail` sempre em português e pronto para exibir ao visitante** — a tela da portaria não é lugar para mensagem técnica.
|
|
|
|
## Critérios de aceite
|
|
|
|
- [ ] Testes de integração com Testcontainers cobrindo os fluxos A, B e C ponta a ponta
|
|
- [ ] **Teste de busca cega:** unidade existente e inexistente produzem resposta idêntica, e a diferença de tempo fica abaixo do ruído de medição
|
|
- [ ] Teste: geofence rejeita fora do raio e aceita dentro
|
|
- [ ] Teste: `quiet_hours` desvia do morador
|
|
- [ ] Teste: dois `resolve` concorrentes — um vence, outro recebe `409` com o autor
|
|
- [ ] Teste: `resolve` repetido com mesma `Idempotency-Key` não duplica
|
|
- [ ] Teste: escalonamento dispara após restart do backend (job persistido)
|
|
- [ ] Teste: EXIF removido da imagem armazenada
|
|
- [ ] Teste: acesso a mídia gera linha em `audit_log`
|
|
- [ ] OpenAPI completo em `/v3/api-docs`
|
|
|
|
## Não faça nesta fase
|
|
|
|
- Integração com LiveKit (FASE 4) — `room_name` fica como placeholder
|
|
- Push real (FASE 5) — `NotificationChannel` com implementação de log
|
|
- Qualquer interface
|