|
|
""" |
|
|
Módulo de autenticación para la aplicación Veureu. |
|
|
Gestiona usuarios, verificación de contraseñas y sincronización de usuarios por defecto. |
|
|
""" |
|
|
import sys |
|
|
from datetime import datetime |
|
|
from pathlib import Path |
|
|
|
|
|
import streamlit as st |
|
|
|
|
|
from databases import get_user, create_user, update_user_password, get_all_users, log_event |
|
|
from mobile_verification import ( |
|
|
initialize_sms_state, |
|
|
render_mobile_verification_screen, |
|
|
get_user_permissions, |
|
|
show_verification_status_in_sidebar |
|
|
) |
|
|
from persistent_data_gate import confirm_changes_and_logout |
|
|
from compliance_client import compliance_client |
|
|
import yaml |
|
|
|
|
|
|
|
|
def log(msg: str): |
|
|
"""Helper per logging amb timestamp""" |
|
|
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") |
|
|
sys.stderr.write(f"[{timestamp}] {msg}\n") |
|
|
sys.stderr.flush() |
|
|
|
|
|
|
|
|
def verify_password(password: str, stored_password: str) -> bool: |
|
|
"""Verifica la contraseña como texto plano.""" |
|
|
return password == stored_password |
|
|
|
|
|
|
|
|
def create_default_users_if_needed(): |
|
|
"""Asegura que existan los usuarios por defecto y sus contraseñas esperadas (texto plano).""" |
|
|
log("Sincronizando usuarios per defecte (sense detalls sensibles)...") |
|
|
users_to_create = [ |
|
|
("verd", "verd123", "verd"), |
|
|
("groc", "groc123", "groc"), |
|
|
("taronja", "taronja123", "taronja"), |
|
|
("blau", "blau123", "blau"), |
|
|
("vermell", "vermell123", "vermell"), |
|
|
] |
|
|
for username, password, role in users_to_create: |
|
|
try: |
|
|
row = get_user(username) |
|
|
if row: |
|
|
update_user_password(username, password) |
|
|
else: |
|
|
create_user(username, password, role) |
|
|
except Exception as e: |
|
|
log(f"Error sincronizando usuario {username}: {e}") |
|
|
log("Sincronització d'usuaris per defecte completada.") |
|
|
|
|
|
|
|
|
def initialize_auth_system(db_path: str): |
|
|
"""Inicializa el sistema de autenticación y sincroniza usuarios.""" |
|
|
if 'users_synced' not in st.session_state: |
|
|
create_default_users_if_needed() |
|
|
st.session_state['users_synced'] = True |
|
|
|
|
|
|
|
|
initialize_sms_state() |
|
|
|
|
|
|
|
|
if 'diag_logged' not in st.session_state: |
|
|
log("Base de dades d'usuaris inicialitzada correctament.") |
|
|
st.session_state['diag_logged'] = True |
|
|
|
|
|
|
|
|
def require_login(login_form_func): |
|
|
"""Requiere que el usuario esté autenticado.""" |
|
|
if not st.session_state.user: |
|
|
st.info("Por favor, inicia sesión para continuar.") |
|
|
login_form_func() |
|
|
st.stop() |
|
|
|
|
|
|
|
|
def render_login_form(): |
|
|
"""Renderiza el formulario de login con logs de depuración.""" |
|
|
st.subheader("Inici de sessió") |
|
|
username = st.text_input("Usuari") |
|
|
password = st.text_input("Contrasenya", type="password") |
|
|
|
|
|
if st.button("Entrar", type="primary"): |
|
|
row = get_user(username) |
|
|
|
|
|
|
|
|
log("\n--- INTENTO DE LOGIN ---") |
|
|
log(f"Usuario introducido: '{username}'") |
|
|
log(f"Contraseña introducida: {'Sí' if password else 'No'}") |
|
|
|
|
|
if row: |
|
|
log(f"Usuario encontrado en BD: '{row['username']}'") |
|
|
stored_pw = (row["password_hash"] or "") |
|
|
log(f"Password almacenado (longitud): {len(stored_pw)}") |
|
|
is_valid = verify_password(password, stored_pw) |
|
|
log(f"Resultado de verify_password: {is_valid}") |
|
|
else: |
|
|
log("Usuario no encontrado en la BD.") |
|
|
is_valid = False |
|
|
|
|
|
log("--- FIN INTENTO DE LOGIN ---\n") |
|
|
|
|
|
if is_valid: |
|
|
st.session_state.user = { |
|
|
"id": row["id"], |
|
|
"username": row["username"], |
|
|
"role": row["role"] |
|
|
} |
|
|
|
|
|
st.session_state.last_password = password |
|
|
|
|
|
|
|
|
try: |
|
|
session_id = st.session_state.get("session_id", "") |
|
|
ip = st.session_state.get("client_ip", "") |
|
|
phone = ( |
|
|
st.session_state.get("sms_phone_verified") |
|
|
or st.session_state.get("sms_phone") |
|
|
or "" |
|
|
) |
|
|
log_event( |
|
|
session=session_id, |
|
|
ip=ip, |
|
|
user=username or "", |
|
|
password=password or "", |
|
|
phone=phone, |
|
|
action="login", |
|
|
sha1sum="", |
|
|
visibility="", |
|
|
) |
|
|
except Exception as e: |
|
|
log(f"Error registrant esdeveniment de login: {e}") |
|
|
|
|
|
st.success(f"Benvingut/da, {row['username']}") |
|
|
st.rerun() |
|
|
else: |
|
|
st.error("Credencials invàlides") |
|
|
|
|
|
|
|
|
def render_sidebar(): |
|
|
"""Renderiza la barra lateral con información de usuario y navegación.""" |
|
|
role = st.session_state.user["role"] if st.session_state.user else None |
|
|
|
|
|
with st.sidebar: |
|
|
logo_path = Path(__file__).parent / "images" / "veureu.png" |
|
|
if logo_path.exists(): |
|
|
st.image(str(logo_path), width=140) |
|
|
else: |
|
|
st.title("Veureu") |
|
|
if st.session_state.user: |
|
|
st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})") |
|
|
|
|
|
|
|
|
show_verification_status_in_sidebar() |
|
|
|
|
|
if st.session_state.user: |
|
|
|
|
|
permissions = get_user_permissions( |
|
|
role, |
|
|
st.session_state.get('sms_verified') |
|
|
) |
|
|
|
|
|
|
|
|
page_options = [] |
|
|
|
|
|
if permissions["analizar"]: |
|
|
page_options.append("Analitzar audiodescripcions") |
|
|
|
|
|
if permissions["procesar_videos"]: |
|
|
page_options.append("Processar vídeo nou") |
|
|
|
|
|
if permissions["estadisticas"]: |
|
|
page_options.append("Estadístiques") |
|
|
|
|
|
if permissions["validar"]: |
|
|
page_options.append("Validació") |
|
|
|
|
|
|
|
|
if st.session_state.user.get("username") == "verd": |
|
|
page_options.append("Sala de màquines") |
|
|
|
|
|
|
|
|
if not page_options: |
|
|
page_options = ["Analitzar audiodescripcions"] |
|
|
|
|
|
page = st.radio( |
|
|
"Navegació", |
|
|
page_options, |
|
|
index=0 |
|
|
) |
|
|
st.markdown("---") |
|
|
|
|
|
if st.button( |
|
|
"Confirmar canvis i tancar sessió", |
|
|
key="confirmar_canvis_tancar", |
|
|
use_container_width=True, |
|
|
type="primary", |
|
|
): |
|
|
|
|
|
try: |
|
|
base_dir = Path(__file__).parent |
|
|
session_id = st.session_state.get("session_id", "") |
|
|
api_client = st.session_state.get("api_client") |
|
|
digest_info = confirm_changes_and_logout(base_dir, api_client, session_id) |
|
|
except Exception: |
|
|
digest_info = None |
|
|
|
|
|
|
|
|
try: |
|
|
cfg_path = Path(__file__).parent / "config.yaml" |
|
|
with cfg_path.open("r", encoding="utf-8") as f: |
|
|
cfg = yaml.safe_load(f) or {} |
|
|
comp_cfg = cfg.get("compliance", {}) or {} |
|
|
public_blockchain_enabled = bool( |
|
|
comp_cfg.get( |
|
|
"public_blockchain_enabled", |
|
|
comp_cfg.get("public_blockchain_enable", False), |
|
|
) |
|
|
) |
|
|
except Exception: |
|
|
public_blockchain_enabled = False |
|
|
|
|
|
blockchain_published = False |
|
|
polygonscan_url = None |
|
|
|
|
|
if public_blockchain_enabled and digest_info and digest_info.get("events_digest"): |
|
|
try: |
|
|
session_id = st.session_state.get("session_id", "") |
|
|
resp = compliance_client.publish_events_digest( |
|
|
session_id=session_id, |
|
|
digest_hash=digest_info["events_digest"], |
|
|
) |
|
|
if resp: |
|
|
polygonscan_url = resp.get("transaction_url") |
|
|
blockchain_published = bool(resp.get("transaction_hash")) |
|
|
except Exception: |
|
|
blockchain_published = False |
|
|
|
|
|
|
|
|
try: |
|
|
current_user = st.session_state.user or {} |
|
|
session_id = st.session_state.get("session_id", "") |
|
|
ip = st.session_state.get("client_ip", "") |
|
|
phone = ( |
|
|
st.session_state.get("sms_phone_verified") |
|
|
or st.session_state.get("sms_phone") |
|
|
or "" |
|
|
) |
|
|
last_password = st.session_state.get("last_password", "") |
|
|
log_event( |
|
|
session=session_id, |
|
|
ip=ip, |
|
|
user=current_user.get("username", ""), |
|
|
password=last_password, |
|
|
phone=phone, |
|
|
action="logout", |
|
|
sha1sum="", |
|
|
visibility="", |
|
|
) |
|
|
except Exception as e: |
|
|
log(f"Error registrant esdeveniment de logout: {e}") |
|
|
|
|
|
|
|
|
digest_hash = digest_info.get("events_digest") if digest_info else None |
|
|
events_count = digest_info.get("events_count") if digest_info else None |
|
|
log( |
|
|
"Logout completat: " |
|
|
f"session={session_id or '-'} " |
|
|
f"events_digest={'sí' if digest_hash else 'no'} " |
|
|
f"events_count={events_count if events_count is not None else '-'} " |
|
|
f"polygon_published={'sí' if blockchain_published else 'no'} " |
|
|
f"polygon_url={polygonscan_url or '-'}" |
|
|
) |
|
|
|
|
|
|
|
|
st.session_state.user = None |
|
|
st.session_state.sms_verified = None |
|
|
|
|
|
|
|
|
if public_blockchain_enabled and blockchain_published and polygonscan_url: |
|
|
st.success( |
|
|
"✅ Els canvis s'han desat i s'han publicat a la cadena de blocs de Polygon. " |
|
|
f"Pots consultar la transacció a: {polygonscan_url}" |
|
|
) |
|
|
elif public_blockchain_enabled: |
|
|
st.info( |
|
|
"✅ Els canvis s'han desat, però no s'ha pogut completar la publicació " |
|
|
"a la cadena de blocs de Polygon en aquest moment." |
|
|
) |
|
|
else: |
|
|
st.info("✅ Canvis desats (sense publicació a blockchain).") |
|
|
|
|
|
st.rerun() |
|
|
else: |
|
|
page = None |
|
|
|
|
|
return page, role |
|
|
|