VeuReu commited on
Commit
867cffa
·
1 Parent(s): 4e0481a

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +25 -2
  2. auth.py +38 -1
  3. mobile_verification.py +311 -0
app.py CHANGED
@@ -16,6 +16,7 @@ from database import set_db_path, init_schema, create_video, update_video_status
16
  from api_client import APIClient
17
  from utils import ensure_dirs, save_bytes, save_text, human_size
18
  from auth import initialize_auth_system, render_login_form, render_sidebar, require_login
 
19
 
20
 
21
  def _get_video_duration(path: str) -> float:
@@ -129,11 +130,33 @@ if not st.session_state.user:
129
  render_login_form()
130
  st.stop()
131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  # --- Pages ---
133
  if page == "Processar vídeo nou":
134
  require_login(render_login_form)
135
- if role != "verd":
136
- st.error("No tens permisos per processar nous vídeos. Canvia d'usuari o sol·licita permisos.")
 
 
 
137
  st.stop()
138
 
139
  st.header("Processar un nou clip de vídeo")
 
16
  from api_client import APIClient
17
  from utils import ensure_dirs, save_bytes, save_text, human_size
18
  from auth import initialize_auth_system, render_login_form, render_sidebar, require_login
19
+ from mobile_verification import render_mobile_verification_screen, get_user_permissions
20
 
21
 
22
  def _get_video_duration(path: str) -> float:
 
130
  render_login_form()
131
  st.stop()
132
 
133
+ # Post-login: Verificación por móvil si es necesaria
134
+ if st.session_state.user and 'sms_verified' not in st.session_state:
135
+ st.session_state.sms_verified = None
136
+
137
+ if st.session_state.user:
138
+ username = st.session_state.user['username']
139
+ role = st.session_state.user['role']
140
+
141
+ # Obtener permisos para ver si requiere SMS
142
+ permissions = get_user_permissions(role, st.session_state.get('sms_verified'))
143
+
144
+ # Si requiere SMS y aún no está verificado/omitido, mostrar pantalla de verificación
145
+ if permissions["requires_sms"] and st.session_state.sms_verified is None:
146
+ result = render_mobile_verification_screen(username, role)
147
+ if result is None:
148
+ # Aún en proceso de verificación
149
+ st.stop()
150
+ # Si result es True o False, ya se ha completado/omitido y continúa
151
+
152
  # --- Pages ---
153
  if page == "Processar vídeo nou":
154
  require_login(render_login_form)
155
+
156
+ # Verificar permisos
157
+ permissions = get_user_permissions(role, st.session_state.get('sms_verified'))
158
+ if not permissions["procesar_videos"]:
159
+ st.error("No tens permisos per processar nous vídeos. Verifica el teu mòbil per obtenir accés complet.")
160
  st.stop()
161
 
162
  st.header("Processar un nou clip de vídeo")
auth.py CHANGED
@@ -6,6 +6,12 @@ import sys
6
  import streamlit as st
7
  from datetime import datetime
8
  from database import get_user, create_user, update_user_password, get_all_users
 
 
 
 
 
 
9
 
10
 
11
  def log(msg: str):
@@ -28,6 +34,7 @@ def create_default_users_if_needed():
28
  ("groc", "groc123", "groc"),
29
  ("taronja", "taronja123", "taronja"),
30
  ("blau", "blau123", "blau"),
 
31
  ]
32
  for username, password, role in users_to_create:
33
  try:
@@ -48,6 +55,9 @@ def initialize_auth_system(db_path: str):
48
  create_default_users_if_needed()
49
  st.session_state['users_synced'] = True
50
 
 
 
 
51
  # Diagnóstico de base de datos
52
  if 'diag_logged' not in st.session_state:
53
  log("\n--- DIAGNÓSTICO DE BASE DE DATOS ---")
@@ -120,14 +130,41 @@ def render_sidebar():
120
  st.title("Veureu")
121
  if st.session_state.user:
122
  st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
 
 
 
 
123
  if st.button("Tancar sessió"):
124
  st.session_state.user = None
 
125
  st.rerun()
126
 
127
  if st.session_state.user:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  page = st.radio(
129
  "Navegació",
130
- ["Analitzar video-transcripcions", "Processar vídeo nou", "Estadístiques"],
131
  index=0
132
  )
133
  else:
 
6
  import streamlit as st
7
  from datetime import datetime
8
  from database import get_user, create_user, update_user_password, get_all_users
9
+ from mobile_verification import (
10
+ initialize_sms_state,
11
+ render_mobile_verification_screen,
12
+ get_user_permissions,
13
+ show_verification_status_in_sidebar
14
+ )
15
 
16
 
17
  def log(msg: str):
 
34
  ("groc", "groc123", "groc"),
35
  ("taronja", "taronja123", "taronja"),
36
  ("blau", "blau123", "blau"),
37
+ ("vermell", "vermell123", "vermell"),
38
  ]
39
  for username, password, role in users_to_create:
40
  try:
 
55
  create_default_users_if_needed()
56
  st.session_state['users_synced'] = True
57
 
58
+ # Inicializar estado de verificación SMS
59
+ initialize_sms_state()
60
+
61
  # Diagnóstico de base de datos
62
  if 'diag_logged' not in st.session_state:
63
  log("\n--- DIAGNÓSTICO DE BASE DE DATOS ---")
 
130
  st.title("Veureu")
131
  if st.session_state.user:
132
  st.write(f"Usuari: **{st.session_state.user['username']}** (rol: {st.session_state.user['role']})")
133
+
134
+ # Mostrar estado de verificación SMS
135
+ show_verification_status_in_sidebar()
136
+
137
  if st.button("Tancar sessió"):
138
  st.session_state.user = None
139
+ st.session_state.sms_verified = None
140
  st.rerun()
141
 
142
  if st.session_state.user:
143
+ # Obtener permisos del usuario
144
+ permissions = get_user_permissions(
145
+ role,
146
+ st.session_state.get('sms_verified')
147
+ )
148
+
149
+ # Construir opciones de navegación según permisos
150
+ page_options = []
151
+
152
+ if permissions["analizar"]:
153
+ page_options.append("Analitzar video-transcripcions")
154
+
155
+ if permissions["procesar_videos"]:
156
+ page_options.append("Processar vídeo nou")
157
+
158
+ if permissions["estadisticas"]:
159
+ page_options.append("Estadístiques")
160
+
161
+ # Si no hay opciones disponibles, mostrar solo análisis
162
+ if not page_options:
163
+ page_options = ["Analitzar video-transcripcions"]
164
+
165
  page = st.radio(
166
  "Navegació",
167
+ page_options,
168
  index=0
169
  )
170
  else:
mobile_verification.py ADDED
@@ -0,0 +1,311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de verificación por móvil/SMS para usuarios que requieren validación adicional.
3
+
4
+ Este módulo gestiona:
5
+ - Mostrar términos y condiciones de uso
6
+ - Solicitar número de teléfono móvil
7
+ - Enviar código de verificación por SMS (simulado en demo)
8
+ - Verificar código introducido por el usuario
9
+ """
10
+
11
+ import sys
12
+ import random
13
+ import streamlit as st
14
+ from datetime import datetime
15
+ from typing import Optional, Tuple
16
+
17
+
18
+ def log(msg: str):
19
+ """Helper per logging amb timestamp"""
20
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
21
+ sys.stderr.write(f"[{timestamp}] {msg}\n")
22
+ sys.stderr.flush()
23
+
24
+
25
+ def initialize_sms_state():
26
+ """Inicializa el estado de verificación SMS si no existe"""
27
+ if 'sms_step' not in st.session_state:
28
+ st.session_state.sms_step = 'phone' # 'phone' o 'verify'
29
+ if 'sms_code' not in st.session_state:
30
+ st.session_state.sms_code = None
31
+ if 'sms_phone' not in st.session_state:
32
+ st.session_state.sms_phone = None
33
+ if 'sms_verified' not in st.session_state:
34
+ st.session_state.sms_verified = None # None=pendiente, True=verificado, False=omitido
35
+
36
+
37
+ def send_sms_code(phone_number: str) -> bool:
38
+ """
39
+ Simula el envío de código SMS (en producción usaría un servicio real como Twilio).
40
+
41
+ Args:
42
+ phone_number: Número de teléfono completo con código de país
43
+
44
+ Returns:
45
+ True si el envío fue exitoso, False en caso de error
46
+ """
47
+ try:
48
+ # Generar código aleatorio de 6 dígitos
49
+ code = f"{random.randint(100000, 999999)}"
50
+ st.session_state.sms_code = code
51
+
52
+ log(f"[SMS] Código generado para {phone_number}: {code}")
53
+
54
+ # En producción aquí iría la llamada a un servicio real de SMS
55
+ # Por ejemplo, usando Twilio:
56
+ # client = TwilioClient(account_sid, auth_token)
57
+ # message = client.messages.create(
58
+ # body=f"El teu codi de verificació Veureu és: {code}",
59
+ # from_=twilio_phone,
60
+ # to=phone_number
61
+ # )
62
+
63
+ return True
64
+
65
+ except Exception as e:
66
+ log(f"[SMS] Error enviando código: {e}")
67
+ return False
68
+
69
+
70
+ def get_terms_and_conditions() -> str:
71
+ """Retorna el texto completo de términos y condiciones"""
72
+ return """
73
+ ### **Condicions d'ús del sistema Veureu**
74
+
75
+ En iniciar sessió i pujar un vídeo al sistema Veureu, l'usuari declara i accepta el següent:
76
+
77
+ **📋 Declaracions:**
78
+ - Declara ser titular dels drets d'ús i difusió del vídeo, o disposar de l'autorització expressa dels titulars.
79
+ - 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ó.
80
+ - Declara que el vídeo no conté:
81
+ - Contingut violent, sexual o d'odi,
82
+ - Informació confidencial o de caràcter sensible,
83
+ - Imatges de menors sense consentiment parental verificable.
84
+
85
+ **✅ Acceptacions:**
86
+ - 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.
87
+ - 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.
88
+ - Accepta que les accions realitzades (pujada, acceptació, validació, revocació) siguin registrades en un sistema immutable (AWS QLDB) mitjançant identificadors no personals.
89
+ - 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.
90
+ - 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.
91
+ """
92
+
93
+
94
+ def render_mobile_verification_screen(username: str, role: str) -> Optional[bool]:
95
+ """
96
+ Renderiza la pantalla de verificación por móvil post-login.
97
+
98
+ Args:
99
+ username: Nombre de usuario autenticado
100
+ role: Rol del usuario
101
+
102
+ Returns:
103
+ True si el usuario completó la verificación
104
+ False si omitió la verificación (solo modo análisis)
105
+ None si aún está en proceso
106
+ """
107
+ st.title("Veureu — Audiodescripció")
108
+ st.markdown(f"Benvingut/da, **{username}**")
109
+
110
+ # Mensaje explicativo según el rol
111
+ if role in ["groc", "blau"]:
112
+ st.info("""
113
+ ### 📱 Verificació per WhatsApp/SMS requerida
114
+
115
+ Per accedir a les funcions completes del sistema (pujar vídeos, validar contingut),
116
+ cal verificar el seu número de mòbil i acceptar les condicions d'ús.
117
+
118
+ **Si únicament vol analitzar i modificar les audiodescripcions existents, pot ometre aquest pas.**
119
+ """)
120
+
121
+ # Ventana desplegable con términos y verificación SMS
122
+ with st.expander("📋 Verificació per WhatsApp/SMS", expanded=True):
123
+ # Términos y condiciones
124
+ st.markdown(get_terms_and_conditions())
125
+
126
+ st.markdown("---")
127
+ st.markdown("#### 📱 Dades de verificació")
128
+
129
+ # Selector de país y número de teléfono
130
+ col_country, col_phone = st.columns([1, 3])
131
+ with col_country:
132
+ country_code = st.selectbox("País", [
133
+ ("🇪🇸 +34", "+34"),
134
+ ("🇫🇷 +33", "+33"),
135
+ ("🇬🇧 +44", "+44"),
136
+ ("🇩🇪 +49", "+49"),
137
+ ("🇮🇹 +39", "+39")
138
+ ], format_func=lambda x: x[0])[1]
139
+
140
+ with col_phone:
141
+ phone_number = st.text_input(
142
+ "Número de telèfon" + (" (opcional)" if role == "groc" else ""),
143
+ placeholder="600 123 000",
144
+ max_chars=15
145
+ )
146
+
147
+ # Checkbox para aceptar términos
148
+ accept_terms = st.checkbox(
149
+ "✅ Accepto les condicions d'ús i la política de privadesa",
150
+ key="mobile_accept_terms"
151
+ )
152
+
153
+ # Botón para enviar código
154
+ if st.button(
155
+ "📤 Enviar codi de verificació",
156
+ type="primary",
157
+ disabled=not (phone_number and accept_terms)
158
+ ):
159
+ full_phone = f"{country_code}{phone_number}"
160
+ if send_sms_code(full_phone):
161
+ st.session_state.sms_step = 'verify'
162
+ st.session_state.sms_phone = full_phone
163
+ st.success(f"✅ Codi enviat a {full_phone}")
164
+ st.rerun()
165
+ else:
166
+ st.error("❌ Error enviant el codi. Torna-ho a intentar.")
167
+
168
+ # Si se envió el SMS, mostrar campo para verificar código
169
+ if st.session_state.sms_step == 'verify':
170
+ st.markdown("#### 🔓 Verificació del codi")
171
+
172
+ # Mostrar código en modo demo
173
+ if st.session_state.sms_code:
174
+ st.info(f"💡 **Mode demo**: El codi és '{st.session_state.sms_code}'")
175
+
176
+ col_code, col_resend = st.columns([2, 1])
177
+ with col_code:
178
+ verification_code = st.text_input(
179
+ "Codi de 6 dígits",
180
+ max_chars=6,
181
+ placeholder="000000",
182
+ key="mobile_verification_code"
183
+ )
184
+
185
+ with col_resend:
186
+ st.markdown("<br>", unsafe_allow_html=True)
187
+ if st.button("🔄 Reenviar", key="mobile_resend"):
188
+ if send_sms_code(st.session_state.sms_phone):
189
+ st.success("✅ Nou codi enviat")
190
+ else:
191
+ st.error("❌ Error enviant el codi")
192
+
193
+ if st.button("🔓 Verificar i continuar", type="primary"):
194
+ if verification_code == st.session_state.sms_code:
195
+ st.session_state.sms_verified = True
196
+ st.session_state.sms_phone_verified = st.session_state.sms_phone
197
+ # Resetear estado SMS
198
+ st.session_state.sms_step = 'phone'
199
+ st.session_state.sms_code = None
200
+ st.session_state.sms_phone = None
201
+ st.success("✅ Verificació completada! Ara pot accedir a totes les funcions.")
202
+ log(f"[SMS] Usuario {username} verificado correctamente")
203
+ st.rerun()
204
+ return True
205
+ else:
206
+ st.error(f"❌ Codi incorrecte. El codi correcte és: {st.session_state.sms_code}")
207
+
208
+ # Botones de acción principales
209
+ st.markdown("---")
210
+ col_continue, col_skip = st.columns([1, 1])
211
+
212
+ with col_continue:
213
+ if st.session_state.sms_verified is True:
214
+ if st.button("🚪 Accedir amb permisos complets", type="primary"):
215
+ st.success("✅ Accedint a l'aplicació amb permisos complets...")
216
+ st.rerun()
217
+ return True
218
+
219
+ with col_skip:
220
+ # Solo mostrar botón de omitir para roles que no requieren verificación obligatoria
221
+ if role in ["groc", "blau"]:
222
+ if st.button("⏭️ Continuar sense verificació (només anàlisi)"):
223
+ st.session_state.sms_verified = False
224
+ st.info("✅ Sessió iniciada. Pot accedir a les funcions d'anàlisi d'audiodescripcions.")
225
+ log(f"[SMS] Usuario {username} omitió verificación SMS")
226
+ st.rerun()
227
+ return False
228
+
229
+ return None
230
+
231
+
232
+ def get_user_permissions(role: str, sms_verified: Optional[bool]) -> dict:
233
+ """
234
+ Retorna los permisos del usuario según su rol y estado de verificación SMS.
235
+
236
+ Args:
237
+ role: Rol del usuario (verd, groc, blau, taronja, vermell)
238
+ sms_verified: Estado de verificación SMS (True, False, None)
239
+
240
+ Returns:
241
+ Diccionario con permisos booleanos
242
+ """
243
+ # Permisos base por rol (sin verificación SMS)
244
+ base_permissions = {
245
+ "verd": {
246
+ "analizar": True,
247
+ "procesar_videos": True,
248
+ "valorar": True,
249
+ "validar": True,
250
+ "estadisticas": True,
251
+ "requires_sms": False
252
+ },
253
+ "groc": {
254
+ "analizar": True,
255
+ "procesar_videos": True, # Requiere SMS si no está verificado
256
+ "valorar": True,
257
+ "validar": False,
258
+ "estadisticas": True,
259
+ "requires_sms": True
260
+ },
261
+ "blau": {
262
+ "analizar": True,
263
+ "procesar_videos": False, # No puede subir videos nunca
264
+ "valorar": True,
265
+ "validar": True, # Requiere SMS si no está verificado
266
+ "estadisticas": True,
267
+ "requires_sms": True
268
+ },
269
+ "taronja": {
270
+ "analizar": True,
271
+ "procesar_videos": False,
272
+ "valorar": True,
273
+ "validar": False,
274
+ "estadisticas": True,
275
+ "requires_sms": False
276
+ },
277
+ "vermell": {
278
+ "analizar": True,
279
+ "procesar_videos": False,
280
+ "valorar": False,
281
+ "validar": False,
282
+ "estadisticas": True,
283
+ "requires_sms": False
284
+ }
285
+ }
286
+
287
+ permissions = base_permissions.get(role, base_permissions["vermell"]).copy()
288
+
289
+ # Ajustar permisos según verificación SMS
290
+ if permissions["requires_sms"]:
291
+ if sms_verified is False: # Omitió verificación
292
+ # Solo análisis permitido
293
+ permissions["procesar_videos"] = False
294
+ permissions["validar"] = False
295
+ elif sms_verified is None: # Pendiente de verificación
296
+ # Pendiente: no dar acceso a funciones que requieren SMS
297
+ permissions["procesar_videos"] = False
298
+ permissions["validar"] = False
299
+ # Si sms_verified is True, mantener permisos originales
300
+
301
+ return permissions
302
+
303
+
304
+ def show_verification_status_in_sidebar():
305
+ """Muestra el estado de verificación SMS en la barra lateral"""
306
+ if st.session_state.get('sms_verified') is True:
307
+ st.success("📱 Verificació SMS activada")
308
+ elif st.session_state.get('sms_verified') is False:
309
+ st.warning("⚠️ Només mode anàlisi")
310
+ else:
311
+ st.info("⏳ Verificació pendent")