diff --git a/install.sh b/install.sh index 7bbb050..1270aea 100755 --- a/install.sh +++ b/install.sh @@ -168,9 +168,9 @@ update_env "BRIDGE_DATABASE_URL" "$BRIDGE_DB_URL" if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'=' -f2) == "" ]]; then read -p "Ingrese la Contraseña de la Base de Datos [password]: " DB_PASS DB_PASS=${DB_PASS:-password} - update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb?sslmode=disable" - update_env "CMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_cms?sslmode=disable" - update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb_lms?sslmode=disable" + update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5433/openccb?sslmode=disable" + update_env "CMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5433/openccb_cms?sslmode=disable" + update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5433/openccb_lms?sslmode=disable" update_env "JWT_SECRET" "supersecretsecret" update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001" update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3003" @@ -221,12 +221,14 @@ fi # Extra buffer for PostgreSQL initialization sleep 2 +echo "🏗️ Creando bases de datos CMS y LMS..." +docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_cms;" || true +docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_lms;" || true + CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-) LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-) -echo "🏗️ Creando bases de datos y ejecutando migraciones..." -DATABASE_URL=$CMS_URL sqlx database create || true -DATABASE_URL=$LMS_URL sqlx database create || true +echo "🏗️ Ejecutando migraciones..." DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations diff --git a/reset_db.sh b/reset_db.sh index 1f3e9c0..01bce26 100755 --- a/reset_db.sh +++ b/reset_db.sh @@ -16,8 +16,16 @@ echo "🏗️ Recreando bases de datos..." docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_cms;" docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_lms;" -echo "🚀 Reiniciando servicios y aplicando migraciones..." +echo "🚀 Reiniciando servicios..." docker compose start studio experience -echo "✅ Base de datos reseteada exitosamente." -echo "Nota: Las migraciones se ejecutarán automáticamente al iniciar los servicios." +echo "⏳ Esperando que los servicios estén listos..." +sleep 5 + +echo "🏗️ Ejecutando migraciones..." +CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-) +LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-) +DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations +DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations + +echo "✅ Base de datos reseteada y migraciones aplicadas exitosamente." diff --git a/services/cms-service/scripts/outputs/image_integration-test-lesson_18d3b195.png b/services/cms-service/scripts/outputs/image_integration-test-lesson_18d3b195.png deleted file mode 100644 index e712bd7..0000000 Binary files a/services/cms-service/scripts/outputs/image_integration-test-lesson_18d3b195.png and /dev/null differ diff --git a/services/cms-service/scripts/requirements.txt b/services/cms-service/scripts/requirements.txt deleted file mode 100644 index c6e8ae2..0000000 --- a/services/cms-service/scripts/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -fastapi -uvicorn -torch -diffusers -transformers -accelerate -pillow -imageio-ffmpeg -pydantic diff --git a/services/cms-service/scripts/video_bridge.py b/services/cms-service/scripts/video_bridge.py deleted file mode 100644 index 663a3fb..0000000 --- a/services/cms-service/scripts/video_bridge.py +++ /dev/null @@ -1,160 +0,0 @@ -import os -import time -from fastapi import FastAPI, HTTPException -from pydantic import BaseModel -import torch -from diffusers import StableDiffusionPipeline, DPMSolverMultistepScheduler -from PIL import Image -import uuid - -app = FastAPI() - -# Configuration -MODEL_ID = "runwayml/stable-diffusion-v1-5" -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) -OUTPUT_DIR = os.path.join(BASE_DIR, "outputs") -os.makedirs(OUTPUT_DIR, exist_ok=True) - -# Global variables for the model -pipe = None - -def load_model(): - global pipe - device = "cuda" if torch.cuda.is_available() else "cpu" - print(f"Loading Stable Diffusion model on {device}...") - - pipe = StableDiffusionPipeline.from_pretrained( - MODEL_ID, - torch_dtype=torch.float16 if device == "cuda" else torch.float32, - ) - # Use a high-quality scheduler for better detail - pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config) - pipe.to(device) - - if device == "cpu": - pipe.enable_attention_slicing() - - print("Model loaded successfully.") - -from contextlib import asynccontextmanager - -@asynccontextmanager -async def lifespan(app: FastAPI): - # Model loading is heavy, we'll do it on first request to avoid timeout at startup - yield - -app = FastAPI(lifespan=lifespan) - -# Serve generated images -from fastapi.staticfiles import StaticFiles -app.mount("/outputs", StaticFiles(directory=OUTPUT_DIR), name="outputs") - -import psycopg2 - -class ImageRequest(BaseModel): - prompt: str - lesson_id: str - database_url: Optional[str] = None - table_name: str = "lessons" - progress_column: str = "generation_progress" - width: Optional[int] = 512 - height: Optional[int] = 512 - -@app.post("/generate") -async def generate_image(request: ImageRequest): - global pipe - if pipe is None: - load_model() - - num_steps = 150 - - def progress_callback(step: int, timestep: int, latents: torch.FloatTensor): - if request.database_url and request.lesson_id: - try: - progress = int((step / num_steps) * 100) - conn = psycopg2.connect(request.database_url) - cur = conn.cursor() - - # Check for cancellation - status_query = f"SELECT {request.table_name.replace('generation_progress', 'generation_status')} FROM {request.table_name} WHERE id = %s" - # Wait, the column name is fixed based on table. - # courses -> generation_status - # lessons -> video_generation_status - status_col = "generation_status" if request.table_name == "courses" else "video_generation_status" - cur.execute(f"SELECT {status_col} FROM {request.table_name} WHERE id = %s", (request.lesson_id,)) - status = cur.fetchone()[0] - - if status == 'idle': - print(f"Generation for {request.lesson_id} was cancelled. Aborting.") - cur.close() - conn.close() - raise Exception("Generation cancelled by user") - - # Update progress - query = f"UPDATE {request.table_name} SET {request.progress_column} = %s WHERE id = %s" - cur.execute(query, (progress, request.lesson_id)) - conn.commit() - cur.close() - conn.close() - except Exception as db_e: - if "cancelled" in str(db_e).lower(): - raise db_e - print(f"Database update error: {db_e}") - - def callback_dynamic_cfg(pipe, step_index, timestep, callback_kwargs): - if progress_callback: - progress_callback(step_index, timestep, None) - return callback_kwargs - - try: - quality_prompt = f"{request.prompt}, highly detailed, high quality, masterpiece, 8k, realistic, photographic, sharp focus, perfect anatomy" - negative_prompt = "deformed, distorted, disfigured, poorly drawn, bad anatomy, wrong anatomy, extra limb, missing limb, floating limbs, disconnected limbs, mutation, mutated, ugly, disgusting, blurry, low quality, low resolution, bad hands, extra fingers, cartoon, anime, illustration, draft, grainy" - - print(f"Generating image ({request.width}x{request.height}) for prompt: {quality_prompt}") - - # Generation with custom resolution - image = pipe( - quality_prompt, - negative_prompt=negative_prompt, - num_inference_steps=num_steps, - guidance_scale=8.5, - width=request.width, - height=request.height, - callback_on_step_end=callback_dynamic_cfg - ).images[0] - - # Ensure progress is 100% at the end - if request.database_url and request.lesson_id: - try: - conn = psycopg2.connect(request.database_url) - cur = conn.cursor() - query = f"UPDATE {request.table_name} SET {request.progress_column} = 100 WHERE id = %s" - cur.execute(query, (request.lesson_id,)) - conn.commit() - cur.close() - conn.close() - except: - pass - - image_filename = f"image_{request.lesson_id}_{uuid.uuid4().hex[:8]}.png" - image_path = os.path.join(OUTPUT_DIR, image_filename) - - image.save(image_path) - - # Return the absolute URL pointing to t-800 so the frontend can find it - hostname = os.getenv("BRIDGE_HOSTNAME", "t-800") - full_url = f"http://{hostname}:8080/outputs/{image_filename}" - - return {"status": "completed", "url": full_url} - - except Exception as e: - print(f"Generation error: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -@app.get("/health") -async def health(): - return {"status": "ok", "model_loaded": pipe is not None} - -if __name__ == "__main__": - import uvicorn - uvicorn.run(app, host="0.0.0.0", port=8080) diff --git a/services/cms-service/src/main.rs b/services/cms-service/src/main.rs index 529464f..d6ec66e 100644 --- a/services/cms-service/src/main.rs +++ b/services/cms-service/src/main.rs @@ -12,7 +12,9 @@ mod webhooks; use axum::{ Router, extract::DefaultBodyLimit, + http::{StatusCode, HeaderValue}, middleware, + response::Response, routing::{delete, get, post, put}, }; use common::health::{self, HealthState}; @@ -27,6 +29,25 @@ use tower_governor::GovernorLayer; use tower_http::cors::{Any, CorsLayer}; use tower_http::set_header::SetResponseHeaderLayer; +/// Handler para requests OPTIONS (CORS preflight) +async fn handle_options() -> Response { + let mut res = Response::new(String::new().into()); + *res.status_mut() = StatusCode::NO_CONTENT; + res.headers_mut().insert( + "Access-Control-Allow-Origin", + HeaderValue::from_static("*"), + ); + res.headers_mut().insert( + "Access-Control-Allow-Methods", + HeaderValue::from_static("GET, POST, PUT, DELETE, OPTIONS"), + ); + res.headers_mut().insert( + "Access-Control-Allow-Headers", + HeaderValue::from_static("Content-Type, Authorization, Origin, Accept"), + ); + res +} + #[tokio::main] async fn main() { dotenv().ok(); @@ -310,18 +331,24 @@ async fn main() { // Rutas públicas que no requieren autenticación let public_routes = Router::new() .nest("/api/external", api_routes) - .route("/auth/register", post(handlers::register)) - .route("/auth/login", post(handlers::login)) - .route("/auth/sso/login/{org_id}", get(handlers::sso_login_init)) - .route("/auth/sso/callback", get(handlers::sso_callback)) + .route("/auth/register", post(handlers::register).options(handle_options)) + .route("/auth/login", post(handlers::login).options(handle_options)) + .route("/auth/sso/login/{org_id}", get(handlers::sso_login_init).options(handle_options)) + .route("/auth/sso/callback", get(handlers::sso_callback).options(handle_options)) .route( "/branding", - get(handlers_branding::get_organization_branding), + get(handlers_branding::get_organization_branding).options(handle_options), ) // Health check routes .merge(health::health_routes(pool.clone()).with_state(health_state)) .nest_service("/assets", tower_http::services::ServeDir::new("uploads")) .merge(protected_routes) + // CORS layer - debe estar PRIMERO (más cerca del servicio) para ejecutarse ULTIMO + .layer(cors) + // Rate limiting + .layer(GovernorLayer { + config: governor_conf, + }) // Security headers .layer(SetResponseHeaderLayer::overriding( http::header::STRICT_TRANSPORT_SECURITY, @@ -343,10 +370,6 @@ async fn main() { http::header::REFERRER_POLICY, http::HeaderValue::from_static("strict-origin-when-cross-origin"), )) - .layer(cors) - .layer(GovernorLayer { - config: governor_conf, - }) .with_state(pool); let addr = SocketAddr::from(([0, 0, 0, 0], 3001));