feat: limite de 20 registros e botao de limpar log (admin)
This commit is contained in:
+38
-5
@@ -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.")
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user