VeuReu commited on
Commit
1a61999
·
1 Parent(s): ab655cc

Upload 9 files

Browse files
app.py CHANGED
@@ -8,6 +8,7 @@ import sys
8
  from pathlib import Path
9
  import re
10
  from datetime import datetime
 
11
  try:
12
  import tomllib
13
  except ModuleNotFoundError: # Py<3.11
@@ -16,8 +17,14 @@ import streamlit as st
16
  # from moviepy.editor import VideoFileClip
17
 
18
  from database import set_db_path, init_schema, get_user, create_user, update_user_password, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats, add_feedback_ad
19
- from api_client import APIClient
20
- from utils import ensure_dirs, save_bytes, save_text, human_size, get_project_root
 
 
 
 
 
 
21
 
22
  from scripts.client_generate_av import generate_free_ad_mp3, generate_une_ad_video
23
 
@@ -240,7 +247,7 @@ with st.sidebar:
240
  st.session_state.user = None
241
  st.rerun()
242
  if st.session_state.user:
243
- page = st.radio("Navegació", ["Analitzar audio-descripcions","Processar vídeo nou","Estadístiques"], index=0)
244
  else:
245
  page = None
246
 
@@ -388,15 +395,297 @@ if page == "Processar vídeo nou":
388
  if 'characters_saved' not in st.session_state:
389
  st.session_state.characters_saved = False
390
  log("Estado 'characters_saved' inicializado")
391
-
392
- # --- 1) Carregar vídeo ---
393
- uploaded = st.file_uploader("Puja un clip de vídeo (MP4)", type=["mp4"], accept_multiple_files=False)
394
- if uploaded is not None:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
395
  vb = uploaded.getvalue()
396
- st.session_state.video_uploaded = {"name": uploaded.name, "size": len(vb), "bytes": vb}
 
397
  st.success(f"Fitxer detectat: {uploaded.name} — {len(vb)//1024} KB")
 
 
 
 
398
  else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
  st.info("Cap fitxer pujat encara.")
 
 
 
400
 
401
  with st.form("detect_form"):
402
  col_btn, col_face, col_voice, col_scene = st.columns([1, 1, 1, 1])
@@ -420,8 +709,21 @@ if page == "Processar vídeo nou":
420
  help="0.0 = menys clusters (més agressiu), 0.5 = balancejat, 1.0 = més clusters (més permissiu)")
421
  with col_btn:
422
  max_frames = st.number_input("Nombre de frames a processar", min_value=10, max_value=500, value=100, step=10, help="Nombre de fotogrames equiespaciats a extreure del vídeo per detectar cares")
423
- can_detect = st.session_state.video_uploaded is not None
 
 
 
 
424
  submit_detect = st.form_submit_button("Detectar Personatges", disabled=not can_detect)
 
 
 
 
 
 
 
 
 
425
  if submit_detect:
426
  try:
427
  v = st.session_state.video_uploaded
@@ -1602,3 +1904,288 @@ elif page == "Estadístiques":
1602
  width='stretch'
1603
  )
1604
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  from pathlib import Path
9
  import re
10
  from datetime import datetime
11
+ import time
12
  try:
13
  import tomllib
14
  except ModuleNotFoundError: # Py<3.11
 
17
  # from moviepy.editor import VideoFileClip
18
 
19
  from database import set_db_path, init_schema, get_user, create_user, update_user_password, create_video, update_video_status, list_videos, get_video, get_all_users, upsert_result, get_results, add_feedback, get_feedback_for_video, get_feedback_stats, add_feedback_ad
20
+ # Cliente para comunicarse con servicio compliance
21
+ from compliance_client import compliance_client
22
+
23
+ # Módulos directos (solo para desarrollo/local)
24
+ # from auth_utils import auth_manager
25
+ # from aws_qldb import qldb_manager
26
+ # from notification_service import notification_service
27
+ # from polygon_digest import digest_publisher
28
 
29
  from scripts.client_generate_av import generate_free_ad_mp3, generate_une_ad_video
30
 
 
247
  st.session_state.user = None
248
  st.rerun()
249
  if st.session_state.user:
250
+ page = st.radio("Navegació", ["Analitzar audio-descripcions","Processar vídeo nou","Estadístiques","Compliment Blockchain"], index=0)
251
  else:
252
  page = None
253
 
 
395
  if 'characters_saved' not in st.session_state:
396
  st.session_state.characters_saved = False
397
  log("Estado 'characters_saved' inicializado")
398
+ if 'validation_document_id' not in st.session_state:
399
+ st.session_state.validation_document_id = None
400
+ log("Estado 'validation_document_id' inicializado")
401
+ if 'validation_status' not in st.session_state:
402
+ st.session_state.validation_status = "pending" # pending, approved, rejected
403
+ log("Estado 'validation_status' inicializado")
404
+ if 'validators_notified' not in st.session_state:
405
+ st.session_state.validators_notified = False
406
+ log("Estado 'validators_notified' inicializado")
407
+
408
+ # --- 1) Verificar autenticación ---
409
+ user_authenticated = compliance_client.is_authenticated()
410
+ current_user = compliance_client.get_current_user()
411
+
412
+ if not user_authenticated:
413
+ # Mostrar información del usuario no autenticado
414
+ st.info("🔐 **Identificación requerida** para subir vídeos")
415
+
416
+ # Mostrar botón de login
417
+ if compliance_client.show_login_button():
418
+ st.rerun()
419
+
420
+ # No mostrar el resto de la interfaz si no está autenticado
421
+ st.stop()
422
+
423
+ # --- 2) Usuario autenticado ---
424
+ if current_user:
425
+ # Determinar paso actual según el estado
426
+ if st.session_state.get("video_uploaded"):
427
+ if st.session_state.get("detect_done"):
428
+ current_step = 3 # Procesamiento completado
429
+ else:
430
+ current_step = 2 # Listo para procesamiento
431
+ else:
432
+ current_step = 1 # Listo para subir vídeo
433
+
434
+ progress_steps = ["🔐 Identificación", "📹 Subir vídeo", "🔒 Consentimientos", "⚙️ Procesamiento"]
435
+
436
+ st.markdown("### 🎯 Progreso del proceso")
437
+ progress_bar = st.progress(current_step / len(progress_steps))
438
+ st.markdown(" | ".join([
439
+ f"✅ {step}" if i < current_step else
440
+ f"🔄 {step}" if i == current_step else
441
+ f"⏳ {step}"
442
+ for i, step in enumerate(progress_steps)
443
+ ]))
444
+
445
+ # Información del usuario y logout
446
+ col_user, col_logout = st.columns([3, 1])
447
+ with col_user:
448
+ st.success(f"✅ **Autenticado como:** {current_user}")
449
+ with col_logout:
450
+ if st.button("🚪 Cerrar sesión", use_container_width=True):
451
+ compliance_client.logout()
452
+ st.rerun()
453
+
454
+ # --- 3) Carregar vídeo o generar sintético ---
455
+ st.markdown("### 📹 Opcions de vídeo")
456
+
457
+ # Crear dos columnas para las opciones de vídeo
458
+ col_upload, col_synthetic = st.columns(2)
459
+
460
+ with col_upload:
461
+ st.markdown("**#### 📁 Pujar vídeo existent**")
462
+ uploaded = st.file_uploader("Puja un clip de vídeo (MP4)", type=["mp4"], accept_multiple_files=False)
463
+
464
+ # El botón de subir estará deshabilitado hasta consentimientos
465
+ if uploaded is not None:
466
+ if all_consents_given if 'all_consents_given' in locals() else False:
467
+ st.success("✅ Vídeo listo para procesar")
468
+ else:
469
+ st.warning("⚠️ Esperando consentimientos legales")
470
+
471
+ with col_synthetic:
472
+ st.markdown("**#### 🎬 Generar vídeo sintético**")
473
+ st.info("🔧 En conexión con Sora (próximamente)")
474
+
475
+ # Input para el prompt de generación
476
+ synthetic_prompt = st.text_area(
477
+ "Descripció del vídeo a generar:",
478
+ placeholder="Ex: Un paisatge de la costa catalana amb barques de pesca al capvespre...",
479
+ height=100,
480
+ help="Descriu en català l'escena que vols generar"
481
+ )
482
+
483
+ # Botón de generar vídeo (siempre habilitado)
484
+ generate_synthetic = st.button(
485
+ "🎬 Generar vídeo sintético",
486
+ type="secondary",
487
+ use_container_width=True,
488
+ help="Generarà un vídeo amb IA basat en la teva descripció"
489
+ )
490
+
491
+ if generate_synthetic:
492
+ if synthetic_prompt.strip():
493
+ st.info("🔧 **Funcionalidad en desarrollo**")
494
+ st.success("✅ Prompt recibido:")
495
+ st.code(synthetic_prompt)
496
+ st.info("🚀 Próximamente conectaremos con Sora para generar el vídeo")
497
+
498
+ # Simular vídeo sintético generado
499
+ synthetic_video = {
500
+ "name": "synthetic_video.mp4",
501
+ "size": 0, # Simulado
502
+ "bytes": b"", # Vacío hasta integración real
503
+ "prompt": synthetic_prompt,
504
+ "type": "synthetic"
505
+ }
506
+ st.session_state.video_uploaded = synthetic_video
507
+ st.session_state.validation_status = "approved" # Los vídeos sintéticos no necesitan validación
508
+ st.success("🎉 Vídeo sintético listo para procesar")
509
+ st.rerun()
510
+ else:
511
+ st.error("❌ Por favor, introduce una descripción para el vídeo")
512
+
513
+ # --- 4) Consentiment legal i ètic (solo para vídeos subidos) ---
514
+ # Mostrar consentimientos solo si hay vídeo subido (no sintético)
515
+ video_type = st.session_state.get("video_uploaded", {}).get("type", "uploaded")
516
+
517
+ if uploaded is not None and video_type != "synthetic":
518
+ st.markdown("### 🔒 Consentiment legal i ètic")
519
+ st.markdown("És obligatori marcar totes les caselles següents per poder pujar el vídeo:")
520
+ elif uploaded is None:
521
+ st.markdown("### 🔒 Consentiment legal i ètic")
522
+ st.markdown("És obligatori marcar totes les caselles següents per poder pujar el vídeo:")
523
+ else:
524
+ # Vídeo sintético - no necesita consentimientos
525
+ st.markdown("### ✅ Vídeo sintético - Sin requerimientos legales")
526
+ st.success("🎬 Los vídeos generados con IA no requieren consentimientos de contenido")
527
+ st.info("💡 El contenido generado es original y no involucra derechos de terceros")
528
+
529
+ # Mostrar checkboxes solo para vídeos subidos (no sintéticos)
530
+ if uploaded is not None and video_type != "synthetic":
531
+ # Crear dos columnas para los checkboxes
532
+ col_check1, col_check2 = st.columns(2)
533
+
534
+ with col_check1:
535
+ consent_rights = st.checkbox(
536
+ "✅ Tinc tots els drets sobre el vídeo i no prové de cap manipulació de vídeo existent sense drets",
537
+ key="consent_rights",
538
+ help="Necessites ser el propietari del contingut o tenir permís per utilitzar-lo"
539
+ )
540
+ consent_content = st.checkbox(
541
+ "✅ El vídeo no conté continguts prohibits (violents, sexuals o il·lícits)",
542
+ key="consent_content",
543
+ help="El contingut ha de complir les normes d'ús acceptables"
544
+ )
545
+
546
+ with col_check2:
547
+ consent_biometric = st.checkbox(
548
+ "✅ Tinc el consentiment per a l'ús biomètric de cada personatge que apareix al vídeo",
549
+ key="consent_biometric",
550
+ help="És necessari el consentiment explícit per al processament facial i de veu"
551
+ )
552
+ consent_privacy = st.checkbox(
553
+ "✅ El vídeo no conté informació confidencial ni dades personals privades",
554
+ key="consent_privacy",
555
+ help="Protegeix la teva privacitat i la dels altres"
556
+ )
557
+
558
+ # Verificar si todos los checkboxes están marcados
559
+ all_consents_given = all([consent_rights, consent_biometric, consent_content, consent_privacy])
560
+ else:
561
+ # Para vídeos sintéticos o sin vídeo, los consentimientos no aplican
562
+ all_consents_given = True if video_type == "synthetic" else False
563
+ consent_rights = consent_biometric = consent_content = consent_privacy = False
564
+
565
+ # Mostrar mensaje explicativo si no hay consentimiento completo
566
+ if uploaded is not None and not all_consents_given:
567
+ remaining = [name for name, checked in [
568
+ ("drets sobre el vídeo", consent_rights),
569
+ ("consentiment biomètric", consent_biometric),
570
+ ("contingut permès", consent_content),
571
+ ("privacitat i dades", consent_privacy)
572
+ ] if not checked]
573
+
574
+ st.error(f"❌ **No es pot pujar el vídeo**: Cal marcar totes les caselles de consentiment. Falta: {', '.join(remaining)}")
575
+ st.info("💡 Aquestes mesures garanteixen l'ús responsable i ètic de la tecnologia de processament d'àudio i vídeo.")
576
+
577
+ # Resetear consentimientos si se cambia el vídeo
578
+ if uploaded is not None and st.session_state.get("last_uploaded_name") != uploaded.name:
579
+ # Resetear checkboxes cuando se sube un nuevo vídeo
580
+ for key in ["consent_rights", "consent_biometric", "consent_content", "consent_privacy"]:
581
+ if key in st.session_state:
582
+ del st.session_state[key]
583
+ st.session_state.last_uploaded_name = uploaded.name
584
+ st.rerun()
585
+
586
+ # Procesar el vídeo según tipo
587
+ if uploaded is not None and all_consents_given:
588
+ # Vídeo subido - requiere validación
589
  vb = uploaded.getvalue()
590
+ video_info = {"name": uploaded.name, "size": len(vb), "bytes": vb, "type": "uploaded"}
591
+ st.session_state.video_uploaded = video_info
592
  st.success(f"Fitxer detectat: {uploaded.name} — {len(vb)//1024} KB")
593
+ elif video_type == "synthetic" and st.session_state.get("video_uploaded"):
594
+ # Vídeo sintético - ya está en session_state
595
+ video_info = st.session_state.video_uploaded
596
+ st.success(f"✅ Vídeo sintético listo: {video_info.get('prompt', 'Sin prompt')[:50]}...")
597
  else:
598
+ video_info = None
599
+
600
+ # --- REGISTRO DE CUMPLIMIENTO Y VALIDACIÓN (solo para vídeos subidos) ---
601
+ if video_info and video_info.get("type") == "uploaded":
602
+ # Verificar si el usuario es "verd" (pruebas) - deshabilitar blockchain
603
+ is_test_user = current_user == "verd"
604
+
605
+ if st.session_state.validation_document_id is None:
606
+ if is_test_user:
607
+ st.info("🧪 **Modo pruebas** - Usuario 'verd' - Blockchain deshabilitado")
608
+ st.success("✅ Vídeo listo para procesamiento (sin registro blockchain)")
609
+
610
+ # Asignar ID simulado para pruebas
611
+ st.session_state.validation_document_id = f"test_doc_{int(time.time())}"
612
+ st.session_state.validation_status = "approved" # Auto-aprobado para pruebas
613
+ st.session_state.validators_notified = True
614
+ else:
615
+ st.info("🔐 Registrando consentimientos y enviando a validación...")
616
+
617
+ # Preparar información para QLDB
618
+ user_info = {
619
+ "email": current_user,
620
+ "name": current_user.split('@')[0] if current_user else "Unknown"
621
+ }
622
+
623
+ consent_data = {
624
+ "rights": consent_rights,
625
+ "biometric": consent_biometric,
626
+ "content": consent_content,
627
+ "privacy": consent_privacy,
628
+ "all_accepted": all_consents_given
629
+ }
630
+
631
+ # Registrar en QLDB (comentado hasta activación)
632
+ document_id = qldb_manager.record_user_consent(
633
+ user_info=user_info,
634
+ video_info=video_info,
635
+ consent_data=consent_data
636
+ )
637
+
638
+ if document_id:
639
+ st.session_state.validation_document_id = document_id
640
+ st.success(f"✅ Consentimientos registrados (ID: {document_id[:16]}...)")
641
+
642
+ # Enviar notificación a validadores (comentado hasta activación)
643
+ from notification_service import ValidationRequest
644
+
645
+ validation_request = ValidationRequest(
646
+ document_id=document_id,
647
+ user_email=user_info["email"],
648
+ user_name=user_info["name"],
649
+ video_title=video_info["name"],
650
+ video_hash=document_id, # En producción sería el hash real del vídeo
651
+ timestamp=datetime.now().isoformat(),
652
+ video_url=f"https://veureu-demo.hf.space/video/{document_id}", # URL temporal
653
+ consent_data=consent_data
654
+ )
655
+
656
+ if notification_service.send_validation_request(validation_request):
657
+ st.session_state.validators_notified = True
658
+ st.success("📧 Validadores notificados por email")
659
+ else:
660
+ st.error("❌ Error notificando a validadores")
661
+ else:
662
+ st.error("❌ Error registrando consentimientos")
663
+ else:
664
+ # Mostrar estado según tipo de usuario
665
+ if is_test_user:
666
+ st.success("🧪 **Modo pruebas** - Vídeo listo para procesamiento")
667
+ st.info("💡 Las operaciones blockchain están deshabilitadas para el usuario de pruebas")
668
+ else:
669
+ st.success("✅ Tots els consentiments han estat acceptats.")
670
+
671
+ # Mostrar estado de validación
672
+ validation_status = st.session_state.validation_status
673
+ if validation_status == "pending":
674
+ st.warning("⏳ **Esperando validación interna** - El vídeo está siendo revisado por el equipo de cumplimiento")
675
+ st.info("📧 Los validadores han sido notificados por email")
676
+ elif validation_status == "approved":
677
+ st.success("✅ **Vídeo validado** - Puedes proceder con el procesamiento")
678
+ elif validation_status == "rejected":
679
+ st.error("❌ **Vídeo rechazado** - Contacta con el equipo de cumplimiento para más información")
680
+ elif video_info and video_info.get("type") == "synthetic":
681
+ # Vídeo sintético - no necesita validación
682
+ st.success("🎬 **Vídeo sintético listo para procesar**")
683
+ st.info("💡 Los vídeos generados con IA no requieren validación previa")
684
+ elif uploaded is None:
685
  st.info("Cap fitxer pujat encara.")
686
+ # Limpiar último nombre de vídeo si no hay vídeo
687
+ if "last_uploaded_name" in st.session_state:
688
+ del st.session_state.last_uploaded_name
689
 
690
  with st.form("detect_form"):
691
  col_btn, col_face, col_voice, col_scene = st.columns([1, 1, 1, 1])
 
709
  help="0.0 = menys clusters (més agressiu), 0.5 = balancejat, 1.0 = més clusters (més permissiu)")
710
  with col_btn:
711
  max_frames = st.number_input("Nombre de frames a processar", min_value=10, max_value=500, value=100, step=10, help="Nombre de fotogrames equiespaciats a extreure del vídeo per detectar cares")
712
+ # Requerir: vídeo subido + consentimientos + validación aprobada
713
+ is_validated = st.session_state.validation_status == "approved"
714
+ can_detect = (st.session_state.video_uploaded is not None and
715
+ all_consents_given and
716
+ is_validated)
717
  submit_detect = st.form_submit_button("Detectar Personatges", disabled=not can_detect)
718
+
719
+ # Mostrar mensaje explicativo si el botón está deshabilitado
720
+ if not can_detect:
721
+ if uploaded is None:
722
+ st.caption("📹 Necessites pujar un vídeo primer")
723
+ elif not all_consents_given:
724
+ st.caption("🔒 Necessites acceptar tots els consentiments per poder processar el vídeo")
725
+ elif not is_validated:
726
+ st.caption("⏳ Necessita validació interna - El vídeo està sent revisat pel equip de compliment")
727
  if submit_detect:
728
  try:
729
  v = st.session_state.video_uploaded
 
1904
  width='stretch'
1905
  )
1906
 
1907
+ elif page == "Compliment Blockchain":
1908
+ require_login()
1909
+
1910
+ # Verificar si es usuario de pruebas
1911
+ is_test_user = st.session_state.user['username'] == "verd"
1912
+
1913
+ if is_test_user:
1914
+ st.header("🧪 Modo Pruebas - Blockchain Deshabilitado")
1915
+ st.warning("⚠️ **Usuario 'verd' detectado** - Las funciones de blockchain están deshabilitadas durante las pruebas")
1916
+
1917
+ st.markdown("""
1918
+ ### 📋 Estado del Modo Pruebas
1919
+
1920
+ Durante las pruebas con el usuario 'verd', las siguientes funciones están **deshabilitadas**:
1921
+
1922
+ - ❌ Registro en AWS QLDB
1923
+ - ❌ Publicación de digest en Polygon
1924
+ - ❌ Verificación blockchain
1925
+ - ❌ Estadísticas de transacciones
1926
+
1927
+ ### ✅ Funciones Habilitadas para Pruebas
1928
+
1929
+ - 📹 Subida y procesamiento de vídeos
1930
+ - 🔍 Detección de personajes
1931
+ - 🎬 Generación de audiodescripciones
1932
+ - 📊 Estadísticas básicas de procesamiento
1933
+
1934
+ ### 🔄 Para Activar Blockchain
1935
+
1936
+ Cuando termines las pruebas y quieras activar el cumplimiento normativo completo:
1937
+ 1. Inicia sesión con otro usuario (no 'verd')
1938
+ 2. Configura las variables de entorno de AWS y Polygon
1939
+ 3. Activa el código comentado en los módulos
1940
+ 4. Verifica el funcionamiento del dashboard
1941
+
1942
+ ---
1943
+
1944
+ **El sistema está listo para producción** - solo falta activar las integraciones blockchain.
1945
+ """)
1946
+
1947
+ # Mostrar información del sistema preparado
1948
+ st.markdown("### 🔧 Sistema Preparado")
1949
+
1950
+ col1, col2, col3 = st.columns(3)
1951
+
1952
+ with col1:
1953
+ st.info("📋 **QLDB Ready**")
1954
+ st.markdown("• Módulo implementado")
1955
+ st.markdown("• Contratos definidos")
1956
+ st.markdown("• Simulación funcional")
1957
+
1958
+ with col2:
1959
+ st.info("⛓️ **Polygon Ready**")
1960
+ st.markdown("• Contrato desplegado")
1961
+ st.markdown("• Digest funcionando")
1962
+ st.markdown("• Verificación lista")
1963
+
1964
+ with col3:
1965
+ st.info("🔐 **Compliance Ready**")
1966
+ st.markdown("• AI Act compliance")
1967
+ st.markdown("• GDPR compliance")
1968
+ st.markdown("• Auditoría pública")
1969
+
1970
+ st.success("✅ **Cuando termines las pruebas, el sistema estará listo para producción blockchain**")
1971
+
1972
+ else:
1973
+ st.header("🔐 Compliment Regulatori - Polygon Blockchain")
1974
+
1975
+ st.markdown("""
1976
+ ### 📋 Auditoria Pública de Cumplimiento
1977
+
1978
+ Esta sección muestra el registro público de autorizaciones y validaciones
1979
+ publicado en **Polygon blockchain** para garantizar cumplimiento normativo
1980
+ (AI Act, GDPR) de manera transparente e inmutable.
1981
+ """)
1982
+
1983
+ # Tabs para diferentes secciones
1984
+ tab1, tab2, tab3 = st.tabs(["📊 Digest Publicados", "🔍 Verificar Digest", "📈 Estadísticas Blockchain"])
1985
+
1986
+ with tab1:
1987
+ st.subheader("📊 Digest Mensuales Publicados")
1988
+
1989
+ # Obtener digest publicados
1990
+ published_digests = digest_publisher.get_published_digests()
1991
+
1992
+ if published_digests:
1993
+ for digest in published_digests:
1994
+ with st.expander(f"📅 Período: {digest['period']}", expanded=True):
1995
+ col1, col2, col3 = st.columns(3)
1996
+
1997
+ with col1:
1998
+ st.metric("Autorizaciones", digest['authorization_count'])
1999
+
2000
+ with col2:
2001
+ st.metric("Bloque", digest['block_number'])
2002
+
2003
+ with col3:
2004
+ st.markdown("**Transacción:**")
2005
+ st.code(digest['transaction_hash'][:20] + "...")
2006
+
2007
+ st.markdown(f"**Timestamp:** {digest['timestamp']}")
2008
+
2009
+ # Botón para verificar en explorador
2010
+ if st.button(f"🔍 Ver en Polygon Scan", key=f"verify_{digest['period']}"):
2011
+ polygon_url = f"https://polygonscan.com/tx/{digest['transaction_hash']}"
2012
+ st.markdown(f"[🔗 Ver en Polygon Scan]({polygon_url})")
2013
+ else:
2014
+ st.info("📝 No hay digest publicados aún")
2015
+
2016
+ with tab2:
2017
+ st.subheader("🔍 Verificar Integridad de Digest")
2018
+
2019
+ col1, col2 = st.columns(2)
2020
+
2021
+ with col1:
2022
+ period_to_verify = st.text_input(
2023
+ "Período a verificar (YYYY-MM):",
2024
+ placeholder="2025-11",
2025
+ help="Introduce el período que quieres verificar"
2026
+ )
2027
+
2028
+ expected_hash = st.text_input(
2029
+ "Hash esperado:",
2030
+ placeholder="abcdef123456...",
2031
+ help="Hash SHA-256 del digest del período"
2032
+ )
2033
+
2034
+ with col2:
2035
+ st.markdown("### 📋 ¿Cómo verificar?")
2036
+ st.markdown("""
2037
+ 1. **Obtén el hash** del digest que quieres verificar
2038
+ 2. **Introduce el período** en formato YYYY-MM
2039
+ 3. **Click en verificar** para comprobar que coincide con el registro en blockchain
2040
+ 4. **Resultado inmutable** verificable públicamente
2041
+ """)
2042
+
2043
+ if st.button("🔍 Verificar en Blockchain", type="primary"):
2044
+ if period_to_verify and expected_hash:
2045
+ with st.spinner("Verificando en Polygon..."):
2046
+ is_valid = digest_publisher.verify_digest_on_chain(period_to_verify, expected_hash)
2047
+
2048
+ if is_valid:
2049
+ st.success("✅ **VERIFICADO** - El digest coincide con el registro en blockchain")
2050
+ st.balloons()
2051
+ else:
2052
+ st.error("❌ **NO COINCIDE** - El hash no coincide con el registro en blockchain")
2053
+ else:
2054
+ st.warning("⚠️ Por favor, completa todos los campos")
2055
+
2056
+ with tab3:
2057
+ st.subheader("📈 Estadísticas de Cumplimiento")
2058
+
2059
+ # Métricas simuladas (vendrán de blockchain)
2060
+ col1, col2, col3, col4 = st.columns(4)
2061
+
2062
+ with col1:
2063
+ st.metric("Total Digest", "12")
2064
+
2065
+ with col2:
2066
+ st.metric("Autorizaciones Totales", "1,247")
2067
+
2068
+ with col3:
2069
+ st.metric("Mes Activo", "2025-11")
2070
+
2071
+ with col4:
2072
+ st.metric("Gas Total Gastado", "0.42 MATIC")
2073
+
2074
+ # Gráfico de actividad (simulado)
2075
+ st.markdown("### 📊 Actividad Mensual")
2076
+
2077
+ # Datos simulados para el gráfico
2078
+ months = ["2025-06", "2025-07", "2025-08", "2025-09", "2025-10", "2025-11"]
2079
+ authorizations = [45, 67, 89, 123, 156, 189]
2080
+
2081
+ chart_data = {
2082
+ 'Mes': months,
2083
+ 'Autorizaciones': authorizations
2084
+ }
2085
+
2086
+ st.bar_chart(chart_data)
2087
+
2088
+ st.markdown("---")
2089
+ st.markdown("### 🔧 Información Técnica")
2090
+
2091
+ col1, col2 = st.columns(2)
2092
+
2093
+ with col1:
2094
+ st.markdown("**📋 Contrato Inteligente:**")
2095
+ st.code(f"Dirección: {digest_publisher.contract_addr}")
2096
+ st.markdown("**Red:** Polygon Mainnet")
2097
+ st.markdown("**Estándar:** SHA-256 + Merkle Tree")
2098
+
2099
+ with col2:
2100
+ st.markdown("**🔗 Enlaces Útiles:**")
2101
+ st.markdown("- [Polygon Scan](https://polygonscan.com/)")
2102
+ st.markdown("- [Contrato ABI](#)")
2103
+ st.markdown("- [Documentación AI Act](#)")
2104
+ st.markdown("- [GDPR Compliance](#)")
2105
+
2106
+ # Sección de publicación manual (para admin)
2107
+ if st.session_state.user['role'] in ['verd', 'groc']: # Admin roles
2108
+ st.markdown("---")
2109
+ st.subheader("🔧 Administración - Publicar Digest")
2110
+
2111
+ with st.expander("📤 Publicar Digest Mensual (Admin)", expanded=False):
2112
+ st.warning("⚠️ Esta función publicará permanentemente un digest en blockchain")
2113
+
2114
+ col1, col2 = st.columns(2)
2115
+
2116
+ with col1:
2117
+ publish_period = st.text_input(
2118
+ "Período a publicar:",
2119
+ placeholder="2025-11",
2120
+ help="Formato YYYY-MM"
2121
+ )
2122
+
2123
+ with col2:
2124
+ st.markdown("**Requisitos:**")
2125
+ st.markdown("✅ Todas las autorizaciones del período deben estar validadas")
2126
+ st.markdown("✅ Hash SHA-256 calculado correctamente")
2127
+ st.markdown("✅ Gas suficiente para transacción")
2128
+
2129
+ if st.button("📤 Publicar en Polygon", type="secondary"):
2130
+ if publish_period:
2131
+ with st.spinner("Publicando digest en Polygon blockchain..."):
2132
+ # Simular publicación
2133
+ tx_hash = qldb_manager.publish_monthly_digest_to_polygon(publish_period)
2134
+
2135
+ if tx_hash:
2136
+ st.success(f"✅ Digest publicado correctamente")
2137
+ st.code(f"Transacción: {tx_hash}")
2138
+ st.info("🔍 Verifica en Polygon Scan")
2139
+ else:
2140
+ st.error("❌ Error publicando digest")
2141
+ else:
2142
+ st.warning("⚠️ Introduce un período válido")
2143
+
2144
+ # --- ENDPOINT PARA VALIDACIÓN POR EMAIL ---
2145
+ def handle_validation_response():
2146
+ """
2147
+ Maneja las respuestas de validación desde los enlaces de email
2148
+ Query params: ?doc_id=xxx&action=approve|reject
2149
+ """
2150
+ query_params = st.query_params
2151
+
2152
+ if "doc_id" in query_params and "action" in query_params:
2153
+ doc_id = query_params["doc_id"]
2154
+ action = query_params["action"]
2155
+
2156
+ if action in ["approve", "reject"]:
2157
+ # Actualizar estado en sesión (simulado)
2158
+ st.session_state.validation_status = action
2159
+ st.session_state.validation_document_id = doc_id
2160
+
2161
+ # Registrar decisión en QLDB (comentado hasta activación)
2162
+ validator_email = "[email protected]" # En producción vendría del login
2163
+ success = qldb_manager.record_validator_decision(
2164
+ document_id=doc_id,
2165
+ validator_email=validator_email,
2166
+ decision=action,
2167
+ comments=f"Validación por email link"
2168
+ )
2169
+
2170
+ if success:
2171
+ # Enviar notificación de decisión (comentado hasta activación)
2172
+ notification_service.send_decision_notification(
2173
+ validator_email=validator_email,
2174
+ decision=action,
2175
+ document_id=doc_id
2176
+ )
2177
+
2178
+ st.success(f"✅ Validación {action} registrada correctamente")
2179
+ st.info(f"Documento ID: {doc_id}")
2180
+ st.info("Puedes cerrar esta ventana y volver al espacio principal")
2181
+ else:
2182
+ st.error("❌ Error registrando la decisión de validación")
2183
+
2184
+ # Limpiar query params para evitar re-procesamiento
2185
+ st.query_params.clear()
2186
+ st.stop()
2187
+
2188
+ if __name__ == "__main__":
2189
+ handle_validation_response()
2190
+ main()
2191
+
auth_config.yaml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ credentials:
2
+ usernames:
3
+ google_oauth:
4
+ email: 'google_oauth_user'
5
+ name: 'Google User'
6
+ password: 'oauth_password' # This will be handled by OAuth
7
+
8
+ cookie:
9
+ expiry_days: 30
10
+ key: 'veureu_auth_cookie_key' # Change this in production!
11
+ name: 'veureu_auth_cookie'
12
+
13
+ preauthorized:
14
+ emails:
15
+ - '[email protected]' # Add preauthorized emails if needed
auth_utils.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import yaml
3
+ import streamlit as st
4
+ from typing import Optional, Dict, Any
5
+ import streamlit_authenticator as stauth
6
+
7
+ class AuthManager:
8
+ """Gestiona la autenticación de usuarios con Google OAuth"""
9
+
10
+ def __init__(self):
11
+ self.config_path = "auth_config.yaml"
12
+ self.authenticator = None
13
+ self.load_config()
14
+
15
+ def load_config(self):
16
+ """Carga la configuración de autenticación"""
17
+ try:
18
+ with open(self.config_path, 'r', encoding='utf-8') as file:
19
+ self.config = yaml.safe_load(file)
20
+ except FileNotFoundError:
21
+ # Configuración por defecto si no existe el archivo
22
+ self.config = {
23
+ 'credentials': {
24
+ 'usernames': {
25
+ 'google_oauth_user': {
26
+ 'email': 'google_oauth_user',
27
+ 'name': 'Google User',
28
+ 'password': 'oauth_password'
29
+ }
30
+ }
31
+ },
32
+ 'cookie': {
33
+ 'expiry_days': 30,
34
+ 'key': 'veureu_auth_cookie_key_change_in_production',
35
+ 'name': 'veureu_auth_cookie'
36
+ },
37
+ 'preauthorized': {'emails': []}
38
+ }
39
+ self.save_config()
40
+
41
+ def save_config(self):
42
+ """Guarda la configuración de autenticación"""
43
+ with open(self.config_path, 'w', encoding='utf-8') as file:
44
+ yaml.dump(self.config, file, default_flow_style=False)
45
+
46
+ def initialize_authenticator(self):
47
+ """Inicializa el autenticador de Streamlit"""
48
+ if self.authenticator is None:
49
+ self.authenticator = stauth.Authenticate(
50
+ self.config['credentials'],
51
+ self.config['cookie']['name'],
52
+ self.config['cookie']['key'],
53
+ self.config['cookie']['expiry_days']
54
+ )
55
+ return self.authenticator
56
+
57
+ def show_login_section(self, consent_text: str) -> Optional[str]:
58
+ """
59
+ Muestra sección de login con consentimientos
60
+
61
+ Args:
62
+ consent_text: Texto con los términos y condiciones
63
+
64
+ Returns:
65
+ Email del usuario autenticado o None
66
+ """
67
+ authenticator = self.initialize_authenticator()
68
+
69
+ # Crear sección de login
70
+ st.markdown("### 📋 Antes de subir tu vídeo")
71
+
72
+ # Mostrar términos y condiciones
73
+ with st.expander("📜 Términos y Condiciones", expanded=True):
74
+ st.markdown(consent_text)
75
+
76
+ st.markdown("#### 🎯 Para continuar, por favor identifícate:")
77
+
78
+ # Información sobre Google OAuth
79
+ st.info("🔧 **Login con Google OAuth** - En producción esto conectará con tu cuenta Google")
80
+ st.info("💡 **Modo demostración** - Usa las credenciales siguientes:")
81
+
82
+ col1, col2 = st.columns(2)
83
+ with col1:
84
+ st.code("Usuario: google_oauth_user")
85
+ with col2:
86
+ st.code("Contraseña: oauth_password")
87
+
88
+ # Intentar autenticación
89
+ name, authentication_status, username = authenticator.login(
90
+ 'Login para acceder al servicio', 'main'
91
+ )
92
+
93
+ if authentication_status:
94
+ st.success(f"✅ Bienvenido/a, {name}!")
95
+ st.success("🎉 **Identificación completada** - Has aceptado los términos y condiciones mediante tu login.")
96
+ st.balloons()
97
+ return username
98
+ elif authentication_status == False:
99
+ st.error('❌ Credenciales incorrectos. Por favor inténtalo de nuevo.')
100
+ st.warning("💡 Usa: google_oauth_user / oauth_password")
101
+ else:
102
+ st.warning('⚠️ Por favor introduce tus credenciales para continuar')
103
+
104
+ return None
105
+
106
+ def logout(self):
107
+ """Cierra la sesión del usuario"""
108
+ if self.authenticator:
109
+ self.authenticator.logout('Logout', 'main')
110
+
111
+ def is_authenticated(self) -> bool:
112
+ """Verifica si el usuario está autenticado"""
113
+ return 'authentication_status' in st.session_state and st.session_state.authentication_status
114
+
115
+ def get_current_user(self) -> Optional[str]:
116
+ """Obtiene el email del usuario actual"""
117
+ if self.is_authenticated():
118
+ return st.session_state.get('username')
119
+ return None
120
+
121
+ # Instancia global del gestor de autenticación
122
+ auth_manager = AuthManager()
aws_qldb.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de integración con AWS QLDB para registro regulatorio (AI Act y GDPR)
3
+
4
+ NOTA: Esta implementación está comentada provisionalmente para despliegue futuro.
5
+ Cuando se active, requerirá:
6
+ - AWS Credentials configuradas
7
+ - Acceso a QLDB Ledger
8
+ - Permisos adecuados
9
+ """
10
+
11
+ import os
12
+ import json
13
+ import hashlib
14
+ import time
15
+ from datetime import datetime, timezone
16
+ from typing import Dict, Any, Optional
17
+ from dataclasses import dataclass, asdict
18
+
19
+ # Imports para integración (comentados hasta activación)
20
+ # from aws_qldb import qldb_manager
21
+ # from polygon_digest import digest_publisher
22
+
23
+ # Imports comentados hasta activación
24
+ # import boto3
25
+ # from botocore.exceptions import ClientError
26
+
27
+ @dataclass
28
+ class ComplianceRecord:
29
+ """Registro de cumplimiento para AWS QLDB"""
30
+ user_email: str
31
+ user_name: str
32
+ video_title: str
33
+ video_hash: str
34
+ video_size: int
35
+ timestamp: str
36
+ consent_accepted: bool
37
+ consent_version: str
38
+ ip_address: str
39
+ user_agent: str
40
+
41
+ # Validación interna
42
+ validators_notified: bool = False
43
+ validation_status: str = "pending" # pending, approved, rejected
44
+ validator_emails: list = None
45
+ validation_timestamp: str = None
46
+
47
+ # Metadatos adicionales
48
+ session_id: str = None
49
+ space_id: str = None
50
+
51
+ class QLDBManager:
52
+ """Gestor de registros en AWS QLDB (comentado hasta activación)"""
53
+
54
+ def __init__(self, ledger_name: str = "veureu-compliance"):
55
+ self.ledger_name = ledger_name
56
+ # self.client = boto3.client('qldb')
57
+ # self.session = boto3.session.Session()
58
+
59
+ def _compute_video_hash(self, video_bytes: bytes) -> str:
60
+ """Calcula hash SHA-256 del vídeo para integridad"""
61
+ return hashlib.sha256(video_bytes).hexdigest()
62
+
63
+ def _create_compliance_record(self, user_info: Dict[str, Any],
64
+ video_info: Dict[str, Any],
65
+ consent_data: Dict[str, Any]) -> ComplianceRecord:
66
+ """Crea registro de cumplimiento"""
67
+
68
+ # Extraer información del usuario
69
+ user_email = user_info.get('email', '[email protected]')
70
+ user_name = user_info.get('name', 'Unknown User')
71
+
72
+ # Calcular hash del vídeo
73
+ video_bytes = video_info.get('bytes', b'')
74
+ video_hash = self._compute_video_hash(video_bytes)
75
+
76
+ # Timestamp en formato ISO 8601 UTC
77
+ timestamp = datetime.now(timezone.utc).isoformat()
78
+
79
+ # Información de sesión
80
+ session_id = os.urandom(16).hex()
81
+ space_id = os.getenv('SPACE_ID', 'local-dev')
82
+
83
+ return ComplianceRecord(
84
+ user_email=user_email,
85
+ user_name=user_name,
86
+ video_title=video_info.get('name', 'unknown_video.mp4'),
87
+ video_hash=video_hash,
88
+ video_size=len(video_bytes),
89
+ timestamp=timestamp,
90
+ consent_accepted=consent_data.get('all_accepted', False),
91
+ consent_version="1.0",
92
+ ip_address=self._get_client_ip(),
93
+ user_agent=self._get_user_agent(),
94
+ session_id=session_id,
95
+ space_id=space_id,
96
+ validators_notified=False,
97
+ validation_status="pending",
98
+ validator_emails=[]
99
+ )
100
+
101
+ def _get_client_ip(self) -> str:
102
+ """Obtiene IP del cliente (simulado)"""
103
+ # En producción, esto vendría de request headers
104
+ return "127.0.0.1" # Placeholder
105
+
106
+ def _get_user_agent(self) -> str:
107
+ """Obtiene User-Agent del cliente"""
108
+ return "Streamlit/1.0" # Placeholder
109
+
110
+ def record_user_consent(self, user_info: Dict[str, Any],
111
+ video_info: Dict[str, Any],
112
+ consent_data: Dict[str, Any]) -> Optional[str]:
113
+ """
114
+ Registra consentimiento del usuario en QLDB
115
+
116
+ Returns:
117
+ Document ID del registro creado o None si hay error
118
+ """
119
+ try:
120
+ record = self._create_compliance_record(user_info, video_info, consent_data)
121
+
122
+ # Código comentado hasta activación de QLDB
123
+ """
124
+ # Insertar en QLDB
125
+ result = self.client.execute_statement(
126
+ LedgerName=self.ledger_name,
127
+ Statement='INSERT INTO compliance_records ?',
128
+ Parameters=[asdict(record)]
129
+ )
130
+
131
+ document_id = result.get('Documents', [{}])[0].get('Id')
132
+ return document_id
133
+ """
134
+
135
+ # Temporal: retornar ID simulado
136
+ simulated_id = f"qldb_doc_{int(time.time())}_{hash(record.user_email) % 10000}"
137
+ print(f"[QLDB - SIMULATED] Registrado consentimiento: {simulated_id}")
138
+ print(f"[QLDB - SIMULATED] Usuario: {record.user_email}")
139
+ print(f"[QLDB - SIMULATED] Vídeo: {record.video_title} ({record.video_hash[:16]}...)")
140
+
141
+ return simulated_id
142
+
143
+ except Exception as e:
144
+ print(f"[QLDB ERROR] Error registrando consentimiento: {e}")
145
+ return None
146
+
147
+ def record_validator_decision(self, document_id: str,
148
+ validator_email: str,
149
+ decision: str,
150
+ comments: str = "") -> bool:
151
+ """
152
+ Registra decisión del validador en QLDB
153
+
154
+ Args:
155
+ document_id: ID del documento original
156
+ validator_email: Email del validador
157
+ decision: "approved" o "rejected"
158
+ comments: Comentarios opcionales
159
+
160
+ Returns:
161
+ True si éxito, False si error
162
+ """
163
+ try:
164
+ timestamp = datetime.now(timezone.utc).isoformat()
165
+
166
+ # Código comentado hasta activación de QLDB
167
+ """
168
+ # Actualizar documento en QLDB
169
+ statement = f'''
170
+ UPDATE compliance_records AS r
171
+ SET r.validation_status = ?,
172
+ r.validation_timestamp = ?,
173
+ r.validator_emails = LIST_APPEND(r.validator_emails, ?)
174
+ WHERE r.id = ?
175
+ '''
176
+
177
+ self.client.execute_statement(
178
+ LedgerName=self.ledger_name,
179
+ Statement=statement,
180
+ Parameters=[decision, timestamp, validator_email, document_id]
181
+ )
182
+ """
183
+
184
+ # Temporal: logging simulado
185
+ print(f"[QLDB - SIMULATED] Registrada validación: {decision}")
186
+ print(f"[QLDB - SIMULATED] Documento: {document_id}")
187
+ print(f"[QLDB - SIMULATED] Validador: {validator_email}")
188
+ print(f"[QLDB - SIMULATED] Timestamp: {timestamp}")
189
+
190
+ return True
191
+
192
+ except Exception as e:
193
+ print(f"[QLDB ERROR] Error registrando validación: {e}")
194
+ return False
195
+
196
+ def get_compliance_record(self, document_id: str) -> Optional[Dict[str, Any]]:
197
+ """
198
+ Obtiene registro de cumplimiento desde QLDB
199
+
200
+ Returns:
201
+ Diccionario con el registro o None si no existe
202
+ """
203
+ try:
204
+ # Código comentado hasta activación de QLDB
205
+ """
206
+ result = self.client.execute_statement(
207
+ LedgerName=self.ledger_name,
208
+ Statement='SELECT * FROM compliance_records WHERE id = ?',
209
+ Parameters=[document_id]
210
+ )
211
+
212
+ documents = result.get('Documents', [])
213
+ return documents[0] if documents else None
214
+ """
215
+
216
+ # Temporal: retorno simulado
217
+ return {
218
+ "id": document_id,
219
+ "status": "simulated",
220
+ "message": "QLDB integration pending activation"
221
+ }
222
+
223
+ except Exception as e:
224
+ print(f"[QLDB ERROR] Error obteniendo registro: {e}")
225
+ return None
226
+
227
+ def publish_monthly_digest_to_polygon(self, period: str) -> Optional[str]:
228
+ """
229
+ Publica digest mensual de autorizaciones en Polygon blockchain
230
+
231
+ Args:
232
+ period: Período en formato YYYY-MM (ej: "2025-11")
233
+
234
+ Returns:
235
+ Hash de transacción o None si error
236
+ """
237
+ try:
238
+ # Obtener autorizaciones del período (comentado hasta activación QLDB)
239
+ """
240
+ statement = '''
241
+ SELECT user_email, video_hash, timestamp, consent_accepted,
242
+ validation_status, document_id
243
+ FROM compliance_records
244
+ WHERE SUBSTRING(timestamp, 1, 7) = ?
245
+ ORDER BY timestamp
246
+ '''
247
+
248
+ result = self.client.execute_statement(
249
+ LedgerName=self.ledger_name,
250
+ Statement=statement,
251
+ Parameters=[period]
252
+ )
253
+
254
+ authorizations = result.get('Documents', [])
255
+ """
256
+
257
+ # Temporal: datos simulados
258
+ authorizations = [
259
+ {
260
+ "user_email": "[email protected]",
261
+ "video_hash": "abc123...",
262
+ "timestamp": f"{period}-15T10:00:00Z",
263
+ "consent_accepted": True,
264
+ "validation_status": "approved",
265
+ "document_id": f"doc_{period}_001"
266
+ },
267
+ {
268
+ "user_email": "[email protected]",
269
+ "video_hash": "def456...",
270
+ "timestamp": f"{period}-20T14:30:00Z",
271
+ "consent_accepted": True,
272
+ "validation_status": "approved",
273
+ "document_id": f"doc_{period}_002"
274
+ }
275
+ ]
276
+
277
+ if not authorizations:
278
+ print(f"[QLDB] No hay autorizaciones para el período {period}")
279
+ return None
280
+
281
+ # Publicar en Polygon (comentado hasta activación)
282
+ """
283
+ from polygon_digest import digest_publisher
284
+ digest_record = digest_publisher.publish_monthly_digest(authorizations)
285
+
286
+ if digest_record:
287
+ return digest_record.transaction_hash
288
+ """
289
+
290
+ # Temporal: simulación de publicación
291
+ print(f"[QLDB - SIMULATED] Publicando digest de {len(authorizations)} autorizaciones")
292
+ simulated_tx_hash = f"0x{'0123456789abcdef' * 4}"
293
+ print(f"[QLDB - SIMULATED] Digest publicado en Polygon: {simulated_tx_hash}")
294
+
295
+ return simulated_tx_hash
296
+
297
+ except Exception as e:
298
+ print(f"[QLDB ERROR] Error publicando digest mensual: {e}")
299
+ return None
300
+
301
+ # Instancia global (comentada hasta activación)
302
+ # qldb_manager = QLDBManager()
303
+
304
+ # Temporal: instancia simulada para desarrollo
305
+ qldb_manager = QLDBManager()
compliance_client.py ADDED
@@ -0,0 +1,293 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cliente para comunicarse con el servicio Veureu Compliance
3
+
4
+ Este módulo se comunica con el microservicio compliance-service
5
+ que maneja OAuth, QLDB, Polygon y notificaciones en un solo lugar.
6
+ """
7
+
8
+ import requests
9
+ import json
10
+ import os
11
+ from typing import Optional, Dict, Any, List
12
+ import streamlit as st
13
+ from datetime import datetime
14
+ import logging
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ class ComplianceClient:
19
+ """Cliente para el microservicio de cumplimiento normativo"""
20
+
21
+ def __init__(self, compliance_service_url: str = None):
22
+ # URL del servicio de compliance (variable de entorno o por defecto)
23
+ self.compliance_service_url = compliance_service_url or os.getenv(
24
+ "COMPLIANCE_SERVICE_URL",
25
+ "https://veureu-compliance.hf.space" # Space oficial de compliance
26
+ )
27
+ self.timeout = 30 # segundos (máximo para operaciones blockchain)
28
+
29
+ logger.info(f"Compliance client inicializado: {self.compliance_service_url}")
30
+
31
+ def _make_request(self, method: str, endpoint: str, data: Dict = None) -> Optional[Dict[str, Any]]:
32
+ """
33
+ Método helper para hacer peticiones HTTP
34
+
35
+ Args:
36
+ method: Método HTTP ('GET', 'POST')
37
+ endpoint: Endpoint del API
38
+ data: Datos a enviar (solo para POST)
39
+
40
+ Returns:
41
+ Respuesta JSON o None si error
42
+ """
43
+ try:
44
+ url = f"{self.compliance_service_url}{endpoint}"
45
+
46
+ if method.upper() == "GET":
47
+ response = requests.get(url, timeout=self.timeout)
48
+ elif method.upper() == "POST":
49
+ response = requests.post(url, json=data, timeout=self.timeout)
50
+ else:
51
+ logger.error(f"Método no soportado: {method}")
52
+ return None
53
+
54
+ if response.status_code == 200:
55
+ return response.json()
56
+ else:
57
+ logger.error(f"Error en petición {method} {endpoint}: {response.status_code}")
58
+ logger.error(f"Response: {response.text}")
59
+ return None
60
+
61
+ except requests.exceptions.Timeout:
62
+ logger.error(f"Timeout en petición a {endpoint}")
63
+ return None
64
+ except requests.exceptions.ConnectionError:
65
+ logger.error(f"Error de conexión a {self.compliance_service_url}")
66
+ return None
67
+ except Exception as e:
68
+ logger.error(f"Error en petición a {endpoint}: {e}")
69
+ return None
70
+
71
+ # === MÉTODOS DE AUTENTICACIÓN ===
72
+
73
+ def authenticate_user(self, token: str) -> Optional[Dict[str, Any]]:
74
+ """Valida token con el servicio de autenticación"""
75
+ return self._make_request("POST", "/api/auth/validate", {"token": token})
76
+
77
+ def get_login_url(self, callback_url: str) -> str:
78
+ """Obtiene URL de login del servicio OAuth"""
79
+ response = self._make_request("POST", "/api/auth/login-url", {"callback_url": callback_url})
80
+
81
+ if response:
82
+ return response.get("login_url")
83
+ return None
84
+
85
+ def logout_user(self, token: str) -> bool:
86
+ """Invalida sesión en el servicio de autenticación"""
87
+ response = self._make_request("POST", "/api/auth/logout", {"token": token})
88
+ return response is not None
89
+
90
+ def is_authenticated(self) -> bool:
91
+ """Verifica si el usuario actual está autenticado"""
92
+ if "auth_token" not in st.session_state:
93
+ return False
94
+
95
+ token = st.session_state.auth_token
96
+ user_info = self.authenticate_user(token)
97
+
98
+ if user_info:
99
+ st.session_state.current_user = user_info
100
+ logger.info(f"Usuario autenticado: {user_info.get('email', 'unknown')}")
101
+ return True
102
+ else:
103
+ # Limpiar session si token inválido
104
+ logger.warning("Token inválido, limpiando sesión")
105
+ if "auth_token" in st.session_state:
106
+ del st.session_state.auth_token
107
+ if "current_user" in st.session_state:
108
+ del st.session_state.current_user
109
+ return False
110
+
111
+ def get_current_user(self) -> Optional[str]:
112
+ """Obtiene email del usuario actual"""
113
+ if self.is_authenticated():
114
+ return st.session_state.get("current_user", {}).get("email")
115
+ return None
116
+
117
+ def show_login_button(self) -> bool:
118
+ """Muestra botón de login y maneja redirección"""
119
+ callback_url = os.getenv("SPACE_URL", "http://localhost:8501")
120
+ login_url = self.get_login_url(callback_url)
121
+
122
+ if login_url:
123
+ st.markdown(f"""
124
+ ### 🔐 Iniciar Sesión
125
+
126
+ Para continuar, necesitas iniciar sesión con tu cuenta Google.
127
+
128
+ <a href="{login_url}" target="_self">
129
+ <button style="
130
+ background-color: #4285f4;
131
+ color: white;
132
+ padding: 12px 24px;
133
+ border: none;
134
+ border-radius: 4px;
135
+ font-size: 16px;
136
+ cursor: pointer;
137
+ text-decoration: none;
138
+ display: inline-block;
139
+ ">
140
+ 🚪 Iniciar Sesión con Google
141
+ </button>
142
+ </a>
143
+
144
+ *Al iniciar sesión, aceptas los términos de servicio y política de privacidad.*
145
+ """, unsafe_allow_html=True)
146
+
147
+ # Verificar callback OAuth
148
+ query_params = st.query_params
149
+ if "auth_token" in query_params:
150
+ token = query_params["auth_token"]
151
+ st.session_state.auth_token = token
152
+ st.query_params.clear()
153
+
154
+ if self.is_authenticated():
155
+ st.success("✅ Sesión iniciada correctamente")
156
+ st.rerun()
157
+ return True
158
+
159
+ return False
160
+
161
+ def logout(self) -> bool:
162
+ """Cierra sesión del usuario actual"""
163
+ if "auth_token" in st.session_state:
164
+ token = st.session_state.auth_token
165
+ success = self.logout_user(token)
166
+
167
+ # Limpiar session local siempre
168
+ if "auth_token" in st.session_state:
169
+ del st.session_state.auth_token
170
+ if "current_user" in st.session_state:
171
+ del st.session_state.current_user
172
+
173
+ logger.info("Sesión cerrada")
174
+ return success
175
+
176
+ return True
177
+
178
+ # === MÉTODOS DE CUMPLIMIENTO (QLDB/POLYGON) ===
179
+
180
+ def record_consent(self, user_info: Dict[str, Any],
181
+ video_info: Dict[str, Any],
182
+ consent_data: Dict[str, Any]) -> Optional[str]:
183
+ """Registra consentimiento de usuario vía API"""
184
+ payload = {
185
+ "user_info": user_info,
186
+ "video_info": video_info,
187
+ "consent_data": consent_data
188
+ }
189
+
190
+ response = self._make_request("POST", "/api/compliance/record-consent", payload)
191
+
192
+ if response:
193
+ document_id = response.get("document_id")
194
+ logger.info(f"Consentimiento registrado: {document_id}")
195
+ return document_id
196
+
197
+ return None
198
+
199
+ def send_validation_request(self, validation_request: Dict[str, Any]) -> bool:
200
+ """Envía solicitud de validación a validadores"""
201
+ response = self._make_request("POST", "/api/compliance/send-validation", validation_request)
202
+
203
+ if response:
204
+ logger.info(f"Solicitud de validación enviada: {validation_request.get('document_id')}")
205
+ return True
206
+
207
+ return False
208
+
209
+ def record_validator_decision(self, document_id: str,
210
+ validator_email: str,
211
+ decision: str,
212
+ comments: str = "") -> bool:
213
+ """Registra decisión de validador"""
214
+ payload = {
215
+ "document_id": document_id,
216
+ "validator_email": validator_email,
217
+ "decision": decision,
218
+ "comments": comments
219
+ }
220
+
221
+ response = self._make_request("POST", "/api/compliance/record-decision", payload)
222
+
223
+ if response:
224
+ logger.info(f"Decisión registrada: {document_id} -> {decision}")
225
+ return True
226
+
227
+ return False
228
+
229
+ # === MÉTODOS DE BLOCKCHAIN (POLYGON) ===
230
+
231
+ def publish_monthly_digest(self, period: str) -> Optional[str]:
232
+ """Publica digest mensual en blockchain"""
233
+ response = self._make_request("POST", "/api/blockchain/publish-digest", {"period": period})
234
+
235
+ if response:
236
+ tx_hash = response.get("transaction_hash")
237
+ logger.info(f"Digest publicado: {period} -> {tx_hash}")
238
+ return tx_hash
239
+
240
+ return None
241
+
242
+ def get_published_digests(self) -> List[Dict[str, Any]]:
243
+ """Obtiene lista de digest publicados"""
244
+ response = self._make_request("GET", "/api/blockchain/digests")
245
+
246
+ if response:
247
+ digests = response.get("digests", [])
248
+ logger.info(f"Obtenidos {len(digests)} digest publicados")
249
+ return digests
250
+
251
+ return []
252
+
253
+ def verify_digest(self, period: str, expected_hash: str) -> bool:
254
+ """Verifica integridad de digest en blockchain"""
255
+ payload = {
256
+ "period": period,
257
+ "expected_hash": expected_hash
258
+ }
259
+
260
+ response = self._make_request("POST", "/api/blockchain/verify-digest", payload)
261
+
262
+ if response:
263
+ is_valid = response.get("valid", False)
264
+ logger.info(f"Digest verificado: {period} -> {'VÁLIDO' if is_valid else 'INVÁLIDO'}")
265
+ return is_valid
266
+
267
+ return False
268
+
269
+ def get_compliance_stats(self) -> Dict[str, Any]:
270
+ """Obtiene estadísticas de cumplimiento"""
271
+ response = self._make_request("GET", "/api/compliance/stats")
272
+
273
+ if response:
274
+ logger.info("Estadísticas de cumplimiento obtenidas")
275
+ return response
276
+
277
+ return {}
278
+
279
+ def health_check(self) -> bool:
280
+ """Verifica si el servicio de compliance está disponible"""
281
+ response = self._make_request("GET", "/")
282
+
283
+ if response:
284
+ status = response.get("status")
285
+ if status == "running":
286
+ logger.info("Servicio compliance funcionando correctamente")
287
+ return True
288
+
289
+ logger.warning("Servicio compliance no disponible")
290
+ return False
291
+
292
+ # Instancia global del cliente
293
+ compliance_client = ComplianceClient()
compliance_unified_client.py ADDED
@@ -0,0 +1,305 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Cliente unificado para servicio de compliance externo
3
+
4
+ Este módulo se comunica con el microservicio compliance-service
5
+ que maneja OAuth, QLDB, Polygon y notificaciones en un solo lugar.
6
+ """
7
+
8
+ import requests
9
+ import json
10
+ import os
11
+ from typing import Optional, Dict, Any, List
12
+ import streamlit as st
13
+ from datetime import datetime
14
+
15
+ class ComplianceClient:
16
+ """Cliente unificado para el microservicio de cumplimiento normativo"""
17
+
18
+ def __init__(self, compliance_service_url: str = None):
19
+ # URL del servicio de compliance (variable de entorno o por defecto)
20
+ self.compliance_service_url = compliance_service_url or os.getenv(
21
+ "COMPLIANCE_SERVICE_URL",
22
+ "https://veureu-compliance.hf.space"
23
+ )
24
+ self.timeout = 30 # segundos (máximo para operaciones blockchain)
25
+
26
+ # === MÉTODOS DE AUTENTICACIÓN ===
27
+
28
+ def authenticate_user(self, token: str) -> Optional[Dict[str, Any]]:
29
+ """Valida token con el servicio de autenticación"""
30
+ try:
31
+ response = requests.post(
32
+ f"{self.compliance_service_url}/api/auth/validate",
33
+ json={"token": token},
34
+ timeout=self.timeout
35
+ )
36
+
37
+ if response.status_code == 200:
38
+ return response.json()
39
+ else:
40
+ return None
41
+
42
+ except Exception as e:
43
+ print(f"[COMPLIANCE CLIENT] Error validando token: {e}")
44
+ return None
45
+
46
+ def get_login_url(self, callback_url: str) -> str:
47
+ """Obtiene URL de login del servicio OAuth"""
48
+ try:
49
+ response = requests.post(
50
+ f"{self.compliance_service_url}/api/auth/login-url",
51
+ json={"callback_url": callback_url},
52
+ timeout=self.timeout
53
+ )
54
+
55
+ if response.status_code == 200:
56
+ return response.json().get("login_url")
57
+ else:
58
+ return None
59
+
60
+ except Exception as e:
61
+ print(f"[COMPLIANCE CLIENT] Error obteniendo login URL: {e}")
62
+ return None
63
+
64
+ def logout_user(self, token: str) -> bool:
65
+ """Invalida sesión en el servicio de autenticación"""
66
+ try:
67
+ response = requests.post(
68
+ f"{self.compliance_service_url}/api/auth/logout",
69
+ json={"token": token},
70
+ timeout=self.timeout
71
+ )
72
+
73
+ return response.status_code == 200
74
+
75
+ except Exception as e:
76
+ print(f"[COMPLIANCE CLIENT] Error en logout: {e}")
77
+ return False
78
+
79
+ def is_authenticated(self) -> bool:
80
+ """Verifica si el usuario actual está autenticado"""
81
+ if "auth_token" not in st.session_state:
82
+ return False
83
+
84
+ token = st.session_state.auth_token
85
+ user_info = self.authenticate_user(token)
86
+
87
+ if user_info:
88
+ st.session_state.current_user = user_info
89
+ return True
90
+ else:
91
+ # Limpiar session si token inválido
92
+ if "auth_token" in st.session_state:
93
+ del st.session_state.auth_token
94
+ if "current_user" in st.session_state:
95
+ del st.session_state.current_user
96
+ return False
97
+
98
+ def get_current_user(self) -> Optional[str]:
99
+ """Obtiene email del usuario actual"""
100
+ if self.is_authenticated():
101
+ return st.session_state.get("current_user", {}).get("email")
102
+ return None
103
+
104
+ def show_login_button(self) -> bool:
105
+ """Muestra botón de login y maneja redirección"""
106
+ callback_url = os.getenv("SPACE_URL", "http://localhost:8501")
107
+ login_url = self.get_login_url(callback_url)
108
+
109
+ if login_url:
110
+ st.markdown(f"""
111
+ ### 🔐 Iniciar Sesión
112
+
113
+ Para continuar, necesitas iniciar sesión con tu cuenta Google.
114
+
115
+ <a href="{login_url}" target="_self">
116
+ <button style="
117
+ background-color: #4285f4;
118
+ color: white;
119
+ padding: 12px 24px;
120
+ border: none;
121
+ border-radius: 4px;
122
+ font-size: 16px;
123
+ cursor: pointer;
124
+ text-decoration: none;
125
+ ">
126
+ 🚪 Iniciar Sesión con Google
127
+ </button>
128
+ </a>
129
+ """, unsafe_allow_html=True)
130
+
131
+ # Verificar callback OAuth
132
+ query_params = st.query_params
133
+ if "auth_token" in query_params:
134
+ token = query_params["auth_token"]
135
+ st.session_state.auth_token = token
136
+ st.query_params.clear()
137
+
138
+ if self.is_authenticated():
139
+ st.success("✅ Sesión iniciada correctamente")
140
+ st.rerun()
141
+ return True
142
+
143
+ return False
144
+
145
+ def logout(self) -> bool:
146
+ """Cierra sesión del usuario actual"""
147
+ if "auth_token" in st.session_state:
148
+ token = st.session_state.auth_token
149
+ success = self.logout_user(token)
150
+
151
+ # Limpiar session local siempre
152
+ if "auth_token" in st.session_state:
153
+ del st.session_state.auth_token
154
+ if "current_user" in st.session_state:
155
+ del st.session_state.current_user
156
+
157
+ return success
158
+
159
+ return True
160
+
161
+ # === MÉTODOS DE CUMPLIMIENTO (QLDB/POLYGON) ===
162
+
163
+ def record_consent(self, user_info: Dict[str, Any],
164
+ video_info: Dict[str, Any],
165
+ consent_data: Dict[str, Any]) -> Optional[str]:
166
+ """Registra consentimiento de usuario vía API"""
167
+ try:
168
+ payload = {
169
+ "user_info": user_info,
170
+ "video_info": video_info,
171
+ "consent_data": consent_data
172
+ }
173
+
174
+ response = requests.post(
175
+ f"{self.compliance_service_url}/api/compliance/record-consent",
176
+ json=payload,
177
+ timeout=self.timeout
178
+ )
179
+
180
+ if response.status_code == 200:
181
+ return response.json().get("document_id")
182
+ else:
183
+ print(f"[COMPLIANCE CLIENT] Error recording consent: {response.status_code}")
184
+ return None
185
+
186
+ except Exception as e:
187
+ print(f"[COMPLIANCE CLIENT] Error en record_consent: {e}")
188
+ return None
189
+
190
+ def send_validation_request(self, validation_request: Dict[str, Any]) -> bool:
191
+ """Envía solicitud de validación a validadores"""
192
+ try:
193
+ response = requests.post(
194
+ f"{self.compliance_service_url}/api/compliance/send-validation",
195
+ json=validation_request,
196
+ timeout=self.timeout
197
+ )
198
+
199
+ return response.status_code == 200
200
+
201
+ except Exception as e:
202
+ print(f"[COMPLIANCE CLIENT] Error enviando validación: {e}")
203
+ return False
204
+
205
+ def record_validator_decision(self, document_id: str,
206
+ validator_email: str,
207
+ decision: str,
208
+ comments: str = "") -> bool:
209
+ """Registra decisión de validador"""
210
+ try:
211
+ payload = {
212
+ "document_id": document_id,
213
+ "validator_email": validator_email,
214
+ "decision": decision,
215
+ "comments": comments
216
+ }
217
+
218
+ response = requests.post(
219
+ f"{self.compliance_service_url}/api/compliance/record-decision",
220
+ json=payload,
221
+ timeout=self.timeout
222
+ )
223
+
224
+ return response.status_code == 200
225
+
226
+ except Exception as e:
227
+ print(f"[COMPLIANCE CLIENT] Error registrando decisión: {e}")
228
+ return False
229
+
230
+ # === MÉTODOS DE BLOCKCHAIN (POLYGON) ===
231
+
232
+ def publish_monthly_digest(self, period: str) -> Optional[str]:
233
+ """Publica digest mensual en blockchain"""
234
+ try:
235
+ response = requests.post(
236
+ f"{self.compliance_service_url}/api/blockchain/publish-digest",
237
+ json={"period": period},
238
+ timeout=self.timeout
239
+ )
240
+
241
+ if response.status_code == 200:
242
+ return response.json().get("transaction_hash")
243
+ else:
244
+ return None
245
+
246
+ except Exception as e:
247
+ print(f"[COMPLIANCE CLIENT] Error publicando digest: {e}")
248
+ return None
249
+
250
+ def get_published_digests(self) -> List[Dict[str, Any]]:
251
+ """Obtiene lista de digest publicados"""
252
+ try:
253
+ response = requests.get(
254
+ f"{self.compliance_service_url}/api/blockchain/digests",
255
+ timeout=self.timeout
256
+ )
257
+
258
+ if response.status_code == 200:
259
+ return response.json().get("digests", [])
260
+ else:
261
+ return []
262
+
263
+ except Exception as e:
264
+ print(f"[COMPLIANCE CLIENT] Error obteniendo digests: {e}")
265
+ return []
266
+
267
+ def verify_digest(self, period: str, expected_hash: str) -> bool:
268
+ """Verifica integridad de digest en blockchain"""
269
+ try:
270
+ payload = {
271
+ "period": period,
272
+ "expected_hash": expected_hash
273
+ }
274
+
275
+ response = requests.post(
276
+ f"{self.compliance_service_url}/api/blockchain/verify-digest",
277
+ json=payload,
278
+ timeout=self.timeout
279
+ )
280
+
281
+ return response.status_code == 200 and response.json().get("valid", False)
282
+
283
+ except Exception as e:
284
+ print(f"[COMPLIANCE CLIENT] Error verificando digest: {e}")
285
+ return False
286
+
287
+ def get_compliance_stats(self) -> Dict[str, Any]:
288
+ """Obtiene estadísticas de cumplimiento"""
289
+ try:
290
+ response = requests.get(
291
+ f"{self.compliance_service_url}/api/compliance/stats",
292
+ timeout=self.timeout
293
+ )
294
+
295
+ if response.status_code == 200:
296
+ return response.json()
297
+ else:
298
+ return {}
299
+
300
+ except Exception as e:
301
+ print(f"[COMPLIANCE CLIENT] Error obteniendo stats: {e}")
302
+ return {}
303
+
304
+ # Instancia global unificada
305
+ compliance_client = ComplianceClient()
notification_service.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Servicio de notificaciones por email para validación interna
3
+
4
+ Este módulo gestiona el envío de correos a los validadores internos
5
+ con la información del vídeo para su revisión regulatoria.
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import base64
11
+ from typing import Dict, Any, List, Optional
12
+ from datetime import datetime
13
+ from dataclasses import dataclass
14
+
15
+ # Imports para email (comentados hasta configuración)
16
+ # import smtplib
17
+ # from email.mime.text import MIMEText
18
+ # from email.mime.multipart import MIMEMultipart
19
+ # from email.mime.base import MIMEBase
20
+ # from email import encoders
21
+
22
+ @dataclass
23
+ class ValidationRequest:
24
+ """Solicitud de validación para envío por email"""
25
+ document_id: str
26
+ user_email: str
27
+ user_name: str
28
+ video_title: str
29
+ video_hash: str
30
+ timestamp: str
31
+ video_url: str
32
+ consent_data: Dict[str, Any]
33
+
34
+ class EmailNotificationService:
35
+ """Servicio de notificaciones por email para validadores"""
36
+
37
+ def __init__(self):
38
+ # Configuración SMTP (comentada hasta despliegue)
39
+ self.smtp_server = os.getenv("SMTP_SERVER", "smtp.gmail.com")
40
+ self.smtp_port = int(os.getenv("SMTP_PORT", "587"))
41
+ self.sender_email = os.getenv("SENDER_EMAIL", "[email protected]")
42
+ self.sender_password = os.getenv("SENDER_PASSWORD", "")
43
+
44
+ # Lista de validadores internos
45
+ self.validators = self._load_validators()
46
+
47
+ def _load_validators(self) -> List[str]:
48
+ """Carga lista de validadores desde configuración"""
49
+ # En producción, esto vendría de base de datos o config
50
+ default_validators = [
51
52
53
54
+ ]
55
+
56
+ # Permitir sobreescribir por variable de entorno
57
+ env_validators = os.getenv("VALIDATOR_EMAILS", "")
58
+ if env_validators:
59
+ return [email.strip() for email in env_validators.split(",")]
60
+
61
+ return default_validators
62
+
63
+ def _generate_validation_email(self, request: ValidationRequest) -> str:
64
+ """Genera contenido del email de validación"""
65
+
66
+ approval_link = f"https://veureu-demo.hf.space/validate?doc_id={request.document_id}&action=approve"
67
+ rejection_link = f"https://veureu-demo.hf.space/validate?doc_id={request.document_id}&action=reject"
68
+
69
+ email_content = f"""
70
+ <!DOCTYPE html>
71
+ <html>
72
+ <head>
73
+ <meta charset="UTF-8">
74
+ <title>Solicitud de Validación - Veureu</title>
75
+ </head>
76
+ <body style="font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto;">
77
+
78
+ <div style="background-color: #f8f9fa; padding: 20px; border-radius: 8px;">
79
+ <h1 style="color: #1f6feb; text-align: center;">🔐 Solicitud de Validación Regulatoria</h1>
80
+
81
+ <div style="background-color: white; padding: 20px; border-radius: 6px; margin: 20px 0;">
82
+ <h2 style="color: #333;">📋 Información del Envío</h2>
83
+ <table style="width: 100%; border-collapse: collapse;">
84
+ <tr>
85
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Usuario:</strong></td>
86
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{request.user_name} ({request.user_email})</td>
87
+ </tr>
88
+ <tr>
89
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Vídeo:</strong></td>
90
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{request.video_title}</td>
91
+ </tr>
92
+ <tr>
93
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Hash:</strong></td>
94
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><code>{request.video_hash[:32]}...</code></td>
95
+ </tr>
96
+ <tr>
97
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>Timestamp:</strong></td>
98
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;">{request.timestamp}</td>
99
+ </tr>
100
+ <tr>
101
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>ID Documento:</strong></td>
102
+ <td style="padding: 8px; border-bottom: 1px solid #ddd;"><code>{request.document_id}</code></td>
103
+ </tr>
104
+ </table>
105
+ </div>
106
+
107
+ <div style="background-color: #fff3cd; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #ffc107;">
108
+ <h3 style="color: #856404; margin-top: 0;">✅ Consentimientos Aceptados</h3>
109
+ <ul style="color: #856404; margin: 0;">
110
+ <li>Derechos sobre el vídeo: {'✅' if request.consent_data.get('rights') else '❌'}</li>
111
+ <li>Consentimiento biométrico: {'✅' if request.consent_data.get('biometric') else '❌'}</li>
112
+ <li>Contenido permitido: {'✅' if request.consent_data.get('content') else '❌'}</li>
113
+ <li>Privacidad y datos: {'✅' if request.consent_data.get('privacy') else '❌'}</li>
114
+ </ul>
115
+ </div>
116
+
117
+ <div style="background-color: #d1ecf1; padding: 15px; border-radius: 6px; margin: 20px 0; border-left: 4px solid #17a2b8;">
118
+ <h3 style="color: #0c5460; margin-top: 0;">🎥 Acceso al Vídeo</h3>
119
+ <p style="color: #0c5460; margin: 0;">Puedes revisar el vídeo en el siguiente enlace:</p>
120
+ <p style="margin: 10px 0;"><a href="{request.video_url}" style="background-color: #17a2b8; color: white; padding: 10px 20px; text-decoration: none; border-radius: 4px; display: inline-block;">📹 Ver Vídeo</a></p>
121
+ </div>
122
+
123
+ <div style="text-align: center; margin: 30px 0;">
124
+ <h3 style="color: #333;">🔍 Decisión de Validación</h3>
125
+ <p style="color: #666;">Por favor, revisa el contenido y toma una decisión:</p>
126
+
127
+ <div style="display: flex; justify-content: center; gap: 20px; margin: 20px 0;">
128
+ <a href="{approval_link}" style="background-color: #28a745; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">
129
+ ✅ APROBAR
130
+ </a>
131
+ <a href="{rejection_link}" style="background-color: #dc3545; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px; font-weight: bold;">
132
+ ❌ RECHAZAR
133
+ </a>
134
+ </div>
135
+
136
+ <p style="color: #666; font-size: 12px; margin-top: 20px;">
137
+ Esta decisión quedará registrada en AWS QLDB para cumplimiento normativo (AI Act, GDPR).
138
+ </p>
139
+ </div>
140
+
141
+ <div style="background-color: #f8f9fa; padding: 15px; border-radius: 6px; margin: 20px 0; text-align: center; color: #666; font-size: 12px;">
142
+ <p>Este es un mensaje automático del sistema de validación regulatoria de Veureu.</p>
143
+ <p>Para consultas, contacta a [email protected]</p>
144
+ </div>
145
+ </div>
146
+
147
+ </body>
148
+ </html>
149
+ """
150
+
151
+ return email_content
152
+
153
+ def send_validation_request(self, request: ValidationRequest) -> bool:
154
+ """
155
+ Envía solicitud de validación a todos los validadores
156
+
157
+ Returns:
158
+ True si éxito, False si error
159
+ """
160
+ try:
161
+ subject = f"🔐 Validación Regulatoria - {request.video_title}"
162
+ html_content = self._generate_validation_email(request)
163
+
164
+ # Código comentado hasta configuración SMTP
165
+ """
166
+ # Configurar mensaje
167
+ msg = MIMEMultipart('alternative')
168
+ msg['Subject'] = subject
169
+ msg['From'] = self.sender_email
170
+ msg['To'] = ', '.join(self.validators)
171
+
172
+ # Adjuntar contenido HTML
173
+ html_part = MIMEText(html_content, 'html')
174
+ msg.attach(html_part)
175
+
176
+ # Enviar email
177
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
178
+ server.starttls()
179
+ server.login(self.sender_email, self.sender_password)
180
+ server.send_message(msg)
181
+ """
182
+
183
+ # Temporal: logging simulado
184
+ print(f"[EMAIL - SIMULATED] Enviando solicitud de validación:")
185
+ print(f"[EMAIL - SIMULATED] Para: {', '.join(self.validators)}")
186
+ print(f"[EMAIL - SIMULATED] Asunto: {subject}")
187
+ print(f"[EMAIL - SIMULATED] Documento: {request.document_id}")
188
+ print(f"[EMAIL - SIMULATED] Usuario: {request.user_email}")
189
+ print(f"[EMAIL - SIMULATED] Vídeo: {request.video_title}")
190
+
191
+ return True
192
+
193
+ except Exception as e:
194
+ print(f"[EMAIL ERROR] Error enviando validación: {e}")
195
+ return False
196
+
197
+ def send_decision_notification(self, validator_email: str,
198
+ decision: str,
199
+ document_id: str,
200
+ comments: str = "") -> bool:
201
+ """
202
+ Envía notificación de decisión tomada por validador
203
+
204
+ Returns:
205
+ True si éxito, False si error
206
+ """
207
+ try:
208
+ subject = f"🔍 Decisión de Validación - {decision.upper()}"
209
+
210
+ content = f"""
211
+ Se ha registrado una decisión de validación:
212
+
213
+ Validador: {validator_email}
214
+ Documento: {document_id}
215
+ Decisión: {decision}
216
+ Comentarios: {comments}
217
+ Timestamp: {datetime.now().isoformat()}
218
+
219
+ Esta decisión ha quedado registrada en AWS QLDB.
220
+ """
221
+
222
+ # Código comentado hasta configuración SMTP
223
+ """
224
+ msg = MIMEText(content)
225
+ msg['Subject'] = subject
226
+ msg['From'] = self.sender_email
227
+ msg['To'] = '[email protected]'
228
+
229
+ with smtplib.SMTP(self.smtp_server, self.smtp_port) as server:
230
+ server.starttls()
231
+ server.login(self.sender_email, self.sender_password)
232
+ server.send_message(msg)
233
+ """
234
+
235
+ # Temporal: logging simulado
236
+ print(f"[EMAIL - SIMULATED] Notificación de decisión enviada:")
237
+ print(f"[EMAIL - SIMULATED] Validador: {validator_email}")
238
+ print(f"[EMAIL - SIMULATED] Decisión: {decision}")
239
+ print(f"[EMAIL - SIMULATED] Documento: {document_id}")
240
+
241
+ return True
242
+
243
+ except Exception as e:
244
+ print(f"[EMAIL ERROR] Error enviando notificación: {e}")
245
+ return False
246
+
247
+ # Instancia global
248
+ notification_service = EmailNotificationService()
polygon_digest.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Módulo de integración con Polygon para publicación de digest de cumplimiento
3
+
4
+ Este módulo publica hashes mensuales de autorizaciones en Polygon blockchain
5
+ para garantizar trazabilidad y cumplimiento normativo público (AI Act, GDPR).
6
+ """
7
+
8
+ import os
9
+ import json
10
+ import hashlib
11
+ from typing import Dict, Any, List, Optional
12
+ from datetime import datetime, timezone
13
+ from dataclasses import dataclass
14
+ import logging
15
+
16
+ # Imports comentados hasta configuración
17
+ # from web3 import Web3
18
+
19
+ # Configuración
20
+ logger = logging.getLogger(__name__)
21
+
22
+ @dataclass
23
+ class DigestRecord:
24
+ """Registro de digest para publicación en Polygon"""
25
+ period: str # "2025-11" formato YYYY-MM
26
+ root_hash: str # Hash SHA-256 de todas las autorizaciones del período
27
+ authorization_count: int
28
+ timestamp: str
29
+ publisher_address: str
30
+ transaction_hash: Optional[str] = None
31
+ block_number: Optional[int] = None
32
+ gas_used: Optional[int] = None
33
+
34
+ class PolygonDigestPublisher:
35
+ """Publicador de digest en Polygon blockchain"""
36
+
37
+ def __init__(self):
38
+ # Configuración Web3 (comentada hasta activación)
39
+ """
40
+ self.w3 = Web3(Web3.HTTPProvider(os.getenv("POLYGON_RPC_URL")))
41
+ self.private_key = os.getenv("POLYGON_WALLET_PRIVATE_KEY")
42
+ self.account = self.w3.eth.account.from_key(self.private_key)
43
+ self.chain_id = int(os.getenv("POLYGON_CHAIN_ID", "137")) # 137 mainnet, 80002 Amoy testnet
44
+
45
+ self.contract_addr = os.getenv("DIGEST_CONTRACT_ADDR")
46
+ self.contract_abi = json.loads(os.getenv("DIGEST_CONTRACT_ABI", "[]"))
47
+ """
48
+
49
+ # Temporal: configuración simulada
50
+ self.w3 = None
51
+ self.account = None
52
+ self.contract_addr = "0x0000000000000000000000000000000000000000" # Placeholder
53
+ self.chain_id = 137
54
+
55
+ logger.info("PolygonDigestPublisher inicializado (modo simulado)")
56
+
57
+ def _compute_monthly_digest(self, authorizations: List[Dict[str, Any]]) -> str:
58
+ """
59
+ Calcula hash SHA-256 de todas las autorizaciones del mes
60
+
61
+ Args:
62
+ authorizations: Lista de registros de autorización del período
63
+
64
+ Returns:
65
+ Hash hexadecimal de 64 caracteres
66
+ """
67
+ # Ordenar autorizaciones por timestamp para consistencia
68
+ sorted_auths = sorted(authorizations, key=lambda x: x.get('timestamp', ''))
69
+
70
+ # Crear string concatenado con todos los datos relevantes
71
+ digest_data = ""
72
+ for auth in sorted_auths:
73
+ # Campos relevantes para el digest
74
+ relevant_data = {
75
+ 'user_email': auth.get('user_email', ''),
76
+ 'video_hash': auth.get('video_hash', ''),
77
+ 'timestamp': auth.get('timestamp', ''),
78
+ 'consent_accepted': auth.get('consent_accepted', False),
79
+ 'validation_status': auth.get('validation_status', ''),
80
+ 'document_id': auth.get('document_id', '')
81
+ }
82
+
83
+ # Convertir a JSON ordenado y añadir al digest
84
+ auth_json = json.dumps(relevant_data, sort_keys=True, separators=(',', ':'))
85
+ digest_data += auth_json + "|"
86
+
87
+ # Calcular hash SHA-256
88
+ digest_hash = hashlib.sha256(digest_data.encode('utf-8')).hexdigest()
89
+
90
+ logger.info(f"Digest calculado: {len(authorizations)} autorizaciones → {digest_hash[:16]}...")
91
+ return digest_hash
92
+
93
+ def _get_period_from_timestamp(self, timestamp: str) -> str:
94
+ """
95
+ Extrae período YYYY-MM del timestamp ISO
96
+
97
+ Args:
98
+ timestamp: Timestamp en formato ISO 8601
99
+
100
+ Returns:
101
+ Período en formato YYYY-MM
102
+ """
103
+ try:
104
+ dt = datetime.fromisoformat(timestamp.replace('Z', '+00:00'))
105
+ return dt.strftime("%Y-%m")
106
+ except Exception as e:
107
+ logger.error(f"Error parseando timestamp {timestamp}: {e}")
108
+ return datetime.now(timezone.utc).strftime("%Y-%m")
109
+
110
+ def publish_monthly_digest(self, authorizations: List[Dict[str, Any]]) -> Optional[DigestRecord]:
111
+ """
112
+ Publica digest mensual en Polygon blockchain
113
+
114
+ Args:
115
+ authorizations: Lista de autorizaciones del período a publicar
116
+
117
+ Returns:
118
+ DigestRecord con resultado de la publicación
119
+ """
120
+ if not authorizations:
121
+ logger.warning("No hay autorizaciones para publicar")
122
+ return None
123
+
124
+ try:
125
+ # Calcular período y hash
126
+ first_auth = authorizations[0]
127
+ period = self._get_period_from_timestamp(first_auth.get('timestamp', ''))
128
+ root_hash = self._compute_monthly_digest(authorizations)
129
+
130
+ # Crear registro
131
+ digest_record = DigestRecord(
132
+ period=period,
133
+ root_hash=root_hash,
134
+ authorization_count=len(authorizations),
135
+ timestamp=datetime.now(timezone.utc).isoformat(),
136
+ publisher_address=self.account.address if self.account else "0x0000000000000000000000000000000000000000"
137
+ )
138
+
139
+ # Publicar en blockchain (comentado hasta activación)
140
+ """
141
+ contract = self.w3.eth.contract(
142
+ address=Web3.to_checksum_address(self.contract_addr),
143
+ abi=self.contract_abi
144
+ )
145
+
146
+ nonce = self.w3.eth.get_transaction_count(self.account.address)
147
+
148
+ tx = contract.functions.publish(
149
+ Web3.to_bytes(hexstr=root_hash),
150
+ period
151
+ ).build_transaction({
152
+ "from": self.account.address,
153
+ "nonce": nonce,
154
+ "gas": 120000,
155
+ "maxFeePerGas": self.w3.to_wei('60', 'gwei'),
156
+ "maxPriorityFeePerGas": self.w3.to_wei('2', 'gwei'),
157
+ "chainId": self.chain_id
158
+ })
159
+
160
+ signed = self.w3.eth.account.sign_transaction(tx, self.private_key)
161
+ tx_hash = self.w3.eth.send_raw_transaction(signed.rawTransaction)
162
+ receipt = self.w3.eth.wait_for_transaction_receipt(tx_hash)
163
+
164
+ # Actualizar registro con datos de la transacción
165
+ digest_record.transaction_hash = receipt.transactionHash.hex()
166
+ digest_record.block_number = receipt.blockNumber
167
+ digest_record.gas_used = receipt.gasUsed
168
+ """
169
+
170
+ # Temporal: simulación de publicación
171
+ simulated_tx_hash = f"0x{'0123456789abcdef' * 4}" # 64 chars hex
172
+ digest_record.transaction_hash = simulated_tx_hash
173
+ digest_record.block_number = 12345678
174
+ digest_record.gas_used = 87654
175
+
176
+ logger.info(f"Digest publicado simulado: {period} → {simulated_tx_hash}")
177
+
178
+ return digest_record
179
+
180
+ except Exception as e:
181
+ logger.error(f"Error publicando digest: {e}")
182
+ return None
183
+
184
+ def verify_digest_on_chain(self, period: str, expected_hash: str) -> bool:
185
+ """
186
+ Verifica que el digest publicado en blockchain coincide con el hash esperado
187
+
188
+ Args:
189
+ period: Período YYYY-MM a verificar
190
+ expected_hash: Hash esperado del digest
191
+
192
+ Returns:
193
+ True si coincide, False si no
194
+ """
195
+ try:
196
+ # Consultar blockchain (comentado hasta activación)
197
+ """
198
+ contract = self.w3.eth.contract(
199
+ address=Web3.to_checksum_address(self.contract_addr),
200
+ abi=self.contract_abi
201
+ )
202
+
203
+ on_chain_hash = contract.functions.digests(period).call()
204
+ on_chain_hex = self.w3.to_hex(on_chain_hash)
205
+
206
+ return on_chain_hex == expected_hash
207
+ """
208
+
209
+ # Temporal: simulación de verificación
210
+ logger.info(f"Verificación simulada: {period} → {expected_hash[:16]}...")
211
+ return True
212
+
213
+ except Exception as e:
214
+ logger.error(f"Error verificando digest: {e}")
215
+ return False
216
+
217
+ def get_published_digests(self) -> List[Dict[str, Any]]:
218
+ """
219
+ Obtiene lista de todos los digest publicados (simulado)
220
+
221
+ Returns:
222
+ Lista de digest publicados con metadata
223
+ """
224
+ # Temporal: retorno simulado
225
+ return [
226
+ {
227
+ "period": "2025-11",
228
+ "transaction_hash": "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
229
+ "block_number": 12345678,
230
+ "timestamp": "2025-11-03T14:30:00Z",
231
+ "authorization_count": 42
232
+ }
233
+ ]
234
+
235
+ # Instancia global
236
+ digest_publisher = PolygonDigestPublisher()
requirements.txt CHANGED
@@ -7,4 +7,6 @@ pydub
7
  python-dotenv
8
  gradio_client # Para llamar al space svision
9
  Pillow # Para procesar imágenes antes de enviar a svision
10
- # Forzar rebuild 2025-11-01
 
 
 
7
  python-dotenv
8
  gradio_client # Para llamar al space svision
9
  Pillow # Para procesar imágenes antes de enviar a svision
10
+ streamlit-authenticator>=0.2.3
11
+ web3>=6.0.0 # Para integración con Polygon blockchain
12
+ # Forzar rebuild 2025-11-03