#!/usr/bin/env bash # Instalador de project.linkedcorp.net para un servidor NUEVO (Debian/Ubuntu, arquetipo B: # backend + frontend como dos servicios systemd separados). # # Uso: cloná el repo en /opt/apps/project.linkedcorp.net/source (ver DEPLOY_WEB.md, paso # "Clonar el repositorio") y desde ahi corré: # # sudo scripts/install.sh # # Que hace: # 1. Verifica requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres). # 2. Pregunta dominio, puertos, y datos de base de datos: MariaDB local (se instala sola si # hace falta) o un servidor MySQL/MariaDB existente. # 3. Crea la base de datos + usuario de la app (clave aleatoria), genera backend/.env con # todos los secretos generados al azar (JWT_SECRET, ENCRYPTION_KEY). # 4. Instala dependencias, compila el frontend, corre migraciones + catalogos base. # 5. Crea y arranca los dos servicios systemd, verifica que respondan. # # Que NO hace (queda manual, es especifico de cada servidor/proveedor): # - Reverse proxy + certificado TLS hacia el dominio publico (ver DEPLOY_WEB.md, paso # "Configurar Nginx Proxy Manager"). # - Crear un usuario de la app: el primer registro publico en /registro se vuelve # superadministrador automaticamente (bootstrap ya implementado en el backend) — no hace # falta ningun paso extra ni script aparte. set -euo pipefail APP_NAME="project.linkedcorp.net" SVC_BACKEND="project-linkedcorp-backend" SVC_FRONTEND="project-linkedcorp-frontend" MIN_NODE_MAJOR=20 MIN_FREE_MB=1024 SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" C_OK="\033[32m"; C_FAIL="\033[31m"; C_WARN="\033[33m"; C_RESET="\033[0m" FAILED=0 ok() { echo -e " ${C_OK}OK${C_RESET} $1"; } fail() { echo -e " ${C_FAIL}FALTA${C_RESET} $1"; FAILED=1; } warn() { echo -e " ${C_WARN}AVISO${C_RESET} $1"; } require_root() { if [[ $EUID -ne 0 ]]; then echo "Este instalador necesita privilegios de root (instala paquetes, crea servicios systemd)." echo "Ejecuta: sudo $0" exit 1 fi } # ---------- 1. verificacion de requisitos ---------- check_requirements() { echo "== Verificando requisitos ==" if command -v node >/dev/null 2>&1; then local major major="$(node -v | sed 's/^v//' | cut -d. -f1)" if [[ "$major" -ge "$MIN_NODE_MAJOR" ]]; then ok "Node.js $(node -v)" else fail "Node.js $(node -v) — se requiere >= v${MIN_NODE_MAJOR}" fi else fail "Node.js no instalado (requiere >= v${MIN_NODE_MAJOR})" fi command -v npm >/dev/null 2>&1 && ok "npm $(npm -v)" || fail "npm no instalado" command -v mysql >/dev/null 2>&1 && ok "cliente mysql" || fail "cliente mysql no instalado (paquete mariadb-client)" command -v systemctl >/dev/null 2>&1 && ok "systemd" || fail "systemd no disponible (este instalador lo necesita)" command -v openssl >/dev/null 2>&1 && ok "openssl" || fail "openssl no instalado (necesario para generar secretos)" command -v curl >/dev/null 2>&1 && ok "curl" || fail "curl no instalado (necesario para verificar al final)" if command -v apt-get >/dev/null 2>&1; then ok "apt-get disponible" else warn "apt-get no disponible — si eliges 'MariaDB local' vas a tener que instalarla vos antes de continuar" fi local avail_kb avail_kb="$(df -Pk /opt 2>/dev/null | tail -1 | awk '{print $4}')" if [[ -n "$avail_kb" && "$avail_kb" -ge $((MIN_FREE_MB * 1024)) ]]; then ok "espacio libre en /opt: $((avail_kb / 1024)) MB" else fail "menos de ${MIN_FREE_MB}MB libres en /opt" fi if [[ -f "$SOURCE_DIR/backend/package.json" && -f "$SOURCE_DIR/frontend/package.json" ]]; then ok "repo detectado en $SOURCE_DIR" else fail "no se encontro backend/frontend en $SOURCE_DIR — corre este script desde la raiz del repo ya clonado" fi echo if [[ "$FAILED" -ne 0 ]]; then echo "Faltan requisitos — resuelve lo anterior antes de continuar." exit 1 fi echo "Requisitos OK." echo } port_free() { local port="$1" ! ss -ltn 2>/dev/null | awk '{print $4}' | grep -qE "[.:]${port}\$" } check_ports() { echo "== Verificando puertos ==" local bad=0 for p in "$BACKEND_PORT" "$FRONTEND_PORT"; do if port_free "$p"; then ok "puerto $p libre" else echo -e " ${C_FAIL}OCUPADO${C_RESET} puerto $p" bad=1 fi done echo if [[ "$bad" -ne 0 ]]; then echo "Elige otro puerto o libera el que esta ocupado, y vuelve a correr el instalador." exit 1 fi } # ---------- 2. configuracion interactiva ---------- ask() { local prompt="$1" default="$2" var read -r -p "$prompt [$default]: " var echo "${var:-$default}" } ensure_run_user() { if id "$RUN_USER" >/dev/null 2>&1; then ok "usuario '$RUN_USER' ya existe" else echo "El usuario '$RUN_USER' no existe — se crea ahora (sistema, sin login)." useradd --system --create-home --shell /usr/sbin/nologin "$RUN_USER" ok "usuario '$RUN_USER' creado" fi } gather_config() { echo "== Configuracion ==" APP_ROOT="$(ask "Directorio de instalacion" "/opt/apps/${APP_NAME}")" DOMAIN="$(ask "Dominio publico (URL completa, para CORS)" "https://${APP_NAME}")" BACKEND_PORT="$(ask "Puerto del backend" "4019")" FRONTEND_PORT="$(ask "Puerto del frontend" "5196")" RUN_USER="$(ask "Usuario del sistema que corre los servicios" "${SUDO_USER:-deploy}")" echo ensure_run_user echo check_ports echo "Base de datos:" echo " 1) MariaDB local (se instala en este mismo servidor si hace falta)" echo " 2) Servidor MySQL/MariaDB existente (otra IP)" local db_choice db_choice="$(ask "Elige una opcion" "1")" DB_NAME="$(ask "Nombre de la base de datos" "project_linkedcorp")" DB_APP_USER="$(ask "Usuario de la app para la BD" "project_linkedcorp")" if [[ "$db_choice" == "2" ]]; then DB_LOCAL=0 DB_HOST="$(ask "Host de la base de datos" "")" DB_PORT="$(ask "Puerto" "3306")" DB_ADMIN_USER="$(ask "Usuario admin de esa BD (solo para crear la BD/usuario — no se guarda)" "root")" read -r -s -p "Clave del usuario admin: " DB_ADMIN_PASSWORD; echo GRANT_HOST="$(ask "IP de este servidor (para el GRANT — desde donde se conecta la app)" "$(hostname -I 2>/dev/null | awk '{print $1}')")" else DB_LOCAL=1 DB_HOST="127.0.0.1" DB_PORT="3306" GRANT_HOST="127.0.0.1" fi echo } # ---------- 3. base de datos ---------- setup_local_mariadb() { if command -v mysql >/dev/null 2>&1 && systemctl is-active --quiet mariadb 2>/dev/null; then ok "MariaDB ya esta instalada y corriendo" return fi echo "Instalando MariaDB local..." apt-get update -y apt-get install -y mariadb-server systemctl enable --now mariadb ok "MariaDB instalada y arrancada" } mysql_admin() { # Ejecuta el SQL que llega por stdin como admin. Local: root via socket (sin clave, tipico en # Debian/Ubuntu con auth_socket). Remoto: con host/usuario/clave provistos. if [[ "$DB_LOCAL" -eq 1 ]]; then mysql -u root else MYSQL_PWD="$DB_ADMIN_PASSWORD" mysql -h "$DB_HOST" -P "$DB_PORT" -u "$DB_ADMIN_USER" fi } create_database() { echo "== Base de datos ==" [[ "$DB_LOCAL" -eq 1 ]] && setup_local_mariadb DB_APP_PASSWORD="$(openssl rand -base64 18 | tr -d '/+=' | cut -c1-20)" if ! mysql_admin < "$APP_ROOT/backend/.env" < "$APP_ROOT/frontend/package.json" <<'EOF' { "name": "project-linkedcorp-frontend-runtime", "private": true, "version": "1.0.0", "dependencies": { "serve": "^14.2.6" } } EOF (cd "$APP_ROOT/frontend" && npm install --omit=dev) chown -R "$RUN_USER":"$RUN_USER" "$APP_ROOT/frontend" ok "compilado y publicado en $APP_ROOT/frontend" echo } # ---------- 6. systemd ---------- write_services() { echo "== Servicios systemd ==" cat > "/etc/systemd/system/${SVC_BACKEND}.service" < "/etc/systemd/system/${SVC_FRONTEND}.service" </dev/null; then ok "backend responde en :${BACKEND_PORT}" else warn "backend no responde todavia — revisa: journalctl -u ${SVC_BACKEND} -n 50" fi if curl -sfo /dev/null "http://127.0.0.1:${FRONTEND_PORT}/"; then ok "frontend responde en :${FRONTEND_PORT}" else warn "frontend no responde todavia — revisa: journalctl -u ${SVC_FRONTEND} -n 50" fi echo } summary() { echo "======================================================" echo " Instalacion lista" echo "======================================================" echo " Codigo: ${SOURCE_DIR}" echo " Runtime: ${APP_ROOT}" echo " Backend: http://127.0.0.1:${BACKEND_PORT} (systemctl status ${SVC_BACKEND})" echo " Frontend: http://127.0.0.1:${FRONTEND_PORT} (systemctl status ${SVC_FRONTEND})" echo echo " Falta (manual, fuera del alcance de este instalador):" echo " - Reverse proxy + TLS hacia ${DOMAIN} (ver DEPLOY_WEB.md, 'Nginx Proxy Manager'):" echo " / -> puerto ${FRONTEND_PORT}" echo " /api -> puerto ${BACKEND_PORT}" echo " - Entra a ${DOMAIN}/registro y crea el primer usuario: se vuelve" echo " superadministrador automaticamente, sin pasos extra." echo " - Configura el correo SMTP desde Configuracion una vez logueado (opcional)." echo " - Opcional: find ${SOURCE_DIR} -type f -iname '*.md' -delete (limpia la documentacion" echo " del checkout en el servidor de produccion)." echo "======================================================" } main() { require_root check_requirements gather_config create_database setup_tree write_env install_backend build_frontend write_services verify summary } main "$@"