""" Limitador de tentativas de login por IP de origem — proteção simples contra força bruta de senha em POST /login (app/routers/auth.py), que é uma rota pré-autenticação e portanto alcançável por qualquer visitante. Em memória (dict por processo), no mesmo espírito de app/session.py (chave de assinatura em memória) e app/db/connection.py (sem pool): este painel roda como container único, sem múltiplas réplicas atrás de um load balancer, então não precisa de armazenamento compartilhado (Redis etc.) para isso funcionar. Reinício do processo zera o histórico — efeito colateral aceito, mesma lógica do SESSION_SECRET em memória. Se um dia o painel passar a rodar atrás de um proxy reverso, a origem (hoje request.client.host, lida em app/routers/auth.py) precisa passar a vir de X-Forwarded-For validado contra uma lista de proxies confiáveis — sem isso, toda tentativa apareceria vindo do IP do proxy e o limite passaria a valer para todo mundo junto (um IP externo malicioso poderia bloquear logins de todo mundo). """ import time MAX_TENTATIVAS = 5 JANELA_SEGUNDOS = 5 * 60 # 5 minutos _falhas: dict[str, list[float]] = {} def _recentes(origem: str, momento: float) -> list[float]: limite = momento - JANELA_SEGUNDOS return [t for t in _falhas.get(origem, []) if t > limite] def registrar_falha(origem: str, agora: float | None = None) -> None: """Chamado a cada tentativa de login com credencial inválida.""" momento = agora if agora is not None else time.time() recentes = _recentes(origem, momento) recentes.append(momento) _falhas[origem] = recentes def bloqueado(origem: str, agora: float | None = None) -> bool: """True quando essa origem já teve MAX_TENTATIVAS falhas nos últimos JANELA_SEGUNDOS — POST /login deve recusar antes mesmo de checar a credencial enviada.""" momento = agora if agora is not None else time.time() return len(_recentes(origem, momento)) >= MAX_TENTATIVAS def limpar(origem: str) -> None: """Chamado após um login bem-sucedido — não faz sentido continuar contando falhas antigas contra quem acabou de provar a senha certa.""" _falhas.pop(origem, None)