diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml new file mode 100644 index 0000000..a8173ba --- /dev/null +++ b/.github/workflows/docker-publish.yml @@ -0,0 +1,42 @@ +name: Docker Build & Publish + +on: + push: + branches: [ "main", "dp-gui" ] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Log in to the Container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Server image + uses: docker/build-push-action@v5 + with: + context: ./server + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-server:${{ github.ref_name }} + + - name: Build and push Client image + uses: docker/build-push-action@v5 + with: + context: ./client + push: true + tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-client:${{ github.ref_name }} \ No newline at end of file diff --git a/client/nginx.conf b/client/nginx.conf index f224d69..c6c24d4 100644 --- a/client/nginx.conf +++ b/client/nginx.conf @@ -1,17 +1,32 @@ server { listen 80; + server_name localhost; + + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript; location / { - root /usr/share/nginx/html; - index index.html index.htm; try_files $uri $uri/ /index.html; } - location /api/ { + proxy_pass http://server:3000; + + 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; + } +} +server { + listen 3000; + server_name localhost; + location / { proxy_pass http://backend:3000/; - 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 Host \$http_host; } } \ No newline at end of file diff --git a/client/src/components/Footer.tsx b/client/src/components/Footer.tsx index 2bc8c9a..0b2fc2f 100644 --- a/client/src/components/Footer.tsx +++ b/client/src/components/Footer.tsx @@ -42,7 +42,7 @@ export default function Footer() { (''); + const [loadingRotate, setLoadingRotate] = useState(false); useEffect(() => { loadSettings(); @@ -37,6 +41,42 @@ export default function SettingsPage() { } }, [settings.rotation_interval]); + const cleanData = () => { + const cleaned = { ...settings }; + + if (cleaned.xui_url) { + cleaned.xui_url = cleaned.xui_url.replace(/\/+$/, ''); + } + + if (cleaned.xui_login) cleaned.xui_login = cleaned.xui_login.trim(); + if (cleaned.xui_password) cleaned.xui_password = cleaned.xui_password.trim(); + + setSettings(prev => ({ ...prev, ...cleaned })); + + return cleaned; + }; + + const handleCheckConnection = async () => { + const data = cleanData(); // Сначала чистим + + try { + setMsg({ open: true, type: 'success', text: 'Проверка...' }); + const res = await api.post('/settings/check', { + xui_url: data.xui_url, + xui_login: data.xui_login, + xui_password: data.xui_password + }); + + if (res.data.success) { + setMsg({ open: true, type: 'success', text: 'Подключение успешно!' }); + } else { + setMsg({ open: true, type: 'error', text: 'Ошибка: Неверные данные или нет доступа' }); + } + } catch (e) { + setMsg({ open: true, type: 'error', text: 'Ошибка сети при проверке' }); + } + }; + const loadSettings = async () => { try { const { data } = await api.get('/settings'); @@ -64,8 +104,10 @@ export default function SettingsPage() { return; } + const data = cleanData(); + try { - await api.post('/settings', settings); + await api.post('/settings', data); setMsg({ open: true, type: 'success', text: 'Настройки сохранены!' }); } catch (e) { setMsg({ open: true, type: 'error', text: 'Ошибка сохранения' }); @@ -89,20 +131,125 @@ export default function SettingsPage() { const handleForceRotate = async () => { if (confirm('ВНИМАНИЕ: Это немедленно обновит конфиги в подписках.\n\nИнтервал автоматической ротации НЕ будет сброшен.\n\nПродолжить?')) { try { - await api.post('/rotation/rotate-all'); - setMsg({ open: true, type: 'success', text: 'Ротация успешно выполнена!' }); + setLoadingRotate(true); + const res = await api.post('/rotation/rotate-all'); + + setLoadingRotate(false); + if (res.data && res.data.success) { + setMsg({ open: true, type: 'success', text: res.data.message || 'Ротация успешно выполнена!' }); + } else { + setMsg({ + open: true, + type: 'error', + text: res.data?.message || 'Ошибка выполнения ротации' + }); + } } catch (e) { - setMsg({ open: true, type: 'error', text: 'Ошибка при запуске ротации' }); + setLoadingRotate(false); + setMsg({ open: true, type: 'error', text: 'Ошибка сети или сервера' }); } } }; + const togglePause = async () => { + const newStatus = settings.rotation_status === 'active' ? 'stopped' : 'active'; + const updatedSettings = { ...settings, rotation_status: newStatus }; + + setSettings(updatedSettings); + + try { + await api.post('/settings', updatedSettings); + + } catch (e) { + setSettings((prev: any) => ({ ...prev, rotation_status: settings.rotation_status })); + setMsg({ open: true, type: 'error', text: 'Не удалось изменить статус' }); + } + }; + + const formatDate = (isoString: string) => { + if (!isoString) return 'Нет данных'; + return new Date(+isoString).toLocaleString('ru-RU', { + day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' + }); + }; + + const getNextRotationDate = () => { + if (settings.rotation_status === 'stopped') return 'Пауза'; + if (!settings.last_rotation_timestamp) return 'Ожидание...'; + + const last = new Date(+settings.last_rotation_timestamp); + const intervalMinutes = parseInt(settings.rotation_interval) || 60; + const next = new Date(last.getTime() + intervalMinutes * 60000); + + return next.toLocaleString('ru-RU', { + day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' + }); + }; + + const isPaused = settings.rotation_status === 'stopped'; + return ( Настройки утилиты + + + + + Статус сервиса + + {isPaused ? + } label="Остановлен" color="warning" size="small" variant="outlined" /> : + } label="Активен" color="success" size="small" variant="outlined" /> + } + + + + {isPaused ? : } + + + + {/* Последняя генерация */} + + + + + Последняя генерация + + + {formatDate(settings.last_rotation_timestamp)} + + + + + + {/* Следующая генерация */} + + + + + Следующая генерация + + + {getNextRotationDate()} + + + + + + + Панель 3x-ui @@ -111,7 +258,7 @@ export default function SettingsPage() { Сохранить подключение + {settings.xui_url && settings.xui_login && settings.xui_password && ( + + )} @@ -162,6 +319,7 @@ export default function SettingsPage() { + diff --git a/install.sh b/install.sh index a3c9226..7628ab7 100644 --- a/install.sh +++ b/install.sh @@ -4,8 +4,11 @@ set -euo pipefail ################################# # КОНФИГУРАЦИЯ И ПЕРЕМЕННЫЕ ################################# -REPO_URL="https://github.com/denpiligrim/3dp-manager/archive/refs/heads/main.tar.gz" PROJECT_DIR="/opt/3dp-manager" +DOCKER_USER="denpiligrim" +DOCKER_TAG="latest" +IMAGE_SERVER="ghcr.io/${DOCKER_USER}/3dp-manager-server:${DOCKER_TAG}" +IMAGE_CLIENT="ghcr.io/${DOCKER_USER}/3dp-manager-client:${DOCKER_TAG}" # Цвета для вывода RED='\033[0;31m' @@ -104,9 +107,6 @@ fi log "Подготовка директории $PROJECT_DIR..." mkdir -p "$PROJECT_DIR" -log "Скачивание последней версии проекта..." -curl -L "$REPO_URL" | tar xz -C "$PROJECT_DIR" --strip-components=1 - cd "$PROJECT_DIR" ################################# @@ -178,51 +178,6 @@ log "Сгенерированы секретные ключи для БД и JWT ################################# # ГЕНЕРАЦИЯ ФАЙЛОВ DOCKER ################################# - -# --- 1. Dockerfile для Client --- -cat > client/Dockerfile < server/Dockerfile < server/.env < client/nginx-client.conf < client/nginx-client.conf <, + private xuiService: XuiService ) {} @Get() @@ -18,6 +20,12 @@ export class SettingsController { return settings.reduce((acc, curr) => ({ ...acc, [curr.key]: curr.value }), {}); } + @Post('check') + async checkConnection(@Body() body: { xui_url: string; xui_login: string; xui_password: string }) { + const success = await this.xuiService.checkConnection(body.xui_url, body.xui_login, body.xui_password); + return { success }; + } + @Post() async update(@Body() settings: Record) { if (settings.xui_url) { diff --git a/server/src/settings/settings.module.ts b/server/src/settings/settings.module.ts index 8088e44..90ebc11 100644 --- a/server/src/settings/settings.module.ts +++ b/server/src/settings/settings.module.ts @@ -2,9 +2,10 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { SettingsController } from './settings.controller'; import { Setting } from './entities/setting.entity'; +import { XuiModule } from 'src/xui/xui.module'; @Module({ - imports: [TypeOrmModule.forFeature([Setting])], + imports: [TypeOrmModule.forFeature([Setting]), XuiModule], controllers: [SettingsController], }) export class SettingsModule {} \ No newline at end of file diff --git a/server/src/subscriptions/subscriptions.service.ts b/server/src/subscriptions/subscriptions.service.ts index f6f71b6..0c1528a 100644 --- a/server/src/subscriptions/subscriptions.service.ts +++ b/server/src/subscriptions/subscriptions.service.ts @@ -11,7 +11,6 @@ export class SubscriptionsService { constructor( @InjectRepository(Subscription) private subRepo: Repository, - @InjectRepository(Inbound) private xuiService: XuiService, ) {} diff --git a/server/src/xui/xui.service.ts b/server/src/xui/xui.service.ts index 8b0d18d..d804dbb 100644 --- a/server/src/xui/xui.service.ts +++ b/server/src/xui/xui.service.ts @@ -119,6 +119,29 @@ export class XuiService { } } + async checkConnection(url: string, username: string, pass: string): Promise { + try { + const tempApi = axios.create({ + baseURL: url, + timeout: 5000, + httpsAgent: new https.Agent({ rejectUnauthorized: false }), + withCredentials: true + }); + + const res = await tempApi.post('/login', { + username: username, + password: pass, + }); + + if (res.headers['set-cookie'] && res.data?.success) { + return true; + } + } catch (e) { + this.logger.warn(`Ошибка авторизации: ${e.message}`); + } + return false; + } + async getNewX25519Cert() { try { const res = await this.api.get('/panel/api/server/getNewX25519Cert'); diff --git a/update.sh b/update.sh index 458a457..a35c343 100644 --- a/update.sh +++ b/update.sh @@ -17,17 +17,10 @@ need_root() { [[ $EUID -eq 0 ]] || die "Запускать только от root" } -if ! command -v curl >/dev/null 2>&1; then - echo "❌ curl не установлен. Установите curl и повторите попытку" - echo " apt install -y curl" - exit 1 -fi - ################################# # CONFIG ################################# PROJECT_DIR="/opt/3dp-manager" -REPO_RAW="https://raw.githubusercontent.com/denpiligrim/3dp-manager/main" ################################# # START @@ -46,53 +39,21 @@ cd "$PROJECT_DIR" command -v docker >/dev/null 2>&1 || die "Docker не установлен" docker compose version >/dev/null 2>&1 || die "docker compose v2 недоступен" -################################# -# DOWNLOAD FILES -################################# -log "Загружаем обновлённые файлы из репозитория" - -mkdir -p app - -curl -fsSL "$REPO_RAW/app/Dockerfile" -o app/Dockerfile -curl -fsSL "$REPO_RAW/app/package.json" -o app/package.json -curl -fsSL "$REPO_RAW/app/index.js" -o app/index.js -curl -fsSL "$REPO_RAW/app/rotate.js" -o app/rotate.js -curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityTcp.js" -o app/builders/buildVlessRealityTcp.js -curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityXhttp.js" -o app/builders/buildVlessRealityXhttp.js -curl -fsSL "$REPO_RAW/app/builders/buildTrojanRealityTcp.js" -o app/builders/buildTrojanRealityTcp.js -curl -fsSL "$REPO_RAW/app/builders/buildShadowsocksTcp.js" -o app/builders/buildShadowsocksTcp.js -curl -fsSL "$REPO_RAW/app/builders/buildVmessTcp.js" -o app/builders/buildVmessTcp.js -curl -fsSL "$REPO_RAW/app/builders/buildVlessRealityGrpc.js" -o app/builders/buildVlessRealityGrpc.js -curl -fsSL "$REPO_RAW/app/builders/buildVlessWs.js" -o app/builders/buildVlessWs.js -curl -fsSL "$REPO_RAW/app/builders/buildInboundLink.js" -o app/builders/buildInboundLink.js -curl -fsSL "$REPO_RAW/whitelist.txt" -o whitelist.txt - -log "Файлы обновлены" - ################################# # REBUILD BACKEND ################################# -log "Пересобираем backend" -docker compose build node - -################################# -# RESTART CONTAINERS -################################# -log "Перезапускаем контейнеры" -docker compose up -d - -if [ -f "app/my_whitelist.txt" ]; then - log "✔ Копируем my_whitelist.txt в контейнер..." - docker cp app/my_whitelist.txt node:/app/my_whitelist.txt +log "Скачивание последних версий Docker-образов..." +if docker compose pull; then + log "Образы успешно загружены." +else + error "Ошибка при скачивании образов. Проверьте подключение к интернету или доступность GitHub Container Registry." fi -################################# -# HEALTH CHECK -################################# -sleep 2 +log "Пересоздание контейнеров..." +docker compose up -d -docker compose ps | grep node >/dev/null || die "Backend не запущен" -docker compose ps | grep nginx >/dev/null || die "Nginx не запущен" +log "Очистка старых Docker-образов (освобождение места)..." +docker image prune -f ################################# # DONE