Files
IdP/install.sh
2026-06-24 15:19:16 +03:00

1543 lines
48 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.1.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"
LOCAL_CONF_DIR="${ROOT_DIR}/nginx/conf.d"
LOCAL_CERTS_DIR="${ROOT_DIR}/nginx/certs"
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
INSTALL_INTRANET=0
OFFLINE="${OFFLINE:-0}"
NGINX_MODE="${NGINX_MODE:-auto}"
CLI_DOMAIN_API=""
CLI_DOMAIN_FRONTEND=""
CLI_DOMAIN_DOCS=""
CLI_DOMAIN_WS=""
CLI_DOMAIN_MINIO=""
CLI_DOMAIN_MINIO_CONSOLE=""
CLI_EMAIL=""
CLI_SSL_TYPE=""
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 <<EOF
${BOLD}Lendry ID v${SCRIPT_VERSION}${NC} — установка и управление
./install.sh Интерактивное меню
./install.sh --install Полная установка
./install.sh --configure-domains Изменить домены / Nginx / SSL
./install.sh --renew-ssl Обновить сертификаты Let's Encrypt
./install.sh --restart Перезапуск контейнеров
./install.sh --status Статус сервисов
./install.sh --remove-nginx Удалить конфиги Nginx IdP
${BOLD}Режим локальной сети (без интернета / AD-домены .local .lpr):${NC}
--intranet Локальная сеть: HTTP или самоподписанный HTTPS
--ssl TYPE none | selfsigned | letsencrypt (по умолчанию в --intranet: none)
--nginx-mode MODE auto | host | docker (Windows → docker)
--offline Не скачивать Docker из интернета (только если уже установлен)
${BOLD}Домены (можно указывать отдельно):${NC}
--api-domain DOMAIN API / Swagger
--frontend-domain DOMAIN Веб-интерфейс
--docs-domain DOMAIN Документация
--ws-domain DOMAIN WebSocket (пусто = API + /ws)
--minio-domain DOMAIN MinIO S3 (опционально)
--minio-console-domain DOMAIN MinIO Console (опционально)
--email EMAIL Email для Let's Encrypt (только ssl=letsencrypt)
${BOLD}Прочие опции:${NC}
-y, --yes Без интерактивных вопросов
--local localhost:3000/3002/3003 без Nginx
-h, --help Справка
${BOLD}Переменные окружения:${NC}
FORCE_ENV=1 Пересоздать .env (секреты будут новые)
OFFLINE=1 То же, что --offline
${BOLD}Примеры:${NC}
./install.sh --intranet --install --yes \\
--api-domain api.idpmvk.lpr \\
--frontend-domain id.idpmvk.lpr \\
--docs-domain docs.idpmvk.lpr \\
--ssl none
./install.sh --intranet --ssl selfsigned \\
--api-domain api.idpmvk.lpr --frontend-domain id.idpmvk.lpr \\
--docs-domain docs.idpmvk.lpr
./install.sh --install --yes \\
--api-domain api.id.example.ru \\
--frontend-domain id.example.ru \\
--docs-domain docs.example.ru \\
--ssl letsencrypt --email admin@example.ru
./install.sh --configure-domains
EOF
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-y | --yes) NON_INTERACTIVE=1; shift ;;
--install) ACTION="install"; shift ;;
--configure-domains) ACTION="configure-domains"; shift ;;
--renew-ssl) ACTION="renew-ssl"; shift ;;
--restart) ACTION="restart"; shift ;;
--status) ACTION="status"; shift ;;
--remove-nginx) ACTION="remove-nginx"; shift ;;
--local) INSTALL_LOCAL=1; shift ;;
--intranet) INSTALL_INTRANET=1; shift ;;
--offline) OFFLINE=1; shift ;;
--ssl) CLI_SSL_TYPE="${2:-}"; shift 2 ;;
--nginx-mode) NGINX_MODE="${2:-auto}"; shift 2 ;;
--api-domain) CLI_DOMAIN_API="${2:-}"; shift 2 ;;
--frontend-domain) CLI_DOMAIN_FRONTEND="${2:-}"; shift 2 ;;
--docs-domain) CLI_DOMAIN_DOCS="${2:-}"; shift 2 ;;
--ws-domain) CLI_DOMAIN_WS="${2:-}"; shift 2 ;;
--minio-domain) CLI_DOMAIN_MINIO="${2:-}"; shift 2 ;;
--minio-console-domain) CLI_DOMAIN_MINIO_CONSOLE="${2:-}"; shift 2 ;;
--email) CLI_EMAIL="${2:-}"; shift 2 ;;
-h | --help) usage; exit 0 ;;
*) fail "Неизвестный аргумент: $1 (используйте --help)" ;;
esac
done
}
has_cmd() { command -v "$1" >/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() {
if [[ "$(uname -s)" != "Linux" ]]; then
warn "ОС не Linux — Nginx будет запущен в Docker (профиль proxy). Убедитесь, что Docker Desktop установлен."
NGINX_MODE="docker"
fi
[[ -f docker-compose.yml ]] || fail "Запустите скрипт из корня репозитория (рядом с docker-compose.yml)"
mkdir -p "$LOCAL_CONF_DIR" "$LOCAL_CERTS_DIR"
}
detect_nginx_mode() {
load_env
if [[ -n "$(env_get NGINX_MODE "")" && "$(env_get NGINX_MODE auto)" != "auto" ]]; then
NGINX_MODE="$(env_get NGINX_MODE auto)"
fi
if [[ "$NGINX_MODE" == "auto" ]]; then
if [[ "$(uname -s)" == "Linux" ]] && { [[ -d /etc/nginx ]] || has_cmd nginx; }; then
NGINX_MODE="host"
else
NGINX_MODE="docker"
fi
fi
log "Режим Nginx: ${NGINX_MODE} (host=системный, docker=контейнер)"
}
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
if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then
fail "Docker не установлен или не запущен. В локальной сети установите Docker вручную (offline-пакет / Docker Desktop) и повторите запуск."
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_host() {
if has_cmd nginx; then
ok "Nginx уже установлен"
return
fi
log "Установка Nginx..."
case "$PKG_MANAGER" in
apt) run_root apt-get install -y -qq nginx ;;
dnf) run_root dnf install -y nginx ;;
*) fail "Автоустановка Nginx для $OS_ID не поддерживается. Установите nginx вручную или используйте --nginx-mode docker" ;;
esac
run_root systemctl enable nginx >/dev/null 2>&1 || true
ok "Nginx установлен"
}
install_certbot() {
if has_cmd certbot; then
ok "Certbot уже установлен"
return
fi
if [[ "$OFFLINE" -eq 1 || "$INSTALL_INTRANET" -eq 1 ]]; then
fail "Certbot недоступен offline. Для локальной сети используйте --ssl none или --ssl selfsigned"
fi
log "Установка Certbot..."
case "$PKG_MANAGER" in
apt) run_root apt-get install -y -qq certbot python3-certbot-nginx ;;
dnf) run_root dnf install -y certbot ;;
*) fail "Автоустановка Certbot для $OS_ID не поддерживается" ;;
esac
run_root mkdir -p "$CERTBOT_WEBROOT"
ok "Certbot установлен"
}
install_nginx_certbot() {
if [[ "$NGINX_MODE" == "docker" ]]; then
ok "Nginx в Docker — установка системного nginx не требуется"
return
fi
install_nginx_host
}
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' </dev/urandom | head -c "$length"
fi
}
prompt() {
local var_name="$1"
local question="$2"
local default="$3"
local value
if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
printf -v "$var_name" '%s' "$default"
return
fi
read -r -p "$question [$default]: " value
if [[ -z "$value" ]]; then
value="$default"
fi
printf -v "$var_name" '%s' "$value"
}
prompt_optional() {
local var_name="$1"
local question="$2"
local default="$3"
local value
if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
printf -v "$var_name" '%s' "$default"
return
fi
if [[ -n "$default" ]]; then
read -r -p "$question [$default, Enter=пропустить]: " value
if [[ -z "$value" ]]; then
value="$default"
fi
else
read -r -p "$question [Enter=пропустить]: " value
fi
printf -v "$var_name" '%s' "$value"
}
validate_domain() {
local domain="$1"
[[ -z "$domain" ]] && return 0
[[ "$domain" =~ ^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$ ]]
}
normalize_domain() {
local d="${1:-}"
d="${d#https://}"
d="${d#http://}"
d="${d#wss://}"
d="${d#ws://}"
d="${d%%/*}"
d="${d%%:*}"
echo "$d"
}
load_env() {
if [[ -f .env ]]; then
# shellcheck disable=SC1091
set -a && source .env && set +a
fi
}
env_get() {
local key="$1"
local default="${2:-}"
if [[ -f .env ]] && grep -q "^${key}=" .env; then
grep "^${key}=" .env | head -n1 | cut -d= -f2-
else
echo "$default"
fi
}
env_set() {
local key="$1"
local value="$2"
local escaped
escaped="$(printf '%s' "$value" | sed 's/[&/\]/\\&/g')"
if [[ -f .env ]] && grep -q "^${key}=" .env; then
sed -i "s|^${key}=.*|${key}=${escaped}|" .env
elif [[ -f .env ]]; then
printf '%s=%s\n' "$key" "$value" >>.env
else
printf '%s=%s\n' "$key" "$value" >.env
fi
chmod 600 .env 2>/dev/null || true
}
derive_public_urls() {
local ssl_type="${1:-none}"
if [[ "$INSTALL_LOCAL" -eq 1 ]]; 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"
SSL_TYPE="none"
INSTALL_MODE="local"
return
fi
fi
local scheme="http"
[[ "$ssl_type" == "letsencrypt" || "$ssl_type" == "selfsigned" ]] && scheme="https"
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
SSL_TYPE="$ssl_type"
USE_NGINX_SSL=$([[ "$ssl_type" == "none" ]] && echo false || echo true)
if [[ "$INSTALL_LOCAL" -eq 1 ]]; then
INSTALL_MODE="local"
elif [[ "$INSTALL_INTRANET" -eq 1 || "$ssl_type" == "none" || "$ssl_type" == "selfsigned" ]]; then
INSTALL_MODE="intranet"
elif [[ -n "${DOMAIN_API:-}" ]]; then
INSTALL_MODE="production"
else
INSTALL_MODE="local"
fi
}
choose_ssl_type_interactive() {
local default="${1:-none}"
if [[ -n "$CLI_SSL_TYPE" ]]; then
SSL_TYPE="$CLI_SSL_TYPE"
return
fi
if [[ "$NON_INTERACTIVE" -eq 1 ]]; then
if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
SSL_TYPE="${default:-none}"
else
SSL_TYPE="${default:-letsencrypt}"
fi
return
fi
echo ""
echo -e "${BOLD}Тип SSL / доступ:${NC}"
echo " 1) HTTP — локальная сеть, без сертификатов (http://api.idpmvk.lpr)"
echo " 2) HTTPS самоподписанный — локальная сеть, браузер покажет предупреждение"
echo " 3) HTTPS Let's Encrypt — нужен интернет и публичный DNS"
echo ""
local ssl_choice
if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
read -r -p "Выберите [1-3] [1]: " ssl_choice
ssl_choice="${ssl_choice:-1}"
else
read -r -p "Выберите [1-3] [3]: " ssl_choice
ssl_choice="${ssl_choice:-3}"
fi
case "$ssl_choice" in
1) SSL_TYPE="none" ;;
2) SSL_TYPE="selfsigned" ;;
3) SSL_TYPE="letsencrypt" ;;
*) SSL_TYPE="$default" ;;
esac
}
configure_domains_interactive() {
local existing_api existing_frontend existing_docs existing_ws existing_email
if [[ "$(env_get INSTALL_MODE "")" == "intranet" ]]; then
INSTALL_INTRANET=1
fi
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 "none"
return
fi
echo ""
echo -e "${BOLD}Настройка доменов${NC}"
if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
echo "Локальная сеть: укажите внутренние DNS-имена (например idpmvk.lpr)."
echo "Пропишите A-записи во внутреннем DNS или файл hosts на клиентах."
echo "Доступ из интернета и Let's Encrypt не требуются."
else
echo "Укажите домен для каждого сервиса (A-запись → IP сервера)."
fi
echo "Enter — оставить текущее значение. localhost без Nginx: ./install.sh --local"
echo ""
if [[ "$INSTALL_INTRANET" -eq 1 ]]; then
if [[ -z "$default_api" ]]; then default_api="api.idpmvk.lpr"; fi
if [[ -z "$default_frontend" ]]; then default_frontend="id.idpmvk.lpr"; fi
if [[ -z "$default_docs" ]]; then default_docs="docs.idpmvk.lpr"; fi
else
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
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)"
choose_ssl_type_interactive "$(env_get SSL_TYPE none)"
case "$SSL_TYPE" in
none | selfsigned | letsencrypt) ;;
*) fail "Некорректный SSL_TYPE: $SSL_TYPE (none | selfsigned | letsencrypt)" ;;
esac
if [[ "$SSL_TYPE" == "letsencrypt" ]]; then
if [[ "$INSTALL_INTRANET" -eq 1 && "$NON_INTERACTIVE" -ne 1 ]]; then
warn "Let's Encrypt требует доступ из интернета. Для чистой локальной сети выберите HTTP или selfsigned."
fi
prompt CERTBOT_EMAIL "Email для Let's Encrypt" "${default_email:-admin@${DOMAIN_FRONTEND}}"
[[ -n "$CERTBOT_EMAIL" ]] || fail "Email обязателен для Let's Encrypt"
else
CERTBOT_EMAIL=""
fi
derive_public_urls "$SSL_TYPE"
}
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 NGINX_MODE "${NGINX_MODE:-auto}"
env_set SSL_TYPE "${SSL_TYPE:-none}"
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
local mode="${NGINX_MODE:-$(env_get NGINX_MODE auto)}"
local has_domains
has_domains="$(env_get DOMAIN_API "")"
if [[ -z "$has_domains" || "$(env_get INSTALL_MODE local)" == "local" ]]; then
[[ -f "$COMPOSE_OVERRIDE" ]] && rm -f "$COMPOSE_OVERRIDE"
return
fi
log "Настройка docker-compose override (Nginx: ${mode})..."
local minio_block=""
if [[ "$mode" == "host" && ( -n "$(env_get DOMAIN_MINIO "")" || -n "$(env_get DOMAIN_MINIO_CONSOLE "")" ) ]]; then
minio_block="
minio:
ports: !reset
- \"127.0.0.1:9000:9000\"
- \"127.0.0.1:9001:9001\""
else
minio_block="
minio:
ports: !reset []"
fi
if [[ "$mode" == "docker" ]]; then
cat >"$COMPOSE_OVERRIDE" <<EOF
services:
api-gateway:
ports: !reset []
frontend:
ports: !reset []
docs:
ports: !reset []
media-ws:
ports: !reset []
${minio_block}
EOF
else
cat >"$COMPOSE_OVERRIDE" <<EOF
services:
api-gateway:
ports: !reset
- "127.0.0.1:3000:3000"
frontend:
ports: !reset
- "127.0.0.1:3002:3000"
docs:
ports: !reset
- "127.0.0.1:3003:3000"
media-ws:
ports: !reset
- "127.0.0.1:8085:8085"
${minio_block}
EOF
fi
}
nginx_upstream() {
case "$1" in
api)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://api-gateway:3000" || echo "http://127.0.0.1:3000"
;;
frontend)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://frontend:3000" || echo "http://127.0.0.1:3002"
;;
docs)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://docs:3000" || echo "http://127.0.0.1:3003"
;;
ws)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://media-ws:8085" || echo "http://127.0.0.1:8085"
;;
minio)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9000" || echo "http://127.0.0.1:9000"
;;
minio-console)
[[ "$NGINX_MODE" == "docker" ]] && echo "http://minio:9001" || echo "http://127.0.0.1:9001"
;;
esac
}
nginx_cert_base_path() {
local domain="$1"
local ssl_type="$2"
if [[ "$ssl_type" == "letsencrypt" ]]; then
echo "/etc/letsencrypt/live/${domain}"
elif [[ "$NGINX_MODE" == "docker" ]]; then
echo "/etc/nginx/certs/${domain}"
else
echo "${LOCAL_CERTS_DIR}/${domain}"
fi
}
generate_self_signed_cert() {
local domain="$1"
local cert_dir="${LOCAL_CERTS_DIR}/${domain}"
mkdir -p "$cert_dir"
if [[ -f "${cert_dir}/fullchain.pem" && -f "${cert_dir}/privkey.pem" ]]; then
return 0
fi
log "Самоподписанный сертификат: ${domain}"
if has_cmd openssl; then
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
-keyout "${cert_dir}/privkey.pem" \
-out "${cert_dir}/fullchain.pem" \
-subj "/CN=${domain}/O=Lendry ID Intranet/C=RU" \
-addext "subjectAltName=DNS:${domain}" 2>/dev/null || \
openssl req -x509 -nodes -days 825 -newkey rsa:2048 \
-keyout "${cert_dir}/privkey.pem" \
-out "${cert_dir}/fullchain.pem" \
-subj "/CN=${domain}/O=Lendry ID Intranet/C=RU"
chmod 600 "${cert_dir}/privkey.pem" 2>/dev/null || true
else
fail "openssl не найден — нужен для самоподписанных сертификатов"
fi
}
generate_all_self_signed_certs() {
load_env
generate_self_signed_cert "$DOMAIN_API"
generate_self_signed_cert "$DOMAIN_FRONTEND"
generate_self_signed_cert "$DOMAIN_DOCS"
if [[ -n "${DOMAIN_WS:-}" && "${DOMAIN_WS}" != "${DOMAIN_API}" ]]; then
generate_self_signed_cert "$DOMAIN_WS"
fi
[[ -n "${DOMAIN_MINIO:-}" ]] && generate_self_signed_cert "$DOMAIN_MINIO"
[[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && generate_self_signed_cert "$DOMAIN_MINIO_CONSOLE"
ok "Самоподписанные сертификаты созданы в ${LOCAL_CERTS_DIR}"
}
nginx_conf_target() {
local label="$1"
if [[ "$NGINX_MODE" == "docker" ]]; then
echo "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-${label}.conf"
else
echo "${NGINX_AVAILABLE}/${NGINX_PREFIX}-${label}.conf"
fi
}
nginx_publish_config() {
local label="$1"
local conf
conf="$(nginx_conf_target "$label")"
if [[ "$NGINX_MODE" == "host" ]]; then
run_root ln -sf "$conf" "${NGINX_ENABLED}/${NGINX_PREFIX}-${label}.conf"
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_write_conf() {
local conf="$1"
if [[ "$NGINX_MODE" == "docker" ]]; then
cat >"$conf"
else
run_root tee "$conf" >/dev/null
fi
}
nginx_ssl_directives() {
local domain="$1"
local ssl_type="$2"
local cert_base
cert_base="$(nginx_cert_base_path "$domain" "$ssl_type")"
if [[ "$ssl_type" == "letsencrypt" ]]; then
cat <<EOF
ssl_certificate ${cert_base}/fullchain.pem;
ssl_certificate_key ${cert_base}/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
EOF
else
cat <<EOF
ssl_certificate ${cert_base}/fullchain.pem;
ssl_certificate_key ${cert_base}/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
EOF
fi
}
nginx_http_server_preamble() {
local domain="$1"
local ssl_type="$2"
if [[ "$ssl_type" == "letsencrypt" ]]; then
cat <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
location /.well-known/acme-challenge/ {
root ${CERTBOT_WEBROOT};
}
location / {
return 301 https://\$host\$request_uri;
}
}
EOF
elif [[ "$ssl_type" == "selfsigned" ]]; then
cat <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
location / {
return 301 https://\$host\$request_uri;
}
}
EOF
fi
}
build_ws_block() {
local ws_on_api="$1"
[[ "$ws_on_api" != "true" ]] && return 0
local upstream
upstream="$(nginx_upstream ws)"
cat <<EOF
location /ws {
proxy_pass ${upstream}/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;
}
EOF
}
write_nginx_site_api() {
local domain="$1"
local ssl_type="${2:-none}"
local ws_on_api="${3:-true}"
local conf upstream ws_block http_preamble ssl_block
conf="$(nginx_conf_target api)"
upstream="$(nginx_upstream api)"
ws_block="$(build_ws_block "$ws_on_api")"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
client_max_body_size 100m;
${ws_block}
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF
${http_preamble}server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${domain};
${ssl_block}
client_max_body_size 100m;
${ws_block}
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
fi
nginx_publish_config "api"
}
write_nginx_site_frontend() {
local domain="$1"
local ssl_type="${2:-none}"
local conf upstream http_preamble ssl_block
conf="$(nginx_conf_target frontend)"
upstream="$(nginx_upstream frontend)"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF
${http_preamble}server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${domain};
${ssl_block}
client_max_body_size 50m;
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
fi
nginx_publish_config "frontend"
}
write_nginx_site_docs() {
local domain="$1"
local ssl_type="${2:-none}"
local conf upstream http_preamble ssl_block
conf="$(nginx_conf_target docs)"
upstream="$(nginx_upstream docs)"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
location = / { return 302 /docs; }
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF
${http_preamble}server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${domain};
${ssl_block}
location = / { return 302 /docs; }
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
fi
nginx_publish_config "docs"
}
write_nginx_site_ws() {
local domain="$1"
local ssl_type="${2:-none}"
local conf upstream http_preamble ssl_block
conf="$(nginx_conf_target ws)"
upstream="$(nginx_upstream ws)"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
location / {
proxy_pass ${upstream}/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;
}
}
EOF
else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF
${http_preamble}server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${domain};
${ssl_block}
location / {
proxy_pass ${upstream}/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;
}
}
EOF
fi
nginx_publish_config "ws"
}
write_nginx_site_minio() {
local domain="$1"
local kind="$2"
local label="$3"
local ssl_type="${4:-none}"
local conf upstream http_preamble ssl_block
conf="$(nginx_conf_target "$label")"
upstream="$(nginx_upstream "$kind")"
http_preamble="$(nginx_http_server_preamble "$domain" "$ssl_type")"
if [[ "$ssl_type" == "none" ]]; then
nginx_write_conf "$conf" <<EOF
server {
listen 80;
listen [::]:80;
server_name ${domain};
client_max_body_size 1024m;
proxy_buffering off;
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
else
ssl_block="$(nginx_ssl_directives "$domain" "$ssl_type")"
nginx_write_conf "$conf" <<EOF
${http_preamble}server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name ${domain};
${ssl_block}
client_max_body_size 1024m;
proxy_buffering off;
location / {
proxy_pass ${upstream};
proxy_http_version 1.1;
${nginx_proxy_headers}
}
}
EOF
fi
nginx_publish_config "$label"
}
remove_nginx_configs() {
log "Удаление конфигов Nginx IdP..."
rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true
if [[ "$NGINX_MODE" == "host" ]]; then
run_root rm -f \
"${NGINX_ENABLED}/${NGINX_PREFIX}-"*.conf \
"${NGINX_AVAILABLE}/${NGINX_PREFIX}-"*.conf 2>/dev/null || true
run_root nginx -t >/dev/null 2>&1 && run_root systemctl reload nginx || true
fi
ok "Конфиги Nginx IdP удалены"
}
write_all_nginx_configs() {
load_env
local ssl_type="${1:-$(env_get SSL_TYPE none)}"
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_type" "$ws_on_api"
write_nginx_site_frontend "$DOMAIN_FRONTEND" "$ssl_type"
write_nginx_site_docs "$DOMAIN_DOCS" "$ssl_type"
if [[ "$ws_on_api" == "false" ]]; then
write_nginx_site_ws "$DOMAIN_WS" "$ssl_type"
else
rm -f "${LOCAL_CONF_DIR}/${NGINX_PREFIX}-ws.conf" 2>/dev/null || true
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" minio minio "$ssl_type"
[[ -n "${DOMAIN_MINIO_CONSOLE:-}" ]] && write_nginx_site_minio "$DOMAIN_MINIO_CONSOLE" minio-console minio-console "$ssl_type"
if [[ "$NGINX_MODE" == "host" ]]; then
run_root nginx -t
run_root systemctl reload nginx
else
ok "Конфиги Nginx: ${LOCAL_CONF_DIR}"
fi
}
write_nginx_http_only() {
write_all_nginx_configs "none"
}
write_nginx_ssl_configs() {
load_env
write_all_nginx_configs "$(env_get SSL_TYPE selfsigned)"
}
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 SSL_TYPE none)" == "letsencrypt" ]] || 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
if [[ "$OFFLINE" -ne 1 ]]; then
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
fi
run_root openssl dhparam -out /etc/letsencrypt/ssl-dhparams.pem 2048 2>/dev/null || true
fi
write_all_nginx_configs "letsencrypt"
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
detect_nginx_mode
env_set NGINX_MODE "$NGINX_MODE"
install_nginx_certbot
local ssl_type
ssl_type="$(env_get SSL_TYPE none)"
case "$ssl_type" in
none)
write_nginx_http_only
ok "Nginx: HTTP — локальная сеть, интернет не нужен"
;;
selfsigned)
generate_all_self_signed_certs
write_all_nginx_configs "selfsigned"
ok "Nginx: HTTPS самоподписанный — импортируйте CA в доверенные на клиентах или примите предупреждение браузера"
;;
letsencrypt)
setup_firewall
install_certbot
run_root mkdir -p "$CERTBOT_WEBROOT"
obtain_all_certificates
;;
*)
fail "Неизвестный SSL_TYPE: $ssl_type"
;;
esac
}
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
local compose_cmd=(compose --env-file .env)
if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then
compose_cmd+=(--profile proxy)
fi
log "Сборка и запуск контейнеров (1020 минут при первом запуске)..."
docker_cmd "${compose_cmd[@]}" up -d --build
wait_for_service "api-gateway" 90
wait_for_service "frontend" 60
wait_for_service "docs" 60
if [[ " ${compose_cmd[*]} " == *" proxy "* ]]; then
wait_for_service "nginx" 30
fi
}
save_install_state() {
local ts
ts="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"
cat >"$STATE_FILE" <<EOF
{"version":"${SCRIPT_VERSION}","updated_at":"${ts}","domains":{"api":"${DOMAIN_API:-}","frontend":"${DOMAIN_FRONTEND:-}","docs":"${DOMAIN_DOCS:-}","ws":"${DOMAIN_WS:-}"}}
EOF
}
print_intranet_hint() {
load_env
[[ "$(env_get INSTALL_MODE local)" == "intranet" ]] || return 0
local server_ip
server_ip="$(hostname -I 2>/dev/null | awk '{print $1}' || true)"
[[ -z "$server_ip" ]] && server_ip="<IP-сервера>"
echo -e " ${BOLD}Локальная сеть (DNS / hosts):${NC}"
echo " Пропишите во внутреннем DNS или C:\\Windows\\System32\\drivers\\etc\\hosts:"
echo " ${server_ip} ${DOMAIN_API} ${DOMAIN_FRONTEND} ${DOMAIN_DOCS}"
echo ""
if [[ "$(env_get SSL_TYPE none)" == "selfsigned" ]]; then
echo " Сертификаты: ${LOCAL_CERTS_DIR}/"
echo " Браузер покажет предупреждение — это нормально для локальных доменов."
echo ""
fi
}
print_summary() {
load_env
echo ""
echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
echo -e "${GREEN} Lendry ID — развёртывание завершено${NC}"
echo -e "${GREEN}══════════════════════════════════════════════════════════════${NC}"
echo ""
echo -e " ${BOLD}Режим:${NC} ${INSTALL_MODE:-local} | SSL: ${SSL_TYPE:-none} | Nginx: ${NGINX_MODE:-auto}"
echo -e " ${BOLD}API / Swagger:${NC} ${PUBLIC_API_URL}/api"
echo -e " ${BOLD}Frontend:${NC} ${PUBLIC_FRONTEND_URL}"
echo -e " ${BOLD}Документация:${NC} ${PUBLIC_DOCS_URL}/docs"
echo -e " ${BOLD}WebSocket:${NC} ${PUBLIC_WS_URL}"
echo ""
print_intranet_hint
echo -e " ${BOLD}Управление:${NC} ./install.sh"
echo -e " ${BOLD}Смена доменов:${NC} ./install.sh --configure-domains"
echo -e " ${BOLD}Логи:${NC} docker compose logs -f api-gateway nginx"
echo ""
echo " Первый зарегистрированный пользователь станет супер-администратором."
echo " Секреты в .env — не коммитьте в git."
echo ""
if [[ "${DOCKER_SUGGESTED_GROUP:-}" == "1" ]]; then
warn "Docker без sudo: выполните newgrp docker или перелогиньтесь"
fi
}
show_status() {
load_env
echo ""
echo -e "${BOLD}Статус Lendry ID${NC}"
echo ""
echo " INSTALL_MODE: $(env_get INSTALL_MODE local)"
echo " SSL_TYPE: $(env_get SSL_TYPE none)"
echo " NGINX_MODE: $(env_get NGINX_MODE auto)"
echo " USE_NGINX_SSL: $(env_get USE_NGINX_SSL false)"
echo " DOMAIN_API: $(env_get DOMAIN_API -)"
echo " DOMAIN_FRONTEND: $(env_get DOMAIN_FRONTEND -)"
echo " DOMAIN_DOCS: $(env_get DOMAIN_DOCS -)"
echo " DOMAIN_WS: $(env_get DOMAIN_WS -)"
echo ""
docker_cmd compose ps 2>/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
local ssl_type
ssl_type="$(env_get SSL_TYPE none)"
case "$ssl_type" in
letsencrypt)
install_certbot
run_root certbot renew --quiet
write_all_nginx_configs "letsencrypt"
ok "Let's Encrypt сертификаты обновлены"
;;
selfsigned)
generate_all_self_signed_certs
write_all_nginx_configs "selfsigned"
ok "Самоподписанные сертификаты пересозданы"
;;
none)
fail "SSL отключён (SSL_TYPE=none). Выберите --ssl selfsigned или letsencrypt."
;;
esac
}
action_restart() {
load_env
write_compose_override
local compose_cmd=(compose --env-file .env)
if [[ -n "$(env_get DOMAIN_API "")" && "$(env_get INSTALL_MODE local)" != "local" ]]; then
compose_cmd+=(--profile proxy)
fi
docker_cmd "${compose_cmd[@]}" 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-сертификаты (selfsigned / 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 "$@"