local branch

This commit is contained in:
iqubik
2026-04-04 21:12:19 +03:00
parent 38ac7b1257
commit e705232cf2
5 changed files with 1363 additions and 13 deletions
-9
View File
@@ -39,12 +39,3 @@ server {
proxy_read_timeout 650s;
}
}
server {
listen 3100;
server_name localhost;
location / {
proxy_pass http://backend:3100/;
proxy_set_header Host $http_host;
}
}
+333
View File
@@ -0,0 +1,333 @@
# Локальная отладка 3dp-manager
> ⚠️ **Внимание:** Локальный стенд использует учётные данные `admin/admin` **только для разработки**. Production-скрипты (`install.sh`, `update.sh`) автоматически генерируют безопасные пароли.
## Порты локального стенда
| Сервис | Порт | Доступ |
|--------|------|--------|
| **frontend** | `http://localhost:8080` | ✅ Доступен из браузера |
| **backend API** | `http://backend:3100/api` | ❌ Скрыт внутри Docker network |
| **postgres** | `5432/tcp` | ❌ Скрыт внутри Docker network |
> 🔒 **Безопасность:** Backend и PostgreSQL не проброшены наружу — доступны только внутри Docker network.
## Учётные данные (по умолчанию)
**Локальный стенд (`.env`):**
```
ADMIN_LOGIN: admin
ADMIN_PASSWORD: admin
POSTGRES_USER: admin
POSTGRES_PASSWORD: admin
POSTGRES_DB: 3dp_manager
JWT_SECRET: localDevSecretKey12345678901234567890
```
> ⚠️ **Важно:** Для локальной разработки используется `admin/admin`. Production-скрипты автоматически генерируют случайные пароли при установке/обновлении.
## Запуск/перезапуск
### Полный запуск (с пересозданием volumes)
```powershell
# Первый запуск или полная пересборка
docker compose -f docker-compose.local.yml --env-file .env up -d --build --force-recreate
# Если нужно очистить БД и пересоздать volumes
docker compose -f docker-compose.local.yml --env-file .env down -v
docker compose -f docker-compose.local.yml --env-file .env up -d --build
```
### Частичный перезапуск
```powershell
# Только frontend (без пересборки)
docker compose -f docker-compose.local.yml --env-file .env restart frontend
# Только backend (без пересборки)
docker compose -f docker-compose.local.yml --env-file .env restart backend
# Frontend с пересборкой
docker compose -f docker-compose.local.yml --env-file .env up -d --build --no-deps frontend
# Backend с пересборкой
docker compose -f docker-compose.local.yml --env-file .env up -d --build --no-deps backend
```
### Статус контейнеров
```powershell
docker compose -f docker-compose.local.yml --env-file .env ps
```
### Просмотр логов
```powershell
# Backend логи
docker compose -f docker-compose.local.yml --env-file .env logs -f backend
# Frontend логи
docker compose -f docker-compose.local.yml --env-file .env logs -f frontend
# Последние 50 строк
docker compose -f docker-compose.local.yml --env-file .env logs --tail 50 backend
```
### Остановка
```powershell
# Остановка без удаления volumes
docker compose -f docker-compose.local.yml --env-file .env down
# Полная очистка (включая volumes)
docker compose -f docker-compose.local.yml --env-file .env down -v
```
## Быстрые проверки
```powershell
# Проверка frontend (должен вернуть 200)
curl --noproxy "*" -s -o /dev/null -w "%{http_code}" http://localhost:8080
# Проверка backend API через frontend-proxy (должен вернуть 401 без токена)
curl --noproxy "*" -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/settings
# Проверка login endpoint
curl --noproxy "*" -s -X POST http://localhost:8080/api/auth/login -H "Content-Type: application/json" -d '{"login":"admin","password":"admin"}'
```
## Тесты и линтинг
### Backend
```powershell
cd server
npm run lint
npm run test
npm run test:cov
```
### Frontend
```powershell
cd client
npm run lint
npm run test
npm run test:cov
```
## Что уже встроено
- Backend scanner (RealiTLScanner) собирается в `server/Dockerfile` и кладется в контейнер backend.
- Блок `Автопоиск SNI (backend scanner)` показывается только если бинарник сканера доступен в контейнере.
- Во время сканирования UI получает статус с backend (`/api/domains/scan/status`) и показывает реальный countdown.
- Если обновить страницу во время скана, UI подхватывает активный запуск по backend-статусу и продолжает отсчет.
- После завершения скана результат можно получить из backend (`/api/domains/scan/last-result`) даже если исходный запрос был прерван (например, после F5).
- Предварительный список сканера:
- ручная проверка ссылками,
- удаление позиций,
- очистка,
- экспорт в `.txt`,
- сохранение состояния после F5.
- Основной список:
- ссылки на домены,
- hover-подсветка строк,
- экспорт полного списка в `.txt`.
## Нормализация доменов (импорт и ручное добавление)
- Единые правила применяются и для загрузки файла, и для добавления по одному.
- `*.domain.com` нормализуется в `domain.com`.
- Комментарии и мусор отбрасываются (`#`, `;`, `//`, URL/пути/порты и т.п.).
- Дубликаты убираются без учета регистра.
- Явно некорректные значения отклоняются.
## Тестовый "больной" список
- Файл для проверки импорта: `checker/domains-import-fuzz.txt`
## Быстрые проверки
```powershell
Invoke-WebRequest -UseBasicParsing http://localhost:8080
Invoke-WebRequest -UseBasicParsing http://localhost:8080/api/settings
```
## Остановка
```powershell
docker compose -f docker-compose.local.yml down
```
## Перед PR
### 1. Проверка локального стенда
```powershell
# Убедиться, что все контейнеры работают
docker compose -f docker-compose.local.yml --env-file .env.local ps
# Проверить логи на наличие ошибок
docker compose -f docker-compose.local.yml --env-file .env.local logs --tail 100 backend
docker compose -f docker-compose.local.yml --env-file .env.local logs --tail 100 frontend
# Быстрый тест API
curl --noproxy "*" -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/settings
```
### 2. Прогнать тесты
```powershell
# Backend тесты
cd server
npm run test
# Frontend тесты
cd client
npm run test
```
### 3. Прогнать линтинг
```powershell
# Backend
cd server
npm run lint
# Frontend
cd client
npm run lint
```
### 4. Проверка изменений
```powershell
git status --short
git diff --name-only
```
Проверяем, что в PR идут только целевые изменения фичи.
## Обновление production (кастомная ветка)
> ⚠️ **Важно:** Начиная с версии 2.0.3, скрипты обновления автоматически проверяют и заменяют учётные данные по умолчанию (`admin/admin`) на безопасные. Новые пароли выводятся в лог при обновлении.
Для сервера, где `bash <(curl ...)` не работает из-за `/dev/fd`, используем пайп:
```bash
curl -fsSL https://raw.githubusercontent.com/iqubik/3dp-manager/dp-fix/update-custom.sh | bash -s -- -r https://github.com/iqubik/3dp-manager.git -b dp-fix
```
Что делает команда:
- подтягивает исходники из `iqubik/3dp-manager` ветки `dp-fix` в `/opt/3dp-manager-src`,
- собирает `backend/frontend` из этих исходников,
- поднимает контейнеры через `docker-compose.custom.yml` поверх текущей установки в `/opt/3dp-manager`.
Проверка после обновления:
```bash
cd /opt/3dp-manager
docker compose -f docker-compose.yml -f docker-compose.custom.yml ps
docker compose -f docker-compose.yml -f docker-compose.custom.yml logs --tail 120 backend
docker compose -f docker-compose.yml -f docker-compose.custom.yml logs --tail 120 frontend
```
Ожидаемо и нормально в логах frontend:
- `can not modify /etc/nginx/conf.d/default.conf (read-only file system?)`
Это штатно при `:ro`-монтировании nginx-конфига.
## Полная custom-установка на VPS (dp-custom)
> ⚠️ **Важно:** Скрипт `install-custom.sh` автоматически генерирует безопасные пароли при установке. Учётные данные выводятся в лог после установки — сохраните их!
Сценарий "с нуля или поверх существующей установки":
```bash
curl -fsSL https://raw.githubusercontent.com/iqubik/3dp-manager/dp-custom/install-custom.sh | bash -s -- -r https://github.com/iqubik/3dp-manager.git -b dp-custom
```
Только обновление custom-кода (без базовой установки):
```bash
curl -fsSL https://raw.githubusercontent.com/iqubik/3dp-manager/dp-custom/update-custom.sh | bash -s -- -r https://github.com/iqubik/3dp-manager.git -b dp-custom
```
## Git workflow: dp-custom -> dp-fix -> автор
Роли веток:
- `dp-custom`: рабочая ветка для всех локальных/продовых/вспомогательных правок (включая `update-custom.sh`, `debug.md`, `pub.md`).
- `dp-fix`: чистая ветка для PR в репозиторий автора (только код, который должен попасть upstream).
Почему GitHub Desktop "не даёт":
- Он не всегда удобно поддерживает сценарий "перенос отдельных коммитов между ветками одного репо".
- Для этого используем terminal и `cherry-pick`.
Базовый цикл работы:
```bash
# 1) Работаем в dp-custom
git checkout dp-custom
# ... правки ...
git add .
git commit -m "feat: ... / fix: ..."
git push origin dp-custom
```
Проверка прода из `dp-custom`:
```bash
curl -fsSL https://raw.githubusercontent.com/iqubik/3dp-manager/dp-custom/update-custom.sh | bash -s -- -r https://github.com/iqubik/3dp-manager.git -b dp-custom
```
Перенос только нужных коммитов в `dp-fix`:
```bash
# 2) Смотрим, что нового в dp-custom относительно dp-fix
git log --oneline dp-fix..dp-custom
# 3) Переходим в чистую ветку PR
git checkout dp-fix
# 4) Переносим только нужные коммиты
git cherry-pick <commit_sha_1> <commit_sha_2> ...
# 5) Пушим чистую ветку и обновляем PR автору
git push origin dp-fix
```
Если конфликт при cherry-pick:
```bash
git status
# исправить конфликтные файлы
git add <resolved_files>
git cherry-pick --continue
```
Если нужно отменить текущий перенос:
```bash
git cherry-pick --abort
```
Проверка перед push в `dp-fix`:
```bash
git status --short
git diff --name-only upstream/dp-fix..HEAD
```
Ожидаем в `dp-fix` только те файлы, которые должны уйти автору.
## Файл .env.local
Файл `.env.local` в корне проекта содержит переменные окружения для локального стенда:
```bash
# Локальные учётные данные для разработки
POSTGRES_USER=admin
POSTGRES_PASSWORD=admin
POSTGRES_DB=3dp_manager
JWT_SECRET=localDevSecretKey12345678901234567890
ADMIN_LOGIN=admin
ADMIN_PASSWORD=admin
PORT=3100
ALLOWED_ORIGINS=http://localhost:8080,http://localhost
```
> ⚠️ **Не коммитьте `.env.local` в репозиторий!** Файл должен быть добавлен в `.gitignore` для безопасности.
### Изменение учётных данных
Если нужно изменить логин/пароль для локальной разработки:
1. Отредактируйте `.env.local`
2. Пересоздайте контейнеры с очисткой volumes:
```powershell
docker compose -f docker-compose.local.yml --env-file .env.local down -v
docker compose -f docker-compose.local.yml --env-file .env.local up -d --build
```
-4
View File
@@ -7,8 +7,6 @@ services:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
networks:
@@ -32,8 +30,6 @@ services:
PORT: ${PORT}
LOG_LEVEL: ${LOG_LEVEL}
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-}
ports:
- "${PORT}:${PORT}"
networks:
- app-network
+633
View File
@@ -0,0 +1,633 @@
#!/usr/bin/env bash
set -euo pipefail
#################################
# КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ
#################################
PROJECT_DIR="/opt/3dp-manager"
SOURCE_DIR="/opt/3dp-manager-src"
DEFAULT_REPO="https://github.com/iqubik/3dp-manager.git"
DEFAULT_BRANCH="dp-custom"
# Цвета
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log() { echo -e "${GREEN}[INFO]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
die() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; }
usage() {
cat <<'EOF'
Использование:
install-custom.sh [-r <repo_url>] [-b <branch>] [-p <project_dir>] [-s <source_dir>]
Параметры:
-r Git URL форка (по умолчанию: https://github.com/iqubik/3dp-manager.git)
-b Ветка форка (по умолчанию: dp-custom)
-p Папка установки (по умолчанию: /opt/3dp-manager)
-s Папка исходников (по умолчанию: /opt/3dp-manager-src)
-h Показать эту справку
Пример:
./install-custom.sh -r https://github.com/iqubik/3dp-manager.git -b dp-custom
EOF
}
need_root() {
[[ $EUID -eq 0 ]] || die "Этот скрипт должен быть запущен от имени root"
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Не найдена команда: $1"
}
resolve_compose_cmd() {
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=("docker" "compose")
return 0
fi
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=("docker-compose")
return 0
fi
warn "Не найден Docker Compose. Пытаемся установить..."
apt-get update
apt-get install -y docker-compose-plugin || true
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=("docker" "compose")
return 0
fi
apt-get install -y docker-compose || true
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=("docker-compose")
return 0
fi
die "Не удалось установить Docker Compose. Установите docker compose plugin (v2) или docker-compose (v1)."
}
check_containers_running() {
log "Проверка статуса контейнеров..."
local timeout=${1:-60}
local elapsed=0
local failed=0
while [ $elapsed -lt $timeout ]; do
failed=0
while IFS=$'\t' read -r container_name status; do
if [ -n "$container_name" ] && [ -n "$status" ]; then
if ! echo "$status" | grep -qiE "^up|running|healthy|restarting"; then
failed=1
warn "Контейнер $container_name в статусе: $status"
fi
fi
done < <("${COMPOSE_CMD[@]}" ps --format "table {{.Name}}\t{{.Status}}" --all 2>/dev/null | tail -n +2)
if [ $failed -eq 0 ]; then
log "Все контейнеры запущены успешно"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
return 1
}
get_random_port() {
local MIN=${1:-3000}
local MAX=${2:-6999}
while :; do
PORT=$(shuf -i "$MIN-$MAX" -n 1)
if ! ss -ltun | awk '{print $4}' | grep -q ":$PORT\$"; then
echo "$PORT"
return
fi
done
}
#################################
# PARSE ARGS
#################################
REPO_URL="$DEFAULT_REPO"
BRANCH="$DEFAULT_BRANCH"
while getopts ":r:b:p:s:h" opt; do
case "$opt" in
r) REPO_URL="$OPTARG" ;;
b) BRANCH="$OPTARG" ;;
p) PROJECT_DIR="$OPTARG" ;;
s) SOURCE_DIR="$OPTARG" ;;
h) usage; exit 0 ;;
:) die "Параметр -$OPTARG требует значение" ;;
\?) die "Неизвестный параметр: -$OPTARG" ;;
esac
done
#################################
# ASCII-баннер
#################################
echo "==================================================="
echo " ____ ____ _ ___ _ "
echo " / __ \___ ____ / __ \(_) (_)___ ______(_)___ ___ "
echo " / / / / _ \/ __ \/ /_/ / / / / __ \/ ___/ / __ \`__ \ "
echo " / /_/ / __/ / / / ____/ / / / /_/ / / / / / / / / /"
echo "/_____/\___/_/ /_/_/ /_/_/_/\__, /_/ /_/_/ /_/ /_/ "
echo " /____/ "
echo ""
echo " 3DP-MANAGER — CUSTOM INSTALL "
echo "==================================================="
echo ""
log "Репозиторий: $REPO_URL"
log "Ветка: $BRANCH"
#################################
# ПРОВЕРКИ
#################################
need_root
. /etc/os-release
if [[ "$ID" != "ubuntu" && "$ID" != "debian" ]]; then
die "Этот скрипт поддерживает только Ubuntu или Debian: $ID"
fi
if [ "$(free -m | grep Mem: | awk '{print $2}')" -lt 2000 ]; then
if [ "$(free -m | grep Swap: | awk '{print $2}')" -eq 0 ]; then
log "Мало RAM и нет Swap. Создаем swap-файл 2GB..."
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
echo '/swapfile none swap sw 0 0' >> /etc/fstab
log "Swap создан."
fi
fi
log "Проверка зависимостей..."
need_cmd curl
if ! command -v jq &> /dev/null; then apt-get update && apt-get install -y jq; fi
if ! command -v openssl &> /dev/null; then apt-get install -y openssl; fi
if ! command -v tar &> /dev/null; then apt-get install -y tar; fi
if ! command -v hostname &> /dev/null; then apt-get install -y net-tools || apt-get install -y hostname; fi
need_cmd git
#################################
# DOCKER
#################################
log "Проверка Docker"
if command -v docker >/dev/null 2>&1; then
log "Docker уже установлен"
else
log "Docker не найден, будет установлен из официального репозитория"
apt update
apt install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
if [[ "$ID" == "ubuntu" ]]; then
curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker"
else
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc || die "Ошибка добавления ключа Docker"
fi
chmod a+r /etc/apt/keyrings/docker.asc
CODENAME=${UBUNTU_CODENAME:-$VERSION_CODENAME}
tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: $CODENAME
Components: stable
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt update
apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable docker
systemctl start docker
fi
resolve_compose_cmd
log "Compose команда: ${COMPOSE_CMD[*]}"
#################################
# КЛОНИРОВАНИЕ ИСХОДНИКОВ
#################################
log "Клонирование исходников из $REPO_URL (ветка: $BRANCH)..."
if [[ -d "$SOURCE_DIR/.git" ]]; then
log "Исходники уже существуют, обновляем..."
cd "$SOURCE_DIR"
git remote set-url origin "$REPO_URL"
git fetch origin "$BRANCH"
if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
git checkout "$BRANCH"
git merge --ff-only FETCH_HEAD || die "Не удалось fast-forward merge."
else
git checkout -b "$BRANCH" FETCH_HEAD
fi
else
rm -rf "$SOURCE_DIR"
git clone --single-branch --branch "$BRANCH" "$REPO_URL" "$SOURCE_DIR"
fi
#################################
# СБОР ДАННЫХ
#################################
read -rp "Введите домен сервера (если пропустить, будет использоваться IP без HTTPS): " INPUT_HOST
USE_SSL=false
CERT_PATH=""
KEY_PATH=""
SKIP_SSL_SETUP=false
if [ -z "$INPUT_HOST" ]; then
UI_HOST=$(hostname -I | awk '{print $1}')
log "Домен не указан. Используется локальный IP: $UI_HOST"
log "Режим HTTPS принудительно отключен для IP-адреса."
USE_SSL=false
SKIP_SSL_SETUP=true
else
UI_HOST=$INPUT_HOST
SKIP_SSL_SETUP=false
fi
# Настройка SSL
if [[ "$SKIP_SSL_SETUP" == "false" ]]; then
LE_CERT="/etc/letsencrypt/live/$UI_HOST/fullchain.pem"
LE_KEY="/etc/letsencrypt/live/$UI_HOST/privkey.pem"
if [[ -f "$LE_CERT" && -f "$LE_KEY" ]]; then
log "Найдены сертификаты Let's Encrypt."
USE_SSL=true
CERT_PATH="$LE_CERT"
KEY_PATH="$LE_KEY"
else
read -rp "Использовать SSL (свои сертификаты)? (y/n): " ssl_ans
if [[ "$ssl_ans" =~ ^[Yy]$ ]]; then
read -rp "Путь к fullchain.pem: " user_cert
read -rp "Путь к privkey.pem: " user_key
if [[ -f "$user_cert" && -f "$user_key" ]]; then
USE_SSL=true
CERT_PATH="$user_cert"
KEY_PATH="$user_key"
else
warn "Файлы сертификатов не найдены. Будет использоваться HTTP."
fi
fi
fi
fi
FINAL_PORT=$(get_random_port)
# Генерация паролей
DB_PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
JWT_SECRET=$(openssl rand -base64 32)
ADMIN_USER=$(openssl rand -base64 8 | tr -dc 'A-Za-z0-9' | cut -c1-8)
ADMIN_PASS=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
# ALLOWED_ORIGINS
if [[ "$UI_HOST" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
ALLOWED_ORIGINS="http://${UI_HOST}"
else
ALLOWED_ORIGINS="https://${UI_HOST}"
fi
log "Сгенерированы секретные ключи для БД и JWT."
echo ""
echo "==================================================="
echo " УЧЁТНЫЕ ДАННЫЕ АДМИНИСТРАТОРА"
echo "==================================================="
echo " ADMIN_LOGIN: ${ADMIN_USER}"
echo " ADMIN_PASSWORD: ${ADMIN_PASS}"
echo " POSTGRES_PASSWORD: ${DB_PASS}"
echo " JWT_SECRET: ${JWT_SECRET}"
echo ""
echo " ⚠️ СОХРАНИТЕ ЭТИ ДАННЫЕ В БЕЗОПАСНОМ МЕСТЕ!"
echo "==================================================="
echo ""
#################################
# Hysteria 2
#################################
if ! systemctl cat hysteria-server.service &> /dev/null; then
log "Установка Hysteria 2..."
bash <(curl -fsSL https://get.hy2.sh/) < /dev/null || true
RANDOM_FREE_PORT=$(get_random_port 10000 20000)
GENERATED_PASSWORD=$(openssl rand -base64 16 | tr -dc 'A-Za-z0-9' | cut -c1-16)
GENERATED_OBFS_PASSWORD=$(openssl rand -base64 16 | tr -dc 'A-Za-z0-9' | cut -c1-16)
echo "=== Настройка Hysteria 2 ==="
read -e -p "Введите email для уведомлений Let's Encrypt: " HYSTERIA_EMAIL
HYSTERIA_EMAIL=$(echo "$HYSTERIA_EMAIL" | tr -cd 'a-zA-Z0-9.@_-')
cat > /etc/hysteria/config.yaml <<EOF
listen: :$RANDOM_FREE_PORT
acme:
domains:
- $UI_HOST
email: $HYSTERIA_EMAIL
auth:
type: password
password: $GENERATED_PASSWORD
obfs:
type: salamander
salamander:
password: $GENERATED_OBFS_PASSWORD
masquerade:
type: proxy
proxy:
url: https://ya.ru/
rewriteHost: true
EOF
systemctl daemon-reload
systemctl enable --now hysteria-server.service
systemctl restart hysteria-server.service
log "Hysteria 2 установлена на порту $RANDOM_FREE_PORT"
else
log "Hysteria 2 уже установлена, пропускаем."
fi
#################################
# ГЕНЕРАЦИЯ КОНФИГОВ
#################################
log "Генерация конфигурационных файлов..."
mkdir -p "$PROJECT_DIR/server"
mkdir -p "$PROJECT_DIR/client"
cd "$PROJECT_DIR"
cat > server/.env <<EOF
DB_HOST=localhost
DB_PORT=5432
DB_USERNAME=admin
DB_PASSWORD=${DB_PASS}
DB_NAME=3dp_manager
ADMIN_LOGIN=${ADMIN_USER}
ADMIN_PASSWORD=${ADMIN_PASS}
PORT=3100
LOG_LEVEL=error
ALLOWED_ORIGINS=${ALLOWED_ORIGINS}
EOF
if [[ "$USE_SSL" == "true" ]]; then
cat > client/nginx-client.conf <<EOF
server {
listen 443 ssl;
server_name $UI_HOST;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50M;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3100/api/;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
location /bus/ {
proxy_pass http://backend:3100/bus/;
proxy_set_header Host \$http_host;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
}
EOF
cat > docker-compose.yml <<EOF
services:
postgres:
image: postgres:18-alpine
container_name: 3dp-postgres
restart: always
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: 3dp_manager
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d 3dp_manager"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
backend:
build: $SOURCE_DIR/server
container_name: 3dp-backend
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: admin
DB_PASSWORD: ${DB_PASS}
DB_NAME: 3dp_manager
JWT_SECRET: ${JWT_SECRET}
ADMIN_LOGIN: ${ADMIN_USER}
ADMIN_PASSWORD: ${ADMIN_PASS}
PORT: 3100
LOG_LEVEL: error
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
volumes:
- /etc/hysteria/config.yaml:/etc/hysteria/config.yaml:ro
networks:
- app-network
frontend:
build: $SOURCE_DIR/client
container_name: 3dp-frontend
restart: always
depends_on:
- backend
ports:
- "${FINAL_PORT}:443"
volumes:
- ./client/nginx-client.conf:/etc/nginx/conf.d/default.conf:ro
- ${CERT_PATH}:/etc/nginx/certs/fullchain.pem:ro
- ${KEY_PATH}:/etc/nginx/certs/privkey.pem:ro
networks:
- app-network
volumes:
pg_data:
networks:
app-network:
driver: bridge
EOF
else
cat > client/nginx-client.conf <<EOF
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
client_max_body_size 50M;
location / {
try_files \$uri \$uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3100/api/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$http_host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
location /bus/ {
proxy_pass http://backend:3100/bus/;
proxy_http_version 1.1;
proxy_set_header Upgrade \$http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host \$http_host;
proxy_cache_bypass \$http_upgrade;
proxy_set_header X-Real-IP \$remote_addr;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_connect_timeout 10s;
proxy_send_timeout 650s;
proxy_read_timeout 650s;
}
}
EOF
cat > docker-compose.yml <<EOF
services:
postgres:
image: postgres:18-alpine
container_name: 3dp-postgres
restart: always
environment:
POSTGRES_USER: admin
POSTGRES_PASSWORD: ${DB_PASS}
POSTGRES_DB: 3dp_manager
volumes:
- pg_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U admin -d 3dp_manager"]
interval: 5s
timeout: 5s
retries: 5
networks:
- app-network
backend:
build: $SOURCE_DIR/server
container_name: 3dp-backend
restart: always
depends_on:
postgres:
condition: service_healthy
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_USERNAME: admin
DB_PASSWORD: ${DB_PASS}
DB_NAME: 3dp_manager
JWT_SECRET: ${JWT_SECRET}
ADMIN_LOGIN: ${ADMIN_USER}
ADMIN_PASSWORD: ${ADMIN_PASS}
PORT: 3100
LOG_LEVEL: error
ALLOWED_ORIGINS: ${ALLOWED_ORIGINS}
volumes:
- /etc/hysteria/config.yaml:/etc/hysteria/config.yaml:ro
networks:
- app-network
frontend:
build: $SOURCE_DIR/client
container_name: 3dp-frontend
restart: always
depends_on:
- backend
ports:
- "${FINAL_PORT}:80"
volumes:
- ./client/nginx-client.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- app-network
volumes:
pg_data:
networks:
app-network:
driver: bridge
EOF
fi
#################################
# СБОРКА И ЗАПУСК
#################################
log "Сборка и запуск контейнеров..."
"${COMPOSE_CMD[@]}" down || true
"${COMPOSE_CMD[@]}" up --build -d --remove-orphans
if ! check_containers_running 60; then
error "Не удалось запустить контейнеры. Логи:"
"${COMPOSE_CMD[@]}" logs --tail=50
die "Установка прервана из-за ошибки запуска контейнеров"
fi
log "Очистка кэша сборки..."
docker image prune -f
# Firewall
if LC_ALL=C ufw status 2>/dev/null | grep -q "Status: active"; then
log "Найден активный UFW. Настраиваю правила..."
ufw allow 443/tcp
ufw allow 443/udp
ufw allow 8443/tcp
ufw allow 8443/udp
ufw allow 10000:60000/tcp
ufw allow 10000:60000/udp
fi
echo ""
echo "==================================================="
if [[ "$USE_SSL" == "true" ]]; then
echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: https://${UI_HOST}:${FINAL_PORT}${NC}"
else
echo -e "${GREEN}✔ Установка завершена! Доступно по адресу: http://${UI_HOST}:${FINAL_PORT}${NC}"
fi
echo -e "${GREEN}Логин: ${ADMIN_USER}${NC}"
echo -e "${GREEN}Пароль: ${ADMIN_PASS}${NC}"
echo ""
echo "Немедленно измените пароль в Настройках утилиты!"
echo "==================================================="
+397
View File
@@ -0,0 +1,397 @@
#!/usr/bin/env bash
set -euo pipefail
#################################
# TRAP
#################################
trap 'echo -e "\033[1;31m[ERROR]\033[0m Ошибка в строке $LINENO"; exit 1' ERR
#################################
# HELPERS
#################################
log() { echo -e "\033[1;32m[INFO]\033[0m $1"; }
warn() { echo -e "\033[1;33m[WARN]\033[0m $1"; }
die() { echo -e "\033[1;31m[ERROR]\033[0m $1"; exit 1; }
usage() {
cat <<'EOF'
Использование:
update-custom.sh -r <repo_url> -b <branch> [-p <project_dir>] [-s <source_dir>]
Параметры:
-r Git URL вашего форка (обязательно), например:
https://github.com/<user>/3dp-manager.git
-b Ветка с вашими правками (обязательно), например:
dp-custom
-p Папка установленного 3dp-manager (по умолчанию /opt/3dp-manager)
-s Папка исходников для сборки (по умолчанию /opt/3dp-manager-src)
-h Показать эту справку
Пример:
./update-custom.sh -r https://github.com/iqubik/3dp-manager.git -b dp-custom
EOF
}
need_root() {
[[ $EUID -eq 0 ]] || die "Запускать только от root"
}
resolve_compose_cmd() {
if docker compose version >/dev/null 2>&1; then
COMPOSE_CMD=("docker" "compose")
return 0
fi
if command -v docker-compose >/dev/null 2>&1; then
COMPOSE_CMD=("docker-compose")
return 0
fi
die "Не найден Docker Compose (ни v2 plugin, ни v1 binary)"
}
check_containers_running() {
log "Проверка статуса контейнеров..."
local timeout=${1:-60}
local elapsed=0
local failed=0
while [ $elapsed -lt $timeout ]; do
failed=0
# Формат: NAME\tSTATUS (например: "3dp-postgres\tUp 2 days" или "3dp-postgres\tError")
while IFS=$'\t' read -r container_name status; do
if [ -n "$container_name" ] && [ -n "$status" ]; then
# Проверяем, что статус содержит Up/running/healthy/restarting
# Up, Up 2 days, Up Less than a second, (healthy), running, restarting
if ! echo "$status" | grep -qiE "^up|running|healthy|restarting"; then
failed=1
warn "Контейнер $container_name в статусе: $status"
fi
fi
done < <("${COMPOSE_CMD[@]}" ps --format "table {{.Name}}\t{{.Status}}" --all 2>/dev/null | tail -n +2)
if [ $failed -eq 0 ]; then
log "Все контейнеры запущены успешно"
return 0
fi
sleep 2
elapsed=$((elapsed + 2))
done
return 1
}
check_and_fix_credentials() {
log "Проверка учётных данных на безопасность..."
local env_file=".env"
local credentials_changed=0
# Проверяем, существует ли .env файл
if [[ ! -f "$env_file" ]]; then
log "Создание .env файла с безопасными учётными данными..."
local db_pass jwt_secret admin_login admin_pass
db_pass=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
jwt_secret=$(openssl rand -base64 32)
admin_login=$(openssl rand -base64 8 | tr -dc 'A-Za-z0-9' | cut -c1-8)
admin_pass=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
# Создаём .env файл
cat > "$env_file" <<EOF
POSTGRES_USER=admin
POSTGRES_PASSWORD=${db_pass}
POSTGRES_DB=3dp_manager
JWT_SECRET=${jwt_secret}
ADMIN_LOGIN=${admin_login}
ADMIN_PASSWORD=${admin_pass}
PORT=3100
LOG_LEVEL=error
ALLOWED_ORIGINS=
EOF
log "Сгенерированы новые учётные данные:"
log " ADMIN_LOGIN: ${admin_login}"
log " ADMIN_PASSWORD: ${admin_pass}"
log " POSTGRES_PASSWORD: ${db_pass}"
log " JWT_SECRET: ${jwt_secret}"
log "⚠️ Сохраните эти данные в безопасном месте!"
credentials_changed=1
else
local admin_login_val admin_pass_val jwt_secret_val db_pass_val
admin_login_val=$(grep -E "^ADMIN_LOGIN=" "$env_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
admin_pass_val=$(grep -E "^ADMIN_PASSWORD=" "$env_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
jwt_secret_val=$(grep -E "^JWT_SECRET=" "$env_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
db_pass_val=$(grep -E "^POSTGRES_PASSWORD=" "$env_file" 2>/dev/null | cut -d'=' -f2 | tr -d '"' | tr -d "'" || echo "")
local needs_update=0
if [[ "$admin_login_val" == "admin" ]] || [[ -z "$admin_login_val" ]]; then
warn "Обнаружен дефолтный ADMIN_LOGIN=admin"
needs_update=1
fi
if [[ "$admin_pass_val" == "admin" ]] || [[ -z "$admin_pass_val" ]]; then
warn "Обнаружен дефолтный ADMIN_PASSWORD=admin"
needs_update=1
fi
if [[ "$jwt_secret_val" == "secretKey" ]] || [[ -z "$jwt_secret_val" ]]; then
warn "Обнаружен дефолтный JWT_SECRET=secretKey"
needs_update=1
fi
if [[ "$db_pass_val" == "admin" ]] || [[ -z "$db_pass_val" ]]; then
warn "Обнаружен дефолтный POSTGRES_PASSWORD=admin"
needs_update=1
fi
if [[ $needs_update -eq 1 ]]; then
log "Генерация новых безопасных учётных данных..."
local new_db_pass new_jwt_secret new_admin_login new_admin_pass
new_db_pass=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
new_jwt_secret=$(openssl rand -base64 32)
new_admin_login=$(openssl rand -base64 8 | tr -dc 'A-Za-z0-9' | cut -c1-8)
new_admin_pass=$(openssl rand -base64 12 | tr -dc 'A-Za-z0-9' | cut -c1-12)
# Сохраняем существующие значения, которые не нужно менять
local existing_postgres_user
existing_postgres_user=$(grep -E "^POSTGRES_USER=" "$env_file" 2>/dev/null | cut -d'=' -f2 || echo "admin")
# Создаём новый .env файл
cat > "$env_file" <<EOF
POSTGRES_USER=${existing_postgres_user:-admin}
POSTGRES_PASSWORD=${new_db_pass}
POSTGRES_DB=3dp_manager
JWT_SECRET=${new_jwt_secret}
ADMIN_LOGIN=${new_admin_login}
ADMIN_PASSWORD=${new_admin_pass}
PORT=3100
LOG_LEVEL=error
ALLOWED_ORIGINS=
EOF
log "Сгенерированы новые учётные данные:"
log " ADMIN_LOGIN: ${new_admin_login}"
log " ADMIN_PASSWORD: ${new_admin_pass}"
log " POSTGRES_PASSWORD: ${new_db_pass}"
log " JWT_SECRET: ${new_jwt_secret}"
log "⚠️ Сохраните эти данные в безопасном месте!"
credentials_changed=1
else
log "Учётные данные безопасны ✅"
fi
fi
return $credentials_changed
}
ensure_nginx_api_timeouts() {
local nginx_conf="$1"
[[ -f "$nginx_conf" ]] || return 0
local tmp_file
tmp_file="$(mktemp)"
awk '
BEGIN { in_api = 0; in_bus = 0; injected = 0 }
{
line = $0
if (line ~ /^[[:space:]]*location[[:space:]]+\/api\/[[:space:]]*\{/) {
in_api = 1
in_bus = 0
injected = 0
}
if (line ~ /^[[:space:]]*location[[:space:]]+\/bus\/[[:space:]]*\{/) {
in_bus = 1
in_api = 0
injected = 0
}
if ((in_api || in_bus) && line ~ /proxy_(connect|send|read)_timeout[[:space:]]+[0-9]+s;/) {
next
}
print line
if ((in_api || in_bus) && line ~ /proxy_set_header[[:space:]]+X-Forwarded-For[[:space:]]+/ && injected == 0) {
print " proxy_connect_timeout 10s;"
print " proxy_send_timeout 650s;"
print " proxy_read_timeout 650s;"
injected = 1
}
if ((in_api || in_bus) && line ~ /^[[:space:]]*}/) {
in_api = 0
in_bus = 0
injected = 0
}
}
' "$nginx_conf" > "$tmp_file"
mv "$tmp_file" "$nginx_conf"
}
ensure_bus_location() {
local nginx_conf="$1"
[[ -f "$nginx_conf" ]] || return 0
# Проверяем, есть ли уже location /bus/
if grep -q "location /bus/" "$nginx_conf"; then
return 0
fi
local tmp_file
tmp_file="$(mktemp)"
awk '
{
print $0
if ($0 ~ /^[[:space:]]*location[[:space:]]+\/api\//) {
found_api = 1
}
if (found_api && $0 ~ /^[[:space:]]*\}/) {
print ""
print " location /bus/ {"
print " proxy_pass http://backend:3100/bus/;"
print " proxy_set_header Host $http_host;"
print " proxy_set_header X-Real-IP $remote_addr;"
print " proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;"
print " proxy_connect_timeout 10s;"
print " proxy_send_timeout 650s;"
print " proxy_read_timeout 650s;"
print " }"
found_api = 0
}
}
' "$nginx_conf" > "$tmp_file"
mv "$tmp_file" "$nginx_conf"
}
#################################
# PARSE ARGS
#################################
REPO_URL="https://github.com/iqubik/3dp-manager.git"
BRANCH="dp-custom"
PROJECT_DIR="/opt/3dp-manager"
SOURCE_DIR="/opt/3dp-manager-src"
while getopts ":r:b:p:s:h" opt; do
case "$opt" in
r) REPO_URL="$OPTARG" ;;
b) BRANCH="$OPTARG" ;;
p) PROJECT_DIR="$OPTARG" ;;
s) SOURCE_DIR="$OPTARG" ;;
h) usage; exit 0 ;;
:) die "Параметр -$OPTARG требует значение" ;;
\?) die "Неизвестный параметр: -$OPTARG" ;;
esac
done
need_root
[[ -d "$PROJECT_DIR" ]] || die "Папка проекта не найдена: $PROJECT_DIR"
[[ -f "$PROJECT_DIR/docker-compose.yml" ]] || die "Не найден docker-compose.yml в $PROJECT_DIR"
command -v git >/dev/null 2>&1 || die "git не установлен"
command -v docker >/dev/null 2>&1 || die "docker не установлен"
resolve_compose_cmd
log "Compose команда: ${COMPOSE_CMD[*]}"
#################################
# CHECK AND FIX CREDENTIALS
#################################
check_and_fix_credentials || true
#################################
# FETCH AND UPDATE SOURCE
#################################
log "Подготовка исходников в $SOURCE_DIR"
if [[ ! -d "$SOURCE_DIR/.git" ]]; then
if [[ -e "$SOURCE_DIR" ]]; then
die "Путь $SOURCE_DIR существует, но это не git-репозиторий. Укажите другой -s или подготовьте папку вручную."
fi
git clone --single-branch --branch "$BRANCH" "$REPO_URL" "$SOURCE_DIR"
else
cd "$SOURCE_DIR"
git remote set-url origin "$REPO_URL"
git fetch origin "$BRANCH"
if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
git checkout "$BRANCH"
# Merge from FETCH_HEAD to support repos cloned with --single-branch
# where origin/<branch> may not exist.
git merge --ff-only FETCH_HEAD || die "Не удалось fast-forward merge. Проверьте локальные изменения в $SOURCE_DIR."
else
git checkout -b "$BRANCH" FETCH_HEAD
fi
fi
#################################
# UPDATE DOCKER-COMPOSE FROM REPO
#################################
log "Обновление docker-compose.yml из ветки $BRANCH..."
curl -fsSL "${REPO_URL%.git}/raw/branch/$BRANCH/docker-compose.yml" \
-o "$PROJECT_DIR/docker-compose.yml" 2>/dev/null || {
# Fallback: скачиваем через raw.githubusercontent.com
if [[ "$REPO_URL" =~ ^https://github\.com/([^/]+)/([^/]+?)(\.git)?$ ]]; then
curl -fsSL "https://raw.githubusercontent.com/${BASH_REMATCH[1]}/${BASH_REMATCH[2]}/$BRANCH/docker-compose.yml" \
-o "$PROJECT_DIR/docker-compose.yml" || die "Не удалось скачать docker-compose.yml"
else
die "Не удалось скачать docker-compose.yml из $REPO_URL"
fi
}
#################################
# CREATE COMPOSE OVERRIDE
#################################
COMPOSE_OVERRIDE="$PROJECT_DIR/docker-compose.custom.yml"
cat > "$COMPOSE_OVERRIDE" <<EOF
services:
backend:
build: $SOURCE_DIR/server
image: 3dp-manager-server:custom
frontend:
build: $SOURCE_DIR/client
image: 3dp-manager-client:custom
EOF
#################################
# FIX NGINX CONFIG
#################################
ensure_nginx_api_timeouts "$PROJECT_DIR/client/nginx-client.conf"
ensure_bus_location "$PROJECT_DIR/client/nginx-client.conf"
#################################
# BUILD AND RESTART
#################################
log "Сборка custom-образов backend/frontend"
cd "$PROJECT_DIR"
"${COMPOSE_CMD[@]}" -f docker-compose.yml -f docker-compose.custom.yml build backend frontend
log "Перезапуск контейнеров с custom-образами"
"${COMPOSE_CMD[@]}" -f docker-compose.yml -f docker-compose.custom.yml up -d --remove-orphans backend frontend
# Проверка: все ли контейнеры запустились
if ! check_containers_running 60; then
error "Не удалось запустить контейнеры. Логи:"
"${COMPOSE_CMD[@]}" -f docker-compose.yml -f docker-compose.custom.yml logs --tail=50
die "Обновление прервано из-за ошибки запуска контейнеров"
fi
if "${COMPOSE_CMD[@]}" -f docker-compose.yml -f docker-compose.custom.yml exec -T backend sh -lc "command -v RealiTLScanner-linux-64 >/dev/null"; then
log "Scanner binary найден в backend-контейнере"
else
warn "Scanner binary не найден в backend-контейнере"
fi
log "Готово: VPS обновлен на ваш fork/branch"