Files
GTI_GERENCIADOR_FISCAL/central/app/control_api.py
T

129 lines
7.6 KiB
Python

"""Human-authenticated JSON API for the control panel."""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlalchemy import select
from .audit import record_audit
from .blocker_resolution import resolve_blocker_with_context
from .control_plane import dashboard_summary, installation_view, list_blockers, list_installations, list_jobs, list_panel_events
from .human_auth import require_csrf, require_human
from .models import Agent, AgentSnapshot, BlockedProduct, Installation, Job, Tenant
from .jobs import create_job
def router(session_factory):
routes = APIRouter(prefix="/v1")
def identity(request: Request):
session = session_factory()
try:
web_session, admin = require_human(request, session)
return session, web_session, admin
except Exception:
session.close(); raise
@routes.get("/dashboard/summary")
def summary(identity=Depends(identity)):
session, _, _ = identity
try: return dashboard_summary(session)
finally: session.close()
@routes.get("/agents")
def agents(page: int = 1, page_size: int = 50, client: str | None = None, health: str | None = None, version: str | None = None, search: str | None = None, last_seen_before: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_installations(session, page=page, page_size=page_size, client=client, health=health, version=version, search=search, last_seen_before=datetime.fromisoformat(last_seen_before) if last_seen_before else None)
finally: session.close()
def get_installation(session, agent_id: int):
row = session.execute(select(Installation, Agent, Tenant, AgentSnapshot).join(Agent, Agent.id == Installation.agent_id).join(Tenant, Tenant.id == Installation.tenant_id).outerjoin(AgentSnapshot, AgentSnapshot.installation_id == Installation.id).where(Agent.id == agent_id)).first()
if not row: raise HTTPException(404, "Agent installation not found")
return row
@routes.get("/agents/{agent_id}")
def agent_detail(agent_id: int, identity=Depends(identity)):
session, _, _ = identity
try:
installation, agent, tenant, snapshot = get_installation(session, agent_id)
return installation_view(installation, agent, tenant, snapshot)
finally: session.close()
@routes.get("/agents/{agent_id}/health")
def agent_health(agent_id: int, identity=Depends(identity)):
return agent_detail(agent_id, identity)
@routes.get("/agents/{agent_id}/coverage")
def agent_coverage(agent_id: int, identity=Depends(identity)):
detail = agent_detail(agent_id, identity)
return {key: detail[key] for key in ("total_products", "fiscally_complete", "safe_blocked", "failed_products", "coverage_percent", "blockers")}
@routes.get("/agents/{agent_id}/blockers")
def agent_blockers(agent_id: int, page: int = 1, page_size: int = 50, blocker_type: str | None = None, field: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try:
installation, agent, _, _ = get_installation(session, agent_id)
return list_blockers(session, tenant_id=installation.tenant_id, agent_id=agent_id, page=page, page_size=page_size, blocker_type=blocker_type, field=field)
finally: session.close()
@routes.get("/agents/{agent_id}/jobs")
def agent_jobs(agent_id: int, page: int = 1, page_size: int = 50, status: str | None = None, kind: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_jobs(session, agent_id=agent_id, page=page, page_size=page_size, status=status, kind=kind)
finally: session.close()
@routes.get("/agents/{agent_id}/events")
def agent_events(agent_id: int, page: int = 1, page_size: int = 50, severity: str | None = None, event_type: str | None = None, since: str | None = None, until: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try:
installation, _, _, _ = get_installation(session, agent_id)
return {"page": max(1, page), "page_size": min(100, max(1, page_size)), "items": list_panel_events(session, installation.id, page=page, page_size=page_size, severity=severity, event_type=event_type, since=datetime.fromisoformat(since) if since else None, until=datetime.fromisoformat(until) if until else None)}
finally: session.close()
@routes.get("/blockers/global")
def global_blockers(page: int = 1, page_size: int = 50, blocker_type: str | None = None, field: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_blockers(session, page=page, page_size=page_size, blocker_type=blocker_type, field=field)
finally: session.close()
@routes.get("/jobs/global")
def global_jobs(page: int = 1, page_size: int = 50, status: str | None = None, kind: str | None = None, since: str | None = None, until: str | None = None, identity=Depends(identity)):
session, _, _ = identity
try: return list_jobs(session, page=page, page_size=page_size, status=status, kind=kind, since=datetime.fromisoformat(since) if since else None, until=datetime.fromisoformat(until) if until else None)
finally: session.close()
@routes.post("/agents/{agent_id}/actions/reprocess")
def reprocess(agent_id: int, payload: dict, request: Request, identity=Depends(identity)):
session, web_session, admin = identity
try:
require_csrf(web_session, payload.get("csrf_token"))
if payload.get("confirm") is not True: raise HTTPException(400, "explicit confirmation required")
installation, agent, _, _ = get_installation(session, agent_id)
kind = payload.get("kind", "REPROCESS_ALL")
if kind not in {"REPROCESS_ALL", "REPROCESS_PRODUCTS"}: raise HTTPException(400, "unsupported action")
job_payload = payload.get("payload") or {}
if kind == "REPROCESS_PRODUCTS" and not job_payload.get("codigos"): raise HTTPException(400, "codigos required")
existing = next((candidate for candidate in session.scalars(select(Job).where(Job.agent_id == agent.id, Job.kind == kind, Job.status == "PENDING")).all() if candidate.payload == job_payload), None)
job = existing or create_job(session, agent, kind, job_payload)
record_audit(session, admin_user_id=admin.id, tenant_id=installation.tenant_id, action=kind, target_type="installation", target_id=installation.installation_id, context={"job_id": job.id, "request_ip": request.client.host if request.client else None})
session.commit()
return {"job_id": job.id, "status": job.status}
finally: session.close()
@routes.post("/blockers/{blocker_id}/resolve-context")
@routes.post("/blockers/{blocker_id}/resolve-human")
def resolve_blocker(blocker_id: int, payload: dict, request: Request, identity=Depends(identity)):
session, web_session, admin = identity
try:
require_csrf(web_session, payload.get("csrf_token"))
entry_payload = {key: payload.get(key) for key in ("scope", "scope_key", "key", "value", "reason")}
try:
return resolve_blocker_with_context(session, blocker_id, admin, entry_payload, request_ip=request.client.host if request.client else None)
except LookupError as error:
raise HTTPException(404, str(error)) from error
except ValueError as error:
raise HTTPException(422, str(error)) from error
finally: session.close()
return routes