From 6bfaab634fb3cbcf13db1c093caabdd0dc5c7f8a Mon Sep 17 00:00:00 2001 From: Carlos Tello Date: Wed, 23 Sep 2026 14:12:09 -0300 Subject: [PATCH] feat(deploy): mover install.sh a la raiz con soporte unificado para backend, frontend y seleccion de base de datos postgres --- README.md | 26 +- backend/install.sh | 474 ---------------------------------- install.sh | 622 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 647 insertions(+), 475 deletions(-) delete mode 100644 backend/install.sh create mode 100755 install.sh diff --git a/README.md b/README.md index 3465823..08ad136 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,8 @@ Fase 1: Paridad Legacy y Estabilización [████████████ Fase 2: Co-docencia y Matriz Conflictos [████████████████████] 100% (Completado) Fase 3: Hitos Evaluativos y Calificaciones[████████████████████] 100% (Completado) Fase 4: UI/UX Drag & Drop e Impersonación[████████████████████] 100% (Completado) -Fase 5: Despliegue Producción e Integrac.[░░░░░░░░░░░░░░░░░░░░] 0% (Planificada) +Fase 5: Integración Auth, Email & Moodle [████████████████████] 100% (Completado) +Fase 6: Despliegue Producción e Integrac.[████░░░░░░░░░░░░░░░░] 25% (En Curso) ``` --- @@ -153,6 +154,29 @@ node src/app.js --- +## 📦 Despliegue Automatizado en Producción (Linux / Proxmox LXC) + +Para servidores Debian 12 / Ubuntu en Proxmox LXC o VPS, el repositorio incluye el instalador unificado [install.sh](install.sh) en la raíz: + +```bash +# Despliegue interactivo (pregunta si mantener o restaurar la BD si ya existe): +sudo bash install.sh + +# Despliegue desatendido manteniendo la base de datos existente: +sudo bash install.sh --keep-db + +# Despliegue desatendido recreando la base de datos desde cero (DROP y restore snapshot): +sudo bash install.sh --fresh-db +``` + +El script configura automáticamente: +1. Entorno Python 3.10+ y virtualenv en `backend/venv` con Gunicorn y servicio `admin-edu-space-backend.service` (puerto 5000). +2. Entorno Node.js 20 LTS en `frontend/` con servicio `admin-edu-space-frontend.service` (puerto 3000). +3. Motor PostgreSQL local con configuración de usuario y opciones seguras para preservar o restaurar la base `classrooms_db`. +4. Servicio maestro unificado `admin-edu-space.service` gestionable vía `systemctl {status|restart|stop} admin-edu-space`. + +--- + ## 🧪 Pruebas Automatizadas Para ejecutar la suite completa de pruebas unitarias de regresión (Fases 2, 3 y 4): diff --git a/backend/install.sh b/backend/install.sh deleted file mode 100644 index 6afb2f2..0000000 --- a/backend/install.sh +++ /dev/null @@ -1,474 +0,0 @@ -#!/usr/bin/env bash -# ============================================================================== -# install.sh — Admin Edu-Space (UniCABA) -# Instalador automatizado para Proxmox VE (LXC) / Debian 12 / Ubuntu (entorno venv) -# Uso: sudo bash install.sh -# ============================================================================== - -set -euo pipefail - -# ─── Colores y Helpers ───────────────────────────────────────────────────────── -RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' -CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' - -ok() { echo -e "${GREEN} ✔ $*${RESET}"; } -info() { echo -e "${CYAN} ▸ $*${RESET}"; } -warn() { echo -e "${YELLOW} ⚠ $*${RESET}"; } -err() { echo -e "${RED} ✘ $*${RESET}" >&2; exit 1; } -hdr() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; } - -# ─── Variables Principales ───────────────────────────────────────────────────── -APP_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -VENV_DIR="${APP_DIR}/venv" -APP_ENV="${APP_DIR}/.env" -SERVICE_NAME="admin-edu-space" -APP_USER="eduspace" -PYTHON_REQUIRED_MAJOR=3 -PYTHON_REQUIRED_MINOR=10 -LOG_DIR="/var/log/${SERVICE_NAME}" -SQL_BACKUP_FILE="${APP_DIR}/classrooms_db.sql" - -# ─── Verificación de Permisos Root ───────────────────────────────────────────── -[[ $EUID -ne 0 ]] && err "Ejecutá este script con permisos de root: sudo bash install.sh" - -# Si el directorio está dentro de /root, lo trasladamos a /opt para evitar problemas de permisos con el usuario de servicio -if [[ "${APP_DIR}" == /root/* ]]; then - NEW_DIR="/opt/admin-edu-space" - warn "Detectado directorio en ${APP_DIR}. Trasladando a ${NEW_DIR} para evitar bloqueos de permisos..." - systemctl stop "${SERVICE_NAME}" 2>/dev/null || true - - # Limpiar cualquier subdirectorio anidado previo si quedó de un intento fallido - rm -rf "${NEW_DIR}/admin-edu-space" 2>/dev/null || true - - mkdir -p "${NEW_DIR}" - # Copiar recursivamente todo el contenido (incluyendo dotfiles) directamente a NEW_DIR - cp -a "${APP_DIR}/." "${NEW_DIR}/" - - cd "${NEW_DIR}" - exec bash install.sh -fi - -# Limpiar posible subdirectorio anidado residual en caso de ejecuciones previas -rm -rf "${APP_DIR}/admin-edu-space" 2>/dev/null || true - -# Verificar e instalar git si no está presente en el sistema -if ! command -v git &>/dev/null; then - info "Git no está instalado. Instalando paquete git..." - export DEBIAN_FRONTEND=noninteractive - apt-get update -qq >/dev/null 2>&1 || true - apt-get install -y -qq git >/dev/null 2>&1 || apt-get install -y git - ok "Git instalado correctamente" -fi - -# Configurar safe.directory en Git para evitar errores de propiedad cruzada (root vs eduspace) -git config --global --add safe.directory '*' 2>/dev/null || true - -[[ ! -f "${APP_DIR}/wsgi.py" ]] && err "No se encontró wsgi.py en ${APP_DIR}. ¿Estás en el directorio raíz del proyecto?" - -echo -e "\n${BOLD}${CYAN}" -echo "╔══════════════════════════════════════════════════════════════╗" -echo "║ Admin Edu-Space (UniCABA) — Proxmox LXC Installer ║" -echo "║ Despliegue Nativo con venv + Motor PostgreSQL ║" -echo "╚══════════════════════════════════════════════════════════════╝" -echo -e "${RESET}" - -OS_NAME=$(grep -oP '(?<=^PRETTY_NAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || uname -s) -info "Sistema Operativo: ${OS_NAME}" -info "Directorio del proyecto: ${APP_DIR}" -info "Servicio systemd: ${SERVICE_NAME}" -info "Usuario de servicio: ${APP_USER}" - -# ─── 1. Actualizar sistema e instalar dependencias base ──────────────────────── -hdr "1. Actualizando repositorios y paquetes base" -export DEBIAN_FRONTEND=noninteractive -apt-get update -qq -apt-get install -y -qq \ - sudo curl wget git ca-certificates gnupg lsb-release \ - build-essential libpq-dev \ - locales \ - > /dev/null 2>&1 -ok "Paquetes base instalados" - -# Función auxiliar para ejecutar comandos psql como superusuario postgres de forma universal (sin depender de sudo) -pg_cmd() { - if command -v sudo >/dev/null 2>&1; then - sudo -u postgres psql "$@" - elif command -v runuser >/dev/null 2>&1; then - runuser -u postgres -- psql "$@" - else - su - postgres -c "psql $(printf '%q ' "$@")" - fi -} - -# ─── 2. Configurar Locale UTF-8 ──────────────────────────────────────────────── -hdr "2. Configurando soporte de codificación (Locales UTF-8)" -if [ -f /etc/locale.gen ]; then - sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen - sed -i 's/# es_AR.UTF-8 UTF-8/es_AR.UTF-8 UTF-8/' /etc/locale.gen - locale-gen en_US.UTF-8 es_AR.UTF-8 > /dev/null 2>&1 || true -fi -update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 > /dev/null 2>&1 || true -export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 -ok "Locales configurados: en_US.UTF-8 / es_AR.UTF-8" - -# ─── 3. Instalar Python 3.10+ y entorno de desarrollo ────────────────────────── -hdr "3. Verificando entorno Python" -apt-get install -y -qq python3 python3-venv python3-pip python3-dev > /dev/null 2>&1 - -PY_VERSION=$(python3 --version 2>/dev/null | awk '{print $2}' || echo "0.0") -PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) -PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) - -if [[ "$PY_MAJOR" -ge "$PYTHON_REQUIRED_MAJOR" && "$PY_MINOR" -ge "$PYTHON_REQUIRED_MINOR" ]]; then - ok "Python $PY_VERSION disponible" -else - info "Python $PY_VERSION es menor a 3.10 — instalando Python 3.11..." - apt-get install -y -qq python3.11 python3.11-venv python3.11-dev > /dev/null 2>&1 - update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 > /dev/null 2>&1 - PY_VERSION=$(python3 --version | awk '{print $2}') - ok "Python $PY_VERSION instalado" -fi - -# ─── 4. Instalar y Levantar Motor PostgreSQL ─────────────────────────────────── -hdr "4. Instalando y configurando motor de base de datos PostgreSQL" -apt-get install -y -qq postgresql postgresql-contrib postgresql-client > /dev/null 2>&1 -PG_VER=$(psql --version 2>/dev/null | awk '{print $3}' || echo "instalado") -PG_MAJOR=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1 || echo "") -[[ -z "$PG_MAJOR" ]] && PG_MAJOR=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1 || echo "") -[[ -z "$PG_MAJOR" ]] && PG_MAJOR="15" - -ok "Motor PostgreSQL instalado: ${PG_VER}" - -# Asegurar directorios de socket y permisos para PostgreSQL en contenedores LXC -mkdir -p /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql -chown -R postgres:postgres /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql -chmod 2775 /run/postgresql /var/run/postgresql - -# Verificar si existe el cluster 'main'. Si no existe (común en Debian si falló la creación automática), crearlo -if ! pg_lsclusters 2>/dev/null | grep -q 'main'; then - info "Cluster PostgreSQL no detectado — inicializando cluster ${PG_MAJOR} main..." - pg_createcluster "${PG_MAJOR}" main --locale en_US.UTF-8 --start >/dev/null 2>&1 \ - || pg_createcluster "${PG_MAJOR}" main --locale C.UTF-8 --start >/dev/null 2>&1 \ - || true -fi - -# Levantar servicio de PostgreSQL asegurando compatibilidad con contenedores LXC y VMs -info "Iniciando servicio de PostgreSQL..." -pg_ctlcluster "${PG_MAJOR}" main start >/dev/null 2>&1 || true -systemctl start "postgresql@${PG_MAJOR}-main" 2>/dev/null || true -systemctl start postgresql 2>/dev/null || true -service postgresql start 2>/dev/null || true - -# Esperar a que el socket de PostgreSQL esté completamente activo -PG_READY=false -for i in {1..20}; do - if pg_cmd -c '\q' >/dev/null 2>&1; then - PG_READY=true - break - fi - if [[ $i -eq 3 || $i -eq 8 ]]; then - pg_ctlcluster "${PG_MAJOR}" main start 2>/dev/null || true - systemctl restart "postgresql@${PG_MAJOR}-main" 2>/dev/null || true - fi - sleep 1 -done - -if [ "$PG_READY" = true ]; then - # Configurar pg_hba.conf para permitir conexiones locales (socket y 127.0.0.1) sin fallos de autenticación - PG_HBA=$(find /etc/postgresql/ -name pg_hba.conf 2>/dev/null | sort -V | tail -n1) - if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then - sed -i -E 's/^(local\s+all\s+all\s+)peer/\1trust/' "$PG_HBA" 2>/dev/null || true - sed -i -E 's/^(host\s+all\s+all\s+127\.0\.0\.1\/32\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true - sed -i -E 's/^(host\s+all\s+all\s+::1\/128\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true - systemctl reload "postgresql@${PG_MAJOR}-main" 2>/dev/null || systemctl reload postgresql 2>/dev/null || true - fi - systemctl enable "postgresql@${PG_MAJOR}-main" > /dev/null 2>&1 || true - systemctl enable postgresql > /dev/null 2>&1 || true - ok "PostgreSQL activo y aceptando conexiones" -else - warn "Diagnóstico de arranque de PostgreSQL en LXC:" - pg_lsclusters 2>/dev/null || true - systemctl status "postgresql@${PG_MAJOR}-main" --no-pager -n 10 2>/dev/null || true - journalctl -u postgresql -n 15 --no-pager 2>/dev/null || true - info "Prueba directa de conexión psql:" - pg_cmd -c '\q' || true - err "No se pudo conectar al motor PostgreSQL. Verificá los registros del sistema." -fi - -# ─── 5. Configurar Variables de Entorno (.env) ───────────────────────────────── -hdr "5. Configurando variables de entorno (.env)" - -# Valores predeterminados (paridad con sistema activo) -DB_NAME="classrooms_db" -DB_USER="postgres" -DB_PASSWORD="password" - -if [[ -f "$APP_ENV" ]]; then - ok ".env ya existe — se preserva la configuración actual" - set -a - # shellcheck disable=SC1090 - source <(grep -v '^#' "$APP_ENV" | grep '=') - set +a - - # Extraer credenciales desde DATABASE_URL si está presente - if [[ -n "${DATABASE_URL:-}" ]]; then - URL_USER=$(echo "$DATABASE_URL" | sed -E 's|^.*://([^:@]+).*|\1|') - URL_PASS=$(echo "$DATABASE_URL" | sed -E 's|^.*://[^:]+:([^@]+)@.*|\1|') - URL_NAME=$(echo "$DATABASE_URL" | sed -E 's|^.*/([^?]+).*|\1|') - - if [[ "$URL_USER" != "$DATABASE_URL" && -n "$URL_USER" ]]; then - DB_USER="$URL_USER" - fi - if [[ "$URL_PASS" != "$DATABASE_URL" && -n "$URL_PASS" ]]; then - DB_PASSWORD="$URL_PASS" - fi - if [[ "$URL_NAME" != "$DATABASE_URL" && -n "$URL_NAME" ]]; then - DB_NAME="$URL_NAME" - fi - fi - - DB_USER="${DB_USER:-${DB_USERNAME:-postgres}}" - DB_PASSWORD="${DB_PASSWORD:-password}" - DB_NAME="${DB_NAME:-classrooms_db}" -else - SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') - cat > "$APP_ENV" </dev/null 2>&1 \ - || pg_cmd -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \ - || true - pg_cmd -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true - fi -done - -pg_cmd -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true -pg_cmd -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true - -# Crear base de datos -pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" >/dev/null 2>&1 \ - || info "La base de datos '${DB_NAME}' ya existía." - -for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do - if [[ -n "$u" ]]; then - pg_cmd -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" >/dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" >/dev/null 2>&1 || true - fi -done - -ok "Motor PostgreSQL configurado (usuario: ${DB_USER}, base: ${DB_NAME})" - -# ─── 7. Crear Entorno Virtual e Instalar Dependencias ────────────────────────── -hdr "7. Creando virtualenv e instalando dependencias Python" - -if [[ -d "$VENV_DIR" ]]; then - info "Recreando entorno virtual para asegurar dependencias limpias..." - rm -rf "$VENV_DIR" -fi - -python3 -m venv "$VENV_DIR" > /dev/null 2>&1 -"${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel --quiet - -info "Instalando paquetes desde requirements.txt..." -if "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" --quiet 2>/dev/null; then - ok "Dependencias instaladas correctamente" -else - warn "Reintentando instalación con --no-build-isolation (compatibilidad Python 3.12/3.13)..." - "${VENV_DIR}/bin/pip" install -r "${APP_DIR}/requirements.txt" --no-build-isolation --quiet - ok "Dependencias instaladas" -fi - -# ─── 8. Despliegue de Datos: Restauración o Inicialización ───────────────────── -hdr "8. Desplegando esquema y datos de la aplicación" -cd "$APP_DIR" - -if [[ -f "$SQL_BACKUP_FILE" ]]; then - BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}') - info "Se detectó snapshot de base de datos '${SQL_BACKUP_FILE}' (${BACKUP_SIZE})." - info "Restaurando volcado integral de aulas, comisiones, materias, reservas y usuarios..." - - # Recrear base de datos limpia para carga sin colisiones - pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true - pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1 - - # Restaurar backup filtrando directivas específicas de PG18 si existiesen - if sed '/^\\restrict/d' "$SQL_BACKUP_FILE" | pg_cmd -d "${DB_NAME}" > /dev/null 2>&1; then - ok "Volcado SQL restaurado exitosamente." - else - warn "Restauración completada con avisos menores — aplicando permisos..." - fi - - # Otorgar propiedad y permisos absolutos sobre todas las tablas y secuencias al usuario de la app - info "Garantizando permisos y secuencias para ${DB_USER}..." - for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do - if [[ -n "$u" ]]; then - pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true - pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO \"${u}\";" > /dev/null 2>&1 || true - fi - done - - if [[ "${DB_USER}" != "postgres" ]]; then - pg_cmd -d "${DB_NAME}" -c "ALTER SCHEMA public OWNER TO \"${DB_USER}\";" > /dev/null 2>&1 || true - fi - - # Ejecutar init_db.py de forma idempotente para verificar roles y migraciones incrementales - info "Validando coherencia de modelos con init_db.py..." - LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ - FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py - ok "Base de datos sincronizada y verificada" -else - info "No se encontró classrooms_db.sql. Creando esquema desde cero con init_db.py..." - LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ - FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py - ok "Esquema, roles RBAC, edificios y usuario admin creados" -fi -cd - > /dev/null - -# ─── 9. Usuario del Sistema y Permisos de Directorio ─────────────────────────── -hdr "9. Configurando permisos y usuario de ejecución del sistema" -if id "$APP_USER" &>/dev/null; then - info "El usuario de sistema '${APP_USER}' ya existe." -else - useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" - ok "Usuario de sistema '${APP_USER}' creado (sin shell)" -fi - -mkdir -p "${APP_DIR}/app/static/uploads" -chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" -chmod -R 755 "$APP_DIR" -chmod -R 775 "${APP_DIR}/app/static/uploads" -chmod 640 "$APP_ENV" - -mkdir -p "$LOG_DIR" -chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" -chmod 755 "$LOG_DIR" -ok "Permisos de seguridad aplicados en ${APP_DIR} y ${LOG_DIR}" - -# ─── 10. Servicio Systemd (Gunicorn de Producción) ───────────────────────────── -hdr "10. Configurando servicio systemd de arranque automático" - -GUNICORN_BIN="${VENV_DIR}/bin/gunicorn" - -cat > "/etc/systemd/system/${SERVICE_NAME}.service" < /dev/null 2>&1 -systemctl restart "${SERVICE_NAME}" -sleep 3 - -if systemctl is-active --quiet "$SERVICE_NAME"; then - ok "Servicio '${SERVICE_NAME}' activo y en ejecución" - - # Validar respuesta HTTP local - info "Verificando respuesta del servidor web..." - sleep 2 - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/ || echo "000") - if [[ "$HTTP_CODE" == "200" || "$HTTP_CODE" == "302" ]]; then - ok "Servidor web respondiendo correctamente (HTTP ${HTTP_CODE})" - else - warn "El servidor web devolvió código HTTP ${HTTP_CODE}. Podés revisar los logs con: journalctl -u ${SERVICE_NAME} -n 30 --no-pager" - fi -else - warn "El servicio tardó en arrancar. Podés revisar los logs con:" - warn " journalctl -u ${SERVICE_NAME} -n 50 --no-pager" -fi - -# ─── Resumen Final y Datos de Acceso ────────────────────────────────────────── -hdr "¡Despliegue Completado con Éxito!" - -IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1") - -echo -e " ${BOLD}Acceso Web a la Aplicación:${RESET}" -echo -e " ${CYAN}http://${IP}:5000${RESET} o ${CYAN}http://localhost:5000${RESET}" -echo "" -echo -e " ${BOLD}Credenciales Iniciales de Administrador:${RESET}" -echo -e " Email: ${CYAN}admin@edu-space.com${RESET}" -echo -e " Password: ${CYAN}admin123${RESET}" -echo "" -echo -e " ${BOLD}Base de Datos Desplegada:${RESET}" -echo -e " Motor: PostgreSQL (${PG_VER})" -echo -e " Base: ${DB_NAME}" -echo -e " Usuario: ${DB_USER}" -echo -e " Snapshot: $( [ -f "$SQL_BACKUP_FILE" ] && echo "Restaurada desde classrooms_db.sql" || echo "Inicializada limpia" )" -echo "" -echo -e " ${BOLD}Comandos de Gestión del Servicio:${RESET}" -echo -e " Estado: ${CYAN}systemctl status ${SERVICE_NAME}${RESET}" -echo -e " Logs: ${CYAN}journalctl -u ${SERVICE_NAME} -f${RESET}" -echo -e " Reiniciar: ${CYAN}systemctl restart ${SERVICE_NAME}${RESET}" -echo -e " Detener: ${CYAN}systemctl stop ${SERVICE_NAME}${RESET}" -echo "" -echo -e " ${BOLD}Generar Nuevos Backups:${RESET}" -echo -e " ${CYAN}bash backup_db.sh${RESET}" -echo "" -ok "Despliegue finalizado. El sistema se encuentra 100% operativo." -echo "" diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..79e46a2 --- /dev/null +++ b/install.sh @@ -0,0 +1,622 @@ +#!/usr/bin/env bash +# ============================================================================== +# install.sh — Admin Edu-Space (UniCABA) +# Instalador automatizado para Proxmox VE (LXC) / Debian 12 / Ubuntu +# Despliegue unificado: Backend Flask (Gunicorn) + Frontend Node.js (BFF) + PostgreSQL +# Uso: sudo bash install.sh [--keep-db | --fresh-db] +# ============================================================================== + +set -euo pipefail + +# ─── Colores y Helpers ───────────────────────────────────────────────────────── +RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' +CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' + +ok() { echo -e "${GREEN} ✔ $*${RESET}"; } +info() { echo -e "${CYAN} ▸ $*${RESET}"; } +warn() { echo -e "${YELLOW} ⚠ $*${RESET}"; } +err() { echo -e "${RED} ✘ $*${RESET}" >&2; exit 1; } +hdr() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; } + +# ─── Argumentos de Línea de Comandos ─────────────────────────────────────────── +ARG_DB_ACTION="" +for arg in "$@"; do + case "$arg" in + --keep-db) + ARG_DB_ACTION="keep" + ;; + --fresh-db|--restore-db) + ARG_DB_ACTION="fresh" + ;; + -h|--help) + echo "Uso: sudo bash install.sh [OPCIONES]" + echo "Opciones:" + echo " --keep-db Mantiene la base de datos existente si ya contiene datos." + echo " --fresh-db Restaura la base de datos desde cero (DROP y recarga snapshot)." + echo " -h, --help Muestra esta ayuda." + exit 0 + ;; + *) + warn "Opción desconocida ignorada: $arg" + ;; + esac +done + +# ─── Variables Principales ───────────────────────────────────────────────────── +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BACKEND_DIR="${ROOT_DIR}/backend" +FRONTEND_DIR="${ROOT_DIR}/frontend" + +VENV_DIR="${BACKEND_DIR}/venv" +BACKEND_ENV="${BACKEND_DIR}/.env" +FRONTEND_ENV="${FRONTEND_DIR}/.env" + +SERVICE_BACKEND="admin-edu-space-backend" +SERVICE_FRONTEND="admin-edu-space-frontend" +SERVICE_META="admin-edu-space" + +APP_USER="eduspace" +PYTHON_REQUIRED_MAJOR=3 +PYTHON_REQUIRED_MINOR=10 +LOG_DIR="/var/log/${SERVICE_META}" + +# Ubicación del snapshot de base de datos +SQL_BACKUP_FILE="${BACKEND_DIR}/classrooms_db.sql" +if [[ ! -f "$SQL_BACKUP_FILE" && -f "${ROOT_DIR}/classrooms_db.sql" ]]; then + SQL_BACKUP_FILE="${ROOT_DIR}/classrooms_db.sql" +fi + +# ─── Verificación de Permisos Root ───────────────────────────────────────────── +[[ $EUID -ne 0 ]] && err "Ejecutá este script con permisos de root: sudo bash install.sh" + +# Si el directorio está dentro de /root, trasladarlo a /opt para evitar problemas de permisos con el usuario de servicio +if [[ "${ROOT_DIR}" == /root/* ]]; then + NEW_DIR="/opt/admin-edu-space" + warn "Detectado directorio en ${ROOT_DIR}. Trasladando a ${NEW_DIR} para evitar bloqueos de permisos..." + systemctl stop "${SERVICE_BACKEND}" 2>/dev/null || true + systemctl stop "${SERVICE_FRONTEND}" 2>/dev/null || true + systemctl stop "${SERVICE_META}" 2>/dev/null || true + + mkdir -p "${NEW_DIR}" + cp -a "${ROOT_DIR}/." "${NEW_DIR}/" + cd "${NEW_DIR}" + exec bash install.sh "$@" +fi + +# Validar estructura básica del repositorio +[[ ! -f "${BACKEND_DIR}/wsgi.py" ]] && err "No se encontró wsgi.py en ${BACKEND_DIR}. Verificá la estructura del proyecto." +[[ ! -f "${FRONTEND_DIR}/package.json" ]] && err "No se encontró package.json en ${FRONTEND_DIR}. Verificá la estructura del proyecto." + +# Verificar e instalar git si no está presente en el sistema +if ! command -v git &>/dev/null; then + info "Git no está instalado. Instalando paquete git..." + export DEBIAN_FRONTEND=noninteractive + apt-get update -qq >/dev/null 2>&1 || true + apt-get install -y -qq git >/dev/null 2>&1 || apt-get install -y git + ok "Git instalado correctamente" +fi +git config --global --add safe.directory '*' 2>/dev/null || true + +echo -e "\n${BOLD}${CYAN}" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ Admin Edu-Space (UniCABA) — Instalador ║" +echo "║ Backend (Flask/Gunicorn) + Frontend (Node.js) + PostgreSQL ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo -e "${RESET}" + +OS_NAME=$(grep -oP '(?<=^PRETTY_NAME=).+' /etc/os-release 2>/dev/null | tr -d '"' || uname -s) +info "Sistema Operativo: ${OS_NAME}" +info "Directorio Raíz: ${ROOT_DIR}" +info "Directorio Backend: ${BACKEND_DIR}" +info "Directorio Frontend: ${FRONTEND_DIR}" +info "Usuario de Servicio: ${APP_USER}" + +# ─── 1. Paquetes Base del Sistema ────────────────────────────────────────────── +hdr "1. Actualizando repositorios y paquetes base" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y -qq \ + sudo curl wget git ca-certificates gnupg lsb-release \ + build-essential libpq-dev \ + locales \ + > /dev/null 2>&1 +ok "Paquetes base instalados" + +pg_cmd() { + if command -v sudo >/dev/null 2>&1; then + sudo -u postgres psql "$@" + elif command -v runuser >/dev/null 2>&1; then + runuser -u postgres -- psql "$@" + else + su - postgres -c "psql $(printf '%q ' "$@")" + fi +} + +# ─── 2. Configurar Locale UTF-8 ──────────────────────────────────────────────── +hdr "2. Configurando soporte de codificación (Locales UTF-8)" +if [ -f /etc/locale.gen ]; then + sed -i 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen + sed -i 's/# es_AR.UTF-8 UTF-8/es_AR.UTF-8 UTF-8/' /etc/locale.gen + locale-gen en_US.UTF-8 es_AR.UTF-8 > /dev/null 2>&1 || true +fi +update-locale LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 > /dev/null 2>&1 || true +export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 +ok "Locales configurados: en_US.UTF-8 / es_AR.UTF-8" + +# ─── 3. Instalar Python 3.10+ y entorno virtual ──────────────────────────────── +hdr "3. Verificando entorno Python" +apt-get install -y -qq python3 python3-venv python3-pip python3-dev > /dev/null 2>&1 + +PY_VERSION=$(python3 --version 2>/dev/null | awk '{print $2}' || echo "0.0") +PY_MAJOR=$(echo "$PY_VERSION" | cut -d. -f1) +PY_MINOR=$(echo "$PY_VERSION" | cut -d. -f2) + +if [[ "$PY_MAJOR" -ge "$PYTHON_REQUIRED_MAJOR" && "$PY_MINOR" -ge "$PYTHON_REQUIRED_MINOR" ]]; then + ok "Python $PY_VERSION disponible" +else + info "Python $PY_VERSION es menor a 3.10 — instalando Python 3.11..." + apt-get install -y -qq python3.11 python3.11-venv python3.11-dev > /dev/null 2>&1 + update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.11 1 > /dev/null 2>&1 + PY_VERSION=$(python3 --version | awk '{print $2}') + ok "Python $PY_VERSION instalado" +fi + +# ─── 4. Instalar Node.js y npm para el Frontend BFF ─────────────────────────── +hdr "4. Verificando entorno Node.js para el Frontend BFF" + +NODE_INSTALLED=false +if command -v node &>/dev/null; then + NODE_VER_NUM=$(node -v | tr -d 'v' | cut -d. -f1 || echo "0") + if [[ "$NODE_VER_NUM" -ge 18 ]]; then + NODE_INSTALLED=true + ok "Node.js $(node -v) y npm $(npm -v 2>/dev/null || echo '') ya disponibles" + fi +fi + +if [ "$NODE_INSTALLED" = false ]; then + info "Instalando Node.js 20 LTS vía repositorio oficial NodeSource..." + mkdir -p /etc/apt/keyrings + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg --yes 2>/dev/null || true + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null + apt-get update -qq >/dev/null 2>&1 || true + apt-get install -y -qq nodejs >/dev/null 2>&1 || apt-get install -y nodejs + ok "Node.js $(node -v) instalado exitosamente" +fi + +# ─── 5. Instalar y Levantar Motor PostgreSQL ─────────────────────────────────── +hdr "5. Instalando y configurando motor de base de datos PostgreSQL" +apt-get install -y -qq postgresql postgresql-contrib postgresql-client > /dev/null 2>&1 +PG_VER=$(psql --version 2>/dev/null | awk '{print $3}' || echo "instalado") +PG_MAJOR=$(psql --version 2>/dev/null | awk '{print $3}' | cut -d. -f1 || echo "") +[[ -z "$PG_MAJOR" ]] && PG_MAJOR=$(ls /etc/postgresql/ 2>/dev/null | sort -V | tail -n1 || echo "") +[[ -z "$PG_MAJOR" ]] && PG_MAJOR="15" + +ok "Motor PostgreSQL instalado: ${PG_VER}" + +mkdir -p /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql +chown -R postgres:postgres /run/postgresql /var/run/postgresql /var/lib/postgresql /var/log/postgresql +chmod 2775 /run/postgresql /var/run/postgresql + +if ! pg_lsclusters 2>/dev/null | grep -q 'main'; then + info "Cluster PostgreSQL no detectado — inicializando cluster ${PG_MAJOR} main..." + pg_createcluster "${PG_MAJOR}" main --locale en_US.UTF-8 --start >/dev/null 2>&1 \ + || pg_createcluster "${PG_MAJOR}" main --locale C.UTF-8 --start >/dev/null 2>&1 \ + || true +fi + +info "Iniciando servicio de PostgreSQL..." +pg_ctlcluster "${PG_MAJOR}" main start >/dev/null 2>&1 || true +systemctl start "postgresql@${PG_MAJOR}-main" 2>/dev/null || true +systemctl start postgresql 2>/dev/null || true +service postgresql start 2>/dev/null || true + +PG_READY=false +for i in {1..20}; do + if pg_cmd -c '\q' >/dev/null 2>&1; then + PG_READY=true + break + fi + sleep 1 +done + +if [ "$PG_READY" = true ]; then + PG_HBA=$(find /etc/postgresql/ -name pg_hba.conf 2>/dev/null | sort -V | tail -n1) + if [[ -n "$PG_HBA" && -f "$PG_HBA" ]]; then + sed -i -E 's/^(local\s+all\s+all\s+)peer/\1trust/' "$PG_HBA" 2>/dev/null || true + sed -i -E 's/^(host\s+all\s+all\s+127\.0\.0\.1\/32\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true + sed -i -E 's/^(host\s+all\s+all\s+::1\/128\s+)(scram-sha-256|md5)/\1trust/' "$PG_HBA" 2>/dev/null || true + systemctl reload "postgresql@${PG_MAJOR}-main" 2>/dev/null || systemctl reload postgresql 2>/dev/null || true + fi + systemctl enable "postgresql@${PG_MAJOR}-main" > /dev/null 2>&1 || true + systemctl enable postgresql > /dev/null 2>&1 || true + ok "PostgreSQL activo y aceptando conexiones" +else + err "No se pudo conectar al motor PostgreSQL local. Verificá los registros del sistema." +fi + +# ─── 6. Configurar Variables de Entorno (.env) ───────────────────────────────── +hdr "6. Configurando variables de entorno (Backend y Frontend)" + +DB_NAME="classrooms_db" +DB_USER="postgres" +DB_PASSWORD="password" + +if [[ -f "$BACKEND_ENV" ]]; then + ok "backend/.env ya existe — se preserva la configuración actual" + set -a + # shellcheck disable=SC1090 + source <(grep -v '^#' "$BACKEND_ENV" | grep '=') + set +a + + if [[ -n "${DATABASE_URL:-}" ]]; then + URL_USER=$(echo "$DATABASE_URL" | sed -E 's|^.*://([^:@]+).*|\1|') + URL_PASS=$(echo "$DATABASE_URL" | sed -E 's|^.*://[^:]+:([^@]+)@.*|\1|') + URL_NAME=$(echo "$DATABASE_URL" | sed -E 's|^.*/([^?]+).*|\1|') + [[ "$URL_USER" != "$DATABASE_URL" && -n "$URL_USER" ]] && DB_USER="$URL_USER" + [[ "$URL_PASS" != "$DATABASE_URL" && -n "$URL_PASS" ]] && DB_PASSWORD="$URL_PASS" + [[ "$URL_NAME" != "$DATABASE_URL" && -n "$URL_NAME" ]] && DB_NAME="$URL_NAME" + fi + DB_USER="${DB_USER:-${DB_USERNAME:-postgres}}" + DB_PASSWORD="${DB_PASSWORD:-password}" + DB_NAME="${DB_NAME:-classrooms_db}" +else + SECRET_KEY=$(python3 -c 'import secrets; print(secrets.token_hex(32))') + cat > "$BACKEND_ENV" < "$FRONTEND_ENV" </dev/null 2>&1 \ + || pg_cmd -c "ALTER USER \"${u}\" WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 \ + || true + pg_cmd -c "ALTER USER \"${u}\" WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true + fi +done + +pg_cmd -c "ALTER USER postgres WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true +pg_cmd -c "ALTER USER postgres WITH SUPERUSER CREATEDB;" >/dev/null 2>&1 || true + +# Detectar existencia y tablas previas +DB_EXISTS=false +TABLE_COUNT=0 +if pg_cmd -lqt 2>/dev/null | cut -d \| -f 1 | grep -qw "${DB_NAME}"; then + DB_EXISTS=true + TABLE_COUNT=$(pg_cmd -d "${DB_NAME}" -tAc "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null || echo "0") +fi + +DO_RESTORE_FRESH=false + +if [ "$DB_EXISTS" = true ] && [ "$TABLE_COUNT" -gt 0 ]; then + info "Base de datos detectada: '${DB_NAME}' con ${TABLE_COUNT} tabla(s) existente(s)." + + if [[ "$ARG_DB_ACTION" == "keep" ]]; then + info "Bandera --keep-db recibida: Se mantiene la base de datos existente intacta." + DO_RESTORE_FRESH=false + elif [[ "$ARG_DB_ACTION" == "fresh" ]]; then + warn "Bandera --fresh-db recibida: Se recreará la base de datos desde cero." + DO_RESTORE_FRESH=true + elif [ -t 0 ]; then + echo -e "\n${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + echo -e "${BOLD} ¿Qué deseas hacer con la base de datos PostgreSQL existente?${RESET}" + echo -e " [1] ${GREEN}${BOLD}Mantener la base de datos actual${RESET} (Recomendado: preserva registros y sincroniza esquema)" + echo -e " [2] ${RED}${BOLD}Restaurar desde cero${RESET} (DROP DATABASE y recargar snapshot ${SQL_BACKUP_FILE##*/})" + echo -e "${BOLD}${YELLOW}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" + + DB_CHOICE="" + read -t 30 -p " Ingresá tu opción [1/2] (por defecto 1 en 30s): " DB_CHOICE || DB_CHOICE="1" + DB_CHOICE="${DB_CHOICE:-1}" + + if [[ "$DB_CHOICE" == "2" ]]; then + warn "Opción [2] seleccionada: Restauración destructiva desde cero." + DO_RESTORE_FRESH=true + else + ok "Opción [1] seleccionada: Mantener base de datos existente." + DO_RESTORE_FRESH=false + fi + else + info "Ejecución no interactiva: se mantiene la base de datos existente por defecto." + DO_RESTORE_FRESH=false + fi +else + info "La base de datos '${DB_NAME}' no existe o está vacía. Se inicializará limpia." + DO_RESTORE_FRESH=true +fi + +# Otorgar privilegios base +pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" >/dev/null 2>&1 || true + +# ─── 8. Crear Entorno Virtual e Instalar Dependencias Backend ─────────────────── +hdr "8. Configurando entorno virtual Python e instalando dependencias backend" +if [[ -d "$VENV_DIR" ]]; then + info "Recreando entorno virtual para asegurar dependencias limpias..." + rm -rf "$VENV_DIR" +fi + +python3 -m venv "$VENV_DIR" > /dev/null 2>&1 +"${VENV_DIR}/bin/pip" install --upgrade pip setuptools wheel --quiet + +info "Instalando dependencias desde backend/requirements.txt..." +if "${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --quiet 2>/dev/null; then + ok "Dependencias de Python instaladas correctamente" +else + warn "Reintentando instalación con --no-build-isolation..." + "${VENV_DIR}/bin/pip" install -r "${BACKEND_DIR}/requirements.txt" --no-build-isolation --quiet + ok "Dependencias de Python instaladas" +fi + +# ─── 9. Ejecución de Despliegue de Datos (Restaurar o Sincronizar) ────────────── +hdr "9. Aplicando esquema y datos en la base de datos" + +cd "$BACKEND_DIR" + +if [ "$DO_RESTORE_FRESH" = true ]; then + if [[ -f "$SQL_BACKUP_FILE" ]]; then + BACKUP_SIZE=$(du -h "$SQL_BACKUP_FILE" | awk '{print $1}') + info "Restaurando snapshot '${SQL_BACKUP_FILE##*/}' (${BACKUP_SIZE})..." + pg_cmd -c "DROP DATABASE IF EXISTS \"${DB_NAME}\";" > /dev/null 2>&1 || true + pg_cmd -c "CREATE DATABASE \"${DB_NAME}\" OWNER postgres;" > /dev/null 2>&1 + + sed '/^\\restrict/d' "$SQL_BACKUP_FILE" | pg_cmd -d "${DB_NAME}" > /dev/null 2>&1 || true + ok "Snapshot SQL restaurado exitosamente" + else + info "No se encontró classrooms_db.sql. Creando esquema limpio con init_db.py..." + fi +else + ok "Conservando tablas existentes en '${DB_NAME}'" +fi + +# Otorgar permisos completos al usuario de la aplicación +for u in "${DB_USER}" "${DB_USERNAME:-}" postgres eduspace_user; do + if [[ -n "$u" ]]; then + pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON DATABASE \"${DB_NAME}\" TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "GRANT ALL ON SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO \"${u}\";" > /dev/null 2>&1 || true + pg_cmd -d "${DB_NAME}" -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO \"${u}\";" > /dev/null 2>&1 || true + fi +done + +# Sincronización idempotente con init_db.py (crea tablas o columnas faltantes sin borrar datos) +info "Sincronizando modelos y esquemas institucionales con init_db.py..." +LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \ +FLASK_APP=wsgi.py "${VENV_DIR}/bin/python" -X utf8 init_db.py +ok "Esquema, usuarios y roles RBAC sincronizados con éxito" + +cd "$ROOT_DIR" + +# ─── 10. Instalar Dependencias del Frontend Node.js ──────────────────────────── +hdr "10. Instalando dependencias de Node.js en frontend/" + +cd "$FRONTEND_DIR" +info "Ejecutando npm install en frontend..." +if [ -f "package-lock.json" ]; then + npm ci --omit=dev --silent 2>/dev/null || npm install --omit=dev --silent +else + npm install --omit=dev --silent +fi +ok "Dependencias de Node.js instaladas en frontend/" +cd "$ROOT_DIR" + +# ─── 11. Usuario del Sistema y Permisos de Directorio ─────────────────────────── +hdr "11. Configurando usuario del sistema y permisos de seguridad" +if id "$APP_USER" &>/dev/null; then + info "El usuario de sistema '${APP_USER}' ya existe." +else + useradd --system --no-create-home --shell /usr/sbin/nologin "$APP_USER" + ok "Usuario de sistema '${APP_USER}' creado (sin shell interactiva)" +fi + +mkdir -p "${BACKEND_DIR}/app/static/uploads" +mkdir -p "$LOG_DIR" + +chown -R "${APP_USER}:${APP_USER}" "$ROOT_DIR" +chown -R "${APP_USER}:${APP_USER}" "$LOG_DIR" +chmod -R 755 "$ROOT_DIR" +chmod -R 775 "${BACKEND_DIR}/app/static/uploads" +chmod 640 "$BACKEND_ENV" "$FRONTEND_ENV" +chmod 755 "$LOG_DIR" +ok "Permisos aplicados en ${ROOT_DIR} y ${LOG_DIR}" + +# ─── 12. Servicios Systemd (Backend Flask y Frontend Node.js) ────────────────── +hdr "12. Configurando servicios systemd de arranque automático" + +GUNICORN_BIN="${VENV_DIR}/bin/gunicorn" +NODE_BIN="$(command -v node)" + +# 12.1 Servicio Backend +cat > "/etc/systemd/system/${SERVICE_BACKEND}.service" < "/etc/systemd/system/${SERVICE_FRONTEND}.service" < "/etc/systemd/system/${SERVICE_META}.service" < /dev/null 2>&1 + +info "Iniciando servicio backend..." +systemctl restart "${SERVICE_BACKEND}" +sleep 3 + +info "Iniciando servicio frontend..." +systemctl restart "${SERVICE_FRONTEND}" +systemctl restart "${SERVICE_META}" +sleep 3 + +# Verificar salud de los servicios +BACKEND_OK=false +if systemctl is-active --quiet "$SERVICE_BACKEND"; then + HTTP_BACK=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:5000/api/v1/openapi.json || echo "000") + if [[ "$HTTP_BACK" == "200" ]]; then + ok "Backend Flask respondiendo activamente en puerto 5000 (OpenAPI HTTP 200)" + BACKEND_OK=true + else + warn "Backend activo pero devolvió HTTP ${HTTP_BACK} al consultar OpenAPI." + fi +else + warn "El servicio backend tardó en iniciar. Logs: journalctl -u ${SERVICE_BACKEND} -n 20 --no-pager" +fi + +FRONTEND_OK=false +if systemctl is-active --quiet "$SERVICE_FRONTEND"; then + HTTP_FRONT=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/ || echo "000") + if [[ "$HTTP_FRONT" == "200" || "$HTTP_FRONT" == "302" ]]; then + ok "Frontend Node.js respondiendo activamente en puerto 3000 (HTTP ${HTTP_FRONT})" + FRONTEND_OK=true + else + warn "Frontend activo pero devolvió HTTP ${HTTP_FRONT} al consultar raíz." + fi +else + warn "El servicio frontend tardó en iniciar. Logs: journalctl -u ${SERVICE_FRONTEND} -n 20 --no-pager" +fi + +# ─── Resumen Final y Datos de Acceso ────────────────────────────────────────── +hdr "¡Despliegue Completado con Éxito!" + +IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "127.0.0.1") + +echo -e " ${BOLD}Acceso Web a la Plataforma (Frontend):${RESET}" +echo -e " ${GREEN}${BOLD}http://${IP}:3000${RESET} o ${GREEN}${BOLD}http://localhost:3000${RESET}" +echo "" +echo -e " ${BOLD}Acceso a la API REST (Backend Flask):${RESET}" +echo -e " ${CYAN}http://${IP}:5000/api/v1${RESET} (OpenAPI spec en /api/v1/openapi.json)" +echo "" +echo -e " ${BOLD}Credenciales de Administrador Iniciales:${RESET}" +echo -e " Email: ${CYAN}admin@edu-space.com${RESET}" +echo -e " Password: ${CYAN}admin123${RESET}" +echo "" +echo -e " ${BOLD}Base de Datos Desplegada:${RESET}" +echo -e " Motor: PostgreSQL (${PG_VER})" +echo -e " Base: ${DB_NAME}" +echo -e " Usuario: ${DB_USER}" +echo -e " Modo: $( [ "$DO_RESTORE_FRESH" = true ] && echo "Restaurada desde cero" || echo "Base existente mantenida y sincronizada" )" +echo "" +echo -e " ${BOLD}Comandos de Gestión del Servicio:${RESET}" +echo -e " Estado General: ${CYAN}systemctl status ${SERVICE_META}${RESET}" +echo -e " Reiniciar Todo: ${CYAN}systemctl restart ${SERVICE_META}${RESET}" +echo -e " Logs Backend: ${CYAN}journalctl -u ${SERVICE_BACKEND} -f${RESET}" +echo -e " Logs Frontend: ${CYAN}journalctl -u ${SERVICE_FRONTEND} -f${RESET}" +echo "" +ok "Despliegue finalizado. Todos los servicios han sido configurados." +echo ""