feat(NetworkConfigManager, server): implement IPSec password management; add CRUD operations for IPSec passwords in the backend and integrate UI components for creating, editing, and deleting passwords in the frontend
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m21s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m21s
This commit is contained in:
@@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* Роуты для управления IPSec паролями
|
||||||
|
* Пароли хранятся в S3 в зашифрованном виде
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { sendError, sendOk } = require('../middleware/errorHandler');
|
||||||
|
const { readS3TextObject, writeS3JsonObject, deleteS3Object } = require('../services/s3Service');
|
||||||
|
const { encrypt, decrypt } = require('../utils/encryption');
|
||||||
|
|
||||||
|
const S3_KEY = 'network-config/ipsec-passwords.json';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/ipsec-passwords - получить список паролей (без расшифровки)
|
||||||
|
*/
|
||||||
|
async function getIpsecPasswords(req, res) {
|
||||||
|
try {
|
||||||
|
const data = await readS3TextObject(S3_KEY).catch(() => ({ body: '[]' }));
|
||||||
|
let passwords = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwords = JSON.parse(data.body || '[]');
|
||||||
|
if (!Array.isArray(passwords)) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Возвращаем список паролей БЕЗ расшифровки (только метаданные)
|
||||||
|
const result = passwords.map(p => ({
|
||||||
|
id: p.id,
|
||||||
|
name: p.name || '',
|
||||||
|
description: p.description || '',
|
||||||
|
createdAt: p.createdAt || null,
|
||||||
|
updatedAt: p.updatedAt || null,
|
||||||
|
// НЕ возвращаем зашифрованный пароль
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading IPSec passwords:', error);
|
||||||
|
return sendError(res, 500, 'Error reading IPSec passwords', 'E_S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GET /api/ipsec-passwords/:id - получить конкретный пароль (расшифрованный)
|
||||||
|
*/
|
||||||
|
async function getIpsecPassword(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await readS3TextObject(S3_KEY).catch(() => ({ body: '[]' }));
|
||||||
|
let passwords = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwords = JSON.parse(data.body || '[]');
|
||||||
|
if (!Array.isArray(passwords)) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const password = passwords.find(p => p.id === id);
|
||||||
|
if (!password) {
|
||||||
|
return sendError(res, 404, 'IPSec password not found', 'E_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Расшифровываем пароль для возврата
|
||||||
|
let decryptedPassword = '';
|
||||||
|
try {
|
||||||
|
decryptedPassword = decrypt(password.encryptedPassword || '');
|
||||||
|
} catch (decryptError) {
|
||||||
|
console.error('Error decrypting password:', decryptError);
|
||||||
|
return sendError(res, 500, 'Error decrypting password', 'E_DECRYPT');
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
id: password.id,
|
||||||
|
name: password.name || '',
|
||||||
|
description: password.description || '',
|
||||||
|
password: decryptedPassword, // Расшифрованный пароль
|
||||||
|
createdAt: password.createdAt || null,
|
||||||
|
updatedAt: password.updatedAt || null,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error reading IPSec password:', error);
|
||||||
|
return sendError(res, 500, 'Error reading IPSec password', 'E_S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* POST /api/ipsec-passwords - создать новый пароль
|
||||||
|
*/
|
||||||
|
async function createIpsecPassword(req, res) {
|
||||||
|
const { name, password, description } = req.body;
|
||||||
|
|
||||||
|
if (!name || !password) {
|
||||||
|
return sendError(res, 400, 'Name and password are required', 'E_BAD_REQUEST');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Читаем текущие пароли
|
||||||
|
const data = await readS3TextObject(S3_KEY).catch(() => ({ body: '[]' }));
|
||||||
|
let passwords = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwords = JSON.parse(data.body || '[]');
|
||||||
|
if (!Array.isArray(passwords)) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, нет ли уже пароля с таким именем
|
||||||
|
if (passwords.some(p => p.name === name)) {
|
||||||
|
return sendError(res, 400, 'Password with this name already exists', 'E_DUPLICATE');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Шифруем пароль
|
||||||
|
const encryptedPassword = encrypt(password);
|
||||||
|
|
||||||
|
// Создаем новый пароль
|
||||||
|
const newPassword = {
|
||||||
|
id: `ipsec-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||||
|
name: String(name).trim(),
|
||||||
|
description: String(description || '').trim(),
|
||||||
|
encryptedPassword,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
passwords.push(newPassword);
|
||||||
|
|
||||||
|
// Сохраняем в S3
|
||||||
|
const meta = await writeS3JsonObject(S3_KEY, passwords);
|
||||||
|
|
||||||
|
// Возвращаем без расшифрованного пароля
|
||||||
|
res.json({
|
||||||
|
id: newPassword.id,
|
||||||
|
name: newPassword.name,
|
||||||
|
description: newPassword.description,
|
||||||
|
createdAt: newPassword.createdAt,
|
||||||
|
updatedAt: newPassword.updatedAt,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating IPSec password:', error);
|
||||||
|
return sendError(res, 500, 'Error creating IPSec password', 'E_S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PUT /api/ipsec-passwords/:id - обновить пароль
|
||||||
|
*/
|
||||||
|
async function updateIpsecPassword(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
const { name, password, description } = req.body;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Читаем текущие пароли
|
||||||
|
const data = await readS3TextObject(S3_KEY).catch(() => ({ body: '[]' }));
|
||||||
|
let passwords = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwords = JSON.parse(data.body || '[]');
|
||||||
|
if (!Array.isArray(passwords)) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = passwords.findIndex(p => p.id === id);
|
||||||
|
if (index === -1) {
|
||||||
|
return sendError(res, 404, 'IPSec password not found', 'E_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем уникальность имени (если изменилось)
|
||||||
|
if (name && name !== passwords[index].name) {
|
||||||
|
if (passwords.some(p => p.id !== id && p.name === name)) {
|
||||||
|
return sendError(res, 400, 'Password with this name already exists', 'E_DUPLICATE');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем пароль
|
||||||
|
if (name !== undefined) {
|
||||||
|
passwords[index].name = String(name).trim();
|
||||||
|
}
|
||||||
|
if (description !== undefined) {
|
||||||
|
passwords[index].description = String(description || '').trim();
|
||||||
|
}
|
||||||
|
if (password !== undefined) {
|
||||||
|
passwords[index].encryptedPassword = encrypt(password);
|
||||||
|
}
|
||||||
|
passwords[index].updatedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
// Сохраняем в S3
|
||||||
|
const meta = await writeS3JsonObject(S3_KEY, passwords);
|
||||||
|
|
||||||
|
// Возвращаем без расшифрованного пароля
|
||||||
|
res.json({
|
||||||
|
id: passwords[index].id,
|
||||||
|
name: passwords[index].name,
|
||||||
|
description: passwords[index].description,
|
||||||
|
createdAt: passwords[index].createdAt,
|
||||||
|
updatedAt: passwords[index].updatedAt,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating IPSec password:', error);
|
||||||
|
return sendError(res, 500, 'Error updating IPSec password', 'E_S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DELETE /api/ipsec-passwords/:id - удалить пароль
|
||||||
|
*/
|
||||||
|
async function deleteIpsecPassword(req, res) {
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Читаем текущие пароли
|
||||||
|
const data = await readS3TextObject(S3_KEY).catch(() => ({ body: '[]' }));
|
||||||
|
let passwords = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
passwords = JSON.parse(data.body || '[]');
|
||||||
|
if (!Array.isArray(passwords)) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
} catch (parseError) {
|
||||||
|
passwords = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = passwords.filter(p => p.id !== id);
|
||||||
|
|
||||||
|
if (filtered.length === passwords.length) {
|
||||||
|
return sendError(res, 404, 'IPSec password not found', 'E_NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем в S3
|
||||||
|
const meta = await writeS3JsonObject(S3_KEY, filtered);
|
||||||
|
|
||||||
|
res.json({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting IPSec password:', error);
|
||||||
|
return sendError(res, 500, 'Error deleting IPSec password', 'E_S3');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
getIpsecPasswords,
|
||||||
|
getIpsecPassword,
|
||||||
|
createIpsecPassword,
|
||||||
|
updateIpsecPassword,
|
||||||
|
deleteIpsecPassword,
|
||||||
|
};
|
||||||
@@ -26,6 +26,7 @@ const communitiesRoutes = require('./routes/communitiesRoutes');
|
|||||||
const filtersRoutes = require('./routes/filtersRoutes');
|
const filtersRoutes = require('./routes/filtersRoutes');
|
||||||
const serverConfigsRoutes = require('./routes/serverConfigsRoutes');
|
const serverConfigsRoutes = require('./routes/serverConfigsRoutes');
|
||||||
const miscRoutes = require('./routes/miscRoutes');
|
const miscRoutes = require('./routes/miscRoutes');
|
||||||
|
const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const port = Number(process.env.PORT) || 3001;
|
const port = Number(process.env.PORT) || 3001;
|
||||||
@@ -477,6 +478,13 @@ app.get('/api/ws/url', miscRoutes.getWsUrl);
|
|||||||
app.get('/api/ui-settings', miscRoutes.getUiSettings);
|
app.get('/api/ui-settings', miscRoutes.getUiSettings);
|
||||||
app.post('/api/ui-settings', miscRoutes.postUiSettings);
|
app.post('/api/ui-settings', miscRoutes.postUiSettings);
|
||||||
|
|
||||||
|
// === IPSEC PASSWORDS ===
|
||||||
|
app.get('/api/ipsec-passwords', ipsecPasswordsRoutes.getIpsecPasswords);
|
||||||
|
app.get('/api/ipsec-passwords/:id', ipsecPasswordsRoutes.getIpsecPassword);
|
||||||
|
app.post('/api/ipsec-passwords', writeLimiter, ipsecPasswordsRoutes.createIpsecPassword);
|
||||||
|
app.put('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.updateIpsecPassword);
|
||||||
|
app.delete('/api/ipsec-passwords/:id', writeLimiter, ipsecPasswordsRoutes.deleteIpsecPassword);
|
||||||
|
|
||||||
// === MIKROTIK VALIDATION ===
|
// === MIKROTIK VALIDATION ===
|
||||||
app.post('/api/mikrotik/validate', async (req, res) => {
|
app.post('/api/mikrotik/validate', async (req, res) => {
|
||||||
const { config } = req.body;
|
const { config } = req.body;
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
/**
|
||||||
|
* Утилиты для шифрования/дешифрования данных
|
||||||
|
* Использует AES-256-GCM для шифрования паролей
|
||||||
|
*/
|
||||||
|
|
||||||
|
const crypto = require('crypto');
|
||||||
|
|
||||||
|
// Ключ шифрования из переменной окружения
|
||||||
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || crypto.randomBytes(32).toString('hex');
|
||||||
|
const ALGORITHM = 'aes-256-gcm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получить ключ шифрования (32 байта)
|
||||||
|
*/
|
||||||
|
function getEncryptionKey() {
|
||||||
|
// Если ENCRYPTION_KEY - hex строка, конвертируем в Buffer
|
||||||
|
if (ENCRYPTION_KEY.length === 64) {
|
||||||
|
return Buffer.from(ENCRYPTION_KEY, 'hex');
|
||||||
|
}
|
||||||
|
// Иначе используем как есть и дополняем/обрезаем до 32 байт
|
||||||
|
const key = Buffer.from(ENCRYPTION_KEY, 'utf8');
|
||||||
|
if (key.length === 32) return key;
|
||||||
|
// Дополняем или обрезаем до 32 байт
|
||||||
|
const result = Buffer.alloc(32);
|
||||||
|
key.copy(result, 0, 0, Math.min(key.length, 32));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Зашифровать текст
|
||||||
|
* @param {string} text - Текст для шифрования
|
||||||
|
* @returns {string} - Зашифрованная строка в формате iv:authTag:encryptedData (все в base64)
|
||||||
|
*/
|
||||||
|
function encrypt(text) {
|
||||||
|
if (!text) return '';
|
||||||
|
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const iv = crypto.randomBytes(16);
|
||||||
|
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
||||||
|
|
||||||
|
let encrypted = cipher.update(String(text), 'utf8', 'base64');
|
||||||
|
encrypted += cipher.final('base64');
|
||||||
|
|
||||||
|
const authTag = cipher.getAuthTag();
|
||||||
|
|
||||||
|
// Формат: iv:authTag:encryptedData (все в base64)
|
||||||
|
return `${iv.toString('base64')}:${authTag.toString('base64')}:${encrypted}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Расшифровать текст
|
||||||
|
* @param {string} encryptedText - Зашифрованная строка в формате iv:authTag:encryptedData
|
||||||
|
* @returns {string} - Расшифрованный текст
|
||||||
|
*/
|
||||||
|
function decrypt(encryptedText) {
|
||||||
|
if (!encryptedText) return '';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parts = encryptedText.split(':');
|
||||||
|
if (parts.length !== 3) {
|
||||||
|
throw new Error('Invalid encrypted format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [ivBase64, authTagBase64, encryptedBase64] = parts;
|
||||||
|
const iv = Buffer.from(ivBase64, 'base64');
|
||||||
|
const authTag = Buffer.from(authTagBase64, 'base64');
|
||||||
|
const encrypted = encryptedBase64;
|
||||||
|
|
||||||
|
const key = getEncryptionKey();
|
||||||
|
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
||||||
|
decipher.setAuthTag(authTag);
|
||||||
|
|
||||||
|
let decrypted = decipher.update(encrypted, 'base64', 'utf8');
|
||||||
|
decrypted += decipher.final('utf8');
|
||||||
|
|
||||||
|
return decrypted;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Decryption error:', error);
|
||||||
|
throw new Error('Failed to decrypt data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
encrypt,
|
||||||
|
decrypt,
|
||||||
|
};
|
||||||
@@ -27,7 +27,8 @@ import {
|
|||||||
IconLayoutGrid,
|
IconLayoutGrid,
|
||||||
IconList,
|
IconList,
|
||||||
IconCode,
|
IconCode,
|
||||||
IconArrowsRightLeft
|
IconArrowsRightLeft,
|
||||||
|
IconLock,
|
||||||
} from '@tabler/icons-react';
|
} from '@tabler/icons-react';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -81,6 +82,7 @@ const getEmptyInterface = () => ({
|
|||||||
remoteIp: '',
|
remoteIp: '',
|
||||||
serverId: '',
|
serverId: '',
|
||||||
serverId2: '', // Второй сервер (опциональный)
|
serverId2: '', // Второй сервер (опциональный)
|
||||||
|
ipsecPasswordId: '', // ID IPSec пароля (опционально, только для IPSec)
|
||||||
});
|
});
|
||||||
|
|
||||||
const getEmptyIpPool = () => ({
|
const getEmptyIpPool = () => ({
|
||||||
@@ -140,6 +142,14 @@ function NetworkConfigManager() {
|
|||||||
const [templateNamePrefix, setTemplateNamePrefix] = useState('');
|
const [templateNamePrefix, setTemplateNamePrefix] = useState('');
|
||||||
const [templateName1, setTemplateName1] = useState(''); // Опциональное имя для сервера 1
|
const [templateName1, setTemplateName1] = useState(''); // Опциональное имя для сервера 1
|
||||||
const [templateName2, setTemplateName2] = useState(''); // Опциональное имя для сервера 2
|
const [templateName2, setTemplateName2] = useState(''); // Опциональное имя для сервера 2
|
||||||
|
const [templateIpsecPasswordId, setTemplateIpsecPasswordId] = useState(''); // Выбранный IPSec пароль
|
||||||
|
|
||||||
|
// === IPSec Passwords Management ===
|
||||||
|
const [ipsecPasswords, setIpsecPasswords] = useState([]);
|
||||||
|
const [ipsecPasswordModalOpen, setIpsecPasswordModalOpen] = useState(false);
|
||||||
|
const [ipsecPasswordsListModalOpen, setIpsecPasswordsListModalOpen] = useState(false);
|
||||||
|
const [editingIpsecPassword, setEditingIpsecPassword] = useState(null);
|
||||||
|
const [ipsecPasswordModalMode, setIpsecPasswordModalMode] = useState('add'); // 'add' | 'edit'
|
||||||
|
|
||||||
// === MikroTik Code Generation ===
|
// === MikroTik Code Generation ===
|
||||||
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
||||||
@@ -149,6 +159,7 @@ function NetworkConfigManager() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchConfig();
|
fetchConfig();
|
||||||
fetchServers();
|
fetchServers();
|
||||||
|
fetchIpsecPasswords();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const fetchConfig = async () => {
|
const fetchConfig = async () => {
|
||||||
@@ -194,6 +205,16 @@ function NetworkConfigManager() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchIpsecPasswords = async () => {
|
||||||
|
try {
|
||||||
|
const response = await api.get('/ipsec-passwords');
|
||||||
|
setIpsecPasswords(Array.isArray(response.data) ? response.data : []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching IPSec passwords:', error);
|
||||||
|
setIpsecPasswords([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// === Сохранение ===
|
// === Сохранение ===
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
@@ -529,7 +550,8 @@ function NetworkConfigManager() {
|
|||||||
localIp: localIp,
|
localIp: localIp,
|
||||||
remoteIp: remoteIp,
|
remoteIp: remoteIp,
|
||||||
serverId: templateServer1,
|
serverId: templateServer1,
|
||||||
serverId2: templateServer2
|
serverId2: templateServer2,
|
||||||
|
ipsecPasswordId: templateType === 'IPSec' && templateIpsecPasswordId ? templateIpsecPasswordId : ''
|
||||||
};
|
};
|
||||||
|
|
||||||
const conflicts = checkInterfaceIpConflict(newInterface);
|
const conflicts = checkInterfaceIpConflict(newInterface);
|
||||||
@@ -554,6 +576,7 @@ function NetworkConfigManager() {
|
|||||||
setTemplateNamePrefix('');
|
setTemplateNamePrefix('');
|
||||||
setTemplateName1('');
|
setTemplateName1('');
|
||||||
setTemplateName2('');
|
setTemplateName2('');
|
||||||
|
setTemplateIpsecPasswordId('');
|
||||||
};
|
};
|
||||||
|
|
||||||
// === CRUD для Interfaces ===
|
// === CRUD для Interfaces ===
|
||||||
@@ -836,6 +859,67 @@ function NetworkConfigManager() {
|
|||||||
setDeleteModalOpen(true);
|
setDeleteModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// === CRUD для IPSec Passwords ===
|
||||||
|
const handleAddIpsecPassword = () => {
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditIpsecPassword = async (passwordId) => {
|
||||||
|
try {
|
||||||
|
const response = await api.get(`/ipsec-passwords/${passwordId}`);
|
||||||
|
setEditingIpsecPassword(response.data);
|
||||||
|
setIpsecPasswordModalMode('edit');
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching IPSec password:', error);
|
||||||
|
notify.error('Не удалось загрузить пароль');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveIpsecPassword = async () => {
|
||||||
|
if (!editingIpsecPassword.name || !editingIpsecPassword.password) {
|
||||||
|
notify.error('Имя и пароль обязательны');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (ipsecPasswordModalMode === 'add') {
|
||||||
|
await api.post('/ipsec-passwords', {
|
||||||
|
name: editingIpsecPassword.name,
|
||||||
|
password: editingIpsecPassword.password,
|
||||||
|
description: editingIpsecPassword.description || ''
|
||||||
|
});
|
||||||
|
notify.success('IPSec пароль создан');
|
||||||
|
} else {
|
||||||
|
await api.put(`/ipsec-passwords/${editingIpsecPassword.id}`, {
|
||||||
|
name: editingIpsecPassword.name,
|
||||||
|
password: editingIpsecPassword.password,
|
||||||
|
description: editingIpsecPassword.description || ''
|
||||||
|
});
|
||||||
|
notify.success('IPSec пароль обновлён');
|
||||||
|
}
|
||||||
|
await fetchIpsecPasswords();
|
||||||
|
setIpsecPasswordModalOpen(false);
|
||||||
|
setEditingIpsecPassword(null);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving IPSec password:', error);
|
||||||
|
notify.error('Не удалось сохранить пароль');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeleteIpsecPassword = async (passwordId) => {
|
||||||
|
try {
|
||||||
|
await api.delete(`/ipsec-passwords/${passwordId}`);
|
||||||
|
notify.success('IPSec пароль удалён');
|
||||||
|
await fetchIpsecPasswords();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting IPSec password:', error);
|
||||||
|
notify.error('Не удалось удалить пароль');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// === CRUD для IP Pools ===
|
// === CRUD для IP Pools ===
|
||||||
const handleAddPool = () => {
|
const handleAddPool = () => {
|
||||||
setEditingPool(getEmptyIpPool());
|
setEditingPool(getEmptyIpPool());
|
||||||
@@ -1908,6 +1992,14 @@ function NetworkConfigManager() {
|
|||||||
<IconSparkles size={16} className="me-1" />
|
<IconSparkles size={16} className="me-1" />
|
||||||
Шаблон
|
Шаблон
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-outline-secondary btn-sm"
|
||||||
|
onClick={() => setIpsecPasswordsListModalOpen(true)}
|
||||||
|
title="Управление IPSec паролями"
|
||||||
|
>
|
||||||
|
<IconLock size={16} className="me-1" />
|
||||||
|
IPSec пароли
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{activeTab === 'pools' && (
|
{activeTab === 'pools' && (
|
||||||
@@ -2630,10 +2722,44 @@ function NetworkConfigManager() {
|
|||||||
name="type"
|
name="type"
|
||||||
type="select"
|
type="select"
|
||||||
value={editingInterface.type}
|
value={editingInterface.type}
|
||||||
onChange={(val) => setEditingInterface({ ...editingInterface, type: val })}
|
onChange={(val) => setEditingInterface({ ...editingInterface, type: val, ipsecPasswordId: val !== 'IPSec' ? '' : editingInterface.ipsecPasswordId })}
|
||||||
options={INTERFACE_TYPES}
|
options={INTERFACE_TYPES}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{editingInterface.type === 'IPSec' && (
|
||||||
|
<div className="col-md-6">
|
||||||
|
<label className="form-label">IPSec пароль</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={editingInterface.ipsecPasswordId || ''}
|
||||||
|
onChange={(e) => setEditingInterface({ ...editingInterface, ipsecPasswordId: e.target.value })}
|
||||||
|
>
|
||||||
|
<option value="">Не выбран</option>
|
||||||
|
{ipsecPasswords.map(pwd => (
|
||||||
|
<option key={pwd.id} value={pwd.id}>
|
||||||
|
{pwd.name} {pwd.description ? `(${pwd.description})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
}}
|
||||||
|
title="Создать новый IPSec пароль"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-text">
|
||||||
|
Выберите сохраненный IPSec пароль или создайте новый
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="col-md-3">
|
<div className="col-md-3">
|
||||||
<label className="form-label">Local IP</label>
|
<label className="form-label">Local IP</label>
|
||||||
<div className="input-group">
|
<div className="input-group">
|
||||||
@@ -2787,6 +2913,7 @@ function NetworkConfigManager() {
|
|||||||
setTemplateNamePrefix('');
|
setTemplateNamePrefix('');
|
||||||
setTemplateName1('');
|
setTemplateName1('');
|
||||||
setTemplateName2('');
|
setTemplateName2('');
|
||||||
|
setTemplateIpsecPasswordId('');
|
||||||
}}
|
}}
|
||||||
onSubmit={handleCreateInterfaceFromTemplate}
|
onSubmit={handleCreateInterfaceFromTemplate}
|
||||||
title="Создать интерфейс из шаблона"
|
title="Создать интерфейс из шаблона"
|
||||||
@@ -2892,9 +3019,43 @@ function NetworkConfigManager() {
|
|||||||
onChange={(e) => setTemplateName2(e.target.value)}
|
onChange={(e) => setTemplateName2(e.target.value)}
|
||||||
placeholder={autoName2}
|
placeholder={autoName2}
|
||||||
/>
|
/>
|
||||||
<div className="form-text">Оставьте пустым для автогенерации</div>
|
<div className="form-text">Оставьте пустым для автогенерации </div>
|
||||||
</div>
|
</div>
|
||||||
|
{templateType === 'IPSec' && (
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
|
<label className="form-label">IPSec пароль</label>
|
||||||
|
<div className="input-group">
|
||||||
|
<select
|
||||||
|
className="form-select"
|
||||||
|
value={templateIpsecPasswordId}
|
||||||
|
onChange={(e) => setTemplateIpsecPasswordId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Не выбран</option>
|
||||||
|
{ipsecPasswords.map(pwd => (
|
||||||
|
<option key={pwd.id} value={pwd.id}>
|
||||||
|
{pwd.name} {pwd.description ? `(${pwd.description})` : ''}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-outline-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
}}
|
||||||
|
title="Создать новый IPSec пароль"
|
||||||
|
>
|
||||||
|
<IconPlus size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="form-text">
|
||||||
|
Выберите сохраненный IPSec пароль или создайте новый
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="col-12">
|
||||||
<div className="card card-sm border">
|
<div className="card card-sm border">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h6 className="card-title mb-3">Предпросмотр интерфейса:</h6>
|
<h6 className="card-title mb-3">Предпросмотр интерфейса:</h6>
|
||||||
@@ -2926,6 +3087,214 @@ function NetworkConfigManager() {
|
|||||||
</div>
|
</div>
|
||||||
</FormModal>
|
</FormModal>
|
||||||
|
|
||||||
|
{/* IPSec Password Modal */}
|
||||||
|
<FormModal
|
||||||
|
show={ipsecPasswordModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIpsecPasswordModalOpen(false);
|
||||||
|
setEditingIpsecPassword(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleSaveIpsecPassword}
|
||||||
|
title={ipsecPasswordModalMode === 'add' ? 'Добавить IPSec пароль' : 'Редактировать IPSec пароль'}
|
||||||
|
submitLabel={ipsecPasswordModalMode === 'add' ? 'Создать' : 'Сохранить'}
|
||||||
|
submitIcon={ipsecPasswordModalMode === 'add' ? IconPlus : IconEdit}
|
||||||
|
>
|
||||||
|
{editingIpsecPassword && (
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Название"
|
||||||
|
name="name"
|
||||||
|
value={editingIpsecPassword.name}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, name: val })}
|
||||||
|
placeholder="Основной IPSec пароль"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Пароль"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={editingIpsecPassword.password}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, password: val })}
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="form-text">Пароль будет зашифрован перед сохранением в S3</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Описание"
|
||||||
|
name="description"
|
||||||
|
value={editingIpsecPassword.description || ''}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, description: val })}
|
||||||
|
placeholder="Описание использования пароля"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
|
{/* IPSec Password Modal */}
|
||||||
|
<FormModal
|
||||||
|
show={ipsecPasswordModalOpen}
|
||||||
|
onClose={() => {
|
||||||
|
setIpsecPasswordModalOpen(false);
|
||||||
|
setEditingIpsecPassword(null);
|
||||||
|
}}
|
||||||
|
onSubmit={handleSaveIpsecPassword}
|
||||||
|
title={ipsecPasswordModalMode === 'add' ? 'Добавить IPSec пароль' : 'Редактировать IPSec пароль'}
|
||||||
|
submitLabel={ipsecPasswordModalMode === 'add' ? 'Создать' : 'Сохранить'}
|
||||||
|
submitIcon={ipsecPasswordModalMode === 'add' ? IconPlus : IconEdit}
|
||||||
|
>
|
||||||
|
{editingIpsecPassword && (
|
||||||
|
<div className="row g-3">
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Название"
|
||||||
|
name="name"
|
||||||
|
value={editingIpsecPassword.name}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, name: val })}
|
||||||
|
placeholder="Основной IPSec пароль"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Пароль"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
value={editingIpsecPassword.password}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, password: val })}
|
||||||
|
placeholder="Введите пароль"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div className="form-text">Пароль будет зашифрован перед сохранением в S3</div>
|
||||||
|
</div>
|
||||||
|
<div className="col-12">
|
||||||
|
<FormField
|
||||||
|
label="Описание"
|
||||||
|
name="description"
|
||||||
|
value={editingIpsecPassword.description || ''}
|
||||||
|
onChange={(val) => setEditingIpsecPassword({ ...editingIpsecPassword, description: val })}
|
||||||
|
placeholder="Описание использования пароля"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</FormModal>
|
||||||
|
|
||||||
|
{/* IPSec Passwords List Modal */}
|
||||||
|
{ipsecPasswordsListModalOpen && (
|
||||||
|
<div className="modal show d-block" style={{ backgroundColor: 'rgba(0,0,0,0.5)' }}>
|
||||||
|
<div className="modal-dialog modal-lg">
|
||||||
|
<div className="modal-content">
|
||||||
|
<div className="modal-header">
|
||||||
|
<h5 className="modal-title d-flex align-items-center">
|
||||||
|
<IconLock size={20} className="me-2" />
|
||||||
|
Управление IPSec паролями
|
||||||
|
</h5>
|
||||||
|
<button type="button" className="btn-close" onClick={() => setIpsecPasswordsListModalOpen(false)}></button>
|
||||||
|
</div>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<div>
|
||||||
|
<h6 className="mb-0">Сохраненные IPSec пароли</h6>
|
||||||
|
<div className="text-muted small">Пароли хранятся в зашифрованном виде в S3</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-1" />
|
||||||
|
Добавить пароль
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{ipsecPasswords.length === 0 ? (
|
||||||
|
<div className="empty">
|
||||||
|
<div className="empty-img">
|
||||||
|
<IconLock size={48} />
|
||||||
|
</div>
|
||||||
|
<p className="empty-title">IPSec пароли не найдены</p>
|
||||||
|
<p className="empty-subtitle text-muted">
|
||||||
|
Создайте первый IPSec пароль для использования в шаблонах интерфейсов
|
||||||
|
</p>
|
||||||
|
<div className="empty-action">
|
||||||
|
<button
|
||||||
|
className="btn btn-primary"
|
||||||
|
onClick={() => {
|
||||||
|
setIpsecPasswordModalMode('add');
|
||||||
|
setEditingIpsecPassword({ name: '', password: '', description: '' });
|
||||||
|
setIpsecPasswordModalOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconPlus size={16} className="me-2" />
|
||||||
|
Добавить пароль
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="table-responsive">
|
||||||
|
<table className="table table-vcenter card-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Название</th>
|
||||||
|
<th>Описание</th>
|
||||||
|
<th className="text-end">Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{ipsecPasswords.map(pwd => (
|
||||||
|
<tr key={pwd.id}>
|
||||||
|
<td><strong>{pwd.name}</strong></td>
|
||||||
|
<td className="text-muted">{pwd.description || '—'}</td>
|
||||||
|
<td className="text-end">
|
||||||
|
<div className="btn-list gap-1">
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost-primary btn-icon btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
handleEditIpsecPassword(pwd.id);
|
||||||
|
setIpsecPasswordsListModalOpen(false);
|
||||||
|
}}
|
||||||
|
title="Редактировать"
|
||||||
|
>
|
||||||
|
<IconEdit size={16} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="btn btn-ghost-danger btn-icon btn-sm"
|
||||||
|
onClick={() => {
|
||||||
|
if (confirm(`Удалить пароль "${pwd.name}"?`)) {
|
||||||
|
handleDeleteIpsecPassword(pwd.id);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title="Удалить"
|
||||||
|
>
|
||||||
|
<IconTrash size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={() => setIpsecPasswordsListModalOpen(false)}>
|
||||||
|
Закрыть
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Pool Modal */}
|
{/* Pool Modal */}
|
||||||
<FormModal
|
<FormModal
|
||||||
show={poolModalOpen}
|
show={poolModalOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user