# Segurança — TV Mabassa API

Modelo de ameaças resumido: (1) fraude de pagamento/activação de assinaturas, (2) acesso ao conteúdo premium sem pagar,
(3) tomada de contas de administrador e alteração de notícias, (4) fuga de dados de clientes, (5) abuso (força bruta, spam, scraping).

## Controlos implementados (e onde)
| Risco | Controlo | Ficheiro |
|---|---|---|
| Palavras-passe | argon2id, mín. 12 caracteres c/ maiúsculas, números, símbolos e verificação de fugas conhecidas; re-hash automático | `AppServiceProvider`, `AuthController` |
| Força bruta | limite por IP **e** por email; bloqueio da conta 15 min após 5 falhas; mensagem única ("email ou palavra-passe incorrectos"); tempo de resposta igualado quando o email não existe | `AuthController`, `AppServiceProvider` |
| Tomada de conta de admin | **2FA TOTP obrigatório** para admin/editor; códigos de recuperação (hash, uso único); cada código TOTP só serve uma vez; tokens temporários só abrem o passo 2FA; sessão do painel 2 h; restrição opcional por IP (404 a quem não está na lista) | `TwoFactorController`, `EnsureAdmin`, `AdminIpAllowlist` |
| Escalada de privilégios | `role` nunca é preenchível em massa; admins só se criam por linha de comandos; editor não vê dinheiro nem apaga; `author` só grava rascunhos | `User`, `CreateAdmin`, rotas |
| Tokens | Sanctum com expiração, capacidades (`reader`/`admin`/`twofa-*`), prefixo `tvm_`, máx. 5 por utilizador, guardados só como hash; mudar a palavra-passe termina as outras sessões | `config/sanctum.php`, `AuthController` |
| Conteúdo premium | o servidor só envia o corpo completo a assinantes/equipa; aos restantes envia 1 parágrafo. Assinaturas expiradas perdem acesso de imediato | `ArticleController`, `ArticleResource` |
| Valor da cobrança | o preço vem SEMPRE da BD; o cliente só escolhe plano e método | `PaymentService` |
| Cobrança dupla | `Idempotency-Key` obrigatório + índice único; linha do pagamento bloqueada (`FOR UPDATE`) na confirmação; só a 1.ª confirmação cria a assinatura | `PaymentController`, `PaymentService` |
| Webhook falsificado | HMAC-SHA256 do corpo **bruto** com data (anti-replay, 5 min), comparação em tempo constante, lista de IPs opcional, evento único por `(provider, event_id)`, conferência de valor e moeda, um "falhou" nunca desfaz um "pago" | `WebhookSignature`, `VerifyPaymentWebhook`, `PaymentWebhookController` |
| Dados de cartão | **nunca** passam pelo servidor: o cartão é pago na página do gateway (âmbito PCI SAQ-A). Só se guarda marca + 4 últimos dígitos, se o gateway os devolver | `PaymentController` |
| Dados pessoais | telemóvel mascarado em API, CSV e logs; `msisdn` e referências do agregador ocultos por omissão; segredo 2FA cifrado | `PaymentResource`, `User` |
| Injecção SQL | só Eloquent/Query Builder com parâmetros; `LIKE` com curingas escapados; validação com listas de valores permitidos | controladores |
| XSS armazenado | o corpo das notícias é texto limpo (sem HTML, tipos de bloco permitidos, tamanhos limitados); CSP `default-src 'none'` nas respostas | `BlockSanitizer`, `SecurityHeaders` |
| Envio de ficheiros | tipo pelo conteúdo (finfo), só JPEG/PNG/WebP, limite de tamanho e pixéis, **re-codificação** (remove EXIF e código embutido), nome aleatório | `UploadController` |
| CSV injection | células começadas por `= + - @` são neutralizadas | `CsvSanitizer` |
| Spam | limites por IP, campo-armadilha no contacto, 5 mensagens/hora | `CatalogController`, `AppServiceProvider` |
| Transporte | HTTPS forçado + HSTS; cabeçalhos `nosniff`, `X-Frame-Options`, `Referrer-Policy`, `Permissions-Policy`; respostas autenticadas com `no-store` | `AppServiceProvider`, `SecurityHeaders` |
| CORS | lista explícita de origens, sem cookies, nunca `*` | `config/cors.php` |
| Erros | respostas JSON genéricas, sem stack traces/SQL; campos sensíveis nunca vão para os logs | `bootstrap/app.php` |
| Auditoria | quem fez o quê (login, falhas, 2FA, criar/editar/apagar, preços, exportações, pagamentos) com IP; o modelo bloqueia edição e eliminação (para reforço, dar à BD um utilizador sem DELETE nesta tabela) | `AuditLogger`, `AuditLog` |
| Dados financeiros | pagamentos e assinaturas nunca se apagam (`RESTRICT`); notícias/utilizadores usam *soft delete* | migrações |

## O que a configuração do servidor tem de garantir (fora do código)
1. **HTTPS** válido (Let's Encrypt) e só TLS 1.2+. O domínio da API separado do site.
2. **Utilizador de BD com privilégios mínimos** (SELECT/INSERT/UPDATE/DELETE; sem DROP/ALTER/GRANT). Migrações com outro utilizador.
3. BD **não exposta à Internet**; palavra-passe forte; ligação cifrada se a BD estiver noutra máquina.
4. `.env` fora da pasta pública e com permissões `600`; **o servidor web aponta para `public/`**.
5. **Cópias de segurança** diárias da BD, cifradas, guardadas fora do servidor, com teste de restauro.
6. `APP_DEBUG=false`, `APP_ENV=production`; `php artisan config:cache route:cache`.
7. Firewall (só 80/443 e SSH por chave), actualizações automáticas do sistema, `fail2ban`.
8. Actualizar dependências: `composer audit` semanalmente (`composer security`).
9. Monitorização: alertas para `Log::critical` (webhook com valor divergente), falhas 5xx e picos de 401/429.
10. **Antes de publicar**: `php artisan app:security-check` sem erros, e correr os testes.

## Responsabilidades do negócio (não resolvíveis só com código)
- **Agregador**: escolher um licenciado em Moçambique, guardar o contrato/KYC, confirmar o formato do webhook e IPs.
- **Reembolsos e disputas**: definir a política (o texto legal tem "[a definir]"); estornos são feitos no agregador e registados manualmente.
- **Lei de protecção de dados**: rever a Política de Privacidade com um jurista; definir prazos de retenção e resposta a pedidos de apagamento (os pagamentos têm de se manter por obrigação fiscal).
- **Equipa**: cada pessoa com a sua conta (nunca partilhar), 2FA obrigatório, retirar acessos a quem sai.
- **Resposta a incidentes**: saber quem contactar, como revogar tokens (`php artisan tinker` → `PersonalAccessToken::query()->delete()`) e rodar o segredo do webhook.
- Fazer um **teste de intrusão** independente antes de lançar com dinheiro real.

## Limitações conhecidas
- O código não foi executado contra MySQL/Laravel no ambiente de criação (ver README).
- O formato do webhook e a integração do agregador são modelos a confirmar.
- Contagem de visualizações/gostos anónimos pode ser inflacionada por quem varia IP/dispositivo; é indicador, não métrica auditada.
- A conta de leitor não tem ainda verificação de email nem recuperação de palavra-passe por email (a fazer com o servidor de email).
- Sem WAF/anti-DDoS: usar Cloudflare ou equivalente à frente da API.
