3m-portal/deploy.sh

242 lines
10 KiB
Bash

#!/bin/bash
# =============================================================
# Deploy script — 3M OMG Portal
#
# Architecture:
# Internet → Load Balancer (SSL termination) → 10.220.168.7:80
# Apache (port 80) → reverse proxy → Node.js (port 3000)
#
# Prerequisites (do these manually before running this script):
# Upload new code to the server (rsync or git pull), then:
#
# Usage:
# sudo bash /opt/3m-portal/deploy.sh # first-time install
# sudo bash /opt/3m-portal/deploy.sh --update # reload after code update
# =============================================================
set -euo pipefail
APP_DIR="/opt/3m-portal"
NODE_PORT=3000
DOMAIN="3m.automation.oliver.solutions"
APP_USER="nodeapp"
APACHE_CONF="/etc/apache2/sites-available/${DOMAIN}.conf"
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
info() { echo -e "${GREEN}[✓]${NC} $1"; }
warn() { echo -e "${YELLOW}[!]${NC} $1"; }
error() { echo -e "${RED}[✗]${NC} $1"; exit 1; }
section() { echo -e "\n${CYAN}══ $1 ══${NC}"; }
UPDATE_ONLY=false
[[ "${1:-}" == "--update" ]] && UPDATE_ONLY=true
# ── 0. Checks ─────────────────────────────────────────────────
[[ $EUID -ne 0 ]] && error "Run as root: sudo bash deploy.sh"
[[ ! -f "$APP_DIR/server.js" ]] && error "Code not found at $APP_DIR. Upload the code first."
[[ ! -d "$APP_DIR/lib" ]] && error "lib/ directory missing — make sure the full codebase is uploaded."
# ── 1. Node.js + build tools ──────────────────────────────────
if ! $UPDATE_ONLY; then
section "Node.js"
if ! command -v node &>/dev/null; then
info "Installing Node.js 20.x..."
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt-get install -y nodejs
fi
info "Node.js $(node -v), npm $(npm -v)"
section "Build tools (required for better-sqlite3 native module)"
apt-get install -y build-essential python3 make g++
info "Build tools installed"
fi
# ── 2. PM2 ────────────────────────────────────────────────────
if ! $UPDATE_ONLY; then
section "PM2"
if ! command -v pm2 &>/dev/null; then
info "Installing PM2..."
npm install -g pm2
fi
info "PM2 $(pm2 -v)"
fi
# ── 3. Dedicated app user ─────────────────────────────────────
if ! $UPDATE_ONLY; then
section "App user"
if ! id "$APP_USER" &>/dev/null; then
useradd --system --shell /bin/false --home "$APP_DIR" "$APP_USER"
info "Created user: $APP_USER"
else
info "User $APP_USER already exists"
fi
fi
# ── 3a. Data directory ────────────────────────────────────────
section "Data directory"
mkdir -p "$APP_DIR/data"
chown "$APP_USER":"$APP_USER" "$APP_DIR/data"
chmod 700 "$APP_DIR/data"
info "Data directory: $APP_DIR/data (owner: $APP_USER, mode: 700)"
# ── 4. .env ───────────────────────────────────────────────────
section ".env"
ENV_FILE="$APP_DIR/.env"
if [ ! -f "$ENV_FILE" ]; then
warn ".env not found — creating template. Fill in all CHANGE_ME values!"
cat > "$ENV_FILE" << EOF
SERVICE_USERNAME=portal@oliver.agency
SERVICE_PASSWORD=CHANGE_ME
PORT=${NODE_PORT}
APP_BASE_URL=https://${DOMAIN}
DATA_DIR=${APP_DIR}/data
COOKIE_NAME=portal_session
COOKIE_SECURE=true
SESSION_TTL_MS=28800000
INITIAL_ADMINS=[{"email":"CHANGE_ME","one2editUsername":"CHANGE_ME","password":"CHANGE_ME"}]
MAILGUN_API_KEY=CHANGE_ME
MAILGUN_DOMAIN=mg.oliver.solutions
MAILGUN_FROM=noreply@mg.oliver.solutions
EOF
warn "──────────────────────────────────────────────────"
warn "Edit all values now: nano $ENV_FILE"
warn "──────────────────────────────────────────────────"
read -rp "Press Enter after updating .env to continue..."
else
info ".env exists"
fi
if grep -q "CHANGE_ME" "$ENV_FILE"; then
error ".env still has CHANGE_ME values. Edit $ENV_FILE first."
fi
# ── 5. Permissions ────────────────────────────────────────────
section "Permissions"
# Own all app files as nodeapp, then lock down .env and data
chown -R "$APP_USER":"$APP_USER" "$APP_DIR"
chmod 600 "$ENV_FILE"
# Re-apply data dir restriction (chown -R sets ownership but not mode)
chmod 700 "$APP_DIR/data"
info "Owner: $APP_USER, .env: 600, data/: 700"
# ── 5a. npm install ───────────────────────────────────────────
section "npm install"
cd "$APP_DIR" && HOME="$APP_DIR" sudo -u "$APP_USER" npm install --omit=dev
info "Dependencies installed (including better-sqlite3 native build)"
# ── 6. PM2 process ────────────────────────────────────────────
section "PM2 process"
if $UPDATE_ONLY; then
# Reload picks up new code + fresh .env without dropping existing sessions
if pm2 describe 3m-portal &>/dev/null; then
pm2 reload 3m-portal --update-env
info "PM2 reloaded with updated env"
else
warn "PM2 process not found — starting fresh"
pm2 start "$APP_DIR/server.js" \
--name 3m-portal \
--user "$APP_USER" \
--log /var/log/3m-portal.log \
--restart-delay 3000 \
--max-restarts 10
fi
else
pm2 stop 3m-portal 2>/dev/null || true
pm2 delete 3m-portal 2>/dev/null || true
pm2 start "$APP_DIR/server.js" \
--name 3m-portal \
--user "$APP_USER" \
--log /var/log/3m-portal.log \
--restart-delay 3000 \
--max-restarts 10
env PATH="$PATH:/usr/bin" pm2 startup systemd -u "$APP_USER" --hp "$APP_DIR" \
| tail -1 | bash || warn "Run 'pm2 startup' manually if autostart is needed"
fi
pm2 save
info "PM2 state saved"
# ── 7. Apache modules ─────────────────────────────────────────
if ! $UPDATE_ONLY; then
section "Apache modules"
a2enmod proxy proxy_http headers rewrite 2>/dev/null || true
info "Modules enabled (proxy, proxy_http, headers, rewrite)"
fi
# ── 8. Apache virtual host ────────────────────────────────────
if ! $UPDATE_ONLY; then
section "Apache vhost"
if [ -f "$APACHE_CONF" ]; then
cp "$APACHE_CONF" "${APACHE_CONF}.bak.$(date +%Y%m%d%H%M%S)"
info "Backed up existing config"
fi
cat > "$APACHE_CONF" << EOF
# 3M OMG Portal — Apache reverse proxy to Node.js
#
# SSL is terminated at the load balancer.
# This server receives plain HTTP on port 80 from the LB
# and proxies it to Node.js on port ${NODE_PORT}.
<VirtualHost *:80>
ServerName ${DOMAIN}
ProxyPreserveHost On
ProxyPass / http://127.0.0.1:${NODE_PORT}/
ProxyPassReverse / http://127.0.0.1:${NODE_PORT}/
RequestHeader set X-Forwarded-Proto "https"
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
ErrorLog /var/log/apache2/${DOMAIN}-error.log
CustomLog /var/log/apache2/${DOMAIN}-access.log combined
</VirtualHost>
EOF
info "Apache config written: $APACHE_CONF"
# ── 9. Enable site & reload Apache ────────────────────────────
section "Apache reload"
a2ensite "${DOMAIN}.conf" 2>/dev/null || true
if apache2ctl configtest 2>&1; then
systemctl reload apache2
info "Apache reloaded"
else
error "Apache config test failed — check $APACHE_CONF"
fi
fi
# ── 10. Smoke test ────────────────────────────────────────────
section "Smoke test"
sleep 2
if curl -sf "http://127.0.0.1:${NODE_PORT}/" -o /dev/null; then
info "Node.js responding on port ${NODE_PORT}"
else
error "Node.js not responding. Check logs: pm2 logs 3m-portal"
fi
if ! $UPDATE_ONLY; then
if curl -sf "http://127.0.0.1/" -o /dev/null; then
info "Apache proxy responding on port 80"
else
warn "Apache not responding on port 80 — check: systemctl status apache2"
fi
fi
# ── Done ──────────────────────────────────────────────────────
echo ""
echo -e "${GREEN}╔══════════════════════════════════════════════════════╗${NC}"
echo -e "${GREEN}║ Deploy complete! ║${NC}"
echo -e "${GREEN}║ ║${NC}"
echo -e "${GREEN}║ Portal: https://${DOMAIN}${NC}"
echo -e "${GREEN}║ Admin: https://${DOMAIN}/admin.html ║${NC}"
echo -e "${GREEN}║ Logs: pm2 logs 3m-portal ║${NC}"
echo -e "${GREEN}║ DB: sqlite3 ${APP_DIR}/data/portal.db ║${NC}"
echo -e "${GREEN}║ Update: upload code → sudo bash deploy.sh --update ║${NC}"
echo -e "${GREEN}╚══════════════════════════════════════════════════════╝${NC}"