Full MVP implementation: - Next.js 16 + React 19 + TypeScript + Tailwind CSS v4 - LiveKit integration (rooms, tokens, webhooks, moderation) - better-auth (email/password, sessions, roles) - Prisma 7 + PostgreSQL (9 models, 3 enums) - 15 API routes (auth, rooms, lobby, chat, files, moderation, hand-raise) - 7 pages (landing, auth, dashboard, join, video room) - SSE-based waiting room with host approval flow - Security: PIN rate limiting, session fingerprint bans, chat/files auth - Python AI Agent (Deepgram STT + OpenAI summarization) - Docker Compose (local + production with Traefik + Let's Encrypt) - Interactive setup script (setup.sh) - Dev protection middleware (DEV_ACCESS_KEY, ALLOWED_IPS) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
329 lines
10 KiB
Bash
329 lines
10 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
|
|
}
|
|
|
|
check_command() {
|
|
if command -v "$1" &>/dev/null; then
|
|
log_ok "$1 найден: $(command -v "$1")"
|
|
return 0
|
|
else
|
|
log_err "$1 не найден"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
# ============================================================
|
|
# Main
|
|
# ============================================================
|
|
|
|
print_banner
|
|
|
|
# --- Step 1: Check dependencies ---
|
|
log_step "Шаг 1/6 — Проверка зависимостей"
|
|
|
|
MISSING=0
|
|
check_command docker || MISSING=1
|
|
check_command "docker compose" 2>/dev/null || check_command docker-compose || MISSING=1
|
|
check_command node || MISSING=1
|
|
check_command npm || MISSING=1
|
|
check_command npx || 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 — Настройка окружения"
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}PostgreSQL${NC}"
|
|
ask "Пароль PostgreSQL" "postgres" PG_PASSWORD
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}MinIO (S3 Storage)${NC}"
|
|
ask "MinIO root user" "minioadmin" MINIO_USER
|
|
ask_secret "MinIO root password" "minioadmin" MINIO_PASS
|
|
ask "Название S3 bucket" "liveserver" S3_BUCKET
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}LiveKit${NC}"
|
|
ask "LiveKit URL (wss://...)" "" LK_URL
|
|
ask "LiveKit API Key" "" LK_KEY
|
|
ask_secret "LiveKit API Secret" "" LK_SECRET
|
|
ask_secret "LiveKit Webhook Secret (Enter чтобы пропустить)" "" LK_WEBHOOK
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}AI Agent (Enter чтобы пропустить, настроишь позже)${NC}"
|
|
ask_secret "Deepgram API Key" "" DG_KEY
|
|
ask_secret "OpenAI API Key" "" OAI_KEY
|
|
|
|
echo ""
|
|
echo -e " ${BOLD}Аутентификация${NC}"
|
|
AUTH_SECRET=$(generate_secret)
|
|
log_ok "Сгенерирован BETTER_AUTH_SECRET"
|
|
|
|
if [[ "$MODE" == "1" ]]; then
|
|
DEV_KEY=$(generate_secret | head -c 16)
|
|
echo ""
|
|
echo -e " ${BOLD}Защита локалки${NC}"
|
|
ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_KEY" DEV_ACCESS_KEY
|
|
ask "Разрешённые IP (через запятую, Enter = все)" "" ALLOWED_IPS
|
|
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
|
|
|
|
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}
|
|
|
|
# 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
|
|
if [[ ! -d "node_modules" ]]; then
|
|
echo -e " Установка npm зависимостей..."
|
|
npm install --silent 2>&1 | tail -3
|
|
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 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
|
|
sleep 1
|
|
done
|
|
log_ok "PostgreSQL готов"
|
|
|
|
# Prisma migrate
|
|
echo -e " Применение миграций..."
|
|
npx prisma db push --skip-generate 2>&1 | tail -3
|
|
log_ok "Схема БД синхронизирована"
|
|
|
|
# Create MinIO bucket
|
|
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
|
|
log_warn "MinIO Client (mc) не найден — создай bucket '${S3_BUCKET}' вручную через http://localhost:9001"
|
|
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
|
|
|
|
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 ""
|