diff --git a/install.sh b/install.sh index 02b319f..e995c8e 100755 --- a/install.sh +++ b/install.sh @@ -125,6 +125,109 @@ ensure_default() { fi } +# --------------------------------------------------------------------------- +# Fail-fast валидация прод-значений .env перед подъёмом стека. +# +# `ensure_secret`/`ensure_default` пишут значение только если ключ ПУСТ, а +# `.env.example` хранит непустые dev-плейсхолдеры (`NGINX_SERVER_NAMES= +# example.com`, `LIVEKIT_PUBLIC_URL=ws://localhost:7880`, `LIVEKIT_API_KEY= +# devkey` и т.д.) — при `cp .env.example .env` без ручной правки install.sh +# доходил до конца и рапортовал об успехе, а на выходе получался прод с +# mixed-content (браузер режет `ws://` на HTTPS-странице) и nginx, слушающим +# несуществующий домен `example.com` (см. .forcc/deploy/SESSION4-FINDINGS.md, +# ГРАБЛИ 1 — реальный инцидент этого проекта). +# +# Признак «это прод, не dev»: `NGINX_SERVER_NAMES` — реальный домен, не +# `localhost`/`127.0.0.1` (дефолт `ensure_default` для локальной разработки). +# Плейсхолдер `example.com` из .env.example — ошибка ВСЕГДА (ни для прода, +# ни как осознанный dev-дефолт: тот — `localhost`), проверяется отдельно и +# раньше остальных. +validate_prod_env() { + nginx_names="$(get_env_var NGINX_SERVER_NAMES)" + + case "$nginx_names" in + *example.com*) + echo "[install] ОШИБКА: NGINX_SERVER_NAMES='${nginx_names}' — непочатый плейсхолдер из .env.example." >&2 + echo "[install] Перед запуском install.sh задайте в .env реальный домен(ы) (боевой сервер)" >&2 + echo "[install] или 'localhost' (локальная разработка) — см. docs/deploy/DEPLOYMENT.md." >&2 + exit 1 + ;; + esac + + case "$nginx_names" in + localhost|127.0.0.1) return 0 ;; + esac + + # Дальше — прод-ветка: NGINX_SERVER_NAMES указывает на реальный домен, + # остальные прод-значения обязаны быть с ним согласованы. + fail=0 + fail_check() { + echo "[install] ОШИБКА: $1" >&2 + fail=1 + } + + livekit_public_url="$(get_env_var LIVEKIT_PUBLIC_URL)" + case "$livekit_public_url" in + wss://*) ;; + *) + fail_check "LIVEKIT_PUBLIC_URL='${livekit_public_url}' — должен быть wss://<домен>/livekit/ (иначе mixed-content на HTTPS, конференции не стартуют — см. .forcc/deploy/SESSION2-FINDINGS.md)." + ;; + esac + + frontend_url="$(get_env_var FRONTEND_URL)" + case "$frontend_url" in + https://*) ;; + *) + fail_check "FRONTEND_URL='${frontend_url}' — должен быть https://<домен> (иначе битые ссылки подтверждения email)." + ;; + esac + + if [ "$(get_env_var AUTH_COOKIE_SECURE)" != "true" ]; then + fail_check "AUTH_COOKIE_SECURE должен быть 'true' на HTTPS-проде (сейчас: '$(get_env_var AUTH_COOKIE_SECURE)')." + fi + + if [ "$(get_env_var LIVEKIT_API_KEY)" = "devkey" ]; then + fail_check "LIVEKIT_API_KEY='devkey' — публично известный ключ из .env.example, замените на реальный." + fi + + node_ip="$(get_env_var LIVEKIT_NODE_IP)" + if [ "$node_ip" = "127.0.0.1" ] || [ -z "$node_ip" ]; then + fail_check "LIVEKIT_NODE_IP='${node_ip}' — задайте внешний IP/домен сервера (иначе ICE-кандидаты недостижимы извне)." + fi + + cert_name="$(get_env_var NGINX_CERT_NAME)" + case "$cert_name" in + *example.com*) + fail_check "NGINX_CERT_NAME='${cert_name}' — непочатый плейсхолдер из .env.example." + ;; + esac + # НЕ хард-фейл: отсутствие реального сертификата — не тихий провал вроде + # mixed-content (тот ломает WS в консоли браузера, легко пропустить). + # docker-entrypoint-certs.sh сгенерирует самоподписанный и nginx стартует + # штатно — HTTPS с явным предупреждением браузера, что сразу заметно. Это + # ОСОЗНАННЫЙ путь бутстрапа: сначала поднять стек (self-signed), потом + # certbot --webroot (challenge идёт через уже работающий nginx), см. + # docs/deploy/DEPLOYMENT.md, раздел «Выпуск SSL». + if [ ! -d "/etc/letsencrypt/live/${cert_name}" ]; then + echo "[install] ПРЕДУПРЕЖДЕНИЕ: /etc/letsencrypt/live/${cert_name} не найден — nginx поднимется с самоподписанным сертификатом." >&2 + echo "[install] Это ожидаемо при первом бутстрапе (см. docs/deploy/DEPLOYMENT.md, «Выпуск SSL» — certbot --webroot ПОСЛЕ первого запуска install.sh)." >&2 + fi + + # TURN понижен до опционального (Сессия 5: реальные сети в тестировании + # прошли без раздачи TURN клиентам) — только предупреждение, не блокер. + turn_ip="$(get_env_var TURN_EXTERNAL_IP)" + if [ "$turn_ip" = "127.0.0.1" ]; then + echo "[install] ПРЕДУПРЕЖДЕНИЕ: TURN_EXTERNAL_IP=127.0.0.1 — coturn-релей не будет работать для внешних клиентов." >&2 + echo "[install] Не блокер (TURN опционален, см. docs/deploy/DEPLOYMENT.md, раздел «TURN для экстремального NAT»), но проверьте, что это осознанный выбор." >&2 + fi + + if [ "$fail" = "1" ]; then + echo "[install] Остановлено: .env содержит dev-плейсхолдеры на боевом домене (NGINX_SERVER_NAMES='${nginx_names}')." >&2 + echo "[install] Полный разбор переменных — docs/deploy/DEPLOYMENT.md." >&2 + exit 1 + fi +} + # --------------------------------------------------------------------------- # Автодетект железа (ADR-004, таблица требований). # --------------------------------------------------------------------------- @@ -365,6 +468,9 @@ esac echo "[install] .env обновлён (пресет ${PRESET}, COMPOSE_PROFILES=$(get_env_var COMPOSE_PROFILES))" +echo "[install] Проверяю .env на dev-плейсхолдеры перед подъёмом стека" +validate_prod_env + # --------------------------------------------------------------------------- # Подъём стека и миграции/seed. # ---------------------------------------------------------------------------