#!/usr/bin/env bash # Lendry ID — установка, Nginx, SSL и управление одной командой # # Использование: # chmod +x install.sh && ./install.sh # ./install.sh --install --api-domain api.example.com --frontend-domain id.example.com \ # --docs-domain docs.example.com --email admin@example.com # ./install.sh --configure-domains # изменить домены и пересобрать # ./install.sh --local # локально без Nginx set -euo pipefail SCRIPT_VERSION="2.0.0" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" NGINX_PREFIX="lendry-id" NGINX_AVAILABLE="/etc/nginx/sites-available" NGINX_ENABLED="/etc/nginx/sites-enabled" CERTBOT_WEBROOT="/var/www/certbot" COMPOSE_OVERRIDE="${ROOT_DIR}/docker-compose.override.yml" STATE_FILE="${ROOT_DIR}/.idp-install.json" NON_INTERACTIVE=0 FORCE_ENV="${FORCE_ENV:-0}" ACTION="" INSTALL_LOCAL=0 CLI_DOMAIN_API="" CLI_DOMAIN_FRONTEND="" CLI_DOMAIN_DOCS="" CLI_DOMAIN_WS="" CLI_DOMAIN_MINIO="" CLI_DOMAIN_MINIO_CONSOLE="" CLI_EMAIL="" RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' log() { echo -e "${CYAN}[IdP]${NC} $*"; } ok() { echo -e "${GREEN}[IdP]${NC} $*"; } warn() { echo -e "${YELLOW}[IdP]${NC} $*"; } fail() { echo -e "${RED}[IdP] ОШИБКА:${NC} $*" >&2; exit 1; } usage() { cat </dev/null 2>&1; } detect_os() { if [[ -f /etc/os-release ]]; then # shellcheck disable=SC1091 . /etc/os-release OS_ID="${ID:-unknown}" OS_VERSION_ID="${VERSION_ID:-}" else OS_ID="unknown" fi case "$OS_ID" in ubuntu | debian | linuxmint | pop) PKG_MANAGER="apt" ;; fedora | rhel | centos | rocky | almalinux) PKG_MANAGER="dnf" ;; opensuse* | sles) PKG_MANAGER="zypper" ;; *) PKG_MANAGER="unknown" ;; esac log "ОС: ${OS_ID:-unknown} ${OS_VERSION_ID:-} (пакеты: $PKG_MANAGER)" } run_root() { if [[ "$(id -u)" -eq 0 ]]; then "$@" elif has_cmd sudo; then sudo "$@" else fail "Нужны права root/sudo для: $*" fi } preflight_checks() { [[ "$(uname -s)" == "Linux" ]] || warn "Скрипт рассчитан на Linux. На других ОС установите Docker вручную." [[ -f docker-compose.yml ]] || fail "Запустите скрипт из корня репозитория (рядом с docker-compose.yml)" } install_packages_apt() { run_root apt-get update -qq run_root apt-get install -y -qq ca-certificates curl git openssl gnupg lsb-release jq } install_packages_dnf() { run_root dnf install -y ca-certificates curl git openssl jq } install_base_packages() { log "Проверка базовых пакетов..." case "$PKG_MANAGER" in apt) install_packages_apt ;; dnf) install_packages_dnf ;; *) warn "Автоустановка пакетов для $OS_ID не поддерживается. Нужны: curl, git, openssl, jq" ;; esac } install_docker() { if has_cmd docker && docker info >/dev/null 2>&1; then ok "Docker: $(docker --version)" return fi if has_cmd docker && ! docker info >/dev/null 2>&1; then warn "Docker найден, но daemon недоступен. Пробуем запустить..." run_root systemctl start docker 2>/dev/null || true if docker info >/dev/null 2>&1; then ok "Docker daemon запущен" return fi fi log "Установка Docker..." curl -fsSL https://get.docker.com | run_root sh run_root systemctl enable docker >/dev/null 2>&1 || true run_root systemctl start docker >/dev/null 2>&1 || true if [[ "$(id -u)" -ne 0 ]] && has_cmd sudo; then if ! groups "$USER" | grep -q docker; then run_root usermod -aG docker "$USER" || true warn "Пользователь $USER добавлен в группу docker. Может потребоваться перелогиниться." export DOCKER_SUGGESTED_GROUP=1 fi fi ok "Docker установлен" } check_docker_compose() { if docker compose version >/dev/null 2>&1; then ok "Docker Compose: $(docker compose version --short 2>/dev/null || docker compose version)" return fi fail "Docker Compose v2 не найден. Переустановите Docker." } docker_cmd() { if docker info >/dev/null 2>&1; then docker "$@" elif has_cmd sudo && sudo docker info >/dev/null 2>&1; then sudo docker "$@" else fail "Docker daemon недоступен. Запустите: sudo systemctl start docker" fi } install_nginx_certbot() { if has_cmd nginx && has_cmd certbot; then ok "Nginx и Certbot уже установлены" return fi log "Установка Nginx и Certbot..." case "$PKG_MANAGER" in apt) run_root apt-get install -y -qq nginx certbot python3-certbot-nginx ;; dnf) run_root dnf install -y nginx certbot ;; *) fail "Автоустановка Nginx/Certbot для $OS_ID не поддерживается" ;; esac run_root mkdir -p "$CERTBOT_WEBROOT" run_root chown -R www-data:www-data "$CERTBOT_WEBROOT" 2>/dev/null \ || run_root chown -R nginx:nginx "$CERTBOT_WEBROOT" 2>/dev/null \ || true run_root systemctl enable nginx >/dev/null 2>&1 || true ok "Nginx и Certbot установлены" } setup_firewall() { if ! has_cmd ufw; then warn "UFW не найден — пропускаем настройку фаервола" return fi if ! run_root ufw status 2>/dev/null | grep -q "Status: active"; then warn "UFW не активен — откройте порты 80 и 443 вручную при необходимости" return fi log "Настройка UFW (80, 443, SSH)..." run_root ufw allow 22/tcp >/dev/null 2>&1 || true run_root ufw allow 80/tcp >/dev/null 2>&1 || true run_root ufw allow 443/tcp >/dev/null 2>&1 || true ok "Порты 80 и 443 разрешены в UFW" } rand_secret() { local length="${1:-48}" if has_cmd openssl; then openssl rand -base64 64 | tr -dc 'A-Za-z0-9' | head -c "$length" else tr -dc 'A-Za-z0-9' >.env else printf '%s=%s\n' "$key" "$value" >.env fi chmod 600 .env 2>/dev/null || true } derive_public_urls() { local use_ssl="${1:-false}" if [[ "$INSTALL_LOCAL" -eq 1 || "$use_ssl" != "true" ]]; then if [[ -z "${DOMAIN_API:-}" && -z "${DOMAIN_FRONTEND:-}" ]]; then PUBLIC_API_URL="http://localhost:3000" PUBLIC_FRONTEND_URL="http://localhost:3002" PUBLIC_DOCS_URL="http://localhost:3003" PUBLIC_WS_URL="ws://localhost:8085/ws" MINIO_PUBLIC_ENDPOINT="${MINIO_PUBLIC_ENDPOINT:-localhost:9000}" MINIO_USE_SSL="false" USE_NGINX_SSL="false" INSTALL_MODE="local" return fi fi local scheme="https" [[ "$use_ssl" == "true" ]] || scheme="http" PUBLIC_API_URL="${scheme}://${DOMAIN_API}" PUBLIC_FRONTEND_URL="${scheme}://${DOMAIN_FRONTEND}" PUBLIC_DOCS_URL="${scheme}://${DOMAIN_DOCS}" if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then if [[ "$scheme" == "https" ]]; then PUBLIC_WS_URL="wss://${DOMAIN_WS}/ws" else PUBLIC_WS_URL="ws://${DOMAIN_WS}/ws" fi else if [[ "$scheme" == "https" ]]; then PUBLIC_WS_URL="wss://${DOMAIN_API}/ws" else PUBLIC_WS_URL="ws://${DOMAIN_API}/ws" fi fi if [[ -n "${DOMAIN_MINIO:-}" ]]; then MINIO_PUBLIC_ENDPOINT="${DOMAIN_MINIO}" MINIO_USE_SSL=$([[ "$scheme" == "https" ]] && echo true || echo false) fi USE_NGINX_SSL=$([[ "$use_ssl" == "true" ]] && echo true || echo false) if [[ "$INSTALL_LOCAL" -eq 1 ]]; then INSTALL_MODE="local" elif [[ -n "${DOMAIN_API:-}" ]]; then INSTALL_MODE="production" else INSTALL_MODE="local" fi } configure_domains_interactive() { local existing_api existing_frontend existing_docs existing_ws existing_email existing_api="$(normalize_domain "$(env_get DOMAIN_API "")")" existing_frontend="$(normalize_domain "$(env_get DOMAIN_FRONTEND "")")" existing_docs="$(normalize_domain "$(env_get DOMAIN_DOCS "")")" existing_ws="$(normalize_domain "$(env_get DOMAIN_WS "")")" existing_email="$(env_get CERTBOT_EMAIL "")" local default_api="${CLI_DOMAIN_API:-$existing_api}" local default_frontend="${CLI_DOMAIN_FRONTEND:-$existing_frontend}" local default_docs="${CLI_DOMAIN_DOCS:-$existing_docs}" local default_ws="${CLI_DOMAIN_WS:-$existing_ws}" local default_minio="${CLI_DOMAIN_MINIO:-$(env_get DOMAIN_MINIO "")}" local default_minio_console="${CLI_DOMAIN_MINIO_CONSOLE:-$(env_get DOMAIN_MINIO_CONSOLE "")}" local default_email="${CLI_EMAIL:-$existing_email}" if [[ "$INSTALL_LOCAL" -eq 1 ]]; then DOMAIN_API="" DOMAIN_FRONTEND="" DOMAIN_DOCS="" DOMAIN_WS="" DOMAIN_MINIO="" DOMAIN_MINIO_CONSOLE="" derive_public_urls false return fi echo "" echo -e "${BOLD}Настройка доменов${NC}" echo "Укажите отдельный домен для каждого сервиса (A-запись → IP этого сервера)." echo "Enter — оставить текущее значение. Для локальной установки: ./install.sh --local" echo "" if [[ -z "$default_api" ]]; then default_api="api.example.com"; fi if [[ -z "$default_frontend" ]]; then default_frontend="id.example.com"; fi if [[ -z "$default_docs" ]]; then default_docs="docs.example.com"; fi prompt DOMAIN_API "Домен API / Swagger" "$default_api" prompt DOMAIN_FRONTEND "Домен Frontend (UI)" "$default_frontend" prompt DOMAIN_DOCS "Домен документации" "$default_docs" prompt_optional DOMAIN_WS "Домен WebSocket (пусто = ${DOMAIN_API}/ws)" "$default_ws" prompt_optional DOMAIN_MINIO "Домен MinIO S3 (опционально)" "$default_minio" prompt_optional DOMAIN_MINIO_CONSOLE "Домен MinIO Console (опционально)" "$default_minio_console" DOMAIN_API="$(normalize_domain "$DOMAIN_API")" DOMAIN_FRONTEND="$(normalize_domain "$DOMAIN_FRONTEND")" DOMAIN_DOCS="$(normalize_domain "$DOMAIN_DOCS")" DOMAIN_WS="$(normalize_domain "$DOMAIN_WS")" DOMAIN_MINIO="$(normalize_domain "$DOMAIN_MINIO")" DOMAIN_MINIO_CONSOLE="$(normalize_domain "$DOMAIN_MINIO_CONSOLE")" for d in "$DOMAIN_API" "$DOMAIN_FRONTEND" "$DOMAIN_DOCS" "$DOMAIN_WS" "$DOMAIN_MINIO" "$DOMAIN_MINIO_CONSOLE"; do [[ -z "$d" ]] && continue validate_domain "$d" || fail "Некорректный домен: $d" done [[ -n "$DOMAIN_API" && -n "$DOMAIN_FRONTEND" && -n "$DOMAIN_DOCS" ]] \ || fail "Домены API, Frontend и Docs обязательны (или используйте --local)" local use_ssl="true" if [[ "$NON_INTERACTIVE" -ne 1 ]]; then local ssl_answer read -r -p "Настроить Nginx + Let's Encrypt SSL? [Y/n]: " ssl_answer ssl_answer="${ssl_answer:-Y}" [[ "$ssl_answer" =~ ^[Yy]$ ]] || use_ssl="false" fi if [[ "$use_ssl" == "true" ]]; then prompt CERTBOT_EMAIL "Email для Let's Encrypt" "${default_email:-admin@${DOMAIN_FRONTEND}}" [[ -n "$CERTBOT_EMAIL" ]] || fail "Email обязателен для SSL" fi derive_public_urls "$use_ssl" } write_env_secrets_if_missing() { [[ -f .env && "$FORCE_ENV" != "1" ]] && return local pg_pass rmq_pass minio_pass jwt_access jwt_refresh data_key pg_pass="$(rand_secret 32)" rmq_pass="$(rand_secret 32)" minio_pass="$(rand_secret 24)" jwt_access="$(rand_secret 64)" jwt_refresh="$(rand_secret 64)" data_key="$(rand_secret 48)" if [[ ! -f .env ]]; then cp .env.example .env 2>/dev/null || touch .env fi if [[ "$FORCE_ENV" == "1" ]] || [[ "$(env_get POSTGRES_PASSWORD change-me-postgres)" == "change-me-postgres" ]]; then env_set POSTGRES_PASSWORD "$pg_pass" env_set RABBITMQ_DEFAULT_PASS "$rmq_pass" env_set MINIO_ROOT_PASSWORD "$minio_pass" env_set JWT_ACCESS_SECRET "$jwt_access" env_set JWT_REFRESH_SECRET "$jwt_refresh" env_set DATA_ENCRYPTION_KEY "$data_key" ok "Секреты сгенерированы" fi } persist_domain_env() { write_env_secrets_if_missing env_set INSTALL_MODE "${INSTALL_MODE:-local}" env_set DOMAIN_API "${DOMAIN_API:-}" env_set DOMAIN_FRONTEND "${DOMAIN_FRONTEND:-}" env_set DOMAIN_DOCS "${DOMAIN_DOCS:-}" env_set DOMAIN_WS "${DOMAIN_WS:-}" env_set DOMAIN_MINIO "${DOMAIN_MINIO:-}" env_set DOMAIN_MINIO_CONSOLE "${DOMAIN_MINIO_CONSOLE:-}" env_set PUBLIC_API_URL "${PUBLIC_API_URL}" env_set PUBLIC_FRONTEND_URL "${PUBLIC_FRONTEND_URL}" env_set PUBLIC_DOCS_URL "${PUBLIC_DOCS_URL}" env_set PUBLIC_WS_URL "${PUBLIC_WS_URL}" env_set USE_NGINX_SSL "${USE_NGINX_SSL:-false}" env_set CERTBOT_EMAIL "${CERTBOT_EMAIL:-}" env_set MINIO_PUBLIC_ENDPOINT "${MINIO_PUBLIC_ENDPOINT:-localhost:9000}" env_set MINIO_USE_SSL "${MINIO_USE_SSL:-false}" ok "Файл .env обновлён" } write_compose_override() { load_env if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then log "Привязка контейнеров к 127.0.0.1 (доступ через Nginx)..." cat >"$COMPOSE_OVERRIDE" <<'EOF' services: api-gateway: ports: - "127.0.0.1:3000:3000" frontend: ports: - "127.0.0.1:3002:3000" docs: ports: - "127.0.0.1:3003:3000" media-ws: ports: - "127.0.0.1:8085:8085" minio: ports: - "127.0.0.1:9000:9000" - "127.0.0.1:9001:9001" EOF else [[ -f "$COMPOSE_OVERRIDE" ]] && rm -f "$COMPOSE_OVERRIDE" fi } nginx_proxy_headers=" proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_set_header X-Forwarded-Host \$host; proxy_read_timeout 300s; proxy_connect_timeout 75s;" nginx_ws_location=" location /ws { proxy_pass http://127.0.0.1:8085/ws; proxy_http_version 1.1; proxy_set_header Upgrade \$http_upgrade; proxy_set_header Connection \"upgrade\"; proxy_set_header Host \$host; proxy_set_header X-Real-IP \$remote_addr; proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto \$scheme; proxy_read_timeout 86400s; }" write_nginx_site_api() { local domain="$1" local ssl="${2:-false}" local ws_on_api="${3:-true}" local conf="${NGINX_AVAILABLE}/${NGINX_PREFIX}-api.conf" local ws_block="" if [[ "$ws_on_api" == "true" ]]; then ws_block="$nginx_ws_location" fi if [[ "$ssl" == "true" ]]; then run_root tee "$conf" >/dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null </dev/null || true run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || true ok "Конфиги Nginx IdP удалены" } write_nginx_http_only() { load_env local ssl="false" local ws_on_api="true" [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]] && ws_on_api="false" remove_nginx_configs write_nginx_site_api "$DOMAIN_API" "$ssl" "$ws_on_api" write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl" write_nginx_site_docs "$DOMAIN_DOCS" "$ssl" if [[ "$ws_on_api" == "false" ]]; then write_nginx_site_ws "$DOMAIN_WS" "$ssl" else run_root rm -f "${NGINX_ENABLED}/${NGINX_PREFIX}-ws.conf" "${NGINX_AVAILABLE}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true fi [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" 9000 "minio" "$ssl" [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" 9001 "minio-console" "$ssl" run_root nginx -t run_root systemctl reload nginx } write_nginx_ssl_configs() { load_env local ssl="true" local ws_on_api="true" [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]] && ws_on_api="false" remove_nginx_configs write_nginx_site_api "$DOMAIN_API" "$ssl" "$ws_on_api" write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl" write_nginx_site_docs "$DOMAIN_DOCS" "$ssl" if [[ "$ws_on_api" == "false" ]]; then write_nginx_site_ws "$DOMAIN_WS" "$ssl" fi [[ -n "${DOMAIN_MINIO:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO" 9000 "minio" "$ssl" [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" 9001 "minio-console" "$ssl" run_root nginx -t run_root systemctl reload nginx } obtain_certificate() { local domain="$1" local email="$2" [[ -z "$domain" ]] && return 0 log "Получение SSL для ${domain}..." if ! run_root certbot certonly --webroot -w "$CERTBOT_WEBROOT" \ --email "$email" --agree-tos --no-eff-email \ -d "$domain" --non-interactive; then fail "Не удалось получить SSL для ${domain}. Проверьте DNS A-запись → IP сервера и порты 80/443." fi ok "Сертификат для ${domain} готов" } obtain_all_certificates() { load_env [[ "$(env_get USE_NGINX_SSL false)" == "true" ]] || return 0 local email="${CERTBOT_EMAIL:-}" [[ -n "$email" ]] || fail "CERTBOT_EMAIL не задан" write_nginx_http_only obtain_certificate "$DOMAIN_API" "$email" obtain_certificate "$DOMAIN_FRONTEND" "$email" obtain_certificate "$DOMAIN_DOCS" "$email" if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then obtain_certificate "$DOMAIN_WS" "$email" fi [[ -n "${DOMAIN_MINIO:-}" ]] && obtain_certificate "$DOMAIN_MINIO" "$email" [[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && obtain_certificate "$DOMAIN_MINIO_CONSOLE" "$email" if [[ ! -f /etc/letsencrypt/options-ssl-nginx.conf ]]; then run_root mkdir -p /etc/letsencrypt run_root curl -fsSL https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf \ -o /etc/letsencrypt/options-ssl-nginx.conf 2>/dev/null || true run_root openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null || true fi write_nginx_ssl_configs setup_certbot_renewal } setup_certbot_renewal() { if has_cmd systemctl && systemctl list-unit-files 2>/dev/null | grep -q certbot.timer; then run_root systemctl enable certbot.timer >/dev/null 2>&1 || true run_root systemctl start certbot.timer >/dev/null 2>&1 || true ok "Автообновление SSL (certbot.timer) включено" else warn "certbot.timer не найден — настройте cron: certbot renew --quiet && systemctl reload nginx" fi } setup_nginx_proxy() { load_env [[ -n "${DOMAIN_API:-}" ]] || return 0 [[ "$INSTALL_LOCAL" -eq 1 ]] && return 0 install_nginx_certbot setup_firewall if [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then run_root mkdir -p "$CERTBOT_WEBROOT" obtain_all_certificates else write_nginx_http_only ok "Nginx настроен (HTTP без SSL)" fi } setup_nginx_stack() { setup_nginx_proxy } wait_for_service() { local name="$1" local max_attempts="${2:-60}" local attempt=0 log "Ожидание сервиса ${name}..." while [[ $attempt -lt $max_attempts ]]; do if docker_cmd compose ps --status running "$name" 2>/dev/null | grep -q "$name"; then local cid health cid="$(docker_cmd compose ps -q "$name" 2>/dev/null | head -n1)" health="$(docker_cmd inspect --format='{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$cid" 2>/dev/null || echo "none")" if [[ "$health" == "healthy" || "$health" == "none" ]]; then ok "Сервис ${name} готов" return 0 fi fi attempt=$((attempt + 1)) sleep 5 done warn "Сервис ${name} не стал healthy — проверьте: docker compose logs $name" } deploy_stack() { export DOCKER_BUILDKIT=1 export COMPOSE_DOCKER_CLI_BUILD=1 write_compose_override load_env log "Сборка и запуск контейнеров (10–20 минут при первом запуске)..." docker_cmd compose --env-file .env up -d --build wait_for_service "api-gateway" 90 wait_for_service "frontend" 60 wait_for_service "docs" 60 } save_install_state() { local ts ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" cat >"$STATE_FILE" </dev/null || warn "Контейнеры не запущены" echo "" if has_cmd nginx && [[ "$(env_get USE_NGINX_SSL false)" == "true" ]]; then run_root nginx -t 2>&1 | sed 's/^/ /' fi } action_full_install() { configure_domains_interactive persist_domain_env setup_nginx_proxy deploy_stack save_install_state print_summary } action_configure_domains() { if [[ ! -f .env ]]; then warn ".env не найден — выполняем полную установку" action_full_install return fi load_env configure_domains_interactive persist_domain_env if [[ "$INSTALL_LOCAL" -eq 1 ]]; then remove_nginx_configs fi setup_nginx_proxy write_compose_override log "Пересборка frontend/docs с новыми URL..." deploy_stack save_install_state print_summary } action_renew_ssl() { load_env [[ "$(env_get USE_NGINX_SSL false)" == "true" ]] || fail "SSL не используется (USE_NGINX_SSL=false)" install_nginx_certbot run_root certbot renew --quiet write_nginx_ssl_configs ok "SSL-сертификаты обновлены" } action_restart() { load_env write_compose_override docker_cmd compose --env-file .env up -d --build ok "Контейнеры перезапущены" show_status } show_menu() { while true; do echo "" echo -e "${BOLD}══════════════════════════════════════════════════════════════${NC}" echo -e "${BOLD} Lendry ID v${SCRIPT_VERSION} — установка и управление${NC}" echo -e "${BOLD}══════════════════════════════════════════════════════════════${NC}" [[ -f .env ]] && load_env && echo -e " Режим: ${INSTALL_MODE:-local} | API: ${DOMAIN_API:-localhost:3000}" echo "" echo " 1) Полная установка / переустановка стека" echo " 2) Настроить / изменить домены и Nginx + SSL" echo " 3) Обновить SSL-сертификаты (Let's Encrypt)" echo " 4) Перезапуск / пересборка контейнеров" echo " 5) Статус сервисов" echo " 6) Удалить конфиги Nginx IdP" echo " 0) Выход" echo "" local choice read -r -p "Выберите пункт [0-6]: " choice case "$choice" in 1) NON_INTERACTIVE=0; action_full_install ;; 2) NON_INTERACTIVE=0; action_configure_domains ;; 3) action_renew_ssl ;; 4) action_restart ;; 5) show_status ;; 6) remove_nginx_configs; ok "Готово" ;; 0) exit 0 ;; *) warn "Неверный пункт" ;; esac done } main() { parse_args "$@" preflight_checks detect_os install_base_packages install_docker check_docker_compose case "${ACTION:-}" in install) action_full_install ;; configure-domains) action_configure_domains ;; renew-ssl) action_renew_ssl ;; restart) action_restart ;; status) show_status ;; remove-nginx) remove_nginx_configs ;; "") if [[ -f .env ]] && [[ "$NON_INTERACTIVE" -ne 1 ]]; then show_menu else action_full_install fi ;; *) fail "Неизвестное действие: $ACTION" ;; esac } main "$@"