#!/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 (2–32 chars): " ADMIN_USER [[ ${#ADMIN_USER} -ge 2 && ${#ADMIN_USER} -le 32 ]] && break warn "Username must be 2–32 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" chmod 777 "$DATA_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" < "$ECOSYSTEM" < "$NGINX_CONF" < "$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 " 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 ""