feat: protecao CSRF (double-submit cookie) e paginacao da lista de clientes

CSRF: novo app/csrf.py + middleware em app/main.py garantem um cookie
csrf_token por navegador; todo formulario que muda estado (criar/editar/
excluir cliente, adicionar/editar/excluir contato) embute esse valor
como campo oculto, e Depends(exigir_csrf) confere no POST que o valor
enviado bate com o cookie antes de deixar a rota executar. Verificado
ponta a ponta com o app real rodando (curl): token errado -> 403,
token correto -> 303.

Paginacao: db_clientes.listar_clientes ganha offset, nova
contar_clientes(); GET /clientes aceita ?pagina=N (100 clientes por
pagina), template mostra Anterior/Proxima e o total.

53/53 testes passando (8 novos).
This commit is contained in:
Gustavo
2026-08-13 11:02:17 -03:00
parent 96cbd8a294
commit bb8fd596b1
12 changed files with 247 additions and 31 deletions
+4 -3
View File
@@ -9,6 +9,7 @@ from fastapi import APIRouter, Depends, Form
from fastapi.responses import RedirectResponse
from ..auth import exigir_login
from ..csrf import exigir_csrf
from ..db import clientes as db_clientes
from ..db import contatos as db_contatos
from ..normalize import normalizar_telefone
@@ -21,7 +22,7 @@ def _redirect_com_mensagem(cliente_id: int, mensagem: str, tipo: str = "success"
return RedirectResponse(f"/clientes/{cliente_id}?{query}", status_code=303)
@router.post("/{cliente_id}/contatos")
@router.post("/{cliente_id}/contatos", dependencies=[Depends(exigir_csrf)])
def adicionar(
cliente_id: int,
nome: str = Form(""),
@@ -43,7 +44,7 @@ def adicionar(
return _redirect_com_mensagem(cliente_id, "Contato adicionado.")
@router.post("/{cliente_id}/contatos/{contato_id}/editar")
@router.post("/{cliente_id}/contatos/{contato_id}/editar", dependencies=[Depends(exigir_csrf)])
def editar(
cliente_id: int,
contato_id: int,
@@ -63,7 +64,7 @@ def editar(
return _redirect_com_mensagem(cliente_id, "Contato atualizado.")
@router.post("/{cliente_id}/contatos/{contato_id}/excluir")
@router.post("/{cliente_id}/contatos/{contato_id}/excluir", dependencies=[Depends(exigir_csrf)])
def excluir(cliente_id: int, contato_id: int, usuario: str = Depends(exigir_login)):
db_contatos.excluir_contato(contato_id, cliente_id)
return _redirect_com_mensagem(cliente_id, "Contato excluído.")