feat: update environment configuration for local development and enhance deployment scripts
This commit is contained in:
@@ -11,19 +11,25 @@ JWT_SECRET=dev_jwt_secret_change_in_production
|
|||||||
# Logging
|
# Logging
|
||||||
RUST_LOG=debug
|
RUST_LOG=debug
|
||||||
|
|
||||||
|
ENVIRONMENT=dev
|
||||||
|
|
||||||
# AI Configuration
|
# AI Configuration
|
||||||
# Providers: 'openai' or 'local'
|
# Providers: 'openai' or 'local'
|
||||||
AI_PROVIDER=local
|
AI_PROVIDER=local
|
||||||
OPENAI_API_KEY=
|
OPENAI_API_KEY=
|
||||||
|
|
||||||
# Local AI (Ollama & Whisper) - Production (HTTPS with SSH tunnel)
|
# Local AI (Ollama & Whisper) - local execution
|
||||||
PROD_WHISPER_URL=http://host.docker.internal:8080
|
LOCAL_OLLAMA_URL=http://localhost:11434
|
||||||
|
LOCAL_WHISPER_URL=http://localhost:9000
|
||||||
|
DEV_OLLAMA_URL=http://localhost:11434
|
||||||
|
DEV_WHISPER_URL=http://localhost:9000
|
||||||
|
DEV_BARK_URL=http://localhost:8000
|
||||||
|
|
||||||
# SMTP for local development
|
# SMTP for local development
|
||||||
SMTP_ENABLED=true
|
SMTP_ENABLED=true
|
||||||
SMTP_HOST=mailpit
|
SMTP_HOST=mailpit
|
||||||
SMTP_PORT=1025
|
SMTP_PORT=1025
|
||||||
SMTP_FROM=OpenCCB Dev <dev@norteamericano.com>
|
SMTP_FROM=[LOCAL-QA] OpenCCB Dev <dev@norteamericano.com>
|
||||||
SMTP_USERNAME=
|
SMTP_USERNAME=
|
||||||
SMTP_PASSWORD=
|
SMTP_PASSWORD=
|
||||||
|
|
||||||
@@ -33,8 +39,11 @@ NEXT_PUBLIC_LMS_API_URL=http://localhost:3002/lms-api
|
|||||||
NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
||||||
NEXT_PUBLIC_LEARNING_DOMAIN=localhost
|
NEXT_PUBLIC_LEARNING_DOMAIN=localhost
|
||||||
|
|
||||||
# MySQL for courses integration (local)
|
# External MySQL/SAM integration (same as production)
|
||||||
MYSQL_DATABASE_URL=mysql://user:password@host.docker.internal:3306/courses_db
|
MYSQL_DATABASE_URL=mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.amazonaws.com:3306/sige_sam_v3
|
||||||
|
SAM_DIAGNOSTICO_DATABASE_URL=mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.amazonaws.com:3306/SAM_diagnostico
|
||||||
|
SAM_DATABASE_URL=mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.amazonaws.com:3306/sige_sam_v3
|
||||||
|
|
||||||
# SAM Diagnostico DB (local)
|
LOCAL_VIDEO_BRIDGE_URL=http://localhost:8000
|
||||||
SAM_DIAGNOSTICO_DATABASE_URL=postgresql://user:password@host.docker.internal:5434/sam_db
|
EMBEDDING_MODEL=nomic-embed-text
|
||||||
|
LOCAL_LLM_MODEL=llama3.2:3b
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ echo " 🚀 OpenCCB Deployment Tool"
|
|||||||
echo "===================================================="
|
echo "===================================================="
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
# Guardrail: deploy.sh es SOLO para producción
|
||||||
|
if [ -f ".env" ]; then
|
||||||
|
CURRENT_ENV=$(grep '^ENVIRONMENT=' .env | cut -d'=' -f2- | tr '[:upper:]' '[:lower:]' | xargs)
|
||||||
|
if [ "$CURRENT_ENV" = "dev" ]; then
|
||||||
|
echo "❌ ERROR: deploy.sh está configurado para PRODUCCIÓN y detectó ENVIRONMENT=dev en .env"
|
||||||
|
echo " - Usa ./install.sh para entorno local"
|
||||||
|
echo " - Ajusta .env a ENVIRONMENT=prod antes de desplegar"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CONFIGURACIÓN
|
# CONFIGURACIÓN
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -16,26 +16,34 @@ services:
|
|||||||
- production
|
- production
|
||||||
|
|
||||||
db:
|
db:
|
||||||
|
container_name: openccb-local-db
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432"
|
- "5432:5432"
|
||||||
|
|
||||||
|
mailpit:
|
||||||
|
container_name: openccb-local-mailpit
|
||||||
|
|
||||||
studio:
|
studio:
|
||||||
|
container_name: openccb-local-studio
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3000:3000"
|
||||||
- "3001:3001"
|
- "3001:3001"
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
environment:
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://user:password@db:5432/openccb_cms
|
||||||
- NEXT_PUBLIC_CMS_API_URL=http://localhost:3001
|
- NEXT_PUBLIC_CMS_API_URL=http://localhost:3001
|
||||||
- NEXT_PUBLIC_LMS_API_URL=http://localhost:3002/lms-api
|
- NEXT_PUBLIC_LMS_API_URL=http://localhost:3002/lms-api
|
||||||
- NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
- NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
||||||
- NEXT_PUBLIC_LEARNING_DOMAIN=localhost
|
- NEXT_PUBLIC_LEARNING_DOMAIN=localhost
|
||||||
|
|
||||||
experience:
|
experience:
|
||||||
|
container_name: openccb-local-experience
|
||||||
ports:
|
ports:
|
||||||
- "3003:3003"
|
- "3003:3003"
|
||||||
- "3002:3002"
|
- "3002:3002"
|
||||||
env_file: .env.dev
|
env_file: .env.dev
|
||||||
environment:
|
environment:
|
||||||
|
- DATABASE_URL=postgresql://user:password@db:5432/openccb_lms
|
||||||
- NEXT_PUBLIC_CMS_API_URL=http://localhost:3001
|
- NEXT_PUBLIC_CMS_API_URL=http://localhost:3001
|
||||||
- NEXT_PUBLIC_LMS_API_URL=http://localhost:3002/lms-api
|
- NEXT_PUBLIC_LMS_API_URL=http://localhost:3002/lms-api
|
||||||
- NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
- NEXT_PUBLIC_STUDIO_DOMAIN=localhost
|
||||||
|
|||||||
+83
-122
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# OpenCCB Local Development Setup
|
# OpenCCB Local Development Setup
|
||||||
# Levanta el stack completo en local usando Docker:
|
# Levanta el stack completo en local usando Docker:
|
||||||
# - PostgreSQL en localhost:5433
|
# - PostgreSQL en localhost:5432
|
||||||
# - CMS API en localhost:3001
|
# - CMS API en localhost:3001
|
||||||
# - Studio (Next.js) en localhost:3000
|
# - Studio (Next.js) en localhost:3000
|
||||||
# - LMS API en localhost:3002
|
# - LMS API en localhost:3002
|
||||||
@@ -26,8 +26,13 @@ LOCAL_CMS_URL="http://localhost:3001"
|
|||||||
LOCAL_LMS_URL="http://localhost:3002/lms-api"
|
LOCAL_LMS_URL="http://localhost:3002/lms-api"
|
||||||
LOCAL_STUDIO_DOMAIN="localhost"
|
LOCAL_STUDIO_DOMAIN="localhost"
|
||||||
LOCAL_LEARNING_DOMAIN="localhost"
|
LOCAL_LEARNING_DOMAIN="localhost"
|
||||||
DB_CONTAINER="openccb-db"
|
LOCAL_PROJECT="openccb-local"
|
||||||
COMPOSE_LOCAL="docker compose -f docker-compose.yml -f docker-compose.local.yml"
|
ENV_FILE=".env.dev"
|
||||||
|
DB_CONTAINER="openccb-local-db"
|
||||||
|
|
||||||
|
compose_local() {
|
||||||
|
docker compose --env-file "$ENV_FILE" -p "$LOCAL_PROJECT" -f docker-compose.yml -f docker-compose.local.yml "$@"
|
||||||
|
}
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
echo "===================================================="
|
echo "===================================================="
|
||||||
@@ -109,98 +114,77 @@ if [ "$FAST_MODE" == "false" ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 4. Environment Configuration
|
# 4. Environment Configuration (LOCAL ONLY)
|
||||||
echo ""
|
echo ""
|
||||||
if [ ! -f ".env" ]; then
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
if [ -f ".env.example" ]; then
|
if [ -f ".env.example" ]; then
|
||||||
cp .env.example .env
|
cp .env.example "$ENV_FILE"
|
||||||
else
|
else
|
||||||
touch .env
|
touch "$ENV_FILE"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
update_env() {
|
update_env() {
|
||||||
local key=$1
|
local key=$1
|
||||||
local val=$2
|
local val=$2
|
||||||
if grep -q "^${key}=" .env; then
|
if grep -q "^${key}=" "$ENV_FILE"; then
|
||||||
sed -i "s|^${key}=.*|${key}=${val}|" .env
|
sed -i "s|^${key}=.*|${key}=${val}|" "$ENV_FILE"
|
||||||
else
|
else
|
||||||
echo "${key}=${val}" >> .env
|
echo "${key}=${val}" >> "$ENV_FILE"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# 5. Configuración de Entorno (Dev/Prod)
|
# 5. Configuración de Entorno (SIEMPRE DEV EN INSTALL)
|
||||||
echo ""
|
echo ""
|
||||||
echo "🌍 Selección de Entorno"
|
echo "🌍 Entorno local forzado: dev"
|
||||||
read -p "¿Es un entorno de DESARROLLO o PRODUCCIÓN? [dev/prod]: " ENV_CHOICE
|
update_env "ENVIRONMENT" "dev"
|
||||||
ENV_CHOICE=$(echo "$ENV_CHOICE" | tr '[:upper:]' '[:lower:]')
|
|
||||||
ENV_CHOICE=${ENV_CHOICE:-prod}
|
|
||||||
update_env "ENVIRONMENT" "$ENV_CHOICE"
|
|
||||||
|
|
||||||
# 6. Configuración de IA Remota (Automática según entorno)
|
# 6. Configuración de IA Local (Automática)
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔍 Configurando Servicios de IA Remota ($ENV_CHOICE)..."
|
echo "🔍 Configurando Servicios de IA Local..."
|
||||||
|
|
||||||
# Configuración automática según entorno
|
LOCAL_OLLAMA_URL="http://localhost:11434"
|
||||||
if [ "$ENV_CHOICE" == "dev" ]; then
|
LOCAL_WHISPER_URL="http://localhost:9000"
|
||||||
DEFAULT_OLLAMA="http://t-800:11434"
|
LOCAL_IMAGE_URL="http://localhost:8000"
|
||||||
DEFAULT_WHISPER="http://t-800:9000"
|
|
||||||
DEFAULT_IMAGE="http://t-800:8080"
|
|
||||||
echo " ✅ Entorno de DESARROLLO detectado"
|
|
||||||
else
|
|
||||||
DEFAULT_OLLAMA="http://t-800.norteamericano.cl:11434"
|
|
||||||
DEFAULT_WHISPER="http://t-800.norteamericano.cl:9000"
|
|
||||||
DEFAULT_IMAGE="http://t-800.norteamericano.cl:8080"
|
|
||||||
echo " ✅ Entorno de PRODUCCIÓN detectado"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Configurar con valores por defecto (sin preguntar)
|
|
||||||
REMOTE_OLLAMA_URL="$DEFAULT_OLLAMA"
|
|
||||||
REMOTE_WHISPER_URL="$DEFAULT_WHISPER"
|
|
||||||
REMOTE_IMAGE_URL="$DEFAULT_IMAGE"
|
|
||||||
LLM_MODEL="llama3.2:3b"
|
LLM_MODEL="llama3.2:3b"
|
||||||
EMBEDDING_MODEL="nomic-embed-text"
|
EMBEDDING_MODEL="nomic-embed-text"
|
||||||
|
|
||||||
echo " 🤖 Ollama: $REMOTE_OLLAMA_URL"
|
echo " 🤖 Ollama: $LOCAL_OLLAMA_URL"
|
||||||
echo " 🎤 Whisper: $REMOTE_WHISPER_URL"
|
echo " 🎤 Whisper: $LOCAL_WHISPER_URL"
|
||||||
echo " 🖼️ Image Bridge: $REMOTE_IMAGE_URL"
|
echo " 🖼️ Image Bridge: $LOCAL_IMAGE_URL"
|
||||||
echo " 🧠 Modelo LLM: $LLM_MODEL"
|
echo " 🧠 Modelo LLM: $LLM_MODEL"
|
||||||
echo " 📊 Embeddings: $EMBEDDING_MODEL"
|
echo " 📊 Embeddings: $EMBEDDING_MODEL"
|
||||||
|
|
||||||
update_env "AI_PROVIDER" "local"
|
update_env "AI_PROVIDER" "local"
|
||||||
update_env "LOCAL_LLM_MODEL" "$LLM_MODEL"
|
update_env "LOCAL_LLM_MODEL" "$LLM_MODEL"
|
||||||
update_env "LOCAL_VIDEO_BRIDGE_URL" "$REMOTE_IMAGE_URL"
|
update_env "LOCAL_VIDEO_BRIDGE_URL" "$LOCAL_IMAGE_URL"
|
||||||
update_env "EMBEDDING_MODEL" "$EMBEDDING_MODEL"
|
update_env "EMBEDDING_MODEL" "$EMBEDDING_MODEL"
|
||||||
update_env "DEV_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "DEV_OLLAMA_URL" "$LOCAL_OLLAMA_URL"
|
||||||
update_env "DEV_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "DEV_WHISPER_URL" "$LOCAL_WHISPER_URL"
|
||||||
update_env "PROD_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
update_env "LOCAL_OLLAMA_URL" "$LOCAL_OLLAMA_URL"
|
||||||
update_env "PROD_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
update_env "LOCAL_WHISPER_URL" "$LOCAL_WHISPER_URL"
|
||||||
update_env "LOCAL_OLLAMA_URL" "$REMOTE_OLLAMA_URL"
|
|
||||||
update_env "LOCAL_WHISPER_URL" "$REMOTE_WHISPER_URL"
|
|
||||||
|
|
||||||
# Configuración SAM (opcional)
|
# Configuración SAM (automática para entorno local)
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔌 Configuración de Integración SAM"
|
echo "🔌 Configuración de Integración SAM (automática)"
|
||||||
read -p "¿Desea configurar la conexión a la base de datos SAM? [y/N]: " CONFIGURE_SAM
|
SAM_SHARED_URL="${SAM_SHARED_URL:-mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.amazonaws.com:3306/sige_sam_v3}"
|
||||||
if [[ "$CONFIGURE_SAM" =~ ^[Yy]$ ]]; then
|
SAM_DIAG_SHARED_URL="${SAM_DIAG_SHARED_URL:-mysql://root:Smith3976!@ec2-18-222-25-254.us-east-2.compute.amazonaws.com:3306/SAM_diagnostico}"
|
||||||
read -p "Ingrese SAM_DATABASE_URL []: " SAM_DB_URL
|
|
||||||
SAM_DB_URL=${SAM_DB_URL:-""}
|
update_env "MYSQL_DATABASE_URL" "$SAM_SHARED_URL"
|
||||||
if [ -n "$SAM_DB_URL" ]; then
|
update_env "SAM_DATABASE_URL" "$SAM_SHARED_URL"
|
||||||
update_env "SAM_DATABASE_URL" "$SAM_DB_URL"
|
update_env "SAM_DIAGNOSTICO_DATABASE_URL" "$SAM_DIAG_SHARED_URL"
|
||||||
echo " ✅ SAM_DATABASE_URL configurada"
|
|
||||||
fi
|
echo " ✅ MYSQL_DATABASE_URL configurada a copia SAM compartida"
|
||||||
else
|
echo " ✅ SAM_DATABASE_URL configurada a copia SAM compartida"
|
||||||
echo " ℹ️ SAM no configurado. Puede configurarlo luego en .env"
|
echo " ✅ SAM_DIAGNOSTICO_DATABASE_URL configurada a copia SAM diagnóstica"
|
||||||
fi
|
|
||||||
|
|
||||||
# Solicitar credenciales de DB si no están configuradas
|
# Solicitar credenciales de DB si no están configuradas
|
||||||
if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'=' -f2) == "" ]]; then
|
if ! grep -q "DATABASE_URL=" "$ENV_FILE" || [[ $(grep "DATABASE_URL=" "$ENV_FILE" | 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}
|
||||||
# Usar puerto 5434 (5432 y 5433 ya están en uso)
|
update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5432/openccb?sslmode=disable"
|
||||||
update_env "DATABASE_URL" "postgresql://user:${DB_PASS}@localhost:5434/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:5434/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:5434/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"
|
||||||
@@ -212,65 +196,42 @@ if ! grep -q "DATABASE_URL=" .env || [[ $(grep "DATABASE_URL=" .env | cut -d'='
|
|||||||
update_env "DEFAULT_SECONDARY_COLOR" "#8B5CF6"
|
update_env "DEFAULT_SECONDARY_COLOR" "#8B5CF6"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 5. Configuración de Pila de IA (Omitido - usando remoto)
|
# 5. Configuración de Pila de IA (LOCAL)
|
||||||
echo "🌐 Usando servicios de IA remotos en $REMOTE_OLLAMA_URL y $REMOTE_WHISPER_URL"
|
echo "🌐 Usando servicios de IA locales en $LOCAL_OLLAMA_URL y $LOCAL_WHISPER_URL"
|
||||||
|
|
||||||
# 6. Inicialización de la Base de Datos
|
# 6. Inicialización de la Base de Datos
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔌 Configuración de Base de Datos"
|
echo "🔌 Configuración de Base de Datos"
|
||||||
echo " Puerto: 5433 (PostgreSQL existente)"
|
echo " Puerto: 5432 (PostgreSQL local docker-compose.local.yml)"
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
# Preguntar si PostgreSQL ya está corriendo (producción)
|
read -p "¿Desea una instalación LIMPIA local? (Esto ELIMINARÁ datos locales) [y/N]: " CLEAN_INSTALL
|
||||||
read -p "¿PostgreSQL ya está corriendo en un contenedor? [y/N]: " DB_EXISTS
|
if [[ "$CLEAN_INSTALL" =~ ^[Yy]$ ]]; then
|
||||||
DB_EXISTS=${DB_EXISTS:-N}
|
echo "🐘 Reseteando stack local para instalación limpia..."
|
||||||
|
compose_local down -v || true
|
||||||
if [[ ! "$DB_EXISTS" =~ ^[Yy]$ ]]; then
|
|
||||||
read -p "¿Desea una instalación LIMPIA? (Esto ELIMINARÁ todos los datos existentes) [y/N]: " CLEAN_INSTALL
|
|
||||||
if [[ "$CLEAN_INSTALL" =~ ^[Yy]$ ]]; then
|
|
||||||
echo "🐘 Reseteando la base de datos para una instalación limpia..."
|
|
||||||
docker compose down -v || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "🐘 Iniciando base de datos con Docker..."
|
|
||||||
docker compose up -d db
|
|
||||||
|
|
||||||
echo "⏳ Esperando a que la base de datos esté lista (contenedor)..."
|
|
||||||
RETRIES=30
|
|
||||||
until docker exec openccb-db-1 pg_isready -U user &> /dev/null || [ $RETRIES -eq 0 ]; do
|
|
||||||
echo -n "."
|
|
||||||
sleep 1
|
|
||||||
RETRIES=$((RETRIES-1))
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
echo "⏳ Esperando al puerto de la base de datos (host)..."
|
|
||||||
RETRIES=10
|
|
||||||
until curl -s localhost:5433 &> /dev/null || [ $RETRIES -eq 0 ]; do
|
|
||||||
echo -n "+"
|
|
||||||
sleep 1
|
|
||||||
RETRIES=$((RETRIES-1))
|
|
||||||
done
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
if [ $RETRIES -eq 0 ]; then
|
|
||||||
echo "⚠️ Tiempo de espera agotado para el puerto del host, pero continuando..."
|
|
||||||
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 postgres -c "CREATE DATABASE openccb_cms;" || true
|
|
||||||
docker exec openccb-db-1 psql -U user -d postgres -c "CREATE DATABASE openccb_lms;" || true
|
|
||||||
else
|
|
||||||
echo "✅ PostgreSQL ya está corriendo. Saltando inicio del contenedor."
|
|
||||||
echo " Verificando conexión al puerto 5433..."
|
|
||||||
sleep 2
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CMS_URL=$(grep "CMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
echo "🐘 Iniciando base de datos local con Docker..."
|
||||||
LMS_URL=$(grep "LMS_DATABASE_URL=" .env | cut -d'=' -f2-)
|
compose_local up -d db
|
||||||
|
|
||||||
|
echo "⏳ Esperando a que la base de datos esté lista..."
|
||||||
|
RETRIES=30
|
||||||
|
until compose_local exec -T db pg_isready -U user &> /dev/null || [ $RETRIES -eq 0 ]; do
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
RETRIES=$((RETRIES-1))
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Extra buffer for PostgreSQL initialization
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "🏗️ Creando bases de datos CMS y LMS..."
|
||||||
|
compose_local exec -T db psql -U user -d postgres -c "CREATE DATABASE openccb_cms;" || true
|
||||||
|
compose_local exec -T db psql -U user -d postgres -c "CREATE DATABASE openccb_lms;" || true
|
||||||
|
|
||||||
|
CMS_URL=$(grep "CMS_DATABASE_URL=" "$ENV_FILE" | cut -d'=' -f2-)
|
||||||
|
LMS_URL=$(grep "LMS_DATABASE_URL=" "$ENV_FILE" | cut -d'=' -f2-)
|
||||||
|
|
||||||
echo "🏗️ Ejecutando migraciones..."
|
echo "🏗️ Ejecutando migraciones..."
|
||||||
DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations
|
DATABASE_URL=$CMS_URL sqlx migrate run --source services/cms-service/migrations
|
||||||
@@ -299,13 +260,13 @@ if curl -s http://localhost:11434/api/tags &> /dev/null; then
|
|||||||
echo "✅ Modelo de embeddings ya disponible"
|
echo "✅ Modelo de embeddings ya disponible"
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "ℹ️ Ollama local no detectado. Se usará el servidor remoto: $REMOTE_OLLAMA_URL"
|
echo "ℹ️ Ollama local no detectado. Se mantendrá la configuración local de $ENV_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 7. System Initialization (Integrated init-system.sh)
|
# 7. System Initialization (Integrated init-system.sh)
|
||||||
echo ""
|
echo ""
|
||||||
echo "🔍 Buscando administrador existente..."
|
echo "🔍 Buscando administrador existente..."
|
||||||
ADMIN_EXISTS=$(docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin');" | xargs 2>/dev/null || echo "f")
|
ADMIN_EXISTS=$(compose_local exec -T db psql -U user -d openccb_cms -t -c "SELECT EXISTS (SELECT 1 FROM users WHERE role = 'admin');" | xargs 2>/dev/null || echo "f")
|
||||||
|
|
||||||
if [ "$ADMIN_EXISTS" != "t" ]; then
|
if [ "$ADMIN_EXISTS" != "t" ]; then
|
||||||
echo "👤 Configurar Administrador Inicial"
|
echo "👤 Configurar Administrador Inicial"
|
||||||
@@ -323,16 +284,16 @@ fi
|
|||||||
# Selective Build/Rebuild
|
# Selective Build/Rebuild
|
||||||
if [ "$FAST_MODE" == "true" ]; then
|
if [ "$FAST_MODE" == "true" ]; then
|
||||||
echo "⚡ Modo FAST activado. Saltando comprobaciones y reconstrucción de imágenes."
|
echo "⚡ Modo FAST activado. Saltando comprobaciones y reconstrucción de imágenes."
|
||||||
docker compose up -d
|
compose_local up -d
|
||||||
else
|
else
|
||||||
echo ""
|
echo ""
|
||||||
read -p "¿Desea RECONSTRUIR las imágenes de Docker? (Recomendado si hay cambios de código) [y/N]: " REBUILD_CHOICE
|
read -p "¿Desea RECONSTRUIR las imágenes de Docker? (Recomendado si hay cambios de código) [y/N]: " REBUILD_CHOICE
|
||||||
if [[ "$REBUILD_CHOICE" =~ ^[Yy]$ ]]; then
|
if [[ "$REBUILD_CHOICE" =~ ^[Yy]$ ]]; then
|
||||||
echo "🚀 Reconstruyendo e iniciando servicios..."
|
echo "🚀 Reconstruyendo e iniciando servicios..."
|
||||||
docker compose up -d --build
|
compose_local up -d --build
|
||||||
else
|
else
|
||||||
echo "🚀 Iniciando servicios (sin reconstruir)..."
|
echo "🚀 Iniciando servicios (sin reconstruir)..."
|
||||||
docker compose up -d
|
compose_local up -d
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -358,7 +319,7 @@ if [ "$ADMIN_EXISTS" != "t" ]; then
|
|||||||
|
|
||||||
# Create admin user directly in database using pgcrypto
|
# Create admin user directly in database using pgcrypto
|
||||||
echo "🔐 Creando administrador en la base de datos..."
|
echo "🔐 Creando administrador en la base de datos..."
|
||||||
docker exec openccb-db-1 psql -U user -d openccb_cms -c "
|
compose_local exec -T db psql -U user -d openccb_cms -c "
|
||||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
SELECT * FROM fn_register_user(
|
SELECT * FROM fn_register_user(
|
||||||
'$ADMIN_EMAIL',
|
'$ADMIN_EMAIL',
|
||||||
@@ -371,7 +332,7 @@ if [ "$ADMIN_EXISTS" != "t" ]; then
|
|||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
if [ $? -eq 0 ]; then
|
||||||
echo "✅ ¡Éxito! Administrador creado."
|
echo "✅ ¡Éxito! Administrador creado."
|
||||||
API_KEY=$(docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT api_key FROM organizations LIMIT 1;" | xargs 2>/dev/null)
|
API_KEY=$(compose_local exec -T db psql -U user -d openccb_cms -t -c "SELECT api_key FROM organizations LIMIT 1;" | xargs 2>/dev/null)
|
||||||
echo "🔑 API Key Inicial: $API_KEY"
|
echo "🔑 API Key Inicial: $API_KEY"
|
||||||
echo ""
|
echo ""
|
||||||
echo "📋 Credenciales de acceso:"
|
echo "📋 Credenciales de acceso:"
|
||||||
@@ -395,7 +356,7 @@ EOF
|
|||||||
|
|
||||||
if echo "$RESPONSE" | grep -q "token"; then
|
if echo "$RESPONSE" | grep -q "token"; then
|
||||||
echo "✅ ¡Éxito! Administrador creado vía API."
|
echo "✅ ¡Éxito! Administrador creado vía API."
|
||||||
API_KEY=$(docker exec openccb-db-1 psql -U user -d openccb_cms -t -c "SELECT api_key FROM organizations LIMIT 1;" | xargs 2>/dev/null)
|
API_KEY=$(compose_local exec -T db psql -U user -d openccb_cms -t -c "SELECT api_key FROM organizations LIMIT 1;" | xargs 2>/dev/null)
|
||||||
echo "🔑 API Key Inicial: $API_KEY"
|
echo "🔑 API Key Inicial: $API_KEY"
|
||||||
else
|
else
|
||||||
echo "⚠️ Fallo al crear el administrador. Respuesta: $RESPONSE"
|
echo "⚠️ Fallo al crear el administrador. Respuesta: $RESPONSE"
|
||||||
|
|||||||
@@ -55,10 +55,10 @@ pub async fn list_organization_email_templates(
|
|||||||
) -> Result<Json<Vec<OrganizationEmailTemplateResponse>>, (StatusCode, String)> {
|
) -> Result<Json<Vec<OrganizationEmailTemplateResponse>>, (StatusCode, String)> {
|
||||||
let org_id = claims.org;
|
let org_id = claims.org;
|
||||||
|
|
||||||
let rows: Vec<OrganizationEmailTemplateRow> = sqlx::query!(
|
let rows: Vec<OrganizationEmailTemplateRow> = sqlx::query_as::<_, OrganizationEmailTemplateRow>(
|
||||||
"SELECT id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at FROM organization_email_templates WHERE organization_id = $1 ORDER BY template_key",
|
"SELECT id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at FROM organization_email_templates WHERE organization_id = $1 ORDER BY template_key",
|
||||||
org_id
|
|
||||||
)
|
)
|
||||||
|
.bind(org_id)
|
||||||
.fetch_all(&pool)
|
.fetch_all(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e: sqlx::Error| {
|
.map_err(|e: sqlx::Error| {
|
||||||
@@ -67,21 +67,7 @@ pub async fn list_organization_email_templates(
|
|||||||
StatusCode::INTERNAL_SERVER_ERROR,
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
"Failed to fetch email templates".to_string(),
|
"Failed to fetch email templates".to_string(),
|
||||||
)
|
)
|
||||||
})?
|
})?;
|
||||||
.into_iter()
|
|
||||||
.map(|row| OrganizationEmailTemplateRow {
|
|
||||||
id: row.id,
|
|
||||||
organization_id: row.organization_id,
|
|
||||||
template_key: row.template_key,
|
|
||||||
display_name: row.display_name,
|
|
||||||
subject_template: row.subject_template,
|
|
||||||
body_template: row.body_template,
|
|
||||||
is_html: row.is_html,
|
|
||||||
is_enabled: row.is_enabled,
|
|
||||||
created_at: row.created_at.expect("created_at should not be null"),
|
|
||||||
updated_at: row.updated_at.expect("updated_at should not be null"),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let responses = rows
|
let responses = rows
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -111,18 +97,18 @@ pub async fn create_organization_email_template(
|
|||||||
|
|
||||||
validate_template_payload(&payload)?;
|
validate_template_payload(&payload)?;
|
||||||
|
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query_as::<_, OrganizationEmailTemplateRow>(
|
||||||
"INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
"INSERT INTO organization_email_templates (organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
||||||
org_id,
|
|
||||||
payload.template_key,
|
|
||||||
payload.display_name,
|
|
||||||
payload.subject_template,
|
|
||||||
payload.body_template,
|
|
||||||
payload.is_html,
|
|
||||||
payload.is_enabled
|
|
||||||
)
|
)
|
||||||
|
.bind(org_id)
|
||||||
|
.bind(&payload.template_key)
|
||||||
|
.bind(&payload.display_name)
|
||||||
|
.bind(&payload.subject_template)
|
||||||
|
.bind(&payload.body_template)
|
||||||
|
.bind(payload.is_html)
|
||||||
|
.bind(payload.is_enabled)
|
||||||
.fetch_one(&pool)
|
.fetch_one(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e: sqlx::Error| {
|
.map_err(|e: sqlx::Error| {
|
||||||
@@ -140,19 +126,6 @@ pub async fn create_organization_email_template(
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
let row = OrganizationEmailTemplateRow {
|
|
||||||
id: row.id,
|
|
||||||
organization_id: row.organization_id,
|
|
||||||
template_key: row.template_key,
|
|
||||||
display_name: row.display_name,
|
|
||||||
subject_template: row.subject_template,
|
|
||||||
body_template: row.body_template,
|
|
||||||
is_html: row.is_html,
|
|
||||||
is_enabled: row.is_enabled,
|
|
||||||
created_at: row.created_at.expect("created_at should not be null"),
|
|
||||||
updated_at: row.updated_at.expect("updated_at should not be null"),
|
|
||||||
};
|
|
||||||
|
|
||||||
log_action(
|
log_action(
|
||||||
&pool,
|
&pool,
|
||||||
org_id,
|
org_id,
|
||||||
@@ -193,19 +166,19 @@ pub async fn update_organization_email_template(
|
|||||||
|
|
||||||
validate_template_payload(&payload)?;
|
validate_template_payload(&payload)?;
|
||||||
|
|
||||||
let row = sqlx::query!(
|
let row = sqlx::query_as::<_, OrganizationEmailTemplateRow>(
|
||||||
"UPDATE organization_email_templates
|
"UPDATE organization_email_templates
|
||||||
SET display_name = $3, subject_template = $4, body_template = $5, is_html = $6, is_enabled = $7, updated_at = NOW()
|
SET display_name = $3, subject_template = $4, body_template = $5, is_html = $6, is_enabled = $7, updated_at = NOW()
|
||||||
WHERE id = $1 AND organization_id = $2
|
WHERE id = $1 AND organization_id = $2
|
||||||
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
RETURNING id, organization_id, template_key, display_name, subject_template, body_template, is_html, is_enabled, created_at, updated_at",
|
||||||
template_id,
|
|
||||||
org_id,
|
|
||||||
payload.display_name,
|
|
||||||
payload.subject_template,
|
|
||||||
payload.body_template,
|
|
||||||
payload.is_html,
|
|
||||||
payload.is_enabled
|
|
||||||
)
|
)
|
||||||
|
.bind(template_id)
|
||||||
|
.bind(org_id)
|
||||||
|
.bind(&payload.display_name)
|
||||||
|
.bind(&payload.subject_template)
|
||||||
|
.bind(&payload.body_template)
|
||||||
|
.bind(payload.is_html)
|
||||||
|
.bind(payload.is_enabled)
|
||||||
.fetch_optional(&pool)
|
.fetch_optional(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e: sqlx::Error| {
|
.map_err(|e: sqlx::Error| {
|
||||||
@@ -215,18 +188,6 @@ pub async fn update_organization_email_template(
|
|||||||
"Failed to update email template".to_string(),
|
"Failed to update email template".to_string(),
|
||||||
)
|
)
|
||||||
})?
|
})?
|
||||||
.map(|row| OrganizationEmailTemplateRow {
|
|
||||||
id: row.id,
|
|
||||||
organization_id: row.organization_id,
|
|
||||||
template_key: row.template_key,
|
|
||||||
display_name: row.display_name,
|
|
||||||
subject_template: row.subject_template,
|
|
||||||
body_template: row.body_template,
|
|
||||||
is_html: row.is_html,
|
|
||||||
is_enabled: row.is_enabled,
|
|
||||||
created_at: row.created_at.expect("created_at should not be null"),
|
|
||||||
updated_at: row.updated_at.expect("updated_at should not be null"),
|
|
||||||
})
|
|
||||||
.ok_or((
|
.ok_or((
|
||||||
StatusCode::NOT_FOUND,
|
StatusCode::NOT_FOUND,
|
||||||
"Email template not found".to_string(),
|
"Email template not found".to_string(),
|
||||||
@@ -270,11 +231,11 @@ pub async fn delete_organization_email_template(
|
|||||||
) -> Result<StatusCode, (StatusCode, String)> {
|
) -> Result<StatusCode, (StatusCode, String)> {
|
||||||
let org_id = claims.org;
|
let org_id = claims.org;
|
||||||
|
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query(
|
||||||
"DELETE FROM organization_email_templates WHERE id = $1 AND organization_id = $2",
|
"DELETE FROM organization_email_templates WHERE id = $1 AND organization_id = $2",
|
||||||
template_id,
|
|
||||||
org_id
|
|
||||||
)
|
)
|
||||||
|
.bind(template_id)
|
||||||
|
.bind(org_id)
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e: sqlx::Error| {
|
.map_err(|e: sqlx::Error| {
|
||||||
|
|||||||
@@ -36,7 +36,16 @@ use tower_http::trace::TraceLayer;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenvy::from_filename(".env.dev").or_else(|_| dotenv()).ok();
|
let env_mode = std::env::var("ENVIRONMENT")
|
||||||
|
.unwrap_or_else(|_| "prod".to_string())
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
if env_mode == "dev" {
|
||||||
|
dotenvy::from_filename(".env.dev").or_else(|_| dotenv()).ok();
|
||||||
|
} else {
|
||||||
|
dotenv().ok();
|
||||||
|
}
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
||||||
@@ -611,7 +620,13 @@ async fn main() {
|
|||||||
let addr = SocketAddr::from(([0, 0, 0, 0], 3001));
|
let addr = SocketAddr::from(([0, 0, 0, 0], 3001));
|
||||||
tracing::info!("Servicio CMS escuchando en {} con limitación de tasa y cabeceras de seguridad", addr);
|
tracing::info!("Servicio CMS escuchando en {} con limitación de tasa y cabeceras de seguridad", addr);
|
||||||
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
axum::serve(listener, public_routes).await.unwrap();
|
// Provide peer connection info so governor key extractors can resolve client IPs.
|
||||||
|
axum::serve(
|
||||||
|
listener,
|
||||||
|
public_routes.into_make_service_with_connect_info::<SocketAddr>(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn sync_default_organization(pool: &sqlx::PgPool) {
|
async fn sync_default_organization(pool: &sqlx::PgPool) {
|
||||||
|
|||||||
@@ -35,7 +35,16 @@ use utoipa::OpenApi;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
dotenvy::from_filename(".env.dev").or_else(|_| dotenv()).ok();
|
let env_mode = std::env::var("ENVIRONMENT")
|
||||||
|
.unwrap_or_else(|_| "prod".to_string())
|
||||||
|
.to_lowercase();
|
||||||
|
|
||||||
|
if env_mode == "dev" {
|
||||||
|
dotenvy::from_filename(".env.dev").or_else(|_| dotenv()).ok();
|
||||||
|
} else {
|
||||||
|
dotenv().ok();
|
||||||
|
}
|
||||||
|
|
||||||
tracing_subscriber::fmt::init();
|
tracing_subscriber::fmt::init();
|
||||||
|
|
||||||
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
let db_url = env::var("DATABASE_URL").expect("DATABASE_URL debe estar configurada");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
cmsApi,
|
cmsApi,
|
||||||
OrganizationEmailTemplate,
|
OrganizationEmailTemplate,
|
||||||
@@ -47,19 +47,14 @@ export default function EmailTemplates() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
const selectedTemplate = useMemo(
|
const loadTemplates = useCallback(async () => {
|
||||||
() => templates.find((t) => t.id === selectedId),
|
|
||||||
[templates, selectedId],
|
|
||||||
);
|
|
||||||
|
|
||||||
const loadTemplates = async () => {
|
|
||||||
const data = await cmsApi.listOrganizationEmailTemplates();
|
const data = await cmsApi.listOrganizationEmailTemplates();
|
||||||
setTemplates(data);
|
setTemplates(data);
|
||||||
if (data.length > 0 && !selectedId) {
|
if (data.length > 0 && !selectedId) {
|
||||||
setSelectedId(data[0].id);
|
setSelectedId(data[0].id);
|
||||||
setForm(toEditable(data[0]));
|
setForm(toEditable(data[0]));
|
||||||
}
|
}
|
||||||
};
|
}, [selectedId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
@@ -73,7 +68,7 @@ export default function EmailTemplates() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
run();
|
run();
|
||||||
}, []);
|
}, [loadTemplates]);
|
||||||
|
|
||||||
const setField = <K extends keyof EditableTemplate>(key: K, value: EditableTemplate[K]) => {
|
const setField = <K extends keyof EditableTemplate>(key: K, value: EditableTemplate[K]) => {
|
||||||
setForm((prev) => ({ ...prev, [key]: value }));
|
setForm((prev) => ({ ...prev, [key]: value }));
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
"webhooks": "Webhooks",
|
"webhooks": "Webhooks",
|
||||||
"globalControl": "Control Global"
|
"globalControl": "Control Global"
|
||||||
},
|
},
|
||||||
|
"dashboard": {
|
||||||
|
"title": "Panel del Studio",
|
||||||
|
"newCourse": "Nuevo Curso",
|
||||||
|
"aiBuilder": "Constructor IA",
|
||||||
|
"noCourses": "No hay cursos disponibles."
|
||||||
|
},
|
||||||
"course": {
|
"course": {
|
||||||
"createCourse": "Crear Curso",
|
"createCourse": "Crear Curso",
|
||||||
"editCourse": "Editar Curso",
|
"editCourse": "Editar Curso",
|
||||||
|
|||||||
Reference in New Issue
Block a user