diff --git a/app/routers/log.py b/app/routers/log.py
index 8d929ab..d5c775d 100644
--- a/app/routers/log.py
+++ b/app/routers/log.py
@@ -2,25 +2,58 @@
Rota /log: histórico de ações do painel (LOG_PAINEL, app/db/log.py) e,
quando configurado, os últimos envios automáticos de chave via WhatsApp
(Data Table pagamentos_processados do n8n, app/n8n_client.py).
+
+Mostra só os 20 registros de LOG_PAINEL mais recentes — sem paginação.
+Administradores têm um botão para limpar o histórico inteiro
+(POST /log/limpar); db_log.limpar() apaga tudo e o router grava, logo em
+seguida, uma única entrada de auditoria registrando quem limpou e quando
+— por isso o histórico nunca fica sem nenhuma explicação do que
+aconteceu com ele.
"""
+from typing import Optional
+from urllib.parse import urlencode
+
from fastapi import APIRouter, Depends, Request
-from fastapi.responses import HTMLResponse
+from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from .. import n8n_client
-from ..auth import exigir_login
+from ..auth import UsuarioLogado, exigir_admin, exigir_login
+from ..csrf import exigir_csrf, token_do_contexto
from ..db import log as db_log
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
+LIMITE_ACOES = 20
+
+
+def _redirect_com_mensagem(mensagem: str, tipo: str = "success") -> RedirectResponse:
+ return RedirectResponse(f"/log?{urlencode({'mensagem': mensagem, 'mensagem_tipo': tipo})}", status_code=303)
+
@router.get("/log", response_class=HTMLResponse)
-def pagina(request: Request, usuario: str = Depends(exigir_login)):
- acoes = db_log.listar(limite=100)
+def pagina(
+ request: Request,
+ mensagem: Optional[str] = None,
+ mensagem_tipo: Optional[str] = None,
+ usuario: UsuarioLogado = Depends(exigir_login),
+):
+ acoes = db_log.listar(limite=LIMITE_ACOES)
envios = n8n_client.listar_envios(limite=50)
return templates.TemplateResponse(
request,
"log.html",
- {"acoes": acoes, "envios": envios, "envios_configurado": n8n_client.configurado()},
+ {
+ "acoes": acoes, "envios": envios, "envios_configurado": n8n_client.configurado(),
+ "eh_admin": usuario.eh_admin, "csrf_token": token_do_contexto(request),
+ "mensagem": mensagem, "mensagem_tipo": mensagem_tipo,
+ },
)
+
+
+@router.post("/log/limpar", dependencies=[Depends(exigir_csrf)])
+def limpar(usuario: UsuarioLogado = Depends(exigir_admin)):
+ db_log.limpar()
+ db_log.registrar(str(usuario), "limpar", "log", None, True)
+ return _redirect_com_mensagem("Log limpo.")
diff --git a/app/templates/log.html b/app/templates/log.html
index e1a0537..19ecac0 100644
--- a/app/templates/log.html
+++ b/app/templates/log.html
@@ -5,6 +5,12 @@
Log
Ações no painel
+ {% if eh_admin %}
+
+ {% endif %}
{% if acoes %}
diff --git a/tests/test_router_log.py b/tests/test_router_log.py
index cd64041..5a1020e 100644
--- a/tests/test_router_log.py
+++ b/tests/test_router_log.py
@@ -1,20 +1,38 @@
+import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
-from app.auth import exigir_login
+from app.auth import UsuarioLogado, exigir_login
from app.routers import log as router_log
app_teste = FastAPI()
app_teste.include_router(router_log.router)
-app_teste.dependency_overrides[exigir_login] = lambda: "teste"
+app_teste.dependency_overrides[exigir_login] = lambda: UsuarioLogado(login="admin", nome="Admin", perfil="ADMIN")
client = TestClient(app_teste, follow_redirects=False)
+CSRF_TESTE = "teste-csrf-token"
+client.cookies.set("csrf_token", CSRF_TESTE)
+
+
+@pytest.fixture(autouse=True)
+def _admin_como_padrao():
+ """Todo teste roda como admin por padrão (override acima). Testes que
+ querem simular usuário padrão sobrescrevem de novo dentro do próprio
+ teste e restauram no fim via yield."""
+ yield
+ app_teste.dependency_overrides[exigir_login] = lambda: UsuarioLogado(login="admin", nome="Admin", perfil="ADMIN")
+
+
+@pytest.fixture(autouse=True)
+def _sem_log_real(monkeypatch):
+ monkeypatch.setattr(router_log.db_log, "registrar", lambda *args, **kwargs: None)
def test_pagina_log_mostra_acoes_do_painel(monkeypatch):
+ capturado = {}
monkeypatch.setattr(
router_log.db_log,
"listar",
- lambda limite=100: [
+ lambda limite=100: capturado.setdefault("limite", limite) and [
{
"ID": 1, "CRIADO_EM": "2026-08-13 10:00:00", "USUARIO": "admin", "ACAO": "criar",
"ENTIDADE": "cliente", "ENTIDADE_ID": "000001", "SUCESSO": 1, "DETALHE": None,
@@ -29,10 +47,11 @@ def test_pagina_log_mostra_acoes_do_painel(monkeypatch):
assert "admin" in resposta.text
assert "criar" in resposta.text
assert "N8N_API_URL" in resposta.text # aviso de integração não configurada
+ assert capturado["limite"] == 20
def test_pagina_log_mostra_envios_quando_configurado(monkeypatch):
- monkeypatch.setattr(router_log.db_log, "listar", lambda limite=100: [])
+ monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
monkeypatch.setattr(
router_log.n8n_client,
"listar_envios",
@@ -50,3 +69,39 @@ def test_pagina_log_mostra_envios_quando_configurado(monkeypatch):
assert resposta.status_code == 200
assert "Cliente Teste" in resposta.text
assert "enviado" in resposta.text
+
+
+def test_pagina_log_admin_ve_botao_de_limpar(monkeypatch):
+ monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
+ monkeypatch.setattr(router_log.n8n_client, "listar_envios", lambda limite=50: [])
+ monkeypatch.setattr(router_log.n8n_client, "configurado", lambda: False)
+
+ resposta = client.get("/log")
+ assert resposta.status_code == 200
+ assert "/log/limpar" in resposta.text
+
+
+def test_pagina_log_usuario_padrao_nao_ve_botao_de_limpar(monkeypatch):
+ app_teste.dependency_overrides[exigir_login] = lambda: UsuarioLogado(login="fulano", nome="Fulano", perfil="PADRAO")
+ monkeypatch.setattr(router_log.db_log, "listar", lambda limite=20: [])
+ monkeypatch.setattr(router_log.n8n_client, "listar_envios", lambda limite=50: [])
+ monkeypatch.setattr(router_log.n8n_client, "configurado", lambda: False)
+
+ resposta = client.get("/log")
+ assert resposta.status_code == 200
+ assert "/log/limpar" not in resposta.text
+
+
+def test_limpar_log_admin_funciona(monkeypatch):
+ chamou = {}
+ monkeypatch.setattr(router_log.db_log, "limpar", lambda: chamou.setdefault("limpou", True))
+ resposta = client.post("/log/limpar", data={"csrf_token": CSRF_TESTE})
+ assert resposta.status_code == 303
+ assert resposta.headers["location"].startswith("/log?")
+ assert chamou.get("limpou") is True
+
+
+def test_limpar_log_usuario_padrao_toma_403():
+ app_teste.dependency_overrides[exigir_login] = lambda: UsuarioLogado(login="fulano", nome="Fulano", perfil="PADRAO")
+ resposta = client.post("/log/limpar", data={"csrf_token": CSRF_TESTE})
+ assert resposta.status_code == 403