feat: Update database port to 5433, refine database creation and migration scripts, and add explicit CORS OPTIONS handling to CMS service routes.

This commit is contained in:
2026-03-13 10:46:54 -03:00
parent f9f1238310
commit 15ecc4e3b2
6 changed files with 51 additions and 187 deletions
+8 -6
View File
@@ -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 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 read -p "Ingrese la Contraseña de la Base de Datos [password]: " DB_PASS
DB_PASS=${DB_PASS:-password} DB_PASS=${DB_PASS:-password}
update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb?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:5432/openccb_cms?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:5432/openccb_lms?sslmode=disable" update_env "LMS_DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5433/openccb_lms?sslmode=disable"
update_env "JWT_SECRET" "supersecretsecret" update_env "JWT_SECRET" "supersecretsecret"
update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001" update_env "NEXT_PUBLIC_CMS_API_URL" "http://localhost:3001"
update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3003" update_env "NEXT_PUBLIC_LMS_API_URL" "http://localhost:3003"
@@ -221,12 +221,14 @@ fi
# Extra buffer for PostgreSQL initialization # Extra buffer for PostgreSQL initialization
sleep 2 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-) CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-)
LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-) LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-)
echo "🏗️ Creando bases de datos y ejecutando migraciones..." echo "🏗️ Ejecutando migraciones..."
DATABASE_URL=$CMS_URL sqlx database create || true
DATABASE_URL=$LMS_URL sqlx database create || true
DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations
DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations DATABASE_URL=$LMS_URL sqlx migrate run --source services/lms-service/migrations
+11 -3
View File
@@ -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_cms;"
docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_lms;" 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 docker compose start studio experience
echo "✅ Base de datos reseteada exitosamente." echo "⏳ Esperando que los servicios estén listos..."
echo "Nota: Las migraciones se ejecutarán automáticamente al iniciar los servicios." 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."
Binary file not shown.

Before

Width:  |  Height:  |  Size: 476 KiB

@@ -1,9 +0,0 @@
fastapi
uvicorn
torch
diffusers
transformers
accelerate
pillow
imageio-ffmpeg
pydantic
@@ -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)
+32 -9
View File
@@ -12,7 +12,9 @@ mod webhooks;
use axum::{ use axum::{
Router, Router,
extract::DefaultBodyLimit, extract::DefaultBodyLimit,
http::{StatusCode, HeaderValue},
middleware, middleware,
response::Response,
routing::{delete, get, post, put}, routing::{delete, get, post, put},
}; };
use common::health::{self, HealthState}; use common::health::{self, HealthState};
@@ -27,6 +29,25 @@ use tower_governor::GovernorLayer;
use tower_http::cors::{Any, CorsLayer}; use tower_http::cors::{Any, CorsLayer};
use tower_http::set_header::SetResponseHeaderLayer; 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] #[tokio::main]
async fn main() { async fn main() {
dotenv().ok(); dotenv().ok();
@@ -310,18 +331,24 @@ async fn main() {
// Rutas públicas que no requieren autenticación // Rutas públicas que no requieren autenticación
let public_routes = Router::new() let public_routes = Router::new()
.nest("/api/external", api_routes) .nest("/api/external", api_routes)
.route("/auth/register", post(handlers::register)) .route("/auth/register", post(handlers::register).options(handle_options))
.route("/auth/login", post(handlers::login)) .route("/auth/login", post(handlers::login).options(handle_options))
.route("/auth/sso/login/{org_id}", get(handlers::sso_login_init)) .route("/auth/sso/login/{org_id}", get(handlers::sso_login_init).options(handle_options))
.route("/auth/sso/callback", get(handlers::sso_callback)) .route("/auth/sso/callback", get(handlers::sso_callback).options(handle_options))
.route( .route(
"/branding", "/branding",
get(handlers_branding::get_organization_branding), get(handlers_branding::get_organization_branding).options(handle_options),
) )
// Health check routes // Health check routes
.merge(health::health_routes(pool.clone()).with_state(health_state)) .merge(health::health_routes(pool.clone()).with_state(health_state))
.nest_service("/assets", tower_http::services::ServeDir::new("uploads")) .nest_service("/assets", tower_http::services::ServeDir::new("uploads"))
.merge(protected_routes) .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 // Security headers
.layer(SetResponseHeaderLayer::overriding( .layer(SetResponseHeaderLayer::overriding(
http::header::STRICT_TRANSPORT_SECURITY, http::header::STRICT_TRANSPORT_SECURITY,
@@ -343,10 +370,6 @@ async fn main() {
http::header::REFERRER_POLICY, http::header::REFERRER_POLICY,
http::HeaderValue::from_static("strict-origin-when-cross-origin"), http::HeaderValue::from_static("strict-origin-when-cross-origin"),
)) ))
.layer(cors)
.layer(GovernorLayer {
config: governor_conf,
})
.with_state(pool); .with_state(pool);
let addr = SocketAddr::from(([0, 0, 0, 0], 3001)); let addr = SocketAddr::from(([0, 0, 0, 0], 3001));