manual auto-rotation
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@ import axios from 'axios';
|
||||
import { Logger } from './utils/logger';
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: `${location.protocol}//${location.hostname}:${location.port}/api`,
|
||||
baseURL: '/api',
|
||||
});
|
||||
|
||||
// Interceptor для добавления токена к каждому запросу
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton, useTheme, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogActions } from '@mui/material';
|
||||
import { Box, TextField, Button, Typography, Paper, Snackbar, Alert, Grid, Divider, InputAdornment, Stack, Chip, Tooltip, IconButton, useTheme, useMediaQuery, Dialog, DialogTitle, DialogContent, DialogActions, List, ListItem, FormControlLabel, Checkbox } from '@mui/material';
|
||||
import api from '../api';
|
||||
import { CheckCircle, PauseCircleFilled, PlayCircleFilled } from '@mui/icons-material';
|
||||
import { CheckCircle, PauseCircleFilled, PlayCircleFilled, Refresh } from '@mui/icons-material';
|
||||
import { Logger } from '../utils/logger';
|
||||
|
||||
const ROTATION_PRESETS = [
|
||||
@@ -10,6 +10,13 @@ const ROTATION_PRESETS = [
|
||||
{ label: 'Неделя', value: 10080 },
|
||||
];
|
||||
|
||||
interface Subscription {
|
||||
id: string;
|
||||
name: string;
|
||||
uuid: string;
|
||||
isAutoRotationEnabled?: boolean;
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const [settings, setSettings] = useState({
|
||||
xui_url: '',
|
||||
@@ -25,6 +32,8 @@ export default function SettingsPage() {
|
||||
password: '',
|
||||
});
|
||||
|
||||
const [subs, setSubs] = useState<Subscription[]>([]);
|
||||
|
||||
const [msg, setMsg] = useState({ open: false, type: 'success' as 'success' | 'error', text: '' });
|
||||
const [loadingRotate, setLoadingRotate] = useState<boolean>(false);
|
||||
const [confirmDialog, setConfirmDialog] = useState({ open: false, title: '', onConfirm: () => {} });
|
||||
@@ -51,9 +60,21 @@ export default function SettingsPage() {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSubscriptions = useCallback(async () => {
|
||||
try {
|
||||
Logger.debug('Loading subscriptions...', 'Settings');
|
||||
const { data } = await api.get('/subscriptions');
|
||||
setSubs(data);
|
||||
Logger.debug(`Loaded ${data.length} subscriptions`, 'Settings');
|
||||
} catch (error) {
|
||||
Logger.error('Failed to load subscriptions', 'Settings', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, [loadSettings]);
|
||||
loadSubscriptions();
|
||||
}, [loadSettings, loadSubscriptions]);
|
||||
|
||||
const getIntervalError = () => {
|
||||
const val = parseInt(settings.rotation_interval, 10);
|
||||
@@ -221,6 +242,63 @@ export default function SettingsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleAutoRotation = async (subscriptionId: string, enabled: boolean) => {
|
||||
try {
|
||||
await api.put('/subscriptions/bulk-auto-rotation', {
|
||||
subscriptionIds: [subscriptionId],
|
||||
enabled
|
||||
});
|
||||
setSubs(prev => prev.map(s =>
|
||||
s.id === subscriptionId ? { ...s, isAutoRotationEnabled: enabled } : s
|
||||
));
|
||||
setMsg({
|
||||
open: true,
|
||||
type: 'success',
|
||||
text: enabled ? 'Авторотация включена' : 'Авторотация выключена'
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
|
||||
Logger.error(`Toggle auto-rotation error: ${message}`, 'Settings');
|
||||
setMsg({ open: true, type: 'error', text: message });
|
||||
loadSubscriptions();
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualRotate = async (sub: Subscription) => {
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: `Обновить подписку "${sub.name}" сейчас?`,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
Logger.debug(`Starting manual rotation for subscription: ${sub.id}`, 'Settings');
|
||||
const res = await api.post(`/rotation/rotate-one/${sub.id}`);
|
||||
Logger.debug('Manual rotation completed', 'Settings');
|
||||
setMsg({ open: true, type: 'success', text: res.data.message || 'Ротация выполнена' });
|
||||
loadSubscriptions();
|
||||
} catch (error: unknown) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка ротации';
|
||||
Logger.error(`Manual rotation error: ${message}`, 'Settings');
|
||||
setMsg({ open: true, type: 'error', text: message });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleBulkUpdate = async (enabled: boolean) => {
|
||||
try {
|
||||
const { data } = await api.put('/subscriptions/bulk-auto-rotation', {
|
||||
subscriptionIds: subs.map(s => s.id),
|
||||
enabled
|
||||
});
|
||||
setMsg({ open: true, type: 'success', text: data.message || 'Настройки обновлены' });
|
||||
loadSubscriptions();
|
||||
} catch (error: unknown) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
|
||||
Logger.error(`Bulk update error: ${message}`, 'Settings');
|
||||
setMsg({ open: true, type: 'error', text: message });
|
||||
}
|
||||
};
|
||||
|
||||
const togglePause = async () => {
|
||||
const newStatus = settings.rotation_status === 'active' ? 'stopped' : 'active';
|
||||
const updatedSettings = { ...settings, rotation_status: newStatus };
|
||||
@@ -398,6 +476,82 @@ export default function SettingsPage() {
|
||||
>
|
||||
Сгенерировать сейчас
|
||||
</Button>
|
||||
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom sx={{ fontWeight: 600 }}>
|
||||
Управление авторотацией подписок
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" paragraph>
|
||||
Выберите подписки для автоматической ротации:
|
||||
</Typography>
|
||||
|
||||
{subs.length === 0 ? (
|
||||
<Typography variant="body2" color="textSecondary" sx={{ mb: 2 }}>
|
||||
Нет активных подписок
|
||||
</Typography>
|
||||
) : (
|
||||
<List sx={{ maxHeight: 400, overflow: 'auto', bgcolor: 'background.default', borderRadius: 1 }}>
|
||||
{subs.map(sub => (
|
||||
<ListItem
|
||||
key={sub.id}
|
||||
sx={{
|
||||
py: 1,
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'divider',
|
||||
'&:last-child': { borderBottom: 'none' }
|
||||
}}
|
||||
>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={sub.isAutoRotationEnabled ?? true}
|
||||
onChange={(e) => handleToggleAutoRotation(sub.id, e.target.checked)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={
|
||||
<Box>
|
||||
<Typography variant="body2" sx={{ fontWeight: 500 }}>{sub.name}</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{sub.uuid.substring(0, 8)}...
|
||||
</Typography>
|
||||
</Box>
|
||||
}
|
||||
sx={{ flexGrow: 1 }}
|
||||
/>
|
||||
<Tooltip title="Обновить подписку вручную">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => handleManualRotate(sub)}
|
||||
color="primary"
|
||||
>
|
||||
<Refresh />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
|
||||
{subs.length > 0 && (
|
||||
<Box sx={{ mt: 2, display: 'flex', gap: 1 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleBulkUpdate(true)}
|
||||
>
|
||||
Включить для всех
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
onClick={() => handleBulkUpdate(false)}
|
||||
>
|
||||
Выключить для всех
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 3 }}>
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
useMediaQuery,
|
||||
Menu,
|
||||
ListItemIcon,
|
||||
ListItemText
|
||||
ListItemText,
|
||||
Checkbox
|
||||
} from '@mui/material';
|
||||
import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router, Edit, MoreVert, Remove } from '@mui/icons-material';
|
||||
import { Delete, Add, Link as LinkIcon, OpenInNew, ContentCopy, Dns, Router, Edit, MoreVert, Remove, Refresh } from '@mui/icons-material';
|
||||
import api from '../api';
|
||||
import { Logger } from '../utils/logger';
|
||||
|
||||
@@ -20,6 +21,7 @@ interface Subscription {
|
||||
uuid: string;
|
||||
inbounds: unknown[];
|
||||
inboundsConfig?: unknown[];
|
||||
isAutoRotationEnabled?: boolean;
|
||||
}
|
||||
|
||||
interface Tunnel {
|
||||
@@ -279,6 +281,48 @@ export default function SubscriptionsPage() {
|
||||
});
|
||||
};
|
||||
|
||||
const handleToggleAutoRotation = async (subscriptionId: string, enabled: boolean) => {
|
||||
try {
|
||||
await api.put('/subscriptions/bulk-auto-rotation', {
|
||||
subscriptionIds: [subscriptionId],
|
||||
enabled
|
||||
});
|
||||
setSubs(prev => prev.map(s =>
|
||||
s.id === subscriptionId ? { ...s, isAutoRotationEnabled: enabled } : s
|
||||
));
|
||||
setSnackbar({
|
||||
open: true,
|
||||
type: 'success',
|
||||
message: enabled ? 'Авторотация включена' : 'Авторотация выключена'
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка обновления';
|
||||
Logger.error(`Toggle auto-rotation error: ${message}`, 'Subs');
|
||||
setSnackbar({ open: true, type: 'error', message });
|
||||
loadSubs();
|
||||
}
|
||||
};
|
||||
|
||||
const handleManualRotate = async (sub: Subscription) => {
|
||||
setConfirmDialog({
|
||||
open: true,
|
||||
title: `Обновить подписку "${sub.name}" сейчас?`,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
Logger.debug(`Starting manual rotation for subscription: ${sub.id}`, 'Subs');
|
||||
const res = await api.post(`/rotation/rotate-one/${sub.id}`);
|
||||
Logger.debug('Manual rotation completed', 'Subs');
|
||||
setSnackbar({ open: true, type: 'success', message: res.data.message || 'Ротация выполнена' });
|
||||
loadSubs();
|
||||
} catch (error: unknown) {
|
||||
const message = (error as { response?: { data?: { message?: string } } })?.response?.data?.message || 'Ошибка ротации';
|
||||
Logger.error(`Manual rotation error: ${message}`, 'Subs');
|
||||
setSnackbar({ open: true, type: 'error', message });
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const showLinks = (sub: Subscription) => {
|
||||
let links: string[] = [];
|
||||
if (selectedServer === 'main') {
|
||||
@@ -334,6 +378,7 @@ export default function SubscriptionsPage() {
|
||||
<TableCell>Имя</TableCell>
|
||||
<TableCell>UUID</TableCell>
|
||||
<TableCell>Инбаунды</TableCell>
|
||||
<TableCell>Авторотация</TableCell>
|
||||
<TableCell align="right">Действия</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@@ -343,6 +388,13 @@ export default function SubscriptionsPage() {
|
||||
<TableCell sx={{ fontWeight: 700 }}>{sub.name}</TableCell>
|
||||
<TableCell sx={{ fontFamily: 'monospace' }}>{sub.uuid}</TableCell>
|
||||
<TableCell>{sub.inbounds?.length || 0}</TableCell>
|
||||
<TableCell>
|
||||
<Checkbox
|
||||
checked={sub.isAutoRotationEnabled ?? true}
|
||||
onChange={(e) => handleToggleAutoRotation(sub.id, e.target.checked)}
|
||||
color="primary"
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{!isMobile && (
|
||||
<>
|
||||
@@ -401,6 +453,12 @@ export default function SubscriptionsPage() {
|
||||
<ListItemText>Показать конфиги</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{activeSub && (
|
||||
<MenuItem onClick={() => handleManualRotate(activeSub)}>
|
||||
<ListItemIcon><Refresh fontSize="small" color="primary" /></ListItemIcon>
|
||||
<ListItemText>Обновить сейчас</ListItemText>
|
||||
</MenuItem>
|
||||
)}
|
||||
{activeSub && (
|
||||
<MenuItem onClick={() => handleOpenEdit(activeSub)}>
|
||||
<ListItemIcon><Edit fontSize="small" /></ListItemIcon>
|
||||
|
||||
Generated
+39
-18
@@ -14,6 +14,7 @@
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
@@ -759,7 +760,7 @@
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "0.3.9"
|
||||
@@ -772,7 +773,7 @@
|
||||
"version": "0.3.9",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.0.3",
|
||||
@@ -2047,7 +2048,7 @@
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -2068,7 +2069,7 @@
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
@@ -2268,6 +2269,26 @@
|
||||
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/mapped-types": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.1.0.tgz",
|
||||
"integrity": "sha512-W+n+rM69XsFdwORF11UqJahn4J3xi4g/ZEOlJNL6KoW5ygWSmBB2p0S2BZ4FQeS/NDH72e6xIcu35SfJnE8bXw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
||||
"class-transformer": "^0.4.0 || ^0.5.0",
|
||||
"class-validator": "^0.13.0 || ^0.14.0",
|
||||
"reflect-metadata": "^0.1.12 || ^0.2.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"class-transformer": {
|
||||
"optional": true
|
||||
},
|
||||
"class-validator": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@nestjs/passport": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-11.0.5.tgz",
|
||||
@@ -2573,28 +2594,28 @@
|
||||
"version": "1.0.12",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
|
||||
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node12": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node14": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tsconfig/node16": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@tybys/wasm-util": {
|
||||
@@ -3724,7 +3745,7 @@
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
@@ -3760,7 +3781,7 @@
|
||||
"version": "8.3.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"acorn": "^8.11.0"
|
||||
@@ -3943,7 +3964,7 @@
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
@@ -4872,7 +4893,7 @@
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cron": {
|
||||
@@ -5038,7 +5059,7 @@
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.3.1"
|
||||
@@ -7755,7 +7776,7 @@
|
||||
"version": "1.3.6",
|
||||
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/makeerror": {
|
||||
@@ -10140,7 +10161,7 @@
|
||||
"version": "10.9.2",
|
||||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
@@ -10524,7 +10545,7 @@
|
||||
"version": "5.9.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -10729,7 +10750,7 @@
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
@@ -11157,7 +11178,7 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
"@nestjs/config": "^4.0.2",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
"@nestjs/jwt": "^11.0.2",
|
||||
"@nestjs/mapped-types": "^2.1.0",
|
||||
"@nestjs/passport": "^11.0.5",
|
||||
"@nestjs/platform-express": "^11.0.1",
|
||||
"@nestjs/schedule": "^6.1.0",
|
||||
|
||||
@@ -15,7 +15,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
return token;
|
||||
},
|
||||
ignoreExpiration: false,
|
||||
secretOrKey: 'SECRET_KEY_CHANGE_ME',
|
||||
secretOrKey: secret,
|
||||
});
|
||||
const maskedSecret =
|
||||
secret.length > 8
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Post } from '@nestjs/common';
|
||||
import { Controller, Post, Param } from '@nestjs/common';
|
||||
import { RotationService } from './rotation.service';
|
||||
|
||||
@Controller('rotation')
|
||||
@@ -9,4 +9,9 @@ export class RotationController {
|
||||
async rotateAll() {
|
||||
return this.rotationService.performRotation();
|
||||
}
|
||||
|
||||
@Post('rotate-one/:id')
|
||||
async rotateSingle(@Param('id') id: string) {
|
||||
return this.rotationService.rotateSingleSubscription(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,10 @@ export class RotationService implements OnModuleInit {
|
||||
}
|
||||
|
||||
const subscriptions = await this.subRepo.find({
|
||||
where: { isEnabled: true },
|
||||
where: {
|
||||
isEnabled: true,
|
||||
isAutoRotationEnabled: true,
|
||||
},
|
||||
relations: ['inbounds'],
|
||||
});
|
||||
if (subscriptions.length === 0) {
|
||||
@@ -344,4 +347,41 @@ export class RotationService implements OnModuleInit {
|
||||
if (!exists) return p;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ручная ротация одной подписки (независимо от флага isAutoRotationEnabled)
|
||||
*/
|
||||
async rotateSingleSubscription(subscriptionId: string) {
|
||||
this.logger.debug(`Запуск ручной ротации подписки: ${subscriptionId}`);
|
||||
|
||||
const sub = await this.subRepo.findOne({
|
||||
where: { id: subscriptionId },
|
||||
relations: ['inbounds'],
|
||||
});
|
||||
|
||||
if (!sub) {
|
||||
this.logger.warn(`Подписка не найдена: ${subscriptionId}`);
|
||||
return {
|
||||
success: false,
|
||||
message: 'Подписка не найдена',
|
||||
};
|
||||
}
|
||||
|
||||
const isLoginSuccess = await this.xuiService.login();
|
||||
if (!isLoginSuccess) {
|
||||
this.logger.error('Отмена ротации: Не удалось войти в панель 3x-ui');
|
||||
return { success: false, message: 'Не удалось войти в панель 3x-ui' };
|
||||
}
|
||||
|
||||
const domains = await this.domainRepo.find({ where: { isEnabled: true } });
|
||||
if (domains.length === 0) {
|
||||
this.logger.warn('Список доменов пуст! Ротация невозможна.');
|
||||
return { success: false, message: 'Список доменов пуст!' };
|
||||
}
|
||||
|
||||
await this.rotateSubscription(sub, domains);
|
||||
|
||||
this.logger.debug(`Ручная ротация подписки ${subscriptionId} завершена.`);
|
||||
return { success: true, message: 'Ротация успешно выполнена' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
IsArray,
|
||||
ValidateNested,
|
||||
IsOptional,
|
||||
IsBoolean,
|
||||
ArrayMinSize,
|
||||
ArrayMaxSize,
|
||||
} from 'class-validator';
|
||||
@@ -35,4 +36,8 @@ export class CreateSubscriptionDto {
|
||||
@ArrayMaxSize(20)
|
||||
@IsOptional()
|
||||
inboundsConfig?: InboundConfigDto[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
isAutoRotationEnabled?: boolean;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { CreateSubscriptionDto } from './create-subscription.dto';
|
||||
|
||||
export class UpdateSubscriptionDto extends PartialType(CreateSubscriptionDto) {}
|
||||
@@ -22,6 +22,9 @@ export class Subscription {
|
||||
@Column({ default: true })
|
||||
isEnabled: boolean;
|
||||
|
||||
@Column({ default: true })
|
||||
isAutoRotationEnabled: boolean;
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
inboundsConfig: Array<{
|
||||
type?: string;
|
||||
|
||||
@@ -6,9 +6,13 @@ import {
|
||||
Body,
|
||||
Param,
|
||||
Put,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { SubscriptionsService } from './subscriptions.service';
|
||||
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
|
||||
import { UpdateSubscriptionDto } from './dto/update-subscription.dto';
|
||||
|
||||
@Controller('subscriptions')
|
||||
export class SubscriptionsController {
|
||||
@@ -24,12 +28,63 @@ export class SubscriptionsController {
|
||||
return this.subscriptionsService.create(createSubscriptionDto);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateSubscriptionDto: CreateSubscriptionDto,
|
||||
@Put('bulk-auto-rotation')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async bulkUpdateAutoRotation(
|
||||
@Body() body: { subscriptionIds: string[]; enabled: boolean },
|
||||
) {
|
||||
return this.subscriptionsService.update(id, updateSubscriptionDto);
|
||||
const { subscriptionIds, enabled } = body;
|
||||
|
||||
if (!Array.isArray(subscriptionIds)) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'subscriptionIds должен быть массивом',
|
||||
};
|
||||
}
|
||||
|
||||
if (subscriptionIds.length > 100) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Максимум 100 ID за раз',
|
||||
};
|
||||
}
|
||||
|
||||
const updated: string[] = [];
|
||||
const notFound: string[] = [];
|
||||
|
||||
for (const id of subscriptionIds) {
|
||||
const result = await this.subscriptionsService.update(id, {
|
||||
isAutoRotationEnabled: enabled,
|
||||
});
|
||||
|
||||
if (result) {
|
||||
updated.push(id);
|
||||
} else {
|
||||
notFound.push(id);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Обновлено ${updated.length} подписок`,
|
||||
updatedCount: updated.length,
|
||||
notFound: notFound.length > 0 ? notFound : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateSubscriptionDto: UpdateSubscriptionDto,
|
||||
) {
|
||||
const result = await this.subscriptionsService.update(
|
||||
id,
|
||||
updateSubscriptionDto,
|
||||
);
|
||||
if (!result) {
|
||||
throw new NotFoundException(`Подписка ${id} не найдена`);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Subscription } from './entities/subscription.entity';
|
||||
import { XuiService } from '../xui/xui.service';
|
||||
import { CreateSubscriptionDto } from './dto/create-subscription.dto';
|
||||
import { UpdateSubscriptionDto } from './dto/update-subscription.dto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
@Injectable()
|
||||
@@ -26,27 +27,35 @@ export class SubscriptionsService {
|
||||
name: dto.name,
|
||||
uuid: uuidv4(),
|
||||
inboundsConfig: dto.inboundsConfig || [],
|
||||
isAutoRotationEnabled: dto.isAutoRotationEnabled ?? true,
|
||||
});
|
||||
|
||||
return this.subRepo.save(sub);
|
||||
}
|
||||
|
||||
async update(id: string, dto: CreateSubscriptionDto) {
|
||||
async update(id: string, dto: UpdateSubscriptionDto) {
|
||||
const sub = await this.subRepo.findOne({
|
||||
where: { id },
|
||||
relations: ['inbounds'],
|
||||
});
|
||||
|
||||
if (!sub) {
|
||||
throw new NotFoundException(`Subscription with ID ${id} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
sub.name = dto.name;
|
||||
// Пустое имя не обновляется — защита от случайной очистки
|
||||
if (dto.name && dto.name.trim().length > 0) {
|
||||
sub.name = dto.name;
|
||||
}
|
||||
|
||||
if (dto.inboundsConfig) {
|
||||
sub.inboundsConfig = dto.inboundsConfig;
|
||||
}
|
||||
|
||||
if (dto.isAutoRotationEnabled !== undefined) {
|
||||
sub.isAutoRotationEnabled = dto.isAutoRotationEnabled;
|
||||
}
|
||||
|
||||
return this.subRepo.save(sub);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user