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