#!/usr/bin/env bash set -euo pipefail # ============================================================ # LiveServer-M1 — Interactive Setup Script # ============================================================ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m' print_banner() { echo "" echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║${NC} ${BOLD}LiveServer-M1${NC} — Setup Wizard ${CYAN}║${NC}" echo -e "${CYAN}║${NC} Образовательная видеоконференц-платформа ${CYAN}║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" echo "" } log_step() { echo -e "\n${BLUE}[$(date +%H:%M:%S)]${NC} ${BOLD}$1${NC}"; } log_ok() { echo -e " ${GREEN}✓${NC} $1"; } log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } log_err() { echo -e " ${RED}✗${NC} $1"; } ask() { local prompt="$1" default="${2:-}" var_name="$3" if [[ -n "$default" ]]; then echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: " else echo -en " ${CYAN}?${NC} ${prompt}: " fi read -r input printf -v "$var_name" '%s' "${input:-$default}" } ask_secret() { local prompt="$1" default="${2:-}" var_name="$3" if [[ -n "$default" ]]; then echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: " else echo -en " ${CYAN}?${NC} ${prompt}: " fi read -rs input echo "" printf -v "$var_name" '%s' "${input:-$default}" } ask_yn() { local prompt="$1" default="${2:-y}" local hint="Y/n" [[ "$default" == "n" ]] && hint="y/N" echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${hint}]${NC}: " read -r input input="${input:-$default}" [[ "${input,,}" == "y" || "${input,,}" == "yes" ]] } generate_secret() { openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d '=/+' | head -c 32 } generate_password() { # 20 символов: буквы + цифры (безопасно для URL и CLI) openssl rand -base64 24 2>/dev/null | tr -d '=/+' | head -c 20 || \ head -c 24 /dev/urandom | base64 | tr -d '=/+' | head -c 20 } check_command() { if command -v "$1" &>/dev/null; then log_ok "$1 найден: $(command -v "$1")" return 0 else log_err "$1 не найден" return 1 fi } install_if_missing() { local cmd="$1" install_hint="$2" if command -v "$cmd" &>/dev/null; then log_ok "$cmd найден: $(command -v "$cmd")" return 0 fi log_warn "$cmd не найден" # Попытка автоустановки через системные менеджеры if [[ "$cmd" == "node" || "$cmd" == "npm" || "$cmd" == "npx" ]]; then if command -v nvm &>/dev/null; then echo -e " Установка Node.js через nvm..." nvm install --lts 2>&1 | tail -3 if command -v "$cmd" &>/dev/null; then log_ok "$cmd установлен через nvm" return 0 fi fi fi # Автоустановка через apt/yum/brew если доступно case "$cmd" in docker) if command -v apt-get &>/dev/null; then if ask_yn "Установить Docker автоматически (apt)?" "y"; then echo -e " Установка Docker..." curl -fsSL https://get.docker.com | sh 2>&1 | tail -5 sudo usermod -aG docker "$USER" 2>/dev/null || true if command -v docker &>/dev/null; then log_ok "Docker установлен" return 0 fi fi fi ;; node|npm|npx) if command -v apt-get &>/dev/null; then if ask_yn "Установить Node.js 22 автоматически (apt)?" "y"; then echo -e " Установка Node.js..." curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - 2>&1 | tail -3 sudo apt-get install -y nodejs 2>&1 | tail -3 if command -v "$cmd" &>/dev/null; then log_ok "$cmd установлен" return 0 fi fi elif command -v brew &>/dev/null; then if ask_yn "Установить Node.js через Homebrew?" "y"; then brew install node 2>&1 | tail -3 if command -v "$cmd" &>/dev/null; then log_ok "$cmd установлен" return 0 fi fi fi ;; esac log_err "$cmd не удалось установить автоматически" echo -e " ${YELLOW}Установи вручную:${NC} $install_hint" return 1 } # ============================================================ # Main # ============================================================ print_banner # --- Step 1: Check & install dependencies --- log_step "Шаг 1/6 — Проверка и установка зависимостей" MISSING=0 install_if_missing docker "https://docs.docker.com/get-docker/" || MISSING=1 check_command "docker compose" 2>/dev/null || check_command docker-compose 2>/dev/null || { log_err "docker compose не найден (нужен Docker Compose V2)" MISSING=1 } install_if_missing node "https://nodejs.org/" || MISSING=1 install_if_missing npm "https://nodejs.org/" || MISSING=1 if [[ "$MISSING" -eq 1 ]]; then log_err "Установи недостающие зависимости и запусти скрипт снова." exit 1 fi NODE_VER=$(node -v) log_ok "Node.js: $NODE_VER" # --- Step 2: Choose mode --- log_step "Шаг 2/6 — Режим запуска" echo "" echo -e " ${BOLD}1)${NC} Локальная разработка (localhost, без SSL)" echo -e " ${BOLD}2)${NC} Production (домен + Traefik + Let's Encrypt SSL)" echo "" ask "Выбери режим" "1" MODE DOMAIN="" ACME_EMAIL="" if [[ "$MODE" == "2" ]]; then echo "" ask "Домен (например live.example.com)" "" DOMAIN ask "Email для Let's Encrypt" "" ACME_EMAIL if [[ -z "$DOMAIN" || -z "$ACME_EMAIL" ]]; then log_err "Домен и email обязательны для production." exit 1 fi log_ok "Production: ${DOMAIN}" log_warn "Убедись что DNS записи указывают на этот сервер:" echo -e " ${DOMAIN} → $(curl -s ifconfig.me 2>/dev/null || echo '')" echo -e " s3.${DOMAIN} → тот же IP" echo -e " minio.${DOMAIN} → тот же IP" echo "" if ! ask_yn "DNS настроен?" "y"; then log_warn "Настрой DNS и запусти скрипт снова." exit 0 fi else log_ok "Локальная разработка" fi # --- Step 3: Configure environment --- log_step "Шаг 3/6 — Настройка окружения" # Автогенерация паролей PG_PASSWORD=$(generate_password) MINIO_USER="minio-$(head -c 4 /dev/urandom | xxd -p)" MINIO_PASS=$(generate_password) AUTH_SECRET=$(generate_secret) DEV_ACCESS_KEY=$(generate_password | head -c 16) S3_BUCKET="liveserver" echo "" echo -e " ${GREEN}${BOLD}Пароли сгенерированы автоматически:${NC}" echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" echo -e " ${CYAN}MinIO user:${NC} ${MINIO_USER}" echo -e " ${CYAN}MinIO password:${NC} ${MINIO_PASS}" echo -e " ${CYAN}Auth secret:${NC} ${AUTH_SECRET:0:16}..." if [[ "$MODE" == "1" ]]; then echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" fi echo "" echo -e " ${YELLOW}Все пароли сохранены в .env — можешь изменить позже${NC}" if ask_yn "Хочешь задать свои пароли вместо сгенерированных?" "n"; then echo "" echo -e " ${BOLD}PostgreSQL${NC}" ask "Пароль PostgreSQL" "$PG_PASSWORD" PG_PASSWORD echo "" echo -e " ${BOLD}MinIO (S3 Storage)${NC}" ask "MinIO root user" "$MINIO_USER" MINIO_USER ask_secret "MinIO root password" "$MINIO_PASS" MINIO_PASS ask "Название S3 bucket" "$S3_BUCKET" S3_BUCKET if [[ "$MODE" == "1" ]]; then echo "" echo -e " ${BOLD}Защита локалки${NC}" ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_ACCESS_KEY" DEV_ACCESS_KEY fi fi echo "" echo -e " ${BOLD}LiveKit${NC} (Enter чтобы пропустить, настроишь позже)" ask "LiveKit URL (wss://...)" "" LK_URL LK_KEY="" LK_SECRET="" LK_WEBHOOK="" if [[ -n "$LK_URL" ]]; then ask "LiveKit API Key" "" LK_KEY ask_secret "LiveKit API Secret" "" LK_SECRET ask_secret "LiveKit Webhook Secret (Enter = пропустить)" "" LK_WEBHOOK fi echo "" echo -e " ${BOLD}AI Agent${NC} (Enter чтобы пропустить, настроишь позже)" ask_secret "Deepgram API Key" "" DG_KEY ask_secret "OpenAI API Key" "" OAI_KEY if [[ "$MODE" == "1" ]]; then ALLOWED_IPS="" if ask_yn "Ограничить доступ по IP? (кроме localhost)" "n"; then ask "Разрешённые IP (через запятую)" "" ALLOWED_IPS fi fi # --- Step 4: Generate .env --- log_step "Шаг 4/6 — Генерация .env" if [[ "$MODE" == "2" ]]; then APP_URL="https://${DOMAIN}" S3_ENDPOINT="http://minio:9000" else APP_URL="http://localhost:3000" S3_ENDPOINT="http://localhost:9000" fi # Бэкап существующего .env if [[ -f .env ]]; then cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" log_ok "Бэкап старого .env создан" fi cat > .env << ENVEOF # === Generated by setup.sh at $(date) === # Все пароли сгенерированы автоматически — можешь заменить на свои. # Domain & SSL DOMAIN=${DOMAIN} ACME_EMAIL=${ACME_EMAIL} # Database DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@localhost:5432/liveserver POSTGRES_PASSWORD=${PG_PASSWORD} # Redis # Для Docker контейнеров: redis://redis:6379 # Для локального npm run dev: redis://localhost:6379 REDIS_URL=redis://localhost:6379 # LiveKit LIVEKIT_URL=${LK_URL} NEXT_PUBLIC_LIVEKIT_URL=${LK_URL} LIVEKIT_API_KEY=${LK_KEY} LIVEKIT_API_SECRET=${LK_SECRET} # AI Agent DEEPGRAM_API_KEY=${DG_KEY} OPENAI_API_KEY=${OAI_KEY} # Storage (MinIO/S3) S3_ENDPOINT=${S3_ENDPOINT} S3_ACCESS_KEY=${MINIO_USER} S3_SECRET_KEY=${MINIO_PASS} S3_BUCKET=${S3_BUCKET} MINIO_ROOT_USER=${MINIO_USER} MINIO_ROOT_PASSWORD=${MINIO_PASS} # Auth BETTER_AUTH_SECRET=${AUTH_SECRET} BETTER_AUTH_URL=${APP_URL} NEXT_PUBLIC_APP_URL=${APP_URL} # Local dev protection DEV_ACCESS_KEY=${DEV_ACCESS_KEY:-} ALLOWED_IPS=${ALLOWED_IPS:-} ENVEOF log_ok ".env создан" # --- Step 5: Install & setup --- log_step "Шаг 5/6 — Установка и настройка" # npm install — автоматически если нет node_modules или package-lock изменился if [[ ! -d "node_modules" ]]; then echo -e " Установка npm зависимостей..." npm install 2>&1 | tail -5 log_ok "npm install завершён" elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then echo -e " package.json обновлён, переустановка зависимостей..." npm install 2>&1 | tail -5 log_ok "npm install завершён (обновление)" else log_ok "node_modules актуален" fi # Prisma generate echo -e " Генерация Prisma Client..." npx prisma generate 2>&1 | tail -1 log_ok "Prisma Client сгенерирован" # Docker compose up echo -e " Запуск Docker контейнеров..." if [[ "$MODE" == "2" ]]; then docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build 2>&1 | tail -5 else docker compose up -d postgres minio redis pgbouncer 2>&1 | tail -5 fi log_ok "Docker контейнеры запущены" # Wait for postgres echo -e " Ожидание PostgreSQL..." for i in $(seq 1 30); do if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then break fi if [[ "$i" -eq 30 ]]; then log_err "PostgreSQL не запустился за 30 секунд" exit 1 fi sleep 1 done log_ok "PostgreSQL готов" # Wait for Redis echo -e " Ожидание Redis..." for i in $(seq 1 15); do if docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then break fi if [[ "$i" -eq 15 ]]; then log_err "Redis не запустился за 15 секунд" exit 1 fi sleep 1 done log_ok "Redis готов" # Wait for PgBouncer echo -e " Ожидание PgBouncer..." for i in $(seq 1 15); do if docker compose exec -T pgbouncer pg_isready -h 127.0.0.1 -p 6432 &>/dev/null; then break fi if [[ "$i" -eq 15 ]]; then log_warn "PgBouncer не отвечает — продолжаем без него" fi sleep 1 done log_ok "PgBouncer готов" # Prisma migrate echo -e " Применение миграций..." npx prisma db push --skip-generate 2>&1 | tail -3 log_ok "Схема БД синхронизирована" # Create MinIO bucket (через docker exec если mc не установлен) echo -e " Создание S3 bucket..." sleep 2 if command -v mc &>/dev/null; then mc alias set local http://localhost:9000 "$MINIO_USER" "$MINIO_PASS" 2>/dev/null || true mc mb "local/${S3_BUCKET}" 2>/dev/null || true log_ok "Bucket '${S3_BUCKET}' создан" else # Создание через curl (MinIO S3 API) if curl -sf -o /dev/null -X PUT "http://localhost:9000/${S3_BUCKET}" \ -u "${MINIO_USER}:${MINIO_PASS}" 2>/dev/null; then log_ok "Bucket '${S3_BUCKET}' создан через API" else log_warn "Не удалось создать bucket автоматически" echo -e " Создай вручную: ${CYAN}http://localhost:9001${NC} → Buckets → Create" echo -e " Логин: ${CYAN}${MINIO_USER}${NC} / пароль в .env" fi fi # --- Step 6: Done --- log_step "Шаг 6/6 — Готово!" echo "" if [[ "$MODE" == "2" ]]; then echo -e " ${GREEN}${BOLD}Production запущен!${NC}" echo "" echo -e " Приложение: ${CYAN}https://${DOMAIN}${NC}" echo -e " MinIO Console: ${CYAN}https://minio.${DOMAIN}${NC}" echo -e " MinIO S3 API: ${CYAN}https://s3.${DOMAIN}${NC}" else echo -e " ${GREEN}${BOLD}Локальная среда готова!${NC}" echo "" echo -e " Запусти приложение: ${CYAN}npm run dev${NC}" echo -e " Приложение: ${CYAN}http://localhost:3000${NC}" echo -e " MinIO Console: ${CYAN}http://localhost:9001${NC}" echo -e " Prisma Studio: ${CYAN}npx prisma studio${NC}" if [[ -n "${DEV_ACCESS_KEY:-}" ]]; then echo "" echo -e " ${YELLOW}Для доступа из сети:${NC}" echo -e " ${CYAN}http://:3000?key=${DEV_ACCESS_KEY}${NC}" fi fi echo "" echo -e " ${BOLD}Сгенерированные пароли (сохранены в .env):${NC}" echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" echo -e " ${CYAN}MinIO:${NC} ${MINIO_USER} / ${MINIO_PASS}" if [[ "$MODE" == "1" && -n "${DEV_ACCESS_KEY:-}" ]]; then echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" fi echo -e " ${YELLOW}Изменить пароли: отредактируй .env и перезапусти docker compose${NC}" if [[ -z "$LK_URL" ]]; then echo "" log_warn "LiveKit не настроен — видеозвонки не будут работать." echo -e " Зарегистрируйся на ${CYAN}https://cloud.livekit.io${NC}" echo -e " и заполни LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET в .env" fi if [[ -z "$DG_KEY" ]]; then echo "" log_warn "Deepgram не настроен — AI транскрипция отключена." echo -e " Получи ключ: ${CYAN}https://console.deepgram.com${NC}" fi if [[ -z "$OAI_KEY" ]]; then echo "" log_warn "OpenAI не настроен — AI суммаризация отключена." echo -e " Получи ключ: ${CYAN}https://platform.openai.com/api-keys${NC}" fi echo "" echo -e "${CYAN}══════════════════════════════════════════════${NC}" echo -e " Документация: ${CYAN}README.md${NC}" echo -e " Спецификация: ${CYAN}PROMPT.md${NC}" echo -e "${CYAN}══════════════════════════════════════════════${NC}" echo ""