#!/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:-} / <пароль, который вы задали>" 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"