|
|
""" |
|
|
Módulo de verificación por móvil/SMS para usuarios que requieren validación adicional. |
|
|
|
|
|
Este módulo gestiona: |
|
|
- Mostrar términos y condiciones de uso |
|
|
- Solicitar número de teléfono móvil |
|
|
- Enviar código de verificación por SMS (simulado en demo) |
|
|
- Verificar código introducido por el usuario |
|
|
""" |
|
|
|
|
|
import sys |
|
|
import os |
|
|
import random |
|
|
import string |
|
|
from pathlib import Path |
|
|
|
|
|
import yaml |
|
|
import streamlit as st |
|
|
from datetime import datetime |
|
|
from typing import Optional, Tuple |
|
|
|
|
|
from compliance_client import compliance_client |
|
|
|
|
|
|
|
|
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 _load_automation_flags() -> dict: |
|
|
"""Llegeix la secció 'automation' de demo/config.yaml. |
|
|
|
|
|
Retorna un dict amb, com a mínim: |
|
|
- twilio_enabled (bool) |
|
|
- zapier_enabled (bool) |
|
|
""" |
|
|
|
|
|
base_dir = Path(__file__).parent |
|
|
cfg_path = base_dir / "config.yaml" |
|
|
flags = { |
|
|
"twilio_enabled": False, |
|
|
"zapier_enabled": False, |
|
|
} |
|
|
|
|
|
try: |
|
|
with cfg_path.open("r", encoding="utf-8") as f: |
|
|
cfg = yaml.safe_load(f) or {} |
|
|
automation = cfg.get("automation", {}) or {} |
|
|
flags["twilio_enabled"] = bool(automation.get("twilio_enabled", False)) |
|
|
flags["zapier_enabled"] = bool(automation.get("zapier_enabled", False)) |
|
|
except Exception: |
|
|
|
|
|
return flags |
|
|
|
|
|
return flags |
|
|
|
|
|
|
|
|
def initialize_sms_state(): |
|
|
"""Inicializa el estado de verificación SMS si no existe""" |
|
|
if 'sms_step' not in st.session_state: |
|
|
st.session_state.sms_step = 'phone' |
|
|
if 'sms_code' not in st.session_state: |
|
|
st.session_state.sms_code = None |
|
|
if 'sms_phone' not in st.session_state: |
|
|
st.session_state.sms_phone = None |
|
|
if 'sms_verified' not in st.session_state: |
|
|
st.session_state.sms_verified = None |
|
|
|
|
|
|
|
|
def send_sms_code(phone_number: str) -> bool: |
|
|
""" |
|
|
Simula el envío de código SMS (en producción usaría un servicio real como Twilio). |
|
|
|
|
|
Args: |
|
|
phone_number: Número de teléfono completo con código de país |
|
|
|
|
|
Returns: |
|
|
True si el envío fue exitoso, False en caso de error |
|
|
""" |
|
|
try: |
|
|
|
|
|
code = "".join(random.choice(string.ascii_uppercase) for _ in range(4)) |
|
|
st.session_state.sms_code = code |
|
|
|
|
|
|
|
|
flags = _load_automation_flags() |
|
|
twilio_enabled = bool(flags.get("twilio_enabled", True)) |
|
|
|
|
|
|
|
|
if not twilio_enabled: |
|
|
log(f"[SMS] Twilio desactivat a config.yaml; mostrant codi en pantalla sense enviar SMS: {code}") |
|
|
st.info(f"El teu codi de verificació és: **{code}**. Introdueix-lo al camp de verificació per continuar.") |
|
|
return True |
|
|
|
|
|
|
|
|
try: |
|
|
|
|
|
normalized_phone = phone_number.strip().replace(" ", "") |
|
|
if not normalized_phone.startswith("+"): |
|
|
normalized_phone = "+34" + normalized_phone |
|
|
|
|
|
log(f"[SMS] Codi generat per a {normalized_phone}: {code}") |
|
|
|
|
|
success = compliance_client.send_login_sms(normalized_phone, code) |
|
|
if not success: |
|
|
log("[SMS] Error retornat per compliance en enviar l'SMS de login") |
|
|
return False |
|
|
|
|
|
|
|
|
try: |
|
|
from databases import log_action |
|
|
|
|
|
session_id = st.session_state.get("session_id", "") |
|
|
user_obj = st.session_state.get("user") or {} |
|
|
username = ( |
|
|
user_obj.get("username") |
|
|
if isinstance(user_obj, dict) |
|
|
else str(user_obj or "") |
|
|
) |
|
|
|
|
|
log_action( |
|
|
session=session_id, |
|
|
user=username, |
|
|
phone=normalized_phone, |
|
|
action="SMS sent for login verification", |
|
|
sha1sum="", |
|
|
) |
|
|
except Exception: |
|
|
pass |
|
|
|
|
|
return True |
|
|
except Exception as e: |
|
|
log(f"[SMS] Error generant/enviant codi via compliance: {e}") |
|
|
return False |
|
|
|
|
|
except Exception as e: |
|
|
log(f"[SMS] Error generant/enviant codi: {e}") |
|
|
return False |
|
|
|
|
|
|
|
|
def get_terms_and_conditions() -> str: |
|
|
"""Retorna el texto completo de términos y condiciones""" |
|
|
return """ |
|
|
### **Condicions d'ús del sistema Veureu** |
|
|
|
|
|
En iniciar sessió i pujar un vídeo al sistema Veureu, l'usuari declara i accepta el següent: |
|
|
|
|
|
**📋 Declaracions:** |
|
|
- Declara ser titular dels drets d'ús i difusió del vídeo, o disposar de l'autorització expressa dels titulars. |
|
|
- Declara que totes les persones identificables en el vídeo han atorgat el seu consentiment informat per a la seva utilització amb finalitats d'accessibilitat i audiodescripció. |
|
|
- Declara que el vídeo no conté: |
|
|
- Contingut violent, sexual o d'odi, |
|
|
- Informació confidencial o de caràcter sensible, |
|
|
- Imatges de menors sense consentiment parental verificable. |
|
|
|
|
|
**✅ Acceptacions:** |
|
|
- Accepta que es processi el vídeo exclusivament amb finalitats de generació i validació d'audiodescripcions, conforme a la normativa UNE-153010:2020 i al RGPD. |
|
|
- Accepta que el sistema pugui enviar les dades necessàries a proveïdors tecnològics externs (p. ex., models d'IA) que actuen com a encarregats de tractament, sota clàusules contractuals tipus i sense reutilització de dades. |
|
|
- Accepta que les accions realitzades (pujada, acceptació, validació, revocació) siguin registrades en un sistema immutable (AWS QLDB) mitjançant identificadors no personals. |
|
|
- Pot exercir en qualsevol moment el seu dret a revocar el consentiment mitjançant el botó "Revocar permisos", el que eliminarà el material audiovisual i deixarà constància de la revocació en el registre. |
|
|
- Accepta que, fins a la validació interna del material per part de l'equip Veureu, el vídeo romandrà en estat "pendent de validació" i no serà utilitzat públicament. |
|
|
""" |
|
|
|
|
|
|
|
|
def render_mobile_verification_screen(username: str, role: str) -> Optional[bool]: |
|
|
""" |
|
|
Renderiza la pantalla de verificación por móvil post-login. |
|
|
|
|
|
Args: |
|
|
username: Nombre de usuario autenticado |
|
|
role: Rol del usuario |
|
|
|
|
|
Returns: |
|
|
True si el usuario completó la verificación |
|
|
False si omitió la verificación (solo modo análisis) |
|
|
None si aún está en proceso |
|
|
""" |
|
|
st.title("Veureu — Audiodescripció") |
|
|
st.markdown(f"Benvingut/da, **{username}**") |
|
|
|
|
|
|
|
|
if role in ["groc", "blau"]: |
|
|
st.info(""" |
|
|
### 📱 Verificació per SMS requerida |
|
|
|
|
|
Per accedir a les funcions completes del sistema (pujar vídeos, validar contingut), |
|
|
cal verificar el seu número de mòbil i acceptar les condicions d'ús. |
|
|
|
|
|
**Si únicament vol analitzar i modificar les audiodescripcions existents, pot ometre aquest pas.** |
|
|
""") |
|
|
|
|
|
|
|
|
with st.expander("📋 Verificació per SMS", expanded=True): |
|
|
|
|
|
st.markdown(get_terms_and_conditions()) |
|
|
|
|
|
st.markdown("---") |
|
|
st.markdown("#### 📱 Dades de verificació") |
|
|
|
|
|
|
|
|
col_country, col_phone = st.columns([1, 3]) |
|
|
with col_country: |
|
|
country_code = st.selectbox("País", [ |
|
|
("🇪🇸 +34", "+34"), |
|
|
("🇫🇷 +33", "+33"), |
|
|
("🇬🇧 +44", "+44"), |
|
|
("🇩🇪 +49", "+49"), |
|
|
("🇮🇹 +39", "+39") |
|
|
], format_func=lambda x: x[0])[1] |
|
|
|
|
|
with col_phone: |
|
|
phone_number = st.text_input( |
|
|
"Número de telèfon" + (" (opcional)" if role == "groc" else ""), |
|
|
placeholder="600 123 000", |
|
|
max_chars=15 |
|
|
) |
|
|
|
|
|
|
|
|
accept_terms = st.checkbox( |
|
|
"✅ Accepto les condicions d'ús i la política de privadesa", |
|
|
key="mobile_accept_terms" |
|
|
) |
|
|
|
|
|
|
|
|
if st.button( |
|
|
"📤 Enviar codi de verificació", |
|
|
type="primary", |
|
|
disabled=not phone_number, |
|
|
): |
|
|
full_phone = f"{country_code}{phone_number}" |
|
|
|
|
|
|
|
|
if not accept_terms: |
|
|
st.error("Has d'acceptar les condicions d'ús abans d'enviar el codi de verificació.") |
|
|
else: |
|
|
|
|
|
with st.spinner(f"Enviant SMS de verificació a {full_phone}..."): |
|
|
|
|
|
if send_sms_code(full_phone): |
|
|
st.session_state.sms_step = 'verify' |
|
|
st.session_state.sms_phone = full_phone |
|
|
st.success(f"✅ Codi enviat a {full_phone}") |
|
|
else: |
|
|
st.error("❌ Error enviant el codi. Torna-ho a intentar.") |
|
|
|
|
|
|
|
|
if st.session_state.sms_step == 'verify': |
|
|
st.markdown("#### 🔓 Verificació del codi") |
|
|
|
|
|
col_code, col_resend = st.columns([2, 1]) |
|
|
with col_code: |
|
|
verification_code = st.text_input( |
|
|
"Codi de 4 lletres", |
|
|
max_chars=4, |
|
|
placeholder="ABCD", |
|
|
key="mobile_verification_code" |
|
|
) |
|
|
|
|
|
with col_resend: |
|
|
st.markdown("<br>", unsafe_allow_html=True) |
|
|
if st.button("🔄 Reenviar", key="mobile_resend"): |
|
|
if send_sms_code(st.session_state.sms_phone): |
|
|
st.success("✅ Nou codi enviat") |
|
|
else: |
|
|
st.error("❌ Error enviant el codi") |
|
|
|
|
|
if st.button("🔓 Verificar i continuar", type="primary"): |
|
|
if verification_code.strip().upper() == str(st.session_state.sms_code).strip().upper(): |
|
|
st.session_state.sms_verified = True |
|
|
st.session_state.sms_phone_verified = st.session_state.sms_phone |
|
|
|
|
|
st.session_state.sms_step = 'phone' |
|
|
st.session_state.sms_code = None |
|
|
st.session_state.sms_phone = None |
|
|
st.success("✅ Verificació completada! Ara pot accedir a totes les funcions.") |
|
|
log(f"[SMS] Usuario {username} verificado correctamente") |
|
|
st.rerun() |
|
|
return True |
|
|
else: |
|
|
st.error("❌ El codi no és correcte. Torna-ho a intentar.") |
|
|
|
|
|
|
|
|
st.markdown("---") |
|
|
col_continue, col_skip = st.columns([1, 1]) |
|
|
|
|
|
with col_continue: |
|
|
if st.session_state.sms_verified is True: |
|
|
if st.button("🚪 Accedir amb permisos complets", type="primary"): |
|
|
st.success("✅ Accedint a l'aplicació amb permisos complets...") |
|
|
st.rerun() |
|
|
return True |
|
|
|
|
|
with col_skip: |
|
|
|
|
|
if role in ["groc", "blau"]: |
|
|
if st.button("⏭️ Continuar sense verificació (només anàlisi)"): |
|
|
st.session_state.sms_verified = False |
|
|
st.info("✅ Sessió iniciada. Pot accedir a les funcions d'anàlisi d'audiodescripcions.") |
|
|
log(f"[SMS] Usuario {username} omitió verificación SMS") |
|
|
st.rerun() |
|
|
return False |
|
|
|
|
|
return None |
|
|
|
|
|
|
|
|
def get_user_permissions(role: str, sms_verified: Optional[bool]) -> dict: |
|
|
""" |
|
|
Retorna los permisos del usuario según su rol y estado de verificación SMS. |
|
|
|
|
|
Args: |
|
|
role: Rol del usuario (verd, groc, blau, taronja, vermell) |
|
|
sms_verified: Estado de verificación SMS (True, False, None) |
|
|
|
|
|
Returns: |
|
|
Diccionario con permisos booleanos |
|
|
""" |
|
|
|
|
|
base_permissions = { |
|
|
"verd": { |
|
|
"analizar": True, |
|
|
"procesar_videos": True, |
|
|
"valorar": True, |
|
|
"validar": True, |
|
|
"estadisticas": True, |
|
|
"requires_sms": False |
|
|
}, |
|
|
"groc": { |
|
|
"analizar": True, |
|
|
"procesar_videos": True, |
|
|
"valorar": True, |
|
|
"validar": False, |
|
|
"estadisticas": True, |
|
|
"requires_sms": True |
|
|
}, |
|
|
"blau": { |
|
|
"analizar": True, |
|
|
"procesar_videos": False, |
|
|
"valorar": True, |
|
|
"validar": True, |
|
|
"estadisticas": True, |
|
|
"requires_sms": True |
|
|
}, |
|
|
"taronja": { |
|
|
"analizar": True, |
|
|
"procesar_videos": False, |
|
|
"valorar": True, |
|
|
"validar": False, |
|
|
"estadisticas": True, |
|
|
"requires_sms": False |
|
|
}, |
|
|
"vermell": { |
|
|
"analizar": True, |
|
|
"procesar_videos": False, |
|
|
"valorar": False, |
|
|
"validar": False, |
|
|
"estadisticas": True, |
|
|
"requires_sms": False |
|
|
} |
|
|
} |
|
|
|
|
|
permissions = base_permissions.get(role, base_permissions["vermell"]).copy() |
|
|
|
|
|
|
|
|
if permissions["requires_sms"]: |
|
|
if sms_verified is False: |
|
|
|
|
|
permissions["procesar_videos"] = False |
|
|
permissions["validar"] = False |
|
|
elif sms_verified is None: |
|
|
|
|
|
permissions["procesar_videos"] = False |
|
|
permissions["validar"] = False |
|
|
|
|
|
|
|
|
return permissions |
|
|
|
|
|
|
|
|
def show_verification_status_in_sidebar(): |
|
|
"""Muestra el estado de verificación SMS en la barra lateral""" |
|
|
if st.session_state.get('sms_verified') is True: |
|
|
st.success("📱 Verificació SMS activada") |
|
|
elif st.session_state.get('sms_verified') is False: |
|
|
st.warning("⚠️ Només mode anàlisi") |
|
|
else: |
|
|
st.info("⏳ Verificació pendent") |
|
|
|