diff --git a/install.sh b/install.sh index d86ba27..d94b4a0 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.6.3" +SCRIPT_VERSION="2.6.4" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -432,6 +432,7 @@ action_fix_docker_nginx() { log "Восстановление Docker Nginx (lendry-id-nginx)..." ensure_docker_nginx_mode_for_fix_all + ensure_docker_nginx_configs prepare_compose_stack ensure_nginx_after_stack_rebuild || fail "Не удалось запустить lendry-id-nginx" sync_nginx_after_api_gateway_restart || true @@ -471,6 +472,7 @@ wait_for_api_gateway_http() { # После recreate api-gateway nginx должен сбросить DNS/keepalive — иначе 502 на старый IP. sync_nginx_after_api_gateway_restart() { docker_nginx_container_running || return 0 + ensure_docker_nginx_configs || true log "Синхронизация Docker Nginx с api-gateway после пересборки..." wait_for_service "api-gateway" 120 || warn "api-gateway не стал healthy в срок" wait_for_api_gateway_http 90 || warn "api-gateway /health на :3000 не отвечает" @@ -522,6 +524,7 @@ ensure_nginx_after_stack_rebuild() { if [[ "$(env_get NGINX_MODE docker)" == "docker" ]]; then log "Запуск и синхронизация Docker Nginx (lendry-id-nginx)..." + ensure_docker_nginx_configs ensure_nginx_gateway_watch_script patch_legacy_nginx_proxy_pass || true @@ -572,10 +575,19 @@ nginx_probe_idp_api_health_from_container() { build_compose_stack_cmd if [[ "$use_ssl" == "true" || "$ssl_type" != "none" ]]; then - docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ + body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ wget -qO- --timeout=6 --no-check-certificate \ --header="Host: ${frontend_domain}" \ - "https://127.0.0.1/idp-api/health" 2>/dev/null || true + "https://127.0.0.1/idp-api/health" 2>/dev/null || true)" + if [[ "$body" == *'"status"'* ]]; then + printf '%s' "$body" + return 0 + fi + body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ + wget -qO- --timeout=6 \ + --header="Host: ${frontend_domain}" \ + "http://127.0.0.1/idp-api/health" 2>/dev/null || true)" + printf '%s' "$body" return 0 fi @@ -1635,6 +1647,7 @@ action_migrate_rebuild() { wait_for_service docs 60 log "Шаг 4/4: Nginx, прокси и проверка..." + ensure_docker_nginx_configs ensure_nginx_after_stack_rebuild || warn "Nginx не восстановлен — выполните: ./install.sh --fix-docker-nginx" post_deploy_proxy_verify 0 || warn "HTTPS-проверка не прошла — выполните: ./install.sh --fix-all" diagnose_common_issues || true @@ -3014,7 +3027,7 @@ verify_nginx_reaches_api_gateway() { wget -qO- --timeout=5 http://api-gateway:3000/health 2>/dev/null || true)" if [[ "$body" == *'"status":"ok"'* ]] || [[ "$body" == *'"status": "ok"'* ]]; then ok "Docker Nginx → api-gateway:3000/health OK (IP api-gateway: ${gw_ip:-?})" - verify_nginx_idp_api_location || return 1 + verify_nginx_idp_api_location || warn "location /idp-api/ — проверьте SSL и nginx/conf.d (upstream OK)" return 0 fi attempt=$((attempt + 1)) @@ -3051,17 +3064,22 @@ verify_nginx_idp_api_location() { return 0 fi - # Fallback: проверка с хоста (как curl из браузера / DevTools) + # Fallback: проверка через локальный Docker Nginx (127.0.0.1), без DNS на сервере if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then - code="$(curl -sk --max-time 6 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/idp-api/health" 1 6)" else - code="$(curl -fsS --max-time 6 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/idp-api/health" 0 6)" fi if [[ "$code" == "200" ]]; then - ok "SSO /idp-api/health → 200 (проверка с хоста; внутри контейнера — см. SSL/301)" + ok "SSO /idp-api/health → 200 (через lendry-id-nginx на 127.0.0.1)" return 0 fi + if ! docker_nginx_configs_present; then + warn "nginx/conf.d пуст — выполните: ./install.sh --fix-all" + return 1 + fi + warn "Nginx location /idp-api/ не проксирует на api-gateway (Host: ${frontend_domain}, HTTP ${code:-?})" warn "Проверьте: curl -sk https://${frontend_domain}/idp-api/health" warn " и: docker compose --env-file .env --profile proxy exec nginx wget -qO- --no-check-certificate --header=\"Host: ${frontend_domain}\" https://127.0.0.1/idp-api/health" @@ -3086,7 +3104,7 @@ verify_nginx_reaches_frontend() { if [[ -n "$body" ]]; then ok "Docker Nginx → frontend:3000/ OK (IP frontend: ${fe_ip:-?})" if [[ -n "$frontend_domain" ]]; then - verify_nginx_frontend_root_location "$frontend_domain" || return 1 + verify_nginx_frontend_root_location "$frontend_domain" || warn "location / — проверьте SSL и nginx/conf.d (upstream OK)" fi return 0 fi @@ -3116,16 +3134,30 @@ verify_nginx_frontend_root_location() { return 0 fi + body="$(docker_cmd "${COMPOSE_STACK_CMD[@]}" exec -T nginx \ + wget -qO- --timeout=8 \ + --header="Host: ${frontend_domain}" \ + "http://127.0.0.1/" 2>/dev/null | head -c 256 || true)" + if [[ -n "$body" ]]; then + ok "Docker Nginx location / → frontend OK по HTTP (Host: ${frontend_domain})" + return 0 + fi + if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then - code="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/" 1 8)" else - code="$(curl -fsS --max-time 8 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/" 0 8)" fi if [[ "$code" == "200" || "$code" == "304" ]]; then - ok "SSO / → HTTP ${code} (проверка с хоста)" + ok "SSO / → HTTP ${code} (через lendry-id-nginx на 127.0.0.1)" return 0 fi + if ! docker_nginx_configs_present; then + warn "nginx/conf.d пуст — выполните: ./install.sh --fix-all" + return 1 + fi + warn "Nginx location / не проксирует на frontend (Host: ${frontend_domain}, HTTP ${code:-?})" warn "Проверьте: curl -sk -o /dev/null -w '%{http_code}' https://${frontend_domain}/" return 1 @@ -3139,9 +3171,9 @@ check_sso_frontend_root() { ssl_type="$(env_get SSL_TYPE none)" if [[ "$(env_get USE_NGINX_SSL false)" == "true" || "$ssl_type" != "none" ]]; then - code="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/" 1 8)" else - code="$(curl -fsS --max-time 8 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/" 0 8)" fi if [[ "$code" == "200" || "$code" == "304" ]]; then @@ -3193,9 +3225,9 @@ check_sso_idp_api_proxy() { fi if [[ "$(env_get USE_NGINX_SSL false)" != "true" ]]; then - code="$(curl -fsS --max-time 5 -o /dev/null -w '%{http_code}' "http://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/idp-api/health" 0 5)" else - code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' "https://${frontend_domain}/idp-api/health" 2>/dev/null || echo 000)" + code="$(curl_local_nginx_code "$frontend_domain" "/idp-api/health" 1 5)" fi if [[ "$code" == "200" ]]; then @@ -3711,6 +3743,66 @@ refresh_host_nginx_configs() { esac } +docker_nginx_configs_present() { + [[ -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-frontend.conf" ]] \ + && [[ -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-api.conf" ]] +} + +# Генерирует ./nginx/conf.d при переходе с host Nginx или первом запуске Docker Nginx. +ensure_docker_nginx_configs() { + load_env + [[ "$(env_get NGINX_MODE docker)" == "docker" ]] || return 0 + [[ -n "$(env_get DOMAIN_API "")" && -n "$(env_get DOMAIN_FRONTEND "")" ]] || return 0 + [[ "$(env_get INSTALL_MODE local)" == "local" ]] && return 0 + docker_nginx_configs_present && return 0 + + warn "Конфиги Docker Nginx отсутствуют в ${LOCAL_CONF_DIR} — генерируем..." + DOMAIN_API="$(env_get DOMAIN_API "")" + DOMAIN_FRONTEND="$(env_get DOMAIN_FRONTEND "")" + DOMAIN_DOCS="$(env_get DOMAIN_DOCS "")" + DOMAIN_WS="$(env_get DOMAIN_WS "")" + DOMAIN_MINIO="$(env_get DOMAIN_MINIO "")" + DOMAIN_MINIO_CONSOLE="$(env_get DOMAIN_MINIO_CONSOLE "")" + NGINX_MODE="docker" + FORCE_DOCKER_NGINX=1 + + local ssl_type + ssl_type="$(env_get SSL_TYPE none)" + case "$ssl_type" in + none) + write_nginx_http_only + ;; + custom) + install_custom_certificates || warn "Не удалось установить custom-сертификаты" + write_all_nginx_configs "custom" + ;; + selfsigned) + generate_all_self_signed_certs || warn "Не удалось создать self-signed сертификаты" + write_all_nginx_configs "selfsigned" + ;; + letsencrypt) + write_all_nginx_configs "letsencrypt" + ;; + *) + write_nginx_http_only + ;; + esac + ok "Конфиги Docker Nginx записаны в ${LOCAL_CONF_DIR}" +} + +# HTTP-код через локальный lendry-id-nginx (--resolve 127.0.0.1, без DNS домена на сервере). +curl_local_nginx_code() { + local domain="$1" path="$2" use_ssl="${3:-1}" timeout="${4:-8}" + local port=443 + local -a curl_opts=(-sk --max-time "$timeout" -o /dev/null -w '%{http_code}') + if [[ "$use_ssl" != "1" ]]; then + port=80 + curl_opts=(-fsS --max-time "$timeout" -o /dev/null -w '%{http_code}') + fi + curl "${curl_opts[@]}" --resolve "${domain}:${port}:127.0.0.1" \ + "$([[ "$use_ssl" == "1" ]] && echo "https" || echo "http")://${domain}${path}" 2>/dev/null || echo 000 +} + check_local_upstream() { local label="$1" url="$2" if curl -fsS --max-time 3 "$url" >/dev/null 2>&1; then @@ -3745,7 +3837,8 @@ check_https_api_with_origin() { [[ "$(env_get USE_NGINX_SSL false)" == "true" ]] || return 0 origin="https://${frontend_domain}" - code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' -H "Origin: ${origin}" "https://${api_domain}/health" 2>/dev/null || echo 000)" + code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' -H "Origin: ${origin}" \ + --resolve "${api_domain}:443:127.0.0.1" "https://${api_domain}/health" 2>/dev/null || echo 000)" if [[ "$code" == "200" ]]; then echo -e " ${GREEN}✔${NC} HTTPS API + Origin: https://${api_domain}/health → 200" elif [[ "$code" == "502" ]]; then @@ -3757,7 +3850,8 @@ check_https_api_with_origin() { return 1 fi - code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' -H "Origin: ${origin}" "https://${api_domain}/notifications/unread-count" 2>/dev/null || echo 000)" + code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' -H "Origin: ${origin}" \ + --resolve "${api_domain}:443:127.0.0.1" "https://${api_domain}/notifications/unread-count" 2>/dev/null || echo 000)" if [[ "$code" == "401" || "$code" == "403" ]]; then echo -e " ${GREEN}✔${NC} HTTPS API + Origin (без токена): /notifications/unread-count → ${code}" elif [[ "$code" == "502" ]]; then @@ -3772,6 +3866,7 @@ check_https_api_with_origin() { code="$(curl -sk --max-time 5 -o /dev/null -w '%{http_code}' -X OPTIONS \ -H "Origin: ${origin}" \ -H "Access-Control-Request-Method: GET" \ + --resolve "${api_domain}:443:127.0.0.1" \ "https://${api_domain}/notifications/unread-count" 2>/dev/null || echo 000)" if [[ "$code" == "204" || "$code" == "200" ]]; then echo -e " ${GREEN}✔${NC} HTTPS API preflight (OPTIONS): /notifications/unread-count → ${code}" @@ -3840,12 +3935,24 @@ diagnose_common_issues() { fi if [[ "$(env_get NGINX_MODE docker)" == "docker" ]]; then shopt -s nullglob - if grep -q 'proxy_pass.*\$request_uri' "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null; then + local conf legacy_conf=0 + for conf in "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf; do + [[ -f "$conf" ]] || continue + if grep -q 'proxy_pass.*\$request_uri' "$conf" 2>/dev/null; then + legacy_conf=1 + break + fi + done + shopt -u nullglob + if [[ "$legacy_conf" -eq 1 ]]; then echo -e " ${RED}✘${NC} nginx/conf.d: proxy_pass \$request_uri — 502 на / (no resolver defined to resolve lendry-id-frontend)" echo " Решение: ./install.sh --fix-all" issues=$((issues + 1)) + elif ! docker_nginx_configs_present; then + echo -e " ${RED}✘${NC} nginx/conf.d пуст — Docker Nginx без vhost (502 в браузере)" + echo " Решение: ./install.sh --fix-all" + issues=$((issues + 1)) fi - shopt -u nullglob fi elif [[ "$(env_get NGINX_MODE docker)" == "docker" ]] && compose_uses_proxy_profile; then echo -e " ${YELLOW}!${NC} Docker Nginx не запущен (ожидался в режиме docker)" @@ -4271,14 +4378,15 @@ action_fix_all_errors() { echo " (502 Bad Gateway, /idp-api, Docker Nginx lendry-id-nginx, порты upstream)" echo "" - log "Шаг 1/7: диагностика..." - diagnose_common_issues || true - - log "Шаг 2/7: Docker Nginx — остановка system nginx, освобождение 80/443..." + log "Шаг 1/7: Docker Nginx — остановка system nginx, конфиги, освобождение 80/443..." ensure_docker_nginx_mode_for_fix_all + ensure_docker_nginx_configs write_compose_override ensure_split_domain_public_urls + log "Шаг 2/7: диагностика..." + diagnose_common_issues || true + log "Шаг 3/7: миграции PostgreSQL (Prisma)..." compose_build_and_up postgres 2>/dev/null || true run_prisma_migrations || warn "Миграции Prisma не применены — sso-core попробует при старте"