diff --git a/.env.dev b/.env.dev index 6bc9cae..46d08eb 100644 --- a/.env.dev +++ b/.env.dev @@ -19,10 +19,10 @@ AI_PROVIDER=local OPENAI_API_KEY= # Local AI (Ollama & Whisper) - local execution -LOCAL_OLLAMA_URL=http://localhost:11434 -LOCAL_WHISPER_URL=http://localhost:9000 -DEV_OLLAMA_URL=http://localhost:11434 -DEV_WHISPER_URL=http://localhost:9000 +LOCAL_OLLAMA_URL=http://192.168.0.5:11434 +LOCAL_WHISPER_URL=http://192.168.0.5:9000 +DEV_OLLAMA_URL=http://192.168.0.5:11434 +DEV_WHISPER_URL=http://192.168.0.5:9000 DEV_BARK_URL=http://localhost:8000 # SMTP for local development @@ -47,3 +47,4 @@ SAM_DATABASE_URL=mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.ama LOCAL_VIDEO_BRIDGE_URL=http://localhost:8000 EMBEDDING_MODEL=nomic-embed-text LOCAL_LLM_MODEL=llama3.2:3b +CMS_API_URL=http://studio:3001 diff --git a/CONFIGURACION_RED.md b/CONFIGURACION_RED.md index e3668a6..facc27d 100644 --- a/CONFIGURACION_RED.md +++ b/CONFIGURACION_RED.md @@ -225,6 +225,9 @@ WHISPER_MODEL=whisper-large-v3 # Frontend URLs NEXT_PUBLIC_CMS_API_URL=https://studio.norteamericano.com NEXT_PUBLIC_LMS_API_URL=https://learning.norteamericano.com + +# Backend-to-backend (LMS -> CMS) +CMS_API_URL=http://studio:3001 ``` --- diff --git a/DESPLIEGUE.md b/DESPLIEGUE.md index e253e9e..0ff94ec 100644 --- a/DESPLIEGUE.md +++ b/DESPLIEGUE.md @@ -161,6 +161,7 @@ Valores esperados en Docker: ```bash DATABASE_URL=postgresql://user:@db:5432/openccb_lms LMS_DATABASE_URL=postgresql://user:@db:5432/openccb_lms +CMS_API_URL=http://studio:3001 ``` Si aparece `localhost:5433` en el contenedor `openccb-experience`, recrea el servicio con la variable correcta: @@ -169,6 +170,24 @@ Si aparece `localhost:5433` en el contenedor `openccb-experience`, recrea el ser LMS_DATABASE_URL='postgresql://user:password@db:5432/openccb_lms' docker compose up -d --force-recreate experience ``` +### LMS `/auth/me` devuelve `502 Bad Gateway` + +**Síntoma común:** login correcto en CMS, pero `GET /auth/me` desde LMS falla con `No se pudo sincronizar el perfil de usuario desde CMS`. + +**Causa común:** `CMS_API_URL` faltante o incorrecta dentro del contenedor `experience`. + +**Solución:** + +```bash +docker exec openccb-experience sh -lc 'echo CMS_API_URL=$CMS_API_URL' + +# Valor recomendado en Docker Compose +CMS_API_URL=http://studio:3001 + +# Recrear el servicio con la variable corregida +CMS_API_URL='http://studio:3001' docker compose up -d --force-recreate experience +``` + ### Reiniciar servicios ```bash # Reiniciar todo diff --git a/ManualDeConfiguracion.md b/ManualDeConfiguracion.md index a0dce38..fb79b47 100644 --- a/ManualDeConfiguracion.md +++ b/ManualDeConfiguracion.md @@ -48,7 +48,7 @@ El script `install.sh` automatiza todo el proceso: ### Pasos del Script -1. **Detección de entorno**: Dev o Prod +1. **Entorno local forzado**: `ENVIRONMENT=dev` 2. **Instalación de dependencias**: Rust, Node.js, Docker, sqlx-cli 3. **Configuración de .env**: Variables automáticas según entorno 4. **Inicialización de DB**: Crea bases de datos y ejecuta migraciones @@ -56,6 +56,25 @@ El script `install.sh` automatiza todo el proceso: 6. **Creación de admin**: Usuario administrador inicial 7. **Inicio de servicios**: Docker Compose +### Overrides útiles para `install.sh` + +`install.sh` permite sobreescribir variables sin editar el script: + +```bash +# IA local por IP/host de red +LOCAL_OLLAMA_URL=http://192.168.0.5:11434 \ +LOCAL_WHISPER_URL=http://192.168.0.5:9000 \ +./install.sh + +# Forzar URL interna de CMS para comunicación LMS -> CMS +CMS_API_URL=http://studio:3001 ./install.sh + +# Reutilizar SAM remoto compartido +SAM_SHARED_URL=mysql://user:pass@host:3306/sige_sam_v3 \ +SAM_DIAG_SHARED_URL=mysql://user:pass@host:3306/SAM_diagnostico \ +./install.sh +``` + ### Instalación Manual ```bash @@ -122,8 +141,12 @@ DEFAULT_PLATFORM_NAME=OpenCCB Learning DEFAULT_PRIMARY_COLOR=#3B82F6 DEFAULT_SECONDARY_COLOR=#8B5CF6 -# SAM Integration (Opcional - para gestión académica) -SAM_DATABASE_URL=postgresql://user:password@sige_sam_v3_host:5432/sige_sam_v3 +# SAM Integration (MySQL compartido) +MYSQL_DATABASE_URL=mysql://user:password@host:3306/sige_sam_v3 +SAM_DATABASE_URL=mysql://user:password@host:3306/sige_sam_v3 +SAM_DIAGNOSTICO_DATABASE_URL=mysql://user:password@host:3306/SAM_diagnostico +# URL interna para sincronización de perfil LMS -> CMS +CMS_API_URL=http://studio:3001 ``` ### Configuración por Entorno @@ -131,8 +154,11 @@ SAM_DATABASE_URL=postgresql://user:password@sige_sam_v3_host:5432/sige_sam_v3 **Desarrollo:** ```bash ENVIRONMENT=dev -LOCAL_OLLAMA_URL=http://t-800:11434 -LOCAL_WHISPER_URL=http://t-800:9000 +LOCAL_OLLAMA_URL=http://localhost:11434 +LOCAL_WHISPER_URL=http://localhost:9000 +# Alternativa LAN: +# LOCAL_OLLAMA_URL=http://192.168.0.5:11434 +# LOCAL_WHISPER_URL=http://192.168.0.5:9000 ``` **Producción:** @@ -159,9 +185,11 @@ OpenCCB se sincroniza con SAM para: ### Configuración -1. **Agregar variable de entorno** en `.env`: +1. **Agregar variables de entorno** en `.env`: ```bash - SAM_DATABASE_URL=postgresql://user:password@host:5432/sige_sam_v3 + MYSQL_DATABASE_URL=mysql://user:password@host:3306/sige_sam_v3 + SAM_DATABASE_URL=mysql://user:password@host:3306/sige_sam_v3 + SAM_DIAGNOSTICO_DATABASE_URL=mysql://user:password@host:3306/SAM_diagnostico ``` 2. **Ejecutar migración**: @@ -223,12 +251,21 @@ OpenCCB se sincroniza con SAM para: **Error: "SAM_DATABASE_URL not configured"** ```bash # Agregar en .env -SAM_DATABASE_URL=postgresql://user:pass@host:5432/sige_sam_v3 +SAM_DATABASE_URL=mysql://user:pass@host:3306/sige_sam_v3 # Reiniciar servicio docker-compose restart cms ``` +**Error: `/auth/me` responde 502 en LMS** +```bash +# Validar URL interna de CMS usada por LMS +grep '^CMS_API_URL=' .env.dev .env 2>/dev/null + +# Valor recomendado en Docker +CMS_API_URL=http://studio:3001 +``` + **Error: "Failed to fetch SAM students"** - Verificar conexión a la base de datos SAM - Confirmar que las tablas `sige_sam_v3.alumnos` existen diff --git a/QWEN.md b/QWEN.md index 6f53a58..74bdd19 100644 --- a/QWEN.md +++ b/QWEN.md @@ -287,6 +287,9 @@ EMBEDDING_MODEL=nomic-embed-text # Frontend URLs NEXT_PUBLIC_CMS_API_URL=http://localhost:3001 NEXT_PUBLIC_LMS_API_URL=http://localhost:3002 + +# Backend-to-backend (LMS -> CMS) +CMS_API_URL=http://studio:3001 ``` ### Default Credentials diff --git a/README.md b/README.md index b21e63c..140056f 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,12 @@ docker-compose up --build # Instalación y configuración automática ./install.sh +# Instalación local usando IA en red LAN (ejemplo) +LOCAL_OLLAMA_URL=http://192.168.0.5:11434 LOCAL_WHISPER_URL=http://192.168.0.5:9000 ./install.sh + +# Instalación local con URLs SAM compartidas explícitas +SAM_SHARED_URL=mysql://user:pass@host:3306/sige_sam_v3 SAM_DIAG_SHARED_URL=mysql://user:pass@host:3306/SAM_diagnostico ./install.sh + # Resetear base de datos de desarrollo ./scripts/reset_db.sh ``` diff --git a/deploy.sh b/deploy.sh index fe5a0ea..cb2a5a9 100755 --- a/deploy.sh +++ b/deploy.sh @@ -526,6 +526,10 @@ sed -i "/^NEXT_PUBLIC_LMS_API_URL=/d" .env 2>/dev/null || true echo "NEXT_PUBLIC_CMS_API_URL=\$CMS_URL" >> .env echo "NEXT_PUBLIC_LMS_API_URL=\$LMS_URL" >> .env +# URL interna de CMS para comunicación backend-to-backend (LMS -> CMS) +sed -i "/^CMS_API_URL=/d" .env 2>/dev/null || true +echo "CMS_API_URL=http://studio:3001" >> .env + # Configurar S3 para almacenamiento de audio if ! grep -q "^ASSETS_STORAGE=" .env || grep -q "^ASSETS_STORAGE=$" .env; then sed -i "/^ASSETS_STORAGE=/d" .env 2>/dev/null || true diff --git a/install.sh b/install.sh index e5dbf19..dc3fdcb 100755 --- a/install.sh +++ b/install.sh @@ -143,17 +143,19 @@ update_env "ENVIRONMENT" "dev" echo "" echo "🔍 Configurando Servicios de IA Local..." -LOCAL_OLLAMA_URL="http://localhost:11434" -LOCAL_WHISPER_URL="http://localhost:9000" -LOCAL_IMAGE_URL="http://localhost:8000" -LLM_MODEL="llama3.2:3b" -EMBEDDING_MODEL="nomic-embed-text" +LOCAL_OLLAMA_URL="${LOCAL_OLLAMA_URL:-http://192.168.0.5:11434}" +LOCAL_WHISPER_URL="${LOCAL_WHISPER_URL:-http://192.168.0.5:9000}" +LOCAL_IMAGE_URL="${LOCAL_IMAGE_URL:-http://localhost:8000}" +LLM_MODEL="${LOCAL_LLM_MODEL:-llama3.2:3b}" +EMBEDDING_MODEL="${EMBEDDING_MODEL:-nomic-embed-text}" +CMS_API_URL="${CMS_API_URL:-http://studio:3001}" echo " 🤖 Ollama: $LOCAL_OLLAMA_URL" echo " 🎤 Whisper: $LOCAL_WHISPER_URL" echo " 🖼️ Image Bridge: $LOCAL_IMAGE_URL" echo " 🧠 Modelo LLM: $LLM_MODEL" echo " 📊 Embeddings: $EMBEDDING_MODEL" +echo " 🔗 CMS API interna: $CMS_API_URL" update_env "AI_PROVIDER" "local" update_env "LOCAL_LLM_MODEL" "$LLM_MODEL" @@ -163,6 +165,7 @@ update_env "DEV_OLLAMA_URL" "$LOCAL_OLLAMA_URL" update_env "DEV_WHISPER_URL" "$LOCAL_WHISPER_URL" update_env "LOCAL_OLLAMA_URL" "$LOCAL_OLLAMA_URL" update_env "LOCAL_WHISPER_URL" "$LOCAL_WHISPER_URL" +update_env "CMS_API_URL" "$CMS_API_URL" # Configuración SAM (automática para entorno local) echo "" diff --git a/services/lms-service/src/handlers.rs b/services/lms-service/src/handlers.rs index 768dc93..00490d5 100644 --- a/services/lms-service/src/handlers.rs +++ b/services/lms-service/src/handlers.rs @@ -1,6 +1,7 @@ use axum::{ Json, extract::{Multipart, Path, Query, State}, + http::{HeaderMap, header::AUTHORIZATION}, http::StatusCode, response::IntoResponse, Extension, @@ -37,25 +38,104 @@ fn count_tokens(text: &str) -> i32 { pub async fn get_me( claims: common::auth::Claims, State(pool): State, + headers: HeaderMap, ) -> Result, (StatusCode, String)> { let user = sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1") .bind(claims.sub) - .fetch_one(&pool) + .fetch_optional(&pool) .await .map_err(|e: sqlx::Error| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; - Ok(Json(UserResponse { - id: user.id, - email: user.email, - full_name: user.full_name, - role: user.role, - organization_id: user.organization_id, - xp: user.xp, - level: user.level, - avatar_url: user.avatar_url, - bio: user.bio, - language: user.language, - })) + if let Some(user) = user { + return Ok(Json(UserResponse { + id: user.id, + email: user.email, + full_name: user.full_name, + role: user.role, + organization_id: user.organization_id, + xp: user.xp, + level: user.level, + avatar_url: user.avatar_url, + bio: user.bio, + language: user.language, + })); + } + + if let Some(auth_header) = headers + .get(AUTHORIZATION) + .and_then(|h| h.to_str().ok()) + .filter(|v| !v.trim().is_empty()) + { + let mut cms_api_candidates: Vec = Vec::new(); + if let Ok(url) = env::var("CMS_API_URL") { + if !url.trim().is_empty() { + cms_api_candidates.push(url); + } + } + cms_api_candidates.push("http://studio:3001".to_string()); + cms_api_candidates.push("http://localhost:3001".to_string()); + + for cms_api_url in cms_api_candidates { + let me_url = format!("{}/auth/me", cms_api_url.trim_end_matches('/')); + + if let Ok(response) = reqwest::Client::new() + .get(&me_url) + .header("Authorization", auth_header) + .send() + .await + { + if response.status().is_success() { + if let Ok(cms_user) = response.json::().await { + let _ = sqlx::query( + r#" + INSERT INTO users ( + id, organization_id, email, password_hash, full_name, role, xp, level, avatar_url, bio, language, updated_at + ) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, NOW()) + ON CONFLICT (id) + DO UPDATE SET + organization_id = EXCLUDED.organization_id, + email = EXCLUDED.email, + full_name = EXCLUDED.full_name, + role = EXCLUDED.role, + xp = EXCLUDED.xp, + level = EXCLUDED.level, + avatar_url = EXCLUDED.avatar_url, + bio = EXCLUDED.bio, + language = EXCLUDED.language, + updated_at = NOW() + "#, + ) + .bind(cms_user.id) + .bind(cms_user.organization_id) + .bind(&cms_user.email) + .bind("synced-from-cms") + .bind(&cms_user.full_name) + .bind(&cms_user.role) + .bind(cms_user.xp) + .bind(cms_user.level) + .bind(&cms_user.avatar_url) + .bind(&cms_user.bio) + .bind(&cms_user.language) + .execute(&pool) + .await; + + return Ok(Json(cms_user)); + } + } + } + } + } + + tracing::warn!( + "Usuario {} no existe en LMS y no se pudo sincronizar desde CMS", + claims.sub + ); + + Err(( + StatusCode::BAD_GATEWAY, + "No se pudo sincronizar el perfil de usuario desde CMS".to_string(), + )) } /// Obtener configuración de idioma del curso diff --git a/services/lms-service/src/main.rs b/services/lms-service/src/main.rs index 54c21a3..03c6188 100644 --- a/services/lms-service/src/main.rs +++ b/services/lms-service/src/main.rs @@ -422,5 +422,10 @@ async fn main() { let addr = SocketAddr::from(([0, 0, 0, 0], 3002)); tracing::info!("LMS Service escuchando en {} con limitación de tasa y encabezados de seguridad", addr); let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); - axum::serve(listener, public_routes).await.unwrap(); + axum::serve( + listener, + public_routes.into_make_service_with_connect_info::(), + ) + .await + .unwrap(); }