- Remove "type": "commonjs" from package.json — fixes Turbopack ESM/CommonJS conflict causing white screen in dev mode - setup.sh: auto-generate passwords (PostgreSQL, MinIO, auth secret) - setup.sh: auto-install Docker/Node.js if missing (apt/nvm/brew) - setup.sh: backup existing .env before overwrite - setup.sh: create MinIO bucket via curl if mc not installed - setup.sh: add timeout errors for service readiness checks - setup.sh: include pgbouncer in local dev docker compose up Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
492 lines
16 KiB
Bash
492 lines
16 KiB
Bash
#!/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 '<server-ip>')"
|
||
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://<ip>: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 ""
|