From 5a220917dc1bf22718b2e9fffa9d4997e2513bfd Mon Sep 17 00:00:00 2001 From: lendry Date: Mon, 29 Jun 2026 18:14:00 +0300 Subject: [PATCH] fix and update --- apps/api-gateway/src/main.ts | 4 +- apps/frontend/lib/api.ts | 15 ++- docker-compose.yml | 2 + install.sh | 218 +++++++++++++++++++++++++---------- 4 files changed, 169 insertions(+), 70 deletions(-) diff --git a/apps/api-gateway/src/main.ts b/apps/api-gateway/src/main.ts index 5457f16..350fc14 100644 --- a/apps/api-gateway/src/main.ts +++ b/apps/api-gateway/src/main.ts @@ -1,12 +1,14 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; +import { NestExpressApplication } from '@nestjs/platform-express'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import cookieParser from 'cookie-parser'; import { AppModule } from './app.module'; import { AllExceptionsFilter } from './grpc-exception.filter'; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); + app.set('trust proxy', 1); app.use(cookieParser()); app.enableCors({ origin: true, credentials: true }); app.useGlobalPipes( diff --git a/apps/frontend/lib/api.ts b/apps/frontend/lib/api.ts index 7275ecd..9c5c77e 100644 --- a/apps/frontend/lib/api.ts +++ b/apps/frontend/lib/api.ts @@ -612,16 +612,21 @@ export function buildSystemSettingPayload(setting: Pick(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise { const resolvedToken = token ?? (typeof window !== 'undefined' ? window.localStorage.getItem(AUTH_TOKEN_KEY) : null); + const method = (options.method ?? 'GET').toUpperCase(); + const hasBody = options.body !== undefined && options.body !== null; + const headers: Record = { + ...(hasBody || method === 'POST' || method === 'PUT' || method === 'PATCH' + ? { 'Content-Type': 'application/json' } + : {}), + ...(resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {}), + ...(options.headers as Record | undefined) + }; let response: Response; try { response = await fetch(`${getApiUrl()}${path}`, { ...options, - headers: { - 'Content-Type': 'application/json', - ...(resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {}), - ...options.headers - } + headers }); } catch (error) { throw mapFetchError(error); diff --git a/docker-compose.yml b/docker-compose.yml index b44785b..f36e047 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -136,6 +136,8 @@ services: MINIO_BUCKET: ${MINIO_BUCKET:-lendry-id} MINIO_USE_SSL: ${MINIO_USE_SSL:-false} PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3002/idp-api} + FEDCM_COOKIE_SECURE: ${FEDCM_COOKIE_SECURE:-false} + TRUST_PROXY: ${TRUST_PROXY:-true} ports: - "127.0.0.1:3000:3000" depends_on: diff --git a/install.sh b/install.sh index 2637da5..ffa32be 100644 --- a/install.sh +++ b/install.sh @@ -10,7 +10,7 @@ set -euo pipefail -SCRIPT_VERSION="2.3.1" +SCRIPT_VERSION="2.3.3" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" @@ -35,6 +35,8 @@ OFFLINE="${OFFLINE:-0}" NGINX_MODE="${NGINX_MODE:-auto}" SKIP_DOCKER_PRUNE="${SKIP_DOCKER_PRUNE:-0}" DOCKER_PRUNE_AGGRESSIVE="${DOCKER_PRUNE_AGGRESSIVE:-0}" +# Принудительный Docker Nginx (контейнер lendry-id-nginx) — включается в --fix-all +FORCE_DOCKER_NGINX="${FORCE_DOCKER_NGINX:-0}" CLI_DOMAIN_API="" CLI_DOMAIN_FRONTEND="" @@ -265,10 +267,65 @@ docker_nginx_ports_available() { ! port_in_use "$http" && ! port_in_use "$https" } +# Снимает конфиги IdP с системного Nginx (sites-enabled), не трогая ./nginx/conf.d +disable_host_nginx_lendry() { + log "Отключение конфигов IdP в системном Nginx (/etc/nginx)..." + run_root rm -f "${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true + if system_nginx_is_active; then + run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx 2>/dev/null || true + fi +} + +# --fix-all: всегда Docker Nginx (lendry-id-nginx), не системный на 80/443 +ensure_docker_nginx_mode_for_fix_all() { + load_env + FORCE_DOCKER_NGINX=1 + NGINX_MODE="docker" + env_set NGINX_MODE "docker" + + log "Переключение на Docker Nginx (контейнер lendry-id-nginx)..." + + disable_host_nginx_lendry + + NGINX_HTTP_PORT="${NGINX_HTTP_PORT:-$(env_get NGINX_HTTP_PORT 80)}" + NGINX_HTTPS_PORT="${NGINX_HTTPS_PORT:-$(env_get NGINX_HTTPS_PORT 443)}" + [[ -n "$CLI_NGINX_HTTP_PORT" ]] && NGINX_HTTP_PORT="$CLI_NGINX_HTTP_PORT" + [[ -n "$CLI_NGINX_HTTPS_PORT" ]] && NGINX_HTTPS_PORT="$CLI_NGINX_HTTPS_PORT" + + if system_nginx_is_active \ + || port_in_use "$NGINX_HTTP_PORT" \ + || port_in_use "$NGINX_HTTPS_PORT"; then + log "Остановка системного Nginx (освобождение портов ${NGINX_HTTP_PORT}/${NGINX_HTTPS_PORT})..." + run_root systemctl stop nginx 2>/dev/null || run_root service nginx stop 2>/dev/null || true + sleep 2 + fi + + docker_cmd rm -f lendry-id-nginx 2>/dev/null || true + + env_set NGINX_HTTP_PORT "$NGINX_HTTP_PORT" + env_set NGINX_HTTPS_PORT "$NGINX_HTTPS_PORT" + + if port_in_use "$NGINX_HTTP_PORT" || port_in_use "$NGINX_HTTPS_PORT"; then + warn "Порты ${NGINX_HTTP_PORT}/${NGINX_HTTPS_PORT} всё ещё заняты после остановки system nginx" + if has_cmd ss; then + ss -tlnp 2>/dev/null | grep -E ":${NGINX_HTTP_PORT}\s|:${NGINX_HTTPS_PORT}\s" || true + fi + fail "Освободите порты ${NGINX_HTTP_PORT}/${NGINX_HTTPS_PORT} и повторите: ./install.sh --fix-all" + fi + + ok "NGINX_MODE=docker — порты ${NGINX_HTTP_PORT}/${NGINX_HTTPS_PORT} готовы для lendry-id-nginx" +} + # Не даём контейнерному nginx занять 80/443, если их уже держит системный nginx # или явно выбран NGINX_MODE=host (типичная причина «address already in use»). ensure_docker_nginx_not_conflicting() { load_env + + if [[ "${FORCE_DOCKER_NGINX:-0}" -eq 1 ]]; then + NGINX_MODE="docker" + return 0 + fi + resolve_nginx_mode_and_ports local http="${NGINX_HTTP_PORT:-80}" @@ -313,6 +370,20 @@ prepare_compose_stack() { resolve_nginx_mode_and_ports() { load_env + + if [[ "${FORCE_DOCKER_NGINX:-0}" -eq 1 ]]; then + NGINX_MODE="docker" + NGINX_HTTP_PORT="${NGINX_HTTP_PORT:-$(env_get NGINX_HTTP_PORT 80)}" + NGINX_HTTPS_PORT="${NGINX_HTTPS_PORT:-$(env_get NGINX_HTTPS_PORT 443)}" + [[ -n "$CLI_NGINX_HTTP_PORT" ]] && NGINX_HTTP_PORT="$CLI_NGINX_HTTP_PORT" + [[ -n "$CLI_NGINX_HTTPS_PORT" ]] && NGINX_HTTPS_PORT="$CLI_NGINX_HTTPS_PORT" + env_set NGINX_MODE "docker" + env_set NGINX_HTTP_PORT "${NGINX_HTTP_PORT}" + env_set NGINX_HTTPS_PORT "${NGINX_HTTPS_PORT}" + log "Nginx: mode=docker (принудительно), HTTP=${NGINX_HTTP_PORT}, HTTPS=${NGINX_HTTPS_PORT}" + return 0 + fi + detect_nginx_mode if system_nginx_is_active || lendry_host_nginx_configured; then @@ -835,6 +906,10 @@ derive_public_urls() { fi fi + if [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then + env_set FEDCM_COOKIE_SECURE "true" + fi + SSL_TYPE="$ssl_type" USE_NGINX_SSL=$([[ "$ssl_type" == "none" ]] && echo false || echo true) @@ -1212,6 +1287,10 @@ persist_domain_env() { env_set MINIO_PUBLIC_ENDPOINT "${MINIO_PUBLIC_ENDPOINT:-localhost:9000}" env_set MINIO_USE_SSL "${MINIO_USE_SSL:-false}" + if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then + env_set FEDCM_COOKIE_SECURE "true" + fi + if [[ "$(env_get INSTALL_MODE local)" == "intranet" && -z "$(env_get LDAP_USE_HOST_NETWORK "")" ]]; then env_set LDAP_USE_HOST_NETWORK "true" fi @@ -1915,31 +1994,27 @@ EOF } # Блок location / для отдельного домена API (api.example.com). -# Так как фронтенд и API на разных доменах (split-domain), браузер шлёт -# cross-origin запросы и preflight (OPTIONS). Nginx сам отвечает на preflight -# и проставляет CORS-заголовки, отражая Origin (нужно для credentials: cookie/JWT). -# Заголовки CORS от upstream скрываем, чтобы не было дублей Access-Control-Allow-Origin. +# CORS полностью на api-gateway (enableCors) — nginx только проксирует, +# иначе add_header/if в nginx ломают ответы на авторизованные cross-origin запросы (502). +build_nginx_api_upstream_block() { + local upstream + if [[ "$NGINX_MODE" == "docker" ]]; then + upstream="api-gateway:3000" + else + upstream="127.0.0.1:3000" + fi + cat </dev/null || true - if [[ "$NGINX_MODE" == "host" ]]; then + if [[ "$NGINX_MODE" == "host" ]] || [[ "${FORCE_DOCKER_NGINX:-0}" -eq 1 ]]; then run_root rm -f \ "${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \ "${NGINX_AVAILABLE}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true @@ -2636,7 +2714,6 @@ check_https_api_with_origin() { 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)" if [[ "$code" == "401" || "$code" == "403" ]]; then echo -e " ${GREEN}✔${NC} HTTPS API + Origin (без токена): /notifications/unread-count → ${code}" - return 0 elif [[ "$code" == "502" ]]; then echo -e " ${RED}✘${NC} HTTPS API + Origin: /notifications/unread-count → 502 (upstream api-gateway)" echo " Проверьте: curl -s http://127.0.0.1:3000/health/ready && docker compose logs --tail=50 api-gateway sso-core" @@ -2645,6 +2722,30 @@ check_https_api_with_origin() { echo -e " ${YELLOW}!${NC} HTTPS API + Origin: /notifications/unread-count → HTTP ${code} (ожидалось 401)" return 1 fi + + local test_token + test_token="$(env_get IDP_TEST_BEARER_TOKEN "")" + if [[ -n "$test_token" ]]; then + code="$(curl -sk --max-time 8 -o /dev/null -w '%{http_code}' \ + -H "Origin: ${origin}" \ + -H "Authorization: Bearer ${test_token}" \ + -H "Content-Type: application/json" \ + "https://${api_domain}/notifications/unread-count" 2>/dev/null || echo 000)" + if [[ "$code" == "200" || "$code" == "403" ]]; then + echo -e " ${GREEN}✔${NC} HTTPS API + Bearer token: /notifications/unread-count → ${code}" + elif [[ "$code" == "502" ]]; then + echo -e " ${RED}✘${NC} HTTPS API + Bearer token → 502 (как в браузере)" + echo " docker compose logs --tail=80 api-gateway sso-core" + return 1 + else + echo -e " ${YELLOW}!${NC} HTTPS API + Bearer token → HTTP ${code}" + fi + else + echo -e " ${YELLOW}!${NC} Для проверки с токеном: IDP_TEST_BEARER_TOKEN= ./install.sh --fix-all" + echo " (скопируйте lendry_access_token из DevTools → Application → Local Storage на sso)" + fi + + return 0 } # Диагностика типичных проблем: 502 на /idp-api, конфликт nginx, порты upstream. @@ -2990,15 +3091,14 @@ action_fix_all_errors() { echo "" echo -e "${BOLD}Комплексное исправление ошибок IdP${NC}" - echo " (502 Bad Gateway, /idp-api, конфликт Nginx, порты 3000/3002/3003)" + echo " (502 Bad Gateway, /idp-api, Docker Nginx lendry-id-nginx, порты upstream)" echo "" log "Шаг 1/6: диагностика..." diagnose_common_issues || true - log "Шаг 2/6: определение режима Nginx и устранение конфликта портов 80/443..." - resolve_nginx_mode_and_ports - ensure_docker_nginx_not_conflicting + log "Шаг 2/6: Docker Nginx — остановка system nginx, освобождение 80/443..." + ensure_docker_nginx_mode_for_fix_all write_compose_override ensure_split_domain_public_urls @@ -3014,40 +3114,30 @@ action_fix_all_errors() { wait_for_service "frontend" 60 wait_for_service "docs" 60 - if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then - log "Шаг 5/6: host Nginx — порты 127.0.0.1 и конфиги..." - ensure_host_upstreams - refresh_host_nginx_configs - apply_intranet_selfsigned_runtime - compose_build_and_up ldap-auth - else - log "Шаг 5/6: Docker Nginx — перегенерация конфигов и перезапуск..." - local ssl_type - ssl_type="$(env_get SSL_TYPE none)" - case "$ssl_type" in - none) write_nginx_http_only || warn "Не удалось записать HTTP-конфиги Nginx" ;; - selfsigned) - generate_all_self_signed_certs - write_all_nginx_configs "selfsigned" || warn "Не удалось обновить конфиги Nginx" - ;; - custom) - install_custom_certificates - write_all_nginx_configs "custom" || warn "Не удалось обновить конфиги Nginx" - ;; - letsencrypt) - write_all_nginx_configs "letsencrypt" || warn "Не удалось обновить конфиги Nginx" - ;; - esac - if compose_uses_proxy_profile; then - compose_build_and_up nginx - wait_for_service "nginx" 30 - else - warn "Docker Nginx не запущен — порты 80/443 заняты; используется системный Nginx" - env_set NGINX_MODE "host" - NGINX_MODE="host" - refresh_host_nginx_configs - fi + log "Шаг 5/6: Docker Nginx — конфиги в ./nginx/conf.d и контейнер lendry-id-nginx..." + local ssl_type + ssl_type="$(env_get SSL_TYPE none)" + case "$ssl_type" in + none) write_nginx_http_only || warn "Не удалось записать HTTP-конфиги Nginx" ;; + selfsigned) + generate_all_self_signed_certs + write_all_nginx_configs "selfsigned" || warn "Не удалось обновить конфиги Nginx" + ;; + custom) + install_custom_certificates + write_all_nginx_configs "custom" || warn "Не удалось обновить конфиги Nginx" + ;; + letsencrypt) + write_all_nginx_configs "letsencrypt" || warn "Не удалось обновить конфиги Nginx" + ;; + esac + apply_intranet_selfsigned_runtime + compose_build_and_up nginx ldap-auth + wait_for_service "nginx" 45 + if ! docker_nginx_container_running; then + fail "Контейнер lendry-id-nginx не запущен. Проверьте: docker compose --profile proxy logs nginx" fi + ok "Docker Nginx запущен — проверьте: docker ps | grep lendry-id-nginx" log "Шаг 6/6: финальная проверка..." diagnose_common_issues || true @@ -3096,7 +3186,7 @@ show_menu() { echo " 6) Удалить конфиги Nginx IdP" echo " 7) Сброс PostgreSQL (если ошибка P1000 / пароль БД)" echo " 8) Очистить Docker (образы/кеш, без пересборки)" - echo " 9) Исправить все ошибки (502, /idp-api, Nginx, порты)" + echo " 9) Исправить все ошибки (502, Docker Nginx, порты upstream)" echo " 0) Выход" echo ""