f8e42d4377
scripts/install.sh automatiza el primer despliegue en un servidor Debian/Ubuntu nuevo: verifica requisitos (Node.js, npm, cliente mysql, systemd, espacio, puertos libres), pregunta dominio, puertos y base de datos (MariaDB local auto-instalada o un servidor existente), genera backend/.env con secretos aleatorios, corre migraciones + catalogos, compila el frontend y crea/arranca los servicios systemd. No crea usuarios: el primer registro publico se vuelve superadmin solo (bootstrap ya existente). Probando el instalador de punta a punta contra una base de datos recien creada goo un bug real: la migracion 006_seed_it_baseline.sql (datos de ejemplo especificos de LinkedCorp) fallaba por foreign key al no existir todavia catalogos ni usuario. Corregido para que detecte esa situacion y quede inerte en instalaciones nuevas, sin tocar el comportamiento ya probado en dev/prod. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
390 lines
12 KiB
Bash
Executable File
390 lines
12 KiB
Bash
Executable File
#!/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 <<SQL
|
|
CREATE DATABASE IF NOT EXISTS \`${DB_NAME}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
|
CREATE USER IF NOT EXISTS '${DB_APP_USER}'@'${GRANT_HOST}' IDENTIFIED BY '${DB_APP_PASSWORD}';
|
|
GRANT ALL PRIVILEGES ON \`${DB_NAME}\`.* TO '${DB_APP_USER}'@'${GRANT_HOST}';
|
|
FLUSH PRIVILEGES;
|
|
SQL
|
|
then
|
|
echo "No se pudo conectar/crear la base de datos con esas credenciales admin."
|
|
exit 1
|
|
fi
|
|
ok "base '${DB_NAME}' y usuario '${DB_APP_USER}'@'${GRANT_HOST}' listos"
|
|
echo
|
|
}
|
|
|
|
# ---------- 4. estructura + .env ----------
|
|
setup_tree() {
|
|
echo "== Directorios =="
|
|
mkdir -p "$APP_ROOT"/{backend,frontend,storage,logs}
|
|
chown -R "$RUN_USER":"$RUN_USER" "$APP_ROOT"
|
|
ok "$APP_ROOT/{backend,frontend,storage,logs}"
|
|
echo
|
|
}
|
|
|
|
write_env() {
|
|
echo "== backend/.env =="
|
|
local jwt_secret enc_key
|
|
jwt_secret="$(openssl rand -hex 32)"
|
|
enc_key="$(openssl rand -hex 32)"
|
|
|
|
cat > "$APP_ROOT/backend/.env" <<EOF
|
|
PORT=${BACKEND_PORT}
|
|
NODE_ENV=production
|
|
DB_HOST=${DB_HOST}
|
|
DB_PORT=${DB_PORT}
|
|
DB_NAME=${DB_NAME}
|
|
DB_USER=${DB_APP_USER}
|
|
DB_PASSWORD=${DB_APP_PASSWORD}
|
|
JWT_SECRET=${jwt_secret}
|
|
JWT_EXPIRES=8h
|
|
ENCRYPTION_KEY=${enc_key}
|
|
CORS_ORIGIN=${DOMAIN}
|
|
STORAGE_ROOT=${APP_ROOT}/storage
|
|
EOF
|
|
chown "$RUN_USER":"$RUN_USER" "$APP_ROOT/backend/.env"
|
|
chmod 640 "$APP_ROOT/backend/.env"
|
|
ok "generado (secretos aleatorios, permisos 640)"
|
|
echo
|
|
}
|
|
|
|
# ---------- 5. dependencias, migraciones, build ----------
|
|
install_backend() {
|
|
echo "== Backend =="
|
|
# El servicio corre DESDE source/backend (ver write_services) — es donde tienen que vivir
|
|
# los node_modules, no en el runtime de $APP_ROOT (que solo guarda el .env persistente).
|
|
(cd "$SOURCE_DIR/backend" && npm install --omit=dev)
|
|
ok "dependencias instaladas"
|
|
|
|
echo "Aplicando migraciones y catalogos base..."
|
|
(
|
|
cd "$SOURCE_DIR/backend"
|
|
set -a; source "$APP_ROOT/backend/.env"; set +a
|
|
node src/scripts/migrate-db.js
|
|
node src/scripts/seed-db.js
|
|
)
|
|
ok "migraciones y catalogos aplicados"
|
|
echo
|
|
}
|
|
|
|
build_frontend() {
|
|
echo "== Frontend =="
|
|
(cd "$SOURCE_DIR/frontend" && npm install && npm run build)
|
|
rm -rf "$APP_ROOT/frontend/dist"
|
|
cp -r "$SOURCE_DIR/frontend/dist" "$APP_ROOT/frontend/dist"
|
|
|
|
cat > "$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" <<EOF
|
|
[Unit]
|
|
Description=${APP_NAME} backend
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${RUN_USER}
|
|
WorkingDirectory=${SOURCE_DIR}/backend
|
|
ExecStart=/usr/bin/node src/server.js
|
|
EnvironmentFile=${APP_ROOT}/backend/.env
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
StandardOutput=append:${APP_ROOT}/logs/backend.log
|
|
StandardError=append:${APP_ROOT}/logs/backend-error.log
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
cat > "/etc/systemd/system/${SVC_FRONTEND}.service" <<EOF
|
|
[Unit]
|
|
Description=${APP_NAME} frontend
|
|
After=network.target
|
|
|
|
[Service]
|
|
Type=simple
|
|
User=${RUN_USER}
|
|
WorkingDirectory=${APP_ROOT}/frontend
|
|
ExecStart=/usr/bin/node node_modules/.bin/serve -s dist -l ${FRONTEND_PORT}
|
|
Restart=on-failure
|
|
RestartSec=5
|
|
StandardOutput=append:${APP_ROOT}/logs/frontend.log
|
|
StandardError=append:${APP_ROOT}/logs/frontend-error.log
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
EOF
|
|
|
|
systemctl daemon-reload
|
|
systemctl enable --now "$SVC_BACKEND" "$SVC_FRONTEND"
|
|
ok "servicios creados y arrancados"
|
|
echo
|
|
}
|
|
|
|
verify() {
|
|
echo "== Verificacion =="
|
|
sleep 2
|
|
if curl -sf "http://127.0.0.1:${BACKEND_PORT}/health" >/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 "$@"
|