Add 'gerenciador/' from commit '34cc9c75a57dedbf61a6e6a1ba1f35f76e72434b'
git-subtree-dir: gerenciador git-subtree-mainline:f4f546c928git-subtree-split:34cc9c75a5
This commit is contained in:
@@ -0,0 +1,100 @@
|
||||
"""
|
||||
Rotas de login/logoff. O painel não usa mais HTTP Basic Auth (ver
|
||||
app/auth.py) — quem não está logado é redirecionado para GET /login pelo
|
||||
exception handler de NaoAutenticado (registrado em app/main.py).
|
||||
|
||||
O cookie de sessão só sai com o atributo Secure quando a requisição já
|
||||
chegou em HTTPS (request.url.scheme) — hoje a produção ainda serve só
|
||||
HTTP puro (sem proxy reverso com TLS na frente), então forçar Secure
|
||||
sempre quebraria o login (o navegador nunca devolveria o cookie). Assim
|
||||
que um TLS for colocado na frente, o cookie passa a sair Secure
|
||||
automaticamente, sem precisar mexer aqui de novo. Se um proxy reverso
|
||||
for adicionado, ele precisa repassar o esquema original (cabeçalho
|
||||
X-Forwarded-Proto + ProxyHeadersMiddleware/--proxy-headers do uvicorn),
|
||||
senão request.url.scheme sempre lê "http" mesmo com o navegador em HTTPS.
|
||||
"""
|
||||
from typing import Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from fastapi import APIRouter, Depends, Form, Request
|
||||
from fastapi.responses import HTMLResponse, RedirectResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .. import rate_limit, session
|
||||
from ..auth import UsuarioLogado, autenticar_credenciais, exigir_login
|
||||
from ..csrf import exigir_csrf, token_do_contexto
|
||||
from .clientes import POR_PAGINA_COOKIE
|
||||
|
||||
router = APIRouter()
|
||||
templates = Jinja2Templates(directory="app/templates")
|
||||
|
||||
|
||||
def _origem(request: Request) -> str:
|
||||
"""IP de quem fez a requisição, usado como chave do limitador de
|
||||
tentativas (app/rate_limit.py). "desconhecido" só ocorre em contexto
|
||||
de teste sem client configurado — nesse caso todas as tentativas sem
|
||||
IP caem na mesma chave, o que é aceitável (ambiente de teste, não
|
||||
produção)."""
|
||||
return request.client.host if request.client else "desconhecido"
|
||||
|
||||
|
||||
def _proximo_seguro(next_: Optional[str]) -> str:
|
||||
"""Só aceita um path interno (começa com "/", sem "//") — nunca uma URL
|
||||
externa, para não virar um open redirect via POST /login?next=..."""
|
||||
if next_ and next_.startswith("/") and not next_.startswith(("//", "/\\")):
|
||||
return next_
|
||||
return "/clientes"
|
||||
|
||||
|
||||
@router.get("/login", response_class=HTMLResponse)
|
||||
def form_login(request: Request, next: Optional[str] = None):
|
||||
return templates.TemplateResponse(
|
||||
request, "login.html", {"csrf_token": token_do_contexto(request), "next": next or ""}
|
||||
)
|
||||
|
||||
|
||||
@router.post("/login", dependencies=[Depends(exigir_csrf)])
|
||||
def login(
|
||||
request: Request,
|
||||
usuario: str = Form(...),
|
||||
senha: str = Form(...),
|
||||
next: Optional[str] = Form(None),
|
||||
):
|
||||
origem = _origem(request)
|
||||
if rate_limit.bloqueado(origem):
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"login.html",
|
||||
{
|
||||
"csrf_token": token_do_contexto(request), "next": next or "",
|
||||
"erro": "Muitas tentativas de login. Aguarde alguns minutos e tente novamente.",
|
||||
},
|
||||
status_code=429,
|
||||
)
|
||||
|
||||
usuario_logado = autenticar_credenciais(usuario, senha)
|
||||
if usuario_logado is None:
|
||||
rate_limit.registrar_falha(origem)
|
||||
return templates.TemplateResponse(
|
||||
request,
|
||||
"login.html",
|
||||
{"csrf_token": token_do_contexto(request), "next": next or "", "erro": "Usuário ou senha inválidos."},
|
||||
)
|
||||
rate_limit.limpar(origem)
|
||||
|
||||
destino = _proximo_seguro(next)
|
||||
resposta = RedirectResponse(url=destino, status_code=303)
|
||||
token = session.criar_token(usuario_logado.login, usuario_logado.nome, usuario_logado.perfil)
|
||||
resposta.set_cookie(
|
||||
session.SESSION_COOKIE_NAME, token, httponly=True, samesite="lax",
|
||||
secure=request.url.scheme == "https",
|
||||
)
|
||||
return resposta
|
||||
|
||||
|
||||
@router.post("/logout", dependencies=[Depends(exigir_csrf)])
|
||||
def logout(usuario: UsuarioLogado = Depends(exigir_login)):
|
||||
resposta = RedirectResponse(url="/login", status_code=303)
|
||||
resposta.delete_cookie(session.SESSION_COOKIE_NAME)
|
||||
resposta.delete_cookie(POR_PAGINA_COOKIE)
|
||||
return resposta
|
||||
Reference in New Issue
Block a user