""" Endpoint interno server-to-server, read-only, para o GTI Fiscal espelhar o cadastro de clientes. GTI Gerenciador continua sendo a fonte única da verdade: o Fiscal NUNCA recebe credencial MySQL direta (nem lê a tabela CLIENTE por fora) — só chama esta rota, autenticada por um bearer token dedicado (FISCAL_INTERNAL_API_TOKEN), nunca reaproveitado de SESSION_SECRET, da chave privada do SSO, de senha de admin ou de token de Agent. Rate limit em memória por IP de origem — mesmo espírito de app/rate_limit.py (container único, sem load balancer; ver ali o aviso sobre proxy reverso). """ from __future__ import annotations import hmac import os import time from fastapi import APIRouter, Header, HTTPException, Request from .db.clientes import listar_clientes_para_fiscal router = APIRouter() MAX_TENTATIVAS = 10 JANELA_SEGUNDOS = 5 * 60 _falhas: dict[str, list[float]] = {} def _token_configurado() -> bool: return bool(os.environ.get("FISCAL_INTERNAL_API_TOKEN")) def _token_valido(fornecido: str | None) -> bool: esperado = os.environ.get("FISCAL_INTERNAL_API_TOKEN", "") if not esperado or not fornecido: return False return hmac.compare_digest(fornecido, esperado) def _bloqueado(origem: str, agora: float | None = None) -> bool: momento = agora if agora is not None else time.time() limite = momento - JANELA_SEGUNDOS return len([t for t in _falhas.get(origem, []) if t > limite]) >= MAX_TENTATIVAS def _registrar_falha(origem: str, agora: float | None = None) -> None: momento = agora if agora is not None else time.time() limite = momento - JANELA_SEGUNDOS recentes = [t for t in _falhas.get(origem, []) if t > limite] recentes.append(momento) _falhas[origem] = recentes @router.get("/internal/fiscal/clients") def listar_clientes_fiscal(request: Request, authorization: str | None = Header(default=None)): origem = request.client.host if request.client else "unknown" if _bloqueado(origem): raise HTTPException(status_code=429, detail="muitas tentativas") if not _token_configurado(): # Sem token configurado = integração desligada, nunca "aberto por padrão". raise HTTPException(status_code=503, detail="FISCAL_INTERNAL_API_TOKEN não configurado") fornecido = authorization.removeprefix("Bearer ").strip() if authorization and authorization.startswith("Bearer ") else None if not _token_valido(fornecido): _registrar_falha(origem) raise HTTPException(status_code=401, detail="token inválido ou ausente") return {"clients": listar_clientes_para_fiscal()}