""" Login por sessão. Duas fontes de credencial, checadas nessa ordem em autenticar_credenciais() (chamada tanto por POST /login quanto, indiretamente, por quem valida o cookie de sessão): 1. ADMIN_USER/ADMIN_PASSWORD (variáveis de ambiente) — conta única de bootstrap, útil para o primeiro acesso (cadastrar o primeiro funcionário em /funcionarios) e como plano B se a tabela de funcionários ficar vazia/inacessível. Continua exigindo as duas variáveis preenchidas (nunca autentica credencial vazia). Sempre perfil ADMIN, não depende do banco. 2. FUNCIONARIO_PAINEL (app/db/funcionarios.py) — conta individual por funcionário, com senha em hash (app/senha.py). Só autentica se ATIVO=1. Perfil vem da coluna PERFIL ("ADMIN" ou "PADRAO"). A credencial em si só é usada em POST /login (app/routers/auth.py), que troca por um cookie de sessão assinado (app/session.py) — as rotas do painel não recebem usuário/senha a cada requisição, só o cookie. """ import os import secrets from dataclasses import dataclass from fastapi import Depends, HTTPException, Request, status from . import session from .db import funcionarios as db_funcionarios from .senha import verificar_senha @dataclass class UsuarioLogado: login: str nome: str perfil: str # "ADMIN" ou "PADRAO" def __str__(self) -> str: return self.nome @property def eh_admin(self) -> bool: return self.perfil == "ADMIN" class NaoAutenticado(Exception): def __init__(self, caminho_original: str = "/"): self.caminho_original = caminho_original def autenticar_credenciais(usuario: str, senha: str) -> UsuarioLogado | None: usuario_admin = os.environ.get("ADMIN_USER", "") senha_admin = os.environ.get("ADMIN_PASSWORD", "") if usuario_admin and senha_admin: usuario_ok = secrets.compare_digest(usuario, usuario_admin) senha_ok = secrets.compare_digest(senha, senha_admin) if usuario_ok and senha_ok: return UsuarioLogado(login=usuario_admin, nome=usuario_admin, perfil="ADMIN") funcionario = db_funcionarios.obter_por_login(usuario) if funcionario and funcionario["ATIVO"] and verificar_senha(senha, funcionario["SENHA_HASH"]): return UsuarioLogado(login=funcionario["LOGIN"], nome=funcionario["NOME"], perfil=funcionario["PERFIL"]) return None def exigir_login(request: Request) -> UsuarioLogado: """ Decodifica o cookie de sessão e, para logins que existem em FUNCIONARIO_PAINEL, reconfirma ATIVO/PERFIL contra o banco a cada requisição — o cookie assinado tem validade de até 12h e não é revogado, então sem essa checagem desativar ou rebaixar um funcionário não teria efeito imediato (continuaria com acesso até o cookie expirar). A conta de bootstrap (ADMIN_USER do .env) não tem linha em FUNCIONARIO_PAINEL e continua sempre admin via variável de ambiente, sem consulta ao banco. """ token = request.cookies.get(session.SESSION_COOKIE_NAME) dados = session.ler_token(token) if dados is None: raise NaoAutenticado(caminho_original=request.url.path) usuario_admin = os.environ.get("ADMIN_USER", "") if usuario_admin and dados["login"] == usuario_admin: return UsuarioLogado(login=dados["login"], nome=dados["nome"], perfil="ADMIN") funcionario = db_funcionarios.obter_por_login(dados["login"]) if funcionario is None or not funcionario["ATIVO"]: raise NaoAutenticado(caminho_original=request.url.path) return UsuarioLogado(login=funcionario["LOGIN"], nome=funcionario["NOME"], perfil=funcionario["PERFIL"]) def exigir_admin(usuario: UsuarioLogado = Depends(exigir_login)) -> UsuarioLogado: if not usuario.eh_admin: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Só administradores acessam esta página.") return usuario