Files

246 lines
11 KiB
Bash
Executable File
Raw Permalink 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
# One-command installer for the Aura CRM platform (core + production).
#
# What it does:
# 1. Checks Docker/Docker Compose are installed.
# 2. Asks for your admin account and business name (only on first run —
# re-running this script after a successful install just leaves your
# existing .env files alone and skips straight to `docker compose up`).
# 3. Generates every secret (JWT signing key, DB passwords, MinIO keys,
# the module token core and production authenticate each other with)
# — you never have to invent or copy-paste a secret by hand.
# 4. Starts core (auth/staff/roles) first, waits for it to come up, logs
# in as the admin account you just described (bootstrapped
# automatically by core itself on first boot), and registers
# "production" as a module against core's API to get the token
# production needs to talk to it.
# 5. Starts production (the CRM itself) with that token already wired in.
#
# Usage:
# ./install.sh
#
# Everything runs locally on http://localhost by default — see the "Going
# public" note this script prints at the end for what to change if you want
# a real domain in front of it.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
CORE_DIR="$SCRIPT_DIR/core"
PROD_DIR="$SCRIPT_DIR/production"
CORE_ENV="$CORE_DIR/.env"
PROD_ENV="$PROD_DIR/.env"
CORE_URL="http://127.0.0.1:18090"
PROD_URL="http://127.0.0.1:18091"
WEB_URL="http://127.0.0.1:18092"
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
info() { printf ' %s\n' "$*"; }
err() { printf '\033[31m✗ %s\033[0m\n' "$*" >&2; }
ok() { printf '\033[32m✓ %s\033[0m\n' "$*"; }
# ---------------------------------------------------------------------------
# 1. Prerequisite checks
# ---------------------------------------------------------------------------
bold "Aura CRM — установка"
echo
if ! command -v docker >/dev/null 2>&1; then
err "Docker не найден. Установите Docker: https://docs.docker.com/engine/install/"
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
err "Docker Compose v2 не найден (нужен плагин 'docker compose', не старый docker-compose)."
exit 1
fi
if ! command -v openssl >/dev/null 2>&1; then
err "openssl не найден (нужен для генерации секретов)."
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
err "curl не найден (нужен для настройки связи между core и production)."
exit 1
fi
ok "Docker, Docker Compose, openssl, curl — на месте."
echo
# ---------------------------------------------------------------------------
# 2. Interactive prompts — only if this is a fresh install
# ---------------------------------------------------------------------------
FRESH_INSTALL=true
if [[ -f "$CORE_ENV" && -f "$PROD_ENV" ]]; then
FRESH_INSTALL=false
bold "Найдены существующие .env — пропускаю вопросы, перехожу сразу к запуску."
echo
fi
if $FRESH_INSTALL; then
bold "Учётная запись владельца (первый вход в CRM)"
read -rp " Имя: " OWNER_NAME
while [[ -z "$OWNER_NAME" ]]; do read -rp " Имя (обязательно): " OWNER_NAME; done
read -rp " Email: " OWNER_EMAIL
while [[ -z "$OWNER_EMAIL" ]]; do read -rp " Email (обязательно): " OWNER_EMAIL; done
while true; do
read -rsp " Пароль (минимум 8 символов): " OWNER_PASSWORD; echo
if [[ ${#OWNER_PASSWORD} -lt 8 ]]; then
err "Слишком короткий пароль, минимум 8 символов."
continue
fi
read -rsp " Повторите пароль: " OWNER_PASSWORD_CONFIRM; echo
if [[ "$OWNER_PASSWORD" != "$OWNER_PASSWORD_CONFIRM" ]]; then
err "Пароли не совпадают, попробуйте снова."
continue
fi
break
done
echo
bold "Реквизиты бизнеса (необязательно — можно заполнить позже в Настройках)"
read -rp " Название организации: " BUSINESS_NAME
echo
fi
# ---------------------------------------------------------------------------
# 3. Generate secrets + write .env files (fresh install only)
# ---------------------------------------------------------------------------
rand_hex() { openssl rand -hex "$1"; }
if $FRESH_INSTALL; then
bold "Генерирую секреты..."
JWT_SECRET="$(rand_hex 32)"
CORE_PG_PASSWORD="$(rand_hex 20)"
CORE_MINIO_ACCESS_KEY="$(rand_hex 10)"
CORE_MINIO_SECRET_KEY="$(rand_hex 20)"
PROD_PG_PASSWORD="$(rand_hex 20)"
PROD_MINIO_ACCESS_KEY="$(rand_hex 10)"
PROD_MINIO_SECRET_KEY="$(rand_hex 20)"
ok "Секреты сгенерированы."
echo
cp "$CORE_DIR/.env.example" "$CORE_ENV"
sed -i \
-e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${CORE_PG_PASSWORD}|" \
-e "s|^DATABASE_URL=.*|DATABASE_URL=postgres://service_center:${CORE_PG_PASSWORD}@postgres:5432/service_center?sslmode=disable|" \
-e "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" \
-e "s|^MINIO_ACCESS_KEY=.*|MINIO_ACCESS_KEY=${CORE_MINIO_ACCESS_KEY}|" \
-e "s|^MINIO_SECRET_KEY=.*|MINIO_SECRET_KEY=${CORE_MINIO_SECRET_KEY}|" \
-e "s|^CORS_ORIGINS=.*|CORS_ORIGINS=${WEB_URL}|" \
-e "s|^OWNER_NAME=.*|OWNER_NAME=${OWNER_NAME}|" \
-e "s|^OWNER_EMAIL=.*|OWNER_EMAIL=${OWNER_EMAIL}|" \
-e "s|^OWNER_PASSWORD=.*|OWNER_PASSWORD=${OWNER_PASSWORD}|" \
"$CORE_ENV"
cp "$PROD_DIR/.env.example" "$PROD_ENV"
sed -i \
-e "s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=${PROD_PG_PASSWORD}|" \
-e "s|^DATABASE_URL=.*|DATABASE_URL=postgres://production:${PROD_PG_PASSWORD}@postgres:5432/production?sslmode=disable|" \
-e "s|^JWT_SECRET=.*|JWT_SECRET=${JWT_SECRET}|" \
-e "s|^MINIO_ACCESS_KEY=.*|MINIO_ACCESS_KEY=${PROD_MINIO_ACCESS_KEY}|" \
-e "s|^MINIO_SECRET_KEY=.*|MINIO_SECRET_KEY=${PROD_MINIO_SECRET_KEY}|" \
-e "s|^CORS_ORIGINS=.*|CORS_ORIGINS=${WEB_URL}|" \
-e "s|^CORE_URL=.*|CORE_URL=http://core:3000|" \
-e "s|^WEB_CORE_URL=.*|WEB_CORE_URL=${CORE_URL}|" \
-e "s|^WEB_PRODUCTION_URL=.*|WEB_PRODUCTION_URL=${PROD_URL}|" \
-e "s|^BUSINESS_NAME=.*|BUSINESS_NAME=${BUSINESS_NAME}|" \
"$PROD_ENV"
# MODULE_TOKEN / CORE_CONTROL_TOKEN get filled in below, once core is up
# and can issue them — they don't exist yet at this point.
ok ".env файлы записаны (core/.env, production/.env)."
echo
fi
# ---------------------------------------------------------------------------
# 4. Shared network + core
# ---------------------------------------------------------------------------
docker network create platform_net >/dev/null 2>&1 || true
bold "Запускаю core (авторизация/сотрудники/роли)..."
(cd "$CORE_DIR" && docker compose up -d --build)
printf " Жду, пока core поднимется"
CORE_UP=false
for _ in $(seq 1 60); do
if curl -sf "$CORE_URL/api/health" >/dev/null 2>&1; then
CORE_UP=true
break
fi
printf '.'
sleep 2
done
echo
if ! $CORE_UP; then
err "core не отвечает на $CORE_URL/api/health после ожидания. Проверьте: cd core && docker compose logs backend"
exit 1
fi
ok "core запущен."
echo
# ---------------------------------------------------------------------------
# 5. Register production as a module against core (only on fresh install —
# already done and stored in production/.env on a re-run)
# ---------------------------------------------------------------------------
if $FRESH_INSTALL; then
bold "Регистрирую production в core..."
LOGIN_RESP="$(curl -sf -X POST "$CORE_URL/api/auth/login" \
-H 'Content-Type: application/json' \
-d "{\"email\":\"${OWNER_EMAIL}\",\"password\":\"${OWNER_PASSWORD}\"}")" \
|| { err "Не удалось войти в core как владелец — проверьте OWNER_EMAIL/OWNER_PASSWORD в core/.env и логи: cd core && docker compose logs backend"; exit 1; }
OWNER_JWT="$(printf '%s' "$LOGIN_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["access_token"])' 2>/dev/null)"
if [[ -z "$OWNER_JWT" ]]; then
err "Не удалось извлечь токен из ответа core. Ответ: $LOGIN_RESP"
exit 1
fi
MODULE_RESP="$(curl -sf -X POST "$CORE_URL/api/modules" \
-H "Authorization: Bearer ${OWNER_JWT}" \
-H 'Content-Type: application/json' \
-d '{"name":"production","base_url":"http://production:3000"}')" \
|| { err "Не удалось зарегистрировать модуль production в core."; exit 1; }
MODULE_TOKEN="$(printf '%s' "$MODULE_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])' 2>/dev/null)"
CONTROL_TOKEN="$(printf '%s' "$MODULE_RESP" | python3 -c 'import json,sys; print(json.load(sys.stdin)["control_token"])' 2>/dev/null)"
if [[ -z "$MODULE_TOKEN" || -z "$CONTROL_TOKEN" ]]; then
err "core вернул неожиданный ответ при регистрации модуля: $MODULE_RESP"
exit 1
fi
sed -i \
-e "s|^MODULE_TOKEN=.*|MODULE_TOKEN=${MODULE_TOKEN}|" \
-e "s|^CORE_CONTROL_TOKEN=.*|CORE_CONTROL_TOKEN=${CONTROL_TOKEN}|" \
"$PROD_ENV"
ok "production зарегистрирован в core."
echo
fi
# ---------------------------------------------------------------------------
# 6. Start production
# ---------------------------------------------------------------------------
bold "Запускаю production (сама CRM)..."
(cd "$PROD_DIR" && docker compose up -d --build)
echo
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
bold "Готово!"
echo
info "CRM: ${WEB_URL}"
info "Вход: ${OWNER_EMAIL:-<email из первого запуска>} / <пароль, который вы задали>"
echo
info "core API: ${CORE_URL} (журнал: cd core && docker compose logs -f backend)"
info "production API: ${PROD_URL} (журнал: cd production && docker compose logs -f backend)"
echo
bold "Выход в интернет (свой домен)"
info "Сейчас всё живёт на localhost. Чтобы выставить наружу под своим доменом:"
info " 1. Поставьте обратный прокси (Caddy/nginx) со своим доменом и TLS перед всеми тремя портами:"
info " ${WEB_URL} (фронтенд), ${CORE_URL} (core API), ${PROD_URL} (production API)."
info " 2. В production/.env поменяйте WEB_CORE_URL и WEB_PRODUCTION_URL на публичные https-адреса."
info " 3. В core/.env и production/.env поменяйте CORS_ORIGINS на публичный адрес фронтенда."
info " 4. Пересоберите фронтенд: cd production && docker compose up -d --build web"