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 serverConfigsRoutes = require('./routes/serverConfigsRoutes');
|
||||
const miscRoutes = require('./routes/miscRoutes');
|
||||
const ipsecPasswordsRoutes = require('./routes/ipsecPasswordsRoutes');
|
||||
|
||||
const app = express();
|
||||
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.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 ===
|
||||
app.post('/api/mikrotik/validate', async (req, res) => {
|
||||
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,
|
||||
};
|
||||
Reference in New Issue
Block a user