Initial open-source release: Aura CRM platform (core + production)

This commit is contained in:
Open Source Release
2026-08-19 08:35:21 +00:00
commit e8d037bef3
515 changed files with 64691 additions and 0 deletions
Executable
+245
View File
@@ -0,0 +1,245 @@
#!/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"