Upload 9 files
Browse files- app.py +596 -9
- auth_config.yaml +15 -0
- auth_utils.py +122 -0
- aws_qldb.py +305 -0
- compliance_client.py +293 -0
- compliance_unified_client.py +305 -0
- notification_service.py +248 -0
- polygon_digest.py +236 -0
- requirements.txt +3 -1
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 |
-
|
| 20 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 393 |
-
|
| 394 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
vb = uploaded.getvalue()
|
| 396 |
-
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
"[email protected]",
|
| 52 |
+
"[email protected]",
|
| 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 |
-
|
|
|
|
|
|
|
|
|
| 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
|