#!/usr/bin/env bash set -Eeuo pipefail # Интерактивный установщик WSChat для VPS. # Домены и сайты FastPanel создаются вручную через интерфейс FastPanel. # Админка публикуется в том же webroot, что и виджет: https://widget.example.ru/admin/ PROJECT_NAME="WSChat" DEFAULT_PROJECT_ROOT="/opt/ws-chat" DEFAULT_REPO_URL="https://github.com/viktor138irk/chat.git" DEFAULT_NODE_MAJOR="20" DEFAULT_BACKEND_HOST="127.0.0.1" DEFAULT_BACKEND_PORT="3000" DEFAULT_PM2_PROCESS="wschat-backend" STEP=0 TOTAL_STEPS=13 LOG_FILE="/tmp/wschat-install-$(date +%Y%m%d-%H%M%S).log" PROJECT_ROOT="${PROJECT_ROOT:-$DEFAULT_PROJECT_ROOT}" SOURCE_PATH="${SOURCE_PATH:-$PROJECT_ROOT/source}" DATA_PATH="${DATA_PATH:-$PROJECT_ROOT/data}" LOGS_PATH="${LOGS_PATH:-$PROJECT_ROOT/logs}" BACKUPS_PATH="${BACKUPS_PATH:-$PROJECT_ROOT/backups}" UPDATES_PATH="${UPDATES_PATH:-$PROJECT_ROOT/updates}" REPO_URL="${REPO_URL:-$DEFAULT_REPO_URL}" NODE_MAJOR="${NODE_MAJOR:-$DEFAULT_NODE_MAJOR}" BACKEND_HOST="${BACKEND_HOST:-$DEFAULT_BACKEND_HOST}" BACKEND_PORT="${BACKEND_PORT:-$DEFAULT_BACKEND_PORT}" PM2_PROCESS_NAME="${PM2_PROCESS_NAME:-$DEFAULT_PM2_PROCESS}" WIDGET_DOMAIN="${WIDGET_DOMAIN:-widget.example.ru}" API_DOMAIN="${API_DOMAIN:-api.example.ru}" WIDGET_WEBROOT="${WIDGET_WEBROOT:-}" TELEGRAM_BOT_TOKEN="${TELEGRAM_BOT_TOKEN:-}" JWT_SECRET="${JWT_SECRET:-}" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' BOLD='\033[1m' NC='\033[0m' log() { printf "%s\n" "$*" | tee -a "$LOG_FILE" } ok() { printf "${GREEN}%s${NC}\n" "$*" | tee -a "$LOG_FILE" } warn() { printf "${YELLOW}%s${NC}\n" "$*" | tee -a "$LOG_FILE" } fail() { printf "${RED}%s${NC}\n" "$*" | tee -a "$LOG_FILE" >&2 } current_step() { STEP=$((STEP + 1)) printf "\n${BOLD}${BLUE}[%02d/%02d] %s${NC}\n" "$STEP" "$TOTAL_STEPS" "$1" | tee -a "$LOG_FILE" } run() { log "→ $*" "$@" 2>&1 | tee -a "$LOG_FILE" } on_error() { local exit_code=$? fail "" fail "Установка остановлена на шаге ${STEP}/${TOTAL_STEPS}. Код ошибки: ${exit_code}" fail "Файл лога: ${LOG_FILE}" fail "Последняя выполняемая задача показана выше. Исправь ошибку и запусти установщик повторно." exit "$exit_code" } trap on_error ERR ask() { local prompt="$1" local default_value="$2" local result read -r -p "$(printf "${BOLD}%s${NC} [%s]: " "$prompt" "$default_value")" result if [[ -z "$result" ]]; then printf "%s" "$default_value" else printf "%s" "$result" fi } ask_secret() { local prompt="$1" local result read -r -s -p "$(printf "${BOLD}%s${NC}: " "$prompt")" result printf "\n" >&2 printf "%s" "$result" } generate_secret() { if command -v openssl >/dev/null 2>&1; then openssl rand -hex 32 else date +%s%N | sha256sum | awk '{print $1}' fi } require_root() { if [[ "${EUID}" -ne 0 ]]; then fail "Запусти установщик от root: bash deploy/vps/install.sh" exit 1 fi } validate_fastpanel_path() { local label="$1" local path="$2" if [[ -z "$path" ]]; then warn "$label webroot не указан. Публикация статики будет пропущена." return 0 fi if [[ "$path" != /var/www/*/data/www/* ]]; then fail "$label webroot должен выглядеть так: /var/www//data/www/" fail "Получено: $path" return 1 fi } write_backend_env() { local env_file="$SOURCE_PATH/backend/.env" cat > "$env_file" < "$PROJECT_ROOT/install-state.env" </admin/\n" printf "Файл лога: %s\n\n" "$LOG_FILE" } print_summary() { cat </dev/null 2>&1 && node -v | grep -q "^v${NODE_MAJOR}\."; then ok "Node.js уже установлен: $(node -v)" return 0 fi run bash -c "curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash -" run apt install -y nodejs ok "Node.js: $(node -v)" ok "npm: $(npm -v)" } install_pm2() { if command -v pm2 >/dev/null 2>&1; then ok "PM2 уже установлен: $(pm2 -v)" else run npm install -g pm2 ok "PM2 установлен: $(pm2 -v)" fi } clone_or_update_repo() { mkdir -p "$PROJECT_ROOT" if [[ -d "$SOURCE_PATH/.git" ]]; then cd "$SOURCE_PATH" run git fetch origin main run git pull --ff-only origin main else mkdir -p "$(dirname "$SOURCE_PATH")" run git clone "$REPO_URL" "$SOURCE_PATH" fi } install_dependencies() { cd "$SOURCE_PATH" run npm install } start_backend_pm2() { cd "$SOURCE_PATH" if pm2 describe "$PM2_PROCESS_NAME" >/dev/null 2>&1; then run pm2 restart "$PM2_PROCESS_NAME" --update-env else run pm2 start backend/src/server.js --name "$PM2_PROCESS_NAME" fi run pm2 save pm2 startup systemd -u root --hp /root 2>&1 | tee -a "$LOG_FILE" || true } check_backend() { sleep 2 run curl -fsS "http://${BACKEND_HOST}:${BACKEND_PORT}/health" } build_frontend() { cd "$SOURCE_PATH" run npm run build:admin run npm run build:widget } publish_static_if_configured() { cd "$SOURCE_PATH" if [[ -z "$WIDGET_WEBROOT" ]]; then warn "Webroot виджета не указан. Публикация виджета и админки пропущена." return 0 fi if [[ ! -d "$WIDGET_WEBROOT" ]]; then warn "Webroot виджета не найден: $WIDGET_WEBROOT" warn "Публикация пропущена. Сначала создай сайт виджета в FastPanel вручную." return 0 fi run rsync -av --delete widget/dist/ "$WIDGET_WEBROOT/" run mkdir -p "$WIDGET_WEBROOT/admin" run rsync -av --delete admin-panel/dist/ "$WIDGET_WEBROOT/admin/" } main() { print_header require_root current_step "Сбор настроек установки" PROJECT_ROOT="$(ask "Каталог проекта" "$PROJECT_ROOT")" SOURCE_PATH="${PROJECT_ROOT}/source" DATA_PATH="${PROJECT_ROOT}/data" LOGS_PATH="${PROJECT_ROOT}/logs" BACKUPS_PATH="${PROJECT_ROOT}/backups" UPDATES_PATH="${PROJECT_ROOT}/updates" WIDGET_DOMAIN="$(ask "Домен виджета и админки" "$WIDGET_DOMAIN")" API_DOMAIN="$(ask "Домен API" "$API_DOMAIN")" WIDGET_WEBROOT="$(ask "Webroot сайта виджета в FastPanel, оставь пустым чтобы пропустить публикацию" "$WIDGET_WEBROOT")" TELEGRAM_BOT_TOKEN="$(ask_secret "Telegram bot token, можно оставить пустым")" JWT_SECRET="$(ask "JWT secret" "$(generate_secret)")" validate_fastpanel_path "Widget" "$WIDGET_WEBROOT" print_summary read -r -p "Продолжить установку? [Y/n]: " confirm if [[ "$confirm" =~ ^[Nn]$ ]]; then warn "Установка отменена." exit 0 fi current_step "Установка базовых пакетов" install_packages current_step "Установка Node.js" install_node current_step "Установка PM2" install_pm2 current_step "Создание каталогов WSChat" mkdir -p "$PROJECT_ROOT" "$DATA_PATH" "$LOGS_PATH" "$BACKUPS_PATH" "$UPDATES_PATH" ok "Каталоги созданы в $PROJECT_ROOT" current_step "Клонирование или обновление репозитория" clone_or_update_repo current_step "Установка зависимостей проекта" install_dependencies current_step "Запись backend .env" write_backend_env write_install_state ok "Backend .env записан: $SOURCE_PATH/backend/.env" ok "Состояние установки записано: $PROJECT_ROOT/install-state.env" current_step "Запуск backend через PM2" start_backend_pm2 current_step "Проверка backend health" check_backend current_step "Сборка админки и виджета" build_frontend current_step "Публикация виджета и админки в один webroot" publish_static_if_configured current_step "Инструкция для proxy в FastPanel" cat <