Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bd3771e7fd | |||
| dd64e4dcb5 | |||
| 53ea8902e6 | |||
| 9f4ef52aaa | |||
| e64773f703 | |||
| 69d030e3b4 | |||
| 1908317d92 | |||
| 9861b87465 | |||
| 9aff2e78d8 | |||
| 98ff14d464 | |||
| 653896ae56 | |||
| eb21838ce6 | |||
| 007afd3b83 |
@@ -0,0 +1,42 @@
|
||||
name: Docker Build & Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main" ]
|
||||
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 }}
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
|
||||
# 3DP-MANAGER
|
||||
|
||||
Утилита для автогенерации инбаундов к панели [3x-ui](https://github.com/MHSanaei/3x-ui), формирования единой подписки и настройки перенаправления трафика с промежуточного сервера на основной.
|
||||
Утилита для автогенерации инбаундов к панели [3x-ui](https://github.com/MHSanaei/3x-ui), формирования единой подписки и настройки перенаправления трафика с промежуточного сервера на основной. Утилита, начиная с версии 2.0.0 имеет графический интерфейс и простые пользовательские настройки.
|
||||
|
||||
**Поддержать проект**
|
||||
|
||||
@@ -61,10 +61,7 @@
|
||||
|
||||
## Установка
|
||||
|
||||
> [!WARNING]
|
||||
> Утилита работает на основном сервере
|
||||
|
||||
У вас должны быть установлены пакеты на сервере `curl`, `jq` командой: `apt install curl jq` и панель управления `3x-ui`, которую можно установить командой: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)`
|
||||
У вас должна быть установлена панель управления `3x-ui`, которую можно поставить командой: `bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)`
|
||||
Установите проект на сервер командой:
|
||||
|
||||
```bash
|
||||
@@ -73,12 +70,6 @@ bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main
|
||||
|
||||
<sup>Краткое описание: запускает скрипт установки и разворачивает контейнеры и сервисы.</sup>
|
||||
|
||||
Если панель 3x-ui находится в Docker контейнере, установите командой:
|
||||
|
||||
```bash
|
||||
REMOTE_PANEL=true bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/install.sh)
|
||||
```
|
||||
|
||||
## Обновление
|
||||
|
||||
Обновление до последней версии:
|
||||
@@ -112,7 +103,7 @@ bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/forwarding_install.sh)
|
||||
```
|
||||
|
||||
<sup>Краткое описание: добавляет правила перенаправления и создает сервис для обновления подписки.</sup>
|
||||
<sup>Краткое описание: добавляет правила перенаправления.</sup>
|
||||
|
||||
## Удаление перенаправления
|
||||
|
||||
@@ -120,20 +111,10 @@ bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main
|
||||
bash <(curl -fsSL https://raw.githubusercontent.com/denpiligrim/3dp-manager/main/forwarding_delete.sh)
|
||||
```
|
||||
|
||||
<sup>Краткое описание: удаляет правила и отключает сервис перенаправления.</sup>
|
||||
<sup>Краткое описание: удаляет правила перенаправления.</sup>
|
||||
|
||||
---
|
||||
|
||||
## Показать URL подписки
|
||||
|
||||
Команда для вывода текущего URL подписки из среды контейнера:
|
||||
|
||||
```bash
|
||||
cd /opt/3dp-manager && docker compose exec node env | grep SUB_URL | cut -d'=' -f2
|
||||
```
|
||||
|
||||
<sup>Краткое описание: выводит статичный URL подписки, который можно использовать в клиентах. Работает как на основном, так и на промежуточном сервере.</sup>
|
||||
|
||||
## Сбор доменов из мульти-подписок
|
||||
|
||||
Утилита извлекает домены из подписок и формирует `whitelist` для генератора.
|
||||
@@ -144,29 +125,6 @@ node get_domains.js
|
||||
|
||||
<sup>Краткое описание: добавьте ссылку на мульти-подписку в скрипт и запустите команду — на выходе получите список доменов. Необходим `Node.js` для работы скрипта.</sup>
|
||||
|
||||
## Использование собственного белого списка
|
||||
|
||||
1. Подготовьте файл в формате `whitelist.txt`.
|
||||
2. Переименуйте на `my_whitelist.txt` и скопируйте в папку `/opt/3dp-manager/app`.
|
||||
|
||||
```bash
|
||||
cd /opt/3dp-manager && docker cp ./app/my_whitelist.txt node:/app/my_whitelist.txt
|
||||
```
|
||||
|
||||
<sup>Краткое описание: добавляет ваш файл доменов в контейнер приложения. Чтобы сразу же сгенерировать инбаунды с новым списком, выполните `docker exec -it node sh` и затем `node rotate.js`.</sup>
|
||||
|
||||
## Ручной запуск генерации
|
||||
|
||||
Чтобы создать новые инбаунды, выполните команды поочередно:
|
||||
|
||||
```bash
|
||||
cd /opt/3dp-manager
|
||||
docker exec -it node sh
|
||||
node rotate.js
|
||||
```
|
||||
|
||||
<sup>Краткое описание: выполняет немедленную генерацию инбаундов, не влияя на заданный интервал ротации.</sup>
|
||||
|
||||
---
|
||||
|
||||
## Замечания и текущие ограничения
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
|
||||
# 3DP-MANAGER
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
|
||||
# 3DP-MANAGER
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
|
||||
# 3DP-MANAGER
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
<p><img src="https://denpiligrim.ru/storage/images/3dp-manager.png" alt="3dp-manager preview"></p>
|
||||
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
 [](https://www.gnu.org/licenses/gpl-3.0) [](https://t.me/denpiligrim_web) [](https://www.youtube.com/@denpiligrim)
|
||||
|
||||
# 3DP-MANAGER
|
||||
|
||||
|
||||
+21
-6
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export default function Footer() {
|
||||
|
||||
<IconButton
|
||||
component="a"
|
||||
href="https://github.com/denpiligrim"
|
||||
href="https://github.com/denpiligrim/3dp-manager"
|
||||
target="_blank"
|
||||
aria-label="GitHub"
|
||||
color="inherit"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip } from '@mui/material';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton } from '@mui/material';
|
||||
import api from '../api';
|
||||
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Schedule, Update } from '@mui/icons-material';
|
||||
|
||||
const ROTATION_PRESETS = [
|
||||
{ label: 'Сутки', value: 1440 },
|
||||
@@ -14,6 +15,8 @@ export default function SettingsPage() {
|
||||
xui_login: '',
|
||||
xui_password: '',
|
||||
rotation_interval: '30',
|
||||
rotation_status: 'active',
|
||||
last_rotation_timestamp: '',
|
||||
});
|
||||
|
||||
const [adminProfile, setAdminProfile] = useState({
|
||||
@@ -22,7 +25,8 @@ export default function SettingsPage() {
|
||||
});
|
||||
|
||||
const [msg, setMsg] = useState({ open: false, type: 'success' as 'success' | 'error', text: '' });
|
||||
const [intervalError, setIntervalError] = useState('');
|
||||
const [intervalError, setIntervalError] = useState<string>('');
|
||||
const [loadingRotate, setLoadingRotate] = useState<boolean>(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 (
|
||||
<Box>
|
||||
<Typography variant="h4" gutterBottom>Настройки утилиты</Typography>
|
||||
|
||||
<Grid container spacing={3}>
|
||||
|
||||
<Grid size={{ xs: 12 }}>
|
||||
<Grid container spacing={1}>
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Typography variant="subtitle2" color="textSecondary" gutterBottom>
|
||||
Статус сервиса
|
||||
</Typography>
|
||||
{isPaused ?
|
||||
<Chip icon={<PauseCircleFilled />} label="Остановлен" color="warning" size="small" variant="outlined" /> :
|
||||
<Chip icon={<CheckCircle />} label="Активен" color="success" size="small" variant="outlined" />
|
||||
}
|
||||
|
||||
<Tooltip title={isPaused ? "Возобновить ротацию" : "Поставить на паузу"}>
|
||||
<IconButton
|
||||
onClick={togglePause}
|
||||
size="small"
|
||||
sx={{
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 2,
|
||||
'&:hover': { bgcolor: 'background.paper' },
|
||||
ml: 1
|
||||
}}
|
||||
>
|
||||
{isPaused ? <PlayCircleFilled fontSize="large" /> : <PauseCircleFilled fontSize="large" />}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
{/* Последняя генерация */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Последняя генерация
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
|
||||
{formatDate(settings.last_rotation_timestamp)}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
|
||||
{/* Следующая генерация */}
|
||||
<Grid size={{ xs: 12, md: 4 }}>
|
||||
<Stack direction="row" alignItems="center" spacing={1}>
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="textSecondary">
|
||||
Следующая генерация
|
||||
</Typography>
|
||||
<Typography variant="body1" sx={{ fontWeight: 500, mt: 2 }}>
|
||||
{getNextRotationDate()}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid size={{ xs: 12, md: 6 }}>
|
||||
<Paper sx={{ p: 3, height: '100%' }}>
|
||||
<Typography variant="h6" gutterBottom>Панель 3x-ui</Typography>
|
||||
@@ -111,7 +258,7 @@ export default function SettingsPage() {
|
||||
<TextField
|
||||
fullWidth margin="normal" label="URL панели"
|
||||
value={settings.xui_url} onChange={handleSettingChange('xui_url')}
|
||||
helperText="Например: https://my-vpn.com:2053/panel_path"
|
||||
helperText="Например: https://my-vpn.com:2053/wfgpoVHaOF"
|
||||
/>
|
||||
<TextField
|
||||
fullWidth margin="normal" label="Логин 3x-ui"
|
||||
@@ -125,6 +272,16 @@ export default function SettingsPage() {
|
||||
<Button variant="contained" sx={{ mt: 2 }} onClick={handleSaveSettings}>
|
||||
Сохранить подключение
|
||||
</Button>
|
||||
{settings.xui_url && settings.xui_login && settings.xui_password && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="info"
|
||||
sx={{ mt: 2, ml: 2 }}
|
||||
onClick={handleCheckConnection}
|
||||
>
|
||||
Проверить
|
||||
</Button>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
@@ -162,6 +319,7 @@ export default function SettingsPage() {
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
loading={loadingRotate}
|
||||
color="warning"
|
||||
onClick={handleForceRotate}
|
||||
sx={{ mt: 2, ml: 2 }}
|
||||
|
||||
@@ -191,12 +191,18 @@ export default function SubscriptionsPage() {
|
||||
</Table>
|
||||
</Paper>
|
||||
|
||||
<Dialog open={open} onClose={() => setOpen(false)}>
|
||||
<Dialog open={open} onClose={() => setOpen(false)} disableRestoreFocus>
|
||||
<DialogTitle>Новая подписка</DialogTitle>
|
||||
<DialogContent>
|
||||
<TextField
|
||||
autoFocus margin="dense" label="Имя пользователя" fullWidth
|
||||
value={name} onChange={(e) => setName(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleCreate();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
@@ -215,7 +221,7 @@ export default function SubscriptionsPage() {
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать всё</Button>
|
||||
<Button onClick={() => navigator.clipboard.writeText(currentLinks.join('\n'))}>Копировать все</Button>
|
||||
<Button onClick={() => setLinksOpen(false)}>Закрыть</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
+21
-61
@@ -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="main"
|
||||
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'
|
||||
@@ -103,9 +106,8 @@ fi
|
||||
#################################
|
||||
log "Подготовка директории $PROJECT_DIR..."
|
||||
mkdir -p "$PROJECT_DIR"
|
||||
|
||||
log "Скачивание последней версии проекта..."
|
||||
curl -L "$REPO_URL" | tar xz -C "$PROJECT_DIR" --strip-components=1
|
||||
mkdir -p "$PROJECT_DIR/server"
|
||||
mkdir -p "$PROJECT_DIR/client"
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
@@ -178,51 +180,6 @@ log "Сгенерированы секретные ключи для БД и JWT
|
||||
#################################
|
||||
# ГЕНЕРАЦИЯ ФАЙЛОВ DOCKER
|
||||
#################################
|
||||
|
||||
# --- 1. Dockerfile для Client ---
|
||||
cat > client/Dockerfile <<EOF
|
||||
FROM node:24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
|
||||
COPY . .
|
||||
|
||||
ENV VITE_API_URL=/api
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:alpine
|
||||
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
COPY nginx-client.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
EOF
|
||||
|
||||
# --- 3. Dockerfile для Server ---
|
||||
cat > server/Dockerfile <<EOF
|
||||
FROM node:24-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
FROM node:24-alpine
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --only=production
|
||||
COPY --from=builder /app/dist ./dist
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=3000
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/main"]
|
||||
EOF
|
||||
|
||||
cat > server/.env <<EOF
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
@@ -237,7 +194,7 @@ if [[ "$USE_SSL" == "true" ]]; then
|
||||
# 1. Nginx Config
|
||||
cat > client/nginx-client.conf <<EOF
|
||||
server {
|
||||
listen $FINAL_PORT ssl;
|
||||
listen 443 ssl;
|
||||
server_name $UI_HOST;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -251,7 +208,7 @@ server {
|
||||
}
|
||||
location /api/ {
|
||||
proxy_pass http://backend:3000/api/;
|
||||
proxy_set_header Host \$host;
|
||||
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;
|
||||
}
|
||||
@@ -266,7 +223,7 @@ server {
|
||||
|
||||
location / {
|
||||
proxy_pass http://backend:3000/;
|
||||
proxy_set_header Host \$host;
|
||||
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_set_header X-Forwarded-Proto https;
|
||||
@@ -296,7 +253,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build: ./server
|
||||
image: ${IMAGE_SERVER}
|
||||
container_name: 3dp-backend
|
||||
restart: always
|
||||
depends_on:
|
||||
@@ -314,15 +271,16 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
build: ./client
|
||||
image: ${IMAGE_CLIENT}
|
||||
container_name: 3dp-frontend
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "${FINAL_PORT}:${FINAL_PORT}"
|
||||
- "${FINAL_PORT}:443"
|
||||
- "3000:3000"
|
||||
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:
|
||||
@@ -342,7 +300,7 @@ else
|
||||
# 1. Nginx Config
|
||||
cat > client/nginx-client.conf <<EOF
|
||||
server {
|
||||
listen ${FINAL_PORT};
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
@@ -356,7 +314,7 @@ server {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host \$host;
|
||||
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;
|
||||
@@ -367,7 +325,7 @@ server {
|
||||
server_name localhost;
|
||||
location / {
|
||||
proxy_pass http://backend:3000/;
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header Host \$http_host;
|
||||
}
|
||||
}
|
||||
EOF
|
||||
@@ -394,7 +352,7 @@ services:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build: ./server
|
||||
image: ${IMAGE_SERVER}
|
||||
container_name: 3dp-backend
|
||||
restart: always
|
||||
depends_on:
|
||||
@@ -412,14 +370,16 @@ services:
|
||||
- app-network
|
||||
|
||||
frontend:
|
||||
build: ./client
|
||||
image: ${IMAGE_CLIENT}
|
||||
container_name: 3dp-frontend
|
||||
restart: always
|
||||
depends_on:
|
||||
- backend
|
||||
ports:
|
||||
- "${FINAL_PORT}:${FINAL_PORT}"
|
||||
- "${FINAL_PORT}:80"
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- ./client/nginx-client.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ export class InboundBuilderService {
|
||||
},
|
||||
tcpSettings: { acceptProxyProtocol: false, header: { type: 'none' } }
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
sniffing: JSON.stringify({ enabled: false, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
};
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ export class InboundBuilderService {
|
||||
protocol: 'vless',
|
||||
remark: `vless-xhttp-reality`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: 'xtls-rprx-vision', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
fallbacks: []
|
||||
@@ -66,9 +66,23 @@ export class InboundBuilderService {
|
||||
shortIds: [crypto.randomBytes(4).toString('hex'), crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
},
|
||||
xhttpSettings: { path: '/', mode: 'auto' }
|
||||
xhttpSettings: {
|
||||
host: domain,
|
||||
path: "/",
|
||||
mode: "auto",
|
||||
noSSEHeader: false,
|
||||
scMaxBufferedPosts: 30,
|
||||
scMaxEachPostBytes: "1000000",
|
||||
scStreamUpServerSecs: "20-80",
|
||||
xPaddingBytes: "100-1000"
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'], metadataOnly: false, routeOnly: false })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -77,17 +91,28 @@ export class InboundBuilderService {
|
||||
return {
|
||||
enable: true,
|
||||
port,
|
||||
protocol: 'vless',
|
||||
remark: `vless-grpc-reality`,
|
||||
protocol: "vless",
|
||||
remark: "vless-reality-grpc",
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
decryption: "none",
|
||||
encryption: "none",
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'grpc',
|
||||
security: 'reality',
|
||||
network: "grpc",
|
||||
security: "reality",
|
||||
externalProxy: [],
|
||||
realitySettings: {
|
||||
show: false,
|
||||
@@ -99,9 +124,18 @@ export class InboundBuilderService {
|
||||
shortIds: [crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
},
|
||||
grpcSettings: { serviceName: 'grpc', multiMode: false }
|
||||
grpcSettings: {
|
||||
serviceName: "myservice",
|
||||
authority: domain,
|
||||
multiMode: false,
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -113,18 +147,39 @@ export class InboundBuilderService {
|
||||
protocol: 'vless',
|
||||
remark: `vless-ws`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, flow: '', email: uuid, enable: true, limitIp: 0, totalGB: 0, expiryTime: 0, tgId: '', subId: '', reset: 0 }],
|
||||
decryption: 'none',
|
||||
encryption: 'none',
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
decryption: "none",
|
||||
encryption: "none",
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'ws',
|
||||
security: 'none',
|
||||
network: "ws",
|
||||
security: "none",
|
||||
externalProxy: [],
|
||||
wsSettings: { path: '/', headers: { Host: domain } }
|
||||
wsSettings: {
|
||||
host: domain,
|
||||
path: "/",
|
||||
acceptProxyProtocol: false,
|
||||
heartbeatPeriod: 0,
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -136,29 +191,77 @@ export class InboundBuilderService {
|
||||
protocol: 'vmess',
|
||||
remark: 'vmess-tcp',
|
||||
settings: JSON.stringify({
|
||||
clients: [{ id: uuid, alterId: 0, email: uuid, limitIp: 0, totalGB: 0, expiryTime: 0, enable: true, tgId: '', subId: '', reset: 0 }],
|
||||
disableInsecureEncryption: false
|
||||
clients: [{
|
||||
id: uuid,
|
||||
flow: "",
|
||||
email: uuid,
|
||||
enable: true,
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "0",
|
||||
alterId: "0",
|
||||
reset: 0
|
||||
}],
|
||||
}),
|
||||
streamSettings: JSON.stringify({ network: 'tcp', security: 'none', tcpSettings: { header: { type: 'http', request: { method: 'GET', path: ['/'], headers: { Host: [] } } } } }),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
streamSettings: JSON.stringify({
|
||||
network: "tcp",
|
||||
security: "none",
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
buildShadowsocksTcp(params: { port: number; uuid: string }) {
|
||||
const { port, uuid } = params;
|
||||
const { port, uuid } = params;
|
||||
return {
|
||||
enable: true,
|
||||
port,
|
||||
protocol: 'shadowsocks',
|
||||
remark: 'shadowsocks-tcp',
|
||||
settings: JSON.stringify({
|
||||
method: 'aes-256-gcm',
|
||||
password: uuid,
|
||||
network: 'tcp,udp',
|
||||
clients: []
|
||||
clients: [{
|
||||
id: "",
|
||||
flow: "",
|
||||
email: uuid,
|
||||
password: crypto.randomBytes(32).toString("base64"),
|
||||
enable: true,
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
ivCheck: false,
|
||||
method: "2022-blake3-aes-256-gcm",
|
||||
network: "tcp",
|
||||
password: crypto.randomBytes(32).toString("base64")
|
||||
}),
|
||||
streamSettings: JSON.stringify({ network: 'tcp', security: 'none' }),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
streamSettings: JSON.stringify({
|
||||
network: "tcp",
|
||||
security: "none",
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -170,12 +273,24 @@ export class InboundBuilderService {
|
||||
protocol: 'trojan',
|
||||
remark: `trojan-tcp-reality`,
|
||||
settings: JSON.stringify({
|
||||
clients: [{ password: uuid, email: uuid, limitIp: 0, totalGB: 0, expiryTime: 0, enable: true, tgId: '', subId: '', reset: 0 }],
|
||||
clients: [{
|
||||
id: uuid,
|
||||
email: uuid,
|
||||
password: crypto.randomBytes(8).toString("hex"),
|
||||
enable: true,
|
||||
flow: "",
|
||||
limitIp: 0,
|
||||
totalGB: 0,
|
||||
expiryTime: 0,
|
||||
tgId: "",
|
||||
subId: "",
|
||||
reset: 0
|
||||
}],
|
||||
fallbacks: []
|
||||
}),
|
||||
streamSettings: JSON.stringify({
|
||||
network: 'tcp',
|
||||
security: 'reality',
|
||||
network: "tcp",
|
||||
security: "reality",
|
||||
externalProxy: [],
|
||||
realitySettings: {
|
||||
show: false,
|
||||
@@ -184,11 +299,34 @@ export class InboundBuilderService {
|
||||
dest: `${domain}:443`,
|
||||
serverNames: [domain],
|
||||
privateKey: privateKey,
|
||||
shortIds: [crypto.randomBytes(4).toString('hex')],
|
||||
settings: { publicKey: publicKey, fingerprint: 'random', serverName: '', spiderX: '/' }
|
||||
shortIds: [
|
||||
crypto.randomBytes(4).toString("hex"),
|
||||
crypto.randomBytes(3).toString("hex"),
|
||||
crypto.randomBytes(8).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(2).toString("hex"),
|
||||
crypto.randomBytes(4).toString("hex")
|
||||
],
|
||||
settings: {
|
||||
publicKey: publicKey,
|
||||
fingerprint: "random",
|
||||
serverName: "",
|
||||
spiderX: "/"
|
||||
}
|
||||
},
|
||||
tcpSettings: {
|
||||
acceptProxyProtocol: false,
|
||||
header: { type: "none" }
|
||||
}
|
||||
}),
|
||||
sniffing: JSON.stringify({ enabled: true, destOverride: ['http', 'tls', 'quic', 'fakedns'] })
|
||||
sniffing: JSON.stringify({
|
||||
enabled: false,
|
||||
destOverride: ["http", "tls", "quic", "fakedns"],
|
||||
metadataOnly: false,
|
||||
routeOnly: false
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
@@ -280,7 +418,7 @@ export class InboundBuilderService {
|
||||
|
||||
const vmessObj = {
|
||||
add: domain,
|
||||
aid: '',
|
||||
aid: '0',
|
||||
alpn: "",
|
||||
fp: "",
|
||||
host: "",
|
||||
@@ -308,9 +446,9 @@ export class InboundBuilderService {
|
||||
|
||||
const method = settings.method;
|
||||
const serverPassword = settings.password;
|
||||
const finalPass = serverPassword || idOrPass;
|
||||
const clientPassword = settings.clients[0].password;
|
||||
|
||||
const userInfo = `${method}:${finalPass}`;
|
||||
const userInfo = `${method}:${serverPassword}:${clientPassword}`;
|
||||
|
||||
const base64 = Buffer
|
||||
.from(userInfo, "utf8")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||
import { Cron, CronExpression } from '@nestjs/schedule';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
@@ -13,7 +13,7 @@ import { InboundBuilderService } from '../inbounds/inbound-builder.service';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
export class RotationService {
|
||||
export class RotationService implements OnModuleInit {
|
||||
private readonly logger = new Logger(RotationService.name);
|
||||
|
||||
constructor(
|
||||
@@ -25,6 +25,26 @@ export class RotationService {
|
||||
private inboundBuilder: InboundBuilderService,
|
||||
) {}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.initDefaultSettings();
|
||||
}
|
||||
|
||||
private async initDefaultSettings() {
|
||||
const key = 'rotation_status';
|
||||
const existing = await this.settingRepo.findOne({ where: { key } });
|
||||
|
||||
if (!existing) {
|
||||
this.logger.log(`Инициализация настройки: ${key} = active`);
|
||||
const newSetting = this.settingRepo.create({
|
||||
key: key,
|
||||
value: 'active',
|
||||
});
|
||||
await this.settingRepo.save(newSetting);
|
||||
} else {
|
||||
this.logger.log(`Текущий статус ротации: ${existing.value}`);
|
||||
}
|
||||
}
|
||||
|
||||
@Cron(CronExpression.EVERY_MINUTE)
|
||||
async handleTicker() {
|
||||
const intervalSetting = await this.settingRepo.findOne({ where: { key: 'rotation_interval' } });
|
||||
@@ -35,8 +55,10 @@ export class RotationService {
|
||||
|
||||
const now = Date.now();
|
||||
const diffMinutes = (now - lastRun) / 1000 / 60;
|
||||
const statusSetting = await this.settingRepo.findOne({ where: { key: 'rotation_status' } });
|
||||
const isStopped = statusSetting?.value === 'stopped';
|
||||
|
||||
if (diffMinutes < intervalMinutes) {
|
||||
if (diffMinutes < intervalMinutes || isStopped) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -52,22 +74,24 @@ export class RotationService {
|
||||
await this.settingRepo.save(s);
|
||||
}
|
||||
|
||||
async performRotation() {
|
||||
async performRotation() {
|
||||
this.logger.log('Запуск плановой ротации...');
|
||||
|
||||
const isLoginSuccess = await this.xuiService.login();
|
||||
if (!isLoginSuccess) {
|
||||
this.logger.error('Отмена ротации: Не удалось войти в панель 3x-ui');
|
||||
return;
|
||||
return { success: false, message: 'Не удалось войти в панель 3x-ui' };
|
||||
}
|
||||
|
||||
const subscriptions = await this.subRepo.find({ where: { isEnabled: true }, relations: ['inbounds'] });
|
||||
if (subscriptions.length === 0) return;
|
||||
if (subscriptions.length === 0) {
|
||||
return { success: false, message: 'Нет активных подписок для ротации' };
|
||||
}
|
||||
|
||||
const domains = await this.domainRepo.find({ where: { isEnabled: true } });
|
||||
if (domains.length === 0) {
|
||||
this.logger.warn('Список доменов пуст! Ротация невозможна.');
|
||||
return;
|
||||
return { success: false, message: 'Список доменов пуст!' };
|
||||
}
|
||||
|
||||
for (const sub of subscriptions) {
|
||||
@@ -75,6 +99,7 @@ export class RotationService {
|
||||
}
|
||||
|
||||
this.logger.log('Ротация завершена.');
|
||||
return { success: true, message: 'Ротация успешно выполнена' };
|
||||
}
|
||||
|
||||
private async rotateSubscription(sub: Subscription, domains: Domain[]) {
|
||||
|
||||
@@ -4,12 +4,14 @@ import { Repository } from 'typeorm';
|
||||
import { Setting } from './entities/setting.entity';
|
||||
import * as dns from 'dns/promises';
|
||||
import { COUNTRIES } from './countries';
|
||||
import { XuiService } from 'src/xui/xui.service';
|
||||
|
||||
@Controller('settings')
|
||||
export class SettingsController {
|
||||
constructor(
|
||||
@InjectRepository(Setting)
|
||||
private settingsRepo: Repository<Setting>,
|
||||
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<string, string>) {
|
||||
if (settings.xui_url) {
|
||||
|
||||
@@ -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 {}
|
||||
@@ -11,7 +11,6 @@ export class SubscriptionsService {
|
||||
constructor(
|
||||
@InjectRepository(Subscription)
|
||||
private subRepo: Repository<Subscription>,
|
||||
@InjectRepository(Inbound)
|
||||
private xuiService: XuiService,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -119,6 +119,29 @@ export class XuiService {
|
||||
}
|
||||
}
|
||||
|
||||
async checkConnection(url: string, username: string, pass: string): Promise<boolean> {
|
||||
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');
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user