fixes deployment scripts

This commit is contained in:
RaineAllDay
2026-03-18 03:24:25 -06:00
parent eaaadd39e4
commit 516a917946
2 changed files with 458 additions and 18 deletions

440
scripts/resume-deploy.sh Normal file
View File

@@ -0,0 +1,440 @@
#!/usr/bin/env bash
# =============================================================================
# ETC PRS — Resume Deployment Script
# Picks up a failed deploy.sh run by detecting what's already been done.
# Usage: sudo bash scripts/resume-deploy.sh
# =============================================================================
set -euo pipefail
# ── Colours ───────────────────────────────────────────────────────────────────
RED='\033[0;31m'; YELLOW='\033[1;33m'; GREEN='\033[0;32m'
CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'
info() { echo -e "${CYAN}$*${RESET}"; }
success() { echo -e "${GREEN}$*${RESET}"; }
warn() { echo -e "${YELLOW}$*${RESET}"; }
error() { echo -e "${RED}$*${RESET}"; exit 1; }
skip() { echo -e "${GREEN}$* — already done, skipping${RESET}"; }
header() { echo -e "\n${BOLD}${CYAN}══ $* ══${RESET}\n"; }
divider() { echo -e "${CYAN}─────────────────────────────────────────────────${RESET}"; }
# ── Root check ────────────────────────────────────────────────────────────────
if [[ $EUID -ne 0 ]]; then
error "This script must be run as root (sudo bash scripts/resume-deploy.sh)"
fi
# ── Constants (must match deploy.sh) ─────────────────────────────────────────
APP_USER="prs"
APP_DIR="/opt/etc-prs/app"
DATA_DIR="/var/lib/etc-prs"
LOG_DIR="/var/log/etc-prs"
BACKUP_DIR="/var/backups/etc-prs"
BACKUP_SCRIPT="/opt/etc-prs/backup.sh"
NGINX_CONF="/etc/nginx/sites-available/etc-prs"
ECOSYSTEM="${APP_DIR}/ecosystem.config.cjs"
# ── Banner ────────────────────────────────────────────────────────────────────
clear
echo -e "${BOLD}${CYAN}"
echo " ███████╗████████╗ ██████╗ ██████╗ ██████╗ ███████╗"
echo " ██╔════╝╚══██╔══╝██╔════╝ ██╔══██╗██╔══██╗██╔════╝"
echo " █████╗ ██║ ██║ ██████╔╝██████╔╝███████╗"
echo " ██╔══╝ ██║ ██║ ██╔═══╝ ██╔══██╗╚════██║"
echo " ███████╗ ██║ ╚██████╗ ██║ ██║ ██║███████║"
echo " ╚══════╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝╚══════╝"
echo -e "${RESET}"
echo -e " ${BOLD}Personality Editor — Resume Deployment${RESET}"
divider
echo ""
# ── State detection ───────────────────────────────────────────────────────────
header "Detecting current state"
HAS_NODE=false; command -v node &>/dev/null && HAS_NODE=true
HAS_PM2=false; command -v pm2 &>/dev/null && HAS_PM2=true
HAS_NGINX=false; command -v nginx &>/dev/null && HAS_NGINX=true
HAS_CERTBOT=false; command -v certbot &>/dev/null && HAS_CERTBOT=true
HAS_USER=false; id "$APP_USER" &>/dev/null && HAS_USER=true
HAS_REPO=false; [[ -d "${APP_DIR}/.git" ]] && HAS_REPO=true
HAS_ENV=false; [[ -f "${APP_DIR}/.env" ]] && HAS_ENV=true
HAS_BUILD=false; [[ -f "${APP_DIR}/build/index.js" ]] && HAS_BUILD=true
HAS_ECOSYSTEM=false; [[ -f "$ECOSYSTEM" ]] && HAS_ECOSYSTEM=true
PM2_RUNNING=false; pm2 list 2>/dev/null | grep -q "etc-prs" && PM2_RUNNING=true
NGINX_ENABLED=false; [[ -L "/etc/nginx/sites-enabled/etc-prs" ]] && NGINX_ENABLED=true
HAS_SSL=false; [[ -d "/etc/letsencrypt/live" ]] && ls /etc/letsencrypt/live/*/fullchain.pem &>/dev/null && HAS_SSL=true
HAS_CRON=false; crontab -l 2>/dev/null | grep -q "$BACKUP_SCRIPT" && HAS_CRON=true
HAS_ADMIN=false; [[ -f "${DATA_DIR}/personalities.db" ]] && \
sqlite3 "${DATA_DIR}/personalities.db" "SELECT COUNT(*) FROM admins;" &>/dev/null && \
[[ $(sqlite3 "${DATA_DIR}/personalities.db" "SELECT COUNT(*) FROM admins;") -gt 0 ]] && \
HAS_ADMIN=true
echo -e " Node.js: $( $HAS_NODE && echo -e "${GREEN}$(node --version)${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " PM2: $( $HAS_PM2 && echo -e "${GREEN}$(pm2 --version)${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " Nginx: $( $HAS_NGINX && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " Certbot: $( $HAS_CERTBOT && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " prs user: $( $HAS_USER && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " Git repo: $( $HAS_REPO && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " .env: $( $HAS_ENV && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " Build: $( $HAS_BUILD && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " PM2 running: $( $PM2_RUNNING && echo -e "${GREEN}${RESET}" || echo -e "${YELLOW}✗ not running${RESET}" )"
echo -e " Nginx site: $( $NGINX_ENABLED && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ not enabled${RESET}" )"
echo -e " SSL cert: $( $HAS_SSL && echo -e "${GREEN}${RESET}" || echo -e "${YELLOW}✗ not issued${RESET}" )"
echo -e " Backup cron: $( $HAS_CRON && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ missing${RESET}" )"
echo -e " Admin user: $( $HAS_ADMIN && echo -e "${GREEN}${RESET}" || echo -e "${RED}✗ none found${RESET}" )"
echo ""
divider
echo ""
# ── Prompt for any info we need ───────────────────────────────────────────────
header "Configuration"
# Domain — try to read from .env first
DOMAIN=""
if $HAS_ENV; then
DOMAIN=$(grep "^PUBLIC_BASE_URL=" "${APP_DIR}/.env" | sed 's|PUBLIC_BASE_URL=https\?://||')
fi
if [[ -z "$DOMAIN" ]]; then
while true; do
read -rp "$(echo -e "${BOLD}Domain name${RESET} (e.g. etcprs.app): ")" DOMAIN
DOMAIN="${DOMAIN#www.}"; DOMAIN="${DOMAIN%/}"
[[ -n "$DOMAIN" ]] && break
warn "Domain cannot be empty."
done
else
echo -e " Domain: ${CYAN}${DOMAIN}${RESET} (read from .env)"
fi
# Repo — only needed if not already cloned
REPO_URL=""
GITEA_PASS=""
if ! $HAS_REPO; then
echo ""
read -rp "$(echo -e "${BOLD}Gitea repo path${RESET} [default: raine/etc-prs-ui]: ")" REPO_PATH
REPO_PATH="${REPO_PATH:-raine/etc-prs-ui}"
REPO_URL="https://git.etcprs.app/${REPO_PATH}.git"
echo ""
echo -e "${YELLOW}If your Gitea repo is private, enter credentials. Leave blank if public.${RESET}"
read -rp "$(echo -e "${BOLD}Gitea username${RESET} (blank = public): ")" GITEA_USER
if [[ -n "${GITEA_USER:-}" ]]; then
read -rsp "$(echo -e "${BOLD}Gitea password / token${RESET}: ")" GITEA_PASS
echo ""
REPO_URL="https://${GITEA_USER}:${GITEA_PASS}@git.etcprs.app/${REPO_PATH}.git"
fi
fi
# Rate limits — only needed if .env is missing
RATE_PUBLISH="5"; RATE_READ="100"
if ! $HAS_ENV; then
echo ""
echo -e "${BOLD}Rate limits${RESET} (press Enter to accept defaults):"
read -rp " Publish rate limit per IP/hour [default: 5]: " RATE_PUBLISH
RATE_PUBLISH="${RATE_PUBLISH:-5}"
read -rp " Read rate limit per IP/hour [default: 100]: " RATE_READ
RATE_READ="${RATE_READ:-100}"
fi
# SSL email — only needed if no cert
SSL_EMAIL=""
if ! $HAS_SSL; then
echo ""
read -rp "$(echo -e "${BOLD}Email for SSL certificate${RESET}: ")" SSL_EMAIL
[[ -n "$SSL_EMAIL" ]] || error "SSL email is required."
fi
# Admin creds — only needed if no admin exists
ADMIN_USER=""; ADMIN_PASS=""
if ! $HAS_ADMIN; then
echo ""
echo -e "${BOLD}Create the first admin account:${RESET}"
while true; do
read -rp " Admin username (232 chars): " ADMIN_USER
[[ ${#ADMIN_USER} -ge 2 && ${#ADMIN_USER} -le 32 ]] && break
warn "Username must be 232 characters."
done
while true; do
read -rsp " Admin password (min 8 chars): " ADMIN_PASS
echo ""
[[ ${#ADMIN_PASS} -ge 8 ]] && break
warn "Password must be at least 8 characters."
done
read -rsp " Confirm password: " ADMIN_PASS_CONFIRM
echo ""
[[ "$ADMIN_PASS" == "$ADMIN_PASS_CONFIRM" ]] || error "Passwords do not match."
fi
echo ""
read -rp "$(echo -e "${BOLD}Resume deployment now? [y/N]: ${RESET}")" CONFIRM
[[ "${CONFIRM,,}" == "y" ]] || { echo "Aborted."; exit 0; }
# ── Step 1: System packages ───────────────────────────────────────────────────
header "Step 1 / 9 — System Packages"
if $HAS_NODE && $HAS_PM2 && $HAS_NGINX && $HAS_CERTBOT; then
skip "All packages already installed"
else
info "Installing missing packages…"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq \
build-essential python3 git nginx certbot python3-certbot-nginx sqlite3 ufw curl
if ! $HAS_NODE; then
info "Installing Node.js 20 LTS…"
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nodejs
fi
if ! $HAS_PM2; then
info "Installing PM2…"
npm install -g pm2 --quiet
fi
success "Packages ready — Node $(node --version) / PM2 $(pm2 --version)"
fi
# ── Step 2: Users & directories ───────────────────────────────────────────────
header "Step 2 / 9 — Users & Directories"
if ! $HAS_USER; then
info "Creating system user '${APP_USER}'…"
useradd --system --shell /bin/false --home /opt/etc-prs "$APP_USER"
success "User '${APP_USER}' created"
else
skip "User '${APP_USER}'"
fi
for DIR in /opt/etc-prs "$APP_DIR" "$DATA_DIR" "$LOG_DIR" "$BACKUP_DIR"; do
mkdir -p "$DIR"
done
chown -R "${APP_USER}:${APP_USER}" /opt/etc-prs "$DATA_DIR" "$LOG_DIR" "$BACKUP_DIR"
success "Directories OK"
# ── Step 3: Clone / pull repo ─────────────────────────────────────────────────
header "Step 3 / 9 — Repository"
if $HAS_REPO; then
info "Repo already cloned — pulling latest…"
git -C "$APP_DIR" pull
success "Repo up to date"
else
info "Cloning from ${REPO_URL//:*@/:***@}"
git clone "$REPO_URL" "$APP_DIR" 2>&1 | \
sed 's/'"${GITEA_PASS:-NOPASS}"'/***REDACTED***/g' || \
error "Git clone failed. Check your repo URL and credentials."
success "Repository cloned to ${APP_DIR}"
fi
chown -R "${APP_USER}:${APP_USER}" "$APP_DIR"
# ── Step 4: Environment file ──────────────────────────────────────────────────
header "Step 4 / 9 — Environment Configuration"
if $HAS_ENV; then
skip ".env (${APP_DIR}/.env)"
else
cat > "${APP_DIR}/.env" <<EOF
DATABASE_URL=${DATA_DIR}/personalities.db
RATE_LIMIT_PUBLISH=${RATE_PUBLISH}
RATE_LIMIT_READ=${RATE_READ}
PUBLIC_BASE_URL=https://${DOMAIN}
EOF
chown "${APP_USER}:${APP_USER}" "${APP_DIR}/.env"
chmod 600 "${APP_DIR}/.env"
success ".env written"
fi
# ── Step 5: Install deps & build ──────────────────────────────────────────────
header "Step 5 / 9 — Install & Build"
if $HAS_BUILD; then
skip "Build already exists (${APP_DIR}/build/index.js)"
else
info "Installing npm dependencies…"
npm --prefix "$APP_DIR" install --quiet
info "Building SvelteKit app…"
npm --prefix "$APP_DIR" run build
chown -R "${APP_USER}:${APP_USER}" "$APP_DIR"
success "Build complete"
fi
# ── Step 6: PM2 ───────────────────────────────────────────────────────────────
header "Step 6 / 9 — PM2 Process Manager"
if $PM2_RUNNING; then
skip "PM2 process 'etc-prs' is already running"
else
# Write ecosystem if missing
if ! $HAS_ECOSYSTEM; then
cat > "$ECOSYSTEM" <<EOF
module.exports = {
apps: [{
name: 'etc-prs',
script: '${APP_DIR}/build/index.js',
cwd: '${APP_DIR}',
env: {
NODE_ENV: 'production',
PORT: '3000',
HOST: '127.0.0.1',
},
error_file: '${LOG_DIR}/error.log',
out_file: '${LOG_DIR}/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
restart_delay: 3000,
max_restarts: 10,
}]
};
EOF
chown "${APP_USER}:${APP_USER}" "$ECOSYSTEM"
fi
info "Starting app with PM2…"
pm2 start "$ECOSYSTEM"
pm2 save
pm2 startup systemd -u root --hp /root | grep -E "^sudo" | bash || true
success "PM2 running on 127.0.0.1:3000"
fi
# ── Step 7: Nginx ─────────────────────────────────────────────────────────────
header "Step 7 / 9 — Nginx"
if $NGINX_ENABLED; then
skip "Nginx site already enabled"
else
info "Writing Nginx config for ${DOMAIN}"
cat > "$NGINX_CONF" <<EOF
server {
listen 80;
server_name ${DOMAIN} www.${DOMAIN};
gzip on;
gzip_vary on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml application/xml+rss text/javascript;
add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";
add_header Referrer-Policy "strict-origin-when-cross-origin";
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_cache_bypass \$http_upgrade;
proxy_read_timeout 30s;
client_max_body_size 2M;
}
}
EOF
rm -f /etc/nginx/sites-enabled/default
ln -sf "$NGINX_CONF" /etc/nginx/sites-enabled/etc-prs
nginx -t || error "Nginx config test failed — check ${NGINX_CONF}"
systemctl reload nginx
success "Nginx configured for ${DOMAIN}"
fi
# ── Step 8: SSL ───────────────────────────────────────────────────────────────
header "Step 8 / 9 — SSL Certificate"
if $HAS_SSL; then
skip "SSL certificate already exists"
else
info "Requesting certificate for ${DOMAIN} and www.${DOMAIN}"
if certbot --nginx \
--non-interactive \
--agree-tos \
--email "$SSL_EMAIL" \
--domains "${DOMAIN},www.${DOMAIN}" \
--redirect; then
success "SSL certificate issued"
certbot renew --dry-run --quiet && success "Auto-renewal check passed"
else
warn "Certbot failed — HTTP is still working."
warn "Once DNS is pointing here, run:"
warn " sudo certbot --nginx -d ${DOMAIN} -d www.${DOMAIN} --email ${SSL_EMAIL} --agree-tos"
fi
fi
# ── Step 9: Backups, firewall & admin ─────────────────────────────────────────
header "Step 9 / 9 — Backups, Firewall & Admin Account"
# Backup script
if [[ ! -f "$BACKUP_SCRIPT" ]]; then
cat > "$BACKUP_SCRIPT" <<'EOF'
#!/usr/bin/env bash
DATE=$(date +%Y-%m-%d_%H%M)
BACKUP_DIR=/var/backups/etc-prs
DB_PATH=/var/lib/etc-prs/personalities.db
mkdir -p "$BACKUP_DIR"
sqlite3 "$DB_PATH" ".backup ${BACKUP_DIR}/personalities-${DATE}.db"
gzip "${BACKUP_DIR}/personalities-${DATE}.db"
find "$BACKUP_DIR" -name "personalities-*.db.gz" -mtime +30 -delete
echo "$(date '+%Y-%m-%d %H:%M:%S') Backup OK: personalities-${DATE}.db.gz"
EOF
chmod +x "$BACKUP_SCRIPT"
success "Backup script written"
else
skip "Backup script"
fi
if $HAS_CRON; then
skip "Backup cron job"
else
CRON_LINE="0 2 * * * ${BACKUP_SCRIPT} >> ${LOG_DIR}/backup.log 2>&1"
( crontab -l 2>/dev/null | grep -v "$BACKUP_SCRIPT"; echo "$CRON_LINE" ) | crontab -
success "Backup cron job scheduled (daily at 2am)"
fi
# Firewall
info "Ensuring firewall rules are set…"
ufw allow OpenSSH > /dev/null
ufw allow 'Nginx Full' > /dev/null
ufw --force enable > /dev/null
success "Firewall OK"
# Admin account
if $HAS_ADMIN; then
skip "Admin account already exists in DB"
else
info "Creating admin account '${ADMIN_USER}'…"
cd "$APP_DIR"
node scripts/create-admin.js "$ADMIN_USER" "$ADMIN_PASS" || \
warn "Admin creation failed — run manually: cd ${APP_DIR} && node scripts/create-admin.js <user> <pass>"
success "Admin account '${ADMIN_USER}' created"
fi
# ── Done ──────────────────────────────────────────────────────────────────────
echo ""
divider
echo -e "${BOLD}${GREEN} Deployment complete!${RESET}"
divider
echo ""
echo -e " ${BOLD}Site:${RESET} https://${DOMAIN}"
echo -e " ${BOLD}Admin panel:${RESET} https://${DOMAIN}/admin"
echo -e " ${BOLD}App logs:${RESET} pm2 logs etc-prs"
echo -e " ${BOLD}DB path:${RESET} ${DATA_DIR}/personalities.db"
echo -e " ${BOLD}Backups:${RESET} ${BACKUP_DIR}"
echo ""
echo -e " ${BOLD}Redeploy after a push:${RESET}"
echo -e " ${CYAN} cd ${APP_DIR} && git pull && \\"
echo -e " npm install && npm run build && \\"
echo -e " chown -R ${APP_USER}:${APP_USER} ${APP_DIR} && \\"
echo -e " pm2 reload etc-prs${RESET}"
echo ""
divider
echo ""
# Verify
info "Checking app is responding on port 3000…"
sleep 2
if curl -sf http://127.0.0.1:3000 > /dev/null; then
success "App is up and responding ✓"
else
warn "App did not respond on port 3000. Check logs: pm2 logs etc-prs --lines 30"
fi
echo ""