feat: limite de 20 registros e botao de limpar log (admin)

This commit is contained in:
Gustavo
2026-08-20 10:52:48 -03:00
parent 9fbc57b0ad
commit 4ec3c8df88
3 changed files with 103 additions and 9 deletions
+38 -5
View File
@@ -2,25 +2,58 @@
Rota /log: histórico de ações do painel (LOG_PAINEL, app/db/log.py) e, 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 quando configurado, os últimos envios automáticos de chave via WhatsApp
(Data Table pagamentos_processados do n8n, app/n8n_client.py). (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 import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates from fastapi.templating import Jinja2Templates
from .. import n8n_client 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 from ..db import log as db_log
router = APIRouter() router = APIRouter()
templates = Jinja2Templates(directory="app/templates") 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) @router.get("/log", response_class=HTMLResponse)
def pagina(request: Request, usuario: str = Depends(exigir_login)): def pagina(
acoes = db_log.listar(limite=100) 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) envios = n8n_client.listar_envios(limite=50)
return templates.TemplateResponse( return templates.TemplateResponse(
request, request,
"log.html", "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.")
+6
View File
@@ -5,6 +5,12 @@
<h1>Log</h1> <h1>Log</h1>
<h2>Ações no painel</h2> <h2>Ações no painel</h2>
{% if eh_admin %}
<form method="post" action="/log/limpar" style="margin-bottom:14px">
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
<button type="submit" onclick="return confirm('Limpar todo o histórico de ações? Isso não pode ser desfeito.')">Limpar log</button>
</form>
{% endif %}
{% if acoes %} {% if acoes %}
<div class="tabela-wrap"> <div class="tabela-wrap">
<table> <table>
+59 -4
View File
@@ -1,20 +1,38 @@
import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient 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 from app.routers import log as router_log
app_teste = FastAPI() app_teste = FastAPI()
app_teste.include_router(router_log.router) 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) 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): def test_pagina_log_mostra_acoes_do_painel(monkeypatch):
capturado = {}
monkeypatch.setattr( monkeypatch.setattr(
router_log.db_log, router_log.db_log,
"listar", "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", "ID": 1, "CRIADO_EM": "2026-08-13 10:00:00", "USUARIO": "admin", "ACAO": "criar",
"ENTIDADE": "cliente", "ENTIDADE_ID": "000001", "SUCESSO": 1, "DETALHE": None, "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 "admin" in resposta.text
assert "criar" in resposta.text assert "criar" in resposta.text
assert "N8N_API_URL" in resposta.text # aviso de integração não configurada 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): 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( monkeypatch.setattr(
router_log.n8n_client, router_log.n8n_client,
"listar_envios", "listar_envios",
@@ -50,3 +69,39 @@ def test_pagina_log_mostra_envios_quando_configurado(monkeypatch):
assert resposta.status_code == 200 assert resposta.status_code == 200
assert "Cliente Teste" in resposta.text assert "Cliente Teste" in resposta.text
assert "enviado" 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