fix and update

This commit is contained in:
lendry
2026-06-29 18:14:00 +03:00
parent 71dfeda873
commit 5a220917dc
4 changed files with 169 additions and 70 deletions

View File

@@ -1,12 +1,14 @@
import { ValidationPipe } from '@nestjs/common'; import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core'; import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import { AppModule } from './app.module'; import { AppModule } from './app.module';
import { AllExceptionsFilter } from './grpc-exception.filter'; import { AllExceptionsFilter } from './grpc-exception.filter';
async function bootstrap() { async function bootstrap() {
const app = await NestFactory.create(AppModule); const app = await NestFactory.create<NestExpressApplication>(AppModule);
app.set('trust proxy', 1);
app.use(cookieParser()); app.use(cookieParser());
app.enableCors({ origin: true, credentials: true }); app.enableCors({ origin: true, credentials: true });
app.useGlobalPipes( app.useGlobalPipes(

View File

@@ -612,16 +612,21 @@ export function buildSystemSettingPayload(setting: Pick<SystemSetting, 'key' | '
export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> { export async function apiFetch<T>(path: string, options: RequestInit = {}, token?: string | null, allowRetry = true): Promise<T> {
const resolvedToken = token ?? (typeof window !== 'undefined' ? window.localStorage.getItem(AUTH_TOKEN_KEY) : null); 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<string, string> = {
...(hasBody || method === 'POST' || method === 'PUT' || method === 'PATCH'
? { 'Content-Type': 'application/json' }
: {}),
...(resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {}),
...(options.headers as Record<string, string> | undefined)
};
let response: Response; let response: Response;
try { try {
response = await fetch(`${getApiUrl()}${path}`, { response = await fetch(`${getApiUrl()}${path}`, {
...options, ...options,
headers: { headers
'Content-Type': 'application/json',
...(resolvedToken ? { Authorization: `Bearer ${resolvedToken}` } : {}),
...options.headers
}
}); });
} catch (error) { } catch (error) {
throw mapFetchError(error); throw mapFetchError(error);

View File

@@ -136,6 +136,8 @@ services:
MINIO_BUCKET: ${MINIO_BUCKET:-lendry-id} MINIO_BUCKET: ${MINIO_BUCKET:-lendry-id}
MINIO_USE_SSL: ${MINIO_USE_SSL:-false} MINIO_USE_SSL: ${MINIO_USE_SSL:-false}
PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3002/idp-api} PUBLIC_API_URL: ${PUBLIC_API_URL:-http://localhost:3002/idp-api}
FEDCM_COOKIE_SECURE: ${FEDCM_COOKIE_SECURE:-false}
TRUST_PROXY: ${TRUST_PROXY:-true}
ports: ports:
- "127.0.0.1:3000:3000" - "127.0.0.1:3000:3000"
depends_on: depends_on:

View File

@@ -10,7 +10,7 @@
set -euo pipefail set -euo pipefail
SCRIPT_VERSION="2.3.1" SCRIPT_VERSION="2.3.3"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$ROOT_DIR" cd "$ROOT_DIR"
@@ -35,6 +35,8 @@ OFFLINE="${OFFLINE:-0}"
NGINX_MODE="${NGINX_MODE:-auto}" NGINX_MODE="${NGINX_MODE:-auto}"
SKIP_DOCKER_PRUNE="${SKIP_DOCKER_PRUNE:-0}" SKIP_DOCKER_PRUNE="${SKIP_DOCKER_PRUNE:-0}"
DOCKER_PRUNE_AGGRESSIVE="${DOCKER_PRUNE_AGGRESSIVE:-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_API=""
CLI_DOMAIN_FRONTEND="" CLI_DOMAIN_FRONTEND=""
@@ -265,10 +267,65 @@ docker_nginx_ports_available() {
! port_in_use "$http" && ! port_in_use "$https" ! 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 занять 80/443, если их уже держит системный nginx
# или явно выбран NGINX_MODE=host (типичная причина «address already in use»). # или явно выбран NGINX_MODE=host (типичная причина «address already in use»).
ensure_docker_nginx_not_conflicting() { ensure_docker_nginx_not_conflicting() {
load_env load_env
if [[ "${FORCE_DOCKER_NGINX:-0}" -eq 1 ]]; then
NGINX_MODE="docker"
return 0
fi
resolve_nginx_mode_and_ports resolve_nginx_mode_and_ports
local http="${NGINX_HTTP_PORT:-80}" local http="${NGINX_HTTP_PORT:-80}"
@@ -313,6 +370,20 @@ prepare_compose_stack() {
resolve_nginx_mode_and_ports() { resolve_nginx_mode_and_ports() {
load_env 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 detect_nginx_mode
if system_nginx_is_active || lendry_host_nginx_configured; then if system_nginx_is_active || lendry_host_nginx_configured; then
@@ -835,6 +906,10 @@ derive_public_urls() {
fi fi
fi fi
if [[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" || "$ssl_type" == "custom" ]]; then
env_set FEDCM_COOKIE_SECURE "true"
fi
SSL_TYPE="$ssl_type" SSL_TYPE="$ssl_type"
USE_NGINX_SSL=$([[ "$ssl_type" == "none" ]] && echo false || echo true) 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_PUBLIC_ENDPOINT "${MINIO_PUBLIC_ENDPOINT:-localhost:9000}"
env_set MINIO_USE_SSL "${MINIO_USE_SSL:-false}" 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 if [[ "$(env_get INSTALL_MODE local)" == "intranet" && -z "$(env_get LDAP_USE_HOST_NETWORK "")" ]]; then
env_set LDAP_USE_HOST_NETWORK "true" env_set LDAP_USE_HOST_NETWORK "true"
fi fi
@@ -1915,31 +1994,27 @@ EOF
} }
# Блок location / для отдельного домена API (api.example.com). # Блок location / для отдельного домена API (api.example.com).
# Так как фронтенд и API на разных доменах (split-domain), браузер шлёт # CORS полностью на api-gateway (enableCors) — nginx только проксирует,
# cross-origin запросы и preflight (OPTIONS). Nginx сам отвечает на preflight # иначе add_header/if в nginx ломают ответы на авторизованные cross-origin запросы (502).
# и проставляет CORS-заголовки, отражая Origin (нужно для credentials: cookie/JWT). build_nginx_api_upstream_block() {
# Заголовки CORS от upstream скрываем, чтобы не было дублей Access-Control-Allow-Origin. local upstream
if [[ "$NGINX_MODE" == "docker" ]]; then
upstream="api-gateway:3000"
else
upstream="127.0.0.1:3000"
fi
cat <<EOF
upstream ${NGINX_PREFIX}_api {
server ${upstream};
keepalive 64;
}
EOF
}
build_nginx_api_location_block() { build_nginx_api_location_block() {
local proxy_api
proxy_api="$(render_proxy_root api)"
cat <<EOF cat <<EOF
location / { location / {
if (\$request_method = OPTIONS) { proxy_pass http://${NGINX_PREFIX}_api;
add_header Access-Control-Allow-Origin \$http_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type, X-Requested-With, Accept, Origin, X-CSRF-Token" always;
add_header Access-Control-Max-Age 86400 always;
add_header Content-Length 0;
return 204;
}
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Credentials;
add_header Access-Control-Allow-Origin \$http_origin always;
add_header Access-Control-Allow-Credentials true always;
${proxy_api}
proxy_http_version 1.1; proxy_http_version 1.1;
proxy_set_header Connection ""; proxy_set_header Connection "";
${nginx_proxy_headers} ${nginx_proxy_headers}
@@ -1951,14 +2026,16 @@ write_nginx_site_api() {
local domain="$1" local domain="$1"
local ssl_type="${2:-none}" local ssl_type="${2:-none}"
local ws_on_api="${3:-true}" local ws_on_api="${3:-true}"
local conf ws_block http_preamble ssl_block api_location local conf ws_block http_preamble ssl_block api_upstream api_location
conf="$(nginx_conf_target api)" conf="$(nginx_conf_target api)"
ws_block="$(build_ws_block "$ws_on_api")" ws_block="$(build_ws_block "$ws_on_api")"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")" http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
api_upstream="$(build_nginx_api_upstream_block)"
api_location="$(build_nginx_api_location_block)" api_location="$(build_nginx_api_location_block)"
if [[ "$ssl_type" == "none" ]]; then if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF nginx_write_conf "$conf" <<EOF
${api_upstream}
server { server {
listen 80; listen 80;
listen [::]:80; listen [::]:80;
@@ -1971,6 +2048,7 @@ EOF
else else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")" ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF nginx_write_conf "$conf" <<EOF
${api_upstream}
${http_preamble}server { ${http_preamble}server {
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
@@ -2269,7 +2347,7 @@ EOF
remove_nginx_configs() { remove_nginx_configs() {
log "Удаление конфигов Nginx IdP..." log "Удаление конфигов Nginx IdP..."
rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true
if [[ "$NGINX_MODE" == "host" ]]; then if [[ "$NGINX_MODE" == "host" ]] || [[ "${FORCE_DOCKER_NGINX:-0}" -eq 1 ]]; then
run_root rm -f \ run_root rm -f \
"${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \ "${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \
"${NGINX_AVAILABLE}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true "${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)" 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 if [[ "$code" == "401" || "$code" == "403" ]]; then
echo -e " ${GREEN}${NC} HTTPS API + Origin (без токена): /notifications/unread-count → ${code}" echo -e " ${GREEN}${NC} HTTPS API + Origin (без токена): /notifications/unread-count → ${code}"
return 0
elif [[ "$code" == "502" ]]; then elif [[ "$code" == "502" ]]; then
echo -e " ${RED}${NC} HTTPS API + Origin: /notifications/unread-count → 502 (upstream api-gateway)" 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" 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)" echo -e " ${YELLOW}!${NC} HTTPS API + Origin: /notifications/unread-count → HTTP ${code} (ожидалось 401)"
return 1 return 1
fi 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=<jwt> ./install.sh --fix-all"
echo " (скопируйте lendry_access_token из DevTools → Application → Local Storage на sso)"
fi
return 0
} }
# Диагностика типичных проблем: 502 на /idp-api, конфликт nginx, порты upstream. # Диагностика типичных проблем: 502 на /idp-api, конфликт nginx, порты upstream.
@@ -2990,15 +3091,14 @@ action_fix_all_errors() {
echo "" echo ""
echo -e "${BOLD}Комплексное исправление ошибок IdP${NC}" 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 "" echo ""
log "Шаг 1/6: диагностика..." log "Шаг 1/6: диагностика..."
diagnose_common_issues || true diagnose_common_issues || true
log "Шаг 2/6: определение режима Nginx и устранение конфликта портов 80/443..." log "Шаг 2/6: Docker Nginx остановка system nginx, освобождение 80/443..."
resolve_nginx_mode_and_ports ensure_docker_nginx_mode_for_fix_all
ensure_docker_nginx_not_conflicting
write_compose_override write_compose_override
ensure_split_domain_public_urls ensure_split_domain_public_urls
@@ -3014,40 +3114,30 @@ action_fix_all_errors() {
wait_for_service "frontend" 60 wait_for_service "frontend" 60
wait_for_service "docs" 60 wait_for_service "docs" 60
if [[ "$(env_get NGINX_MODE docker)" == "host" ]]; then log "Шаг 5/6: Docker Nginx — конфиги в ./nginx/conf.d и контейнер lendry-id-nginx..."
log "Шаг 5/6: host Nginx — порты 127.0.0.1 и конфиги..." local ssl_type
ensure_host_upstreams ssl_type="$(env_get SSL_TYPE none)"
refresh_host_nginx_configs case "$ssl_type" in
apply_intranet_selfsigned_runtime none) write_nginx_http_only || warn "Не удалось записать HTTP-конфиги Nginx" ;;
compose_build_and_up ldap-auth selfsigned)
else generate_all_self_signed_certs
log "Шаг 5/6: Docker Nginx — перегенерация конфигов и перезапуск..." write_all_nginx_configs "selfsigned" || warn "Не удалось обновить конфиги Nginx"
local ssl_type ;;
ssl_type="$(env_get SSL_TYPE none)" custom)
case "$ssl_type" in install_custom_certificates
none) write_nginx_http_only || warn "Не удалось записать HTTP-конфиги Nginx" ;; write_all_nginx_configs "custom" || warn "Не удалось обновить конфиги Nginx"
selfsigned) ;;
generate_all_self_signed_certs letsencrypt)
write_all_nginx_configs "selfsigned" || warn "Не удалось обновить конфиги Nginx" write_all_nginx_configs "letsencrypt" || warn "Не удалось обновить конфиги Nginx"
;; ;;
custom) esac
install_custom_certificates apply_intranet_selfsigned_runtime
write_all_nginx_configs "custom" || warn "Не удалось обновить конфиги Nginx" compose_build_and_up nginx ldap-auth
;; wait_for_service "nginx" 45
letsencrypt) if ! docker_nginx_container_running; then
write_all_nginx_configs "letsencrypt" || warn "Не удалось обновить конфиги Nginx" fail "Контейнер lendry-id-nginx не запущен. Проверьте: docker compose --profile proxy logs 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
fi fi
ok "Docker Nginx запущен — проверьте: docker ps | grep lendry-id-nginx"
log "Шаг 6/6: финальная проверка..." log "Шаг 6/6: финальная проверка..."
diagnose_common_issues || true diagnose_common_issues || true
@@ -3096,7 +3186,7 @@ show_menu() {
echo " 6) Удалить конфиги Nginx IdP" echo " 6) Удалить конфиги Nginx IdP"
echo " 7) Сброс PostgreSQL (если ошибка P1000 / пароль БД)" echo " 7) Сброс PostgreSQL (если ошибка P1000 / пароль БД)"
echo " 8) Очистить Docker (образы/кеш, без пересборки)" echo " 8) Очистить Docker (образы/кеш, без пересборки)"
echo " 9) Исправить все ошибки (502, /idp-api, Nginx, порты)" echo " 9) Исправить все ошибки (502, Docker Nginx, порты upstream)"
echo " 0) Выход" echo " 0) Выход"
echo "" echo ""