IOI-RUN / login.py
Roudrigus's picture
Update login.py
d6d6963 verified
# -*- coding: utf-8 -*-
"""
login.py — Sistema de autenticação seguro para Streamlit
Compatível com HuggingFace Spaces (Linux) e Windows.
Recursos:
- Login normal (usuários no banco SQLite/Postgres/MySQL)
- Hash seguro com bcrypt
- Autologin para testes (DISABLE_AUTH=1)
- Login emergencial (ALLOW_EMERGENCY_LOGIN=1 + EMERG_USER + EMERG_PASS_BCRYPT)
- Auditoria opcional via registrar_log
- Compatível com múltiplos nomes de coluna de senha (senha_hash, password_hash, senha, etc.)
"""
import os
import bcrypt
import streamlit as st
# Auditoria opcional
try:
from utils_auditoria import registrar_log
_HAS_AUDIT = True
except Exception:
_HAS_AUDIT = False
# Banco (ORM)
from banco import SessionLocal
from models import Usuario
# Flags
_DISABLE_AUTH = os.getenv("DISABLE_AUTH", "0") == "1"
_ALLOW_EMERG = os.getenv("ALLOW_EMERGENCY_LOGIN", "0") == "1"
_DEMO_USER = os.getenv("DEMO_USER", "demo")
_DEMO_PERFIL = os.getenv("DEMO_PERFIL", "admin")
_DEMO_EMAIL = os.getenv("DEMO_EMAIL", "demo@example.com")
# ===== Helpers de compatibilidade com modelos diferentes =====
# Você pode forçar o(s) nome(s) da(s) coluna(s) de senha via Secrets:
# PASSWORD_FIELD=senha (ou lista: "senha,password_hash")
_PASSWORD_FIELDS_ENV = os.getenv("PASSWORD_FIELD", "").strip()
_PASSWORD_FIELDS = [f.strip() for f in _PASSWORD_FIELDS_ENV.split(",") if f.strip()]
# candidatos padrão, em ordem de preferência
_DEFAULT_PASS_FIELDS = ["senha_hash", "password_hash", "senha", "hash", "pass_hash", "pwd_hash"]
def _get_password_hash_from_row(row) -> str | None:
"""Retorna o hash de senha a partir do objeto ORM 'row', testando vários nomes possíveis."""
# 1) Nomes vindos do ambiente, se houver
for fname in (_PASSWORD_FIELDS or []):
if hasattr(row, fname):
v = getattr(row, fname)
if v:
return str(v)
# 2) Nomes padrão
for fname in _DEFAULT_PASS_FIELDS:
if hasattr(row, fname):
v = getattr(row, fname)
if v:
return str(v)
# 3) Fallback: inspeciona __dict__ por chaves "parecidas"
try:
for k, v in getattr(row, "__dict__", {}).items():
if k.startswith("_"):
continue
if k.lower() in set(_DEFAULT_PASS_FIELDS):
if v:
return str(v)
except Exception:
pass
return None
def _get_user_identity_fields(row):
"""
Retorna (usuario, perfil, email) com fallbacks para nomes alternativos.
"""
usuario = (
getattr(row, "usuario", None)
or getattr(row, "username", None)
or getattr(row, "login", None)
or getattr(row, "nome_usuario", None)
)
perfil = (
getattr(row, "perfil", None)
or getattr(row, "role", None)
or getattr(row, "papel", None)
or "usuario"
)
email = (
getattr(row, "email", None)
or getattr(row, "e_mail", None)
or getattr(row, "mail", None)
)
return usuario, perfil, email
def _fetch_user_by_login(db, user_login: str):
"""
Busca um usuário tentando múltiplas colunas de login, sem quebrar se alguma não existir.
Ordem: usuario, username, login, email.
"""
candidates = ["usuario", "username", "login", "email"]
for field in candidates:
try:
col = getattr(Usuario, field)
row = db.query(Usuario).filter(col == user_login.strip()).first()
if row:
return row
except Exception:
continue
return None
# =========================================================
# 🛡️ HASH DE SENHAS
# =========================================================
def verificar_hash(senha_digitada: str, senha_hash: str) -> bool:
"""Retorna True se a senha confere com o hash bcrypt."""
try:
return bcrypt.checkpw(
senha_digitada.encode("utf-8"),
senha_hash.encode("utf-8")
)
except Exception:
return False
# =========================================================
# 🔐 AUTOLOGIN (apenas para testes)
# =========================================================
def _autologin_if_allowed() -> bool:
if not _DISABLE_AUTH:
return False
st.session_state.logado = True
st.session_state.usuario = _DEMO_USER
st.session_state.perfil = _DEMO_PERFIL
st.session_state.email = _DEMO_EMAIL
if _HAS_AUDIT:
try:
registrar_log(
usuario=_DEMO_USER,
acao="Autologin (DISABLE_AUTH=1)",
tabela="login",
registro_id=None
)
except Exception:
pass
st.success(f"🔓 Autologin ativo: {_DEMO_USER} ({_DEMO_PERFIL})")
return True
# =========================================================
# 🚨 LOGIN EMERGENCIAL
# =========================================================
def _try_emergency_login(usuario: str, senha: str) -> bool:
if not _ALLOW_EMERG:
return False
EMU = os.getenv("EMERG_USER")
EHP = os.getenv("EMERG_PASS_BCRYPT") # hash bcrypt
if not EMU or not EHP:
return False
if usuario.strip().lower() != EMU.strip().lower():
return False
if not verificar_hash(senha, EHP):
return False
st.session_state.logado = True
st.session_state.usuario = EMU
st.session_state.perfil = "admin"
st.session_state.email = f"{EMU}@local"
st.success("🔑 Login emergencial bem-sucedido.")
if _HAS_AUDIT:
try:
registrar_log(
usuario=EMU,
acao="Login emergencial",
tabela="login",
registro_id=None
)
except Exception:
pass
return True
# =========================================================
# 🔑 LOGIN NORMAL
# =========================================================
def _login_normal(usuario: str, senha: str) -> bool:
db = SessionLocal()
try:
row = _fetch_user_by_login(db, usuario)
except Exception:
row = None
finally:
try:
db.close()
except Exception:
pass
if not row:
return False
# Obtém o hash de senha de forma robusta
pwd_hash = _get_password_hash_from_row(row)
if not pwd_hash:
# Sem hash → não conseguimos validar a senha
return False
if not verificar_hash(senha, pwd_hash):
return False
# Preenche identidade do usuário com fallbacks
u, p, e = _get_user_identity_fields(row)
st.session_state.logado = True
st.session_state.usuario = u or usuario
st.session_state.perfil = (p or "usuario").strip().lower()
st.session_state.email = e
if _HAS_AUDIT:
try:
registrar_log(
usuario=st.session_state.usuario,
acao="Login normal",
tabela="login",
registro_id=getattr(row, "id", None)
)
except Exception:
pass
return True
# =========================================================
# 🎯 TELA DE LOGIN
# =========================================================
def login():
# 1) Autologin de teste
if _autologin_if_allowed():
return
st.markdown("### 🔐 Login")
with st.form("form_login"):
usuario = st.text_input("Usuário")
senha = st.text_input("Senha", type="password")
btn = st.form_submit_button("Entrar")
if btn:
usuario = usuario.strip()
senha = senha.strip()
# Login normal
if _login_normal(usuario, senha):
st.rerun()
return
# Login emergencial
if _try_emergency_login(usuario, senha):
st.rerun()
return
st.error("Usuário ou senha incorretos.")
# Exibe link do login emergencial apenas se habilitado
if _ALLOW_EMERG:
st.info("🔑 Login emergencial disponível.")
# =========================================================
# Utilitário (opcional)
# =========================================================
def logout():
st.session_state.logado = False
st.session_state.usuario = None
st.session_state.perfil = None
st.session_state.email = None
st.rerun()