diff --git a/scripts/deploy.sh b/scripts/deploy.sh index b6a53c1..a94bfc3 100644 --- a/scripts/deploy.sh +++ b/scripts/deploy.sh @@ -173,13 +173,15 @@ info "Cloning from ${REPO_URL//:*@/:***@}…" if [[ -d "${APP_DIR}/.git" ]]; then warn "App directory already contains a git repo — pulling latest instead." - sudo -u "$APP_USER" git -C "$APP_DIR" pull + git -C "$APP_DIR" pull else - sudo -u "$APP_USER" git clone "$REPO_URL" "$APP_DIR" 2>&1 | \ + 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." fi +chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" + success "Repository cloned to ${APP_DIR}" # ── Step 4: Environment file ────────────────────────────────────────────────── @@ -201,10 +203,13 @@ success ".env written to ${ENV_FILE}" header "Step 5 / 9 — Install & Build" info "Installing npm dependencies (this may take a minute)…" -sudo -u "$APP_USER" npm --prefix "$APP_DIR" install --quiet +npm --prefix "$APP_DIR" install --quiet info "Building SvelteKit app…" -sudo -u "$APP_USER" npm --prefix "$APP_DIR" run build +npm --prefix "$APP_DIR" run build + +# Fix ownership after build +chown -R "${APP_USER}:${APP_USER}" "$APP_DIR" success "Build complete" @@ -218,7 +223,6 @@ module.exports = { name: 'etc-prs', script: '${APP_DIR}/build/index.js', cwd: '${APP_DIR}', - user: '${APP_USER}', env: { NODE_ENV: 'production', PORT: '3000', @@ -236,15 +240,11 @@ EOF chown "${APP_USER}:${APP_USER}" "$ECOSYSTEM" info "Starting app with PM2…" -sudo -u "$APP_USER" pm2 start "$ECOSYSTEM" -sudo -u "$APP_USER" pm2 save +pm2 start "$ECOSYSTEM" +pm2 save info "Configuring PM2 to start on boot…" -# Capture the startup command PM2 emits and run it -PM2_STARTUP=$(sudo -u "$APP_USER" pm2 startup systemd -u "$APP_USER" --hp /opt/etc-prs 2>&1 | grep "sudo env") -if [[ -n "$PM2_STARTUP" ]]; then - eval "$PM2_STARTUP" -fi +pm2 startup systemd -u root --hp /root | grep -E "^sudo" | bash || true success "PM2 configured — app is running on 127.0.0.1:3000" @@ -351,7 +351,7 @@ success "Firewall enabled (SSH + HTTP + HTTPS)" # Admin account info "Creating admin account '${ADMIN_USER}'…" cd "$APP_DIR" -sudo -u "$APP_USER" node scripts/create-admin.js "$ADMIN_USER" "$ADMIN_PASS" || \ +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" @@ -363,15 +363,15 @@ divider echo "" echo -e " ${BOLD}Site:${RESET} https://${DOMAIN}" echo -e " ${BOLD}Admin panel:${RESET} https://${DOMAIN}/admin" -echo -e " ${BOLD}App logs:${RESET} sudo -u ${APP_USER} pm2 logs etc-prs" +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} && sudo -u ${APP_USER} git pull && \\" -echo -e " sudo -u ${APP_USER} npm install && \\" -echo -e " sudo -u ${APP_USER} npm run build && \\" -echo -e " sudo -u ${APP_USER} pm2 reload etc-prs${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 "" diff --git a/scripts/resume-deploy.sh b/scripts/resume-deploy.sh new file mode 100644 index 0000000..a5f2a49 --- /dev/null +++ b/scripts/resume-deploy.sh @@ -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 (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" +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 ""