| | """ |
| | 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") |
| |
|