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:
@@ -27,7 +27,8 @@ import {
|
||||
IconLayoutGrid,
|
||||
IconList,
|
||||
IconCode,
|
||||
IconArrowsRightLeft
|
||||
IconArrowsRightLeft,
|
||||
IconLock,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
@@ -81,6 +82,7 @@ const getEmptyInterface = () => ({
|
||||
remoteIp: '',
|
||||
serverId: '',
|
||||
serverId2: '', // Второй сервер (опциональный)
|
||||
ipsecPasswordId: '', // ID IPSec пароля (опционально, только для IPSec)
|
||||
});
|
||||
|
||||
const getEmptyIpPool = () => ({
|
||||
@@ -140,6 +142,14 @@ function NetworkConfigManager() {
|
||||
const [templateNamePrefix, setTemplateNamePrefix] = useState('');
|
||||
const [templateName1, setTemplateName1] = useState(''); // Опциональное имя для сервера 1
|
||||
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 ===
|
||||
const [mikrotikCodeModalOpen, setMikrotikCodeModalOpen] = useState(false);
|
||||
@@ -149,6 +159,7 @@ function NetworkConfigManager() {
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
fetchServers();
|
||||
fetchIpsecPasswords();
|
||||
}, []);
|
||||
|
||||
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 () => {
|
||||
setSaving(true);
|
||||
@@ -529,7 +550,8 @@ function NetworkConfigManager() {
|
||||
localIp: localIp,
|
||||
remoteIp: remoteIp,
|
||||
serverId: templateServer1,
|
||||
serverId2: templateServer2
|
||||
serverId2: templateServer2,
|
||||
ipsecPasswordId: templateType === 'IPSec' && templateIpsecPasswordId ? templateIpsecPasswordId : ''
|
||||
};
|
||||
|
||||
const conflicts = checkInterfaceIpConflict(newInterface);
|
||||
@@ -554,6 +576,7 @@ function NetworkConfigManager() {
|
||||
setTemplateNamePrefix('');
|
||||
setTemplateName1('');
|
||||
setTemplateName2('');
|
||||
setTemplateIpsecPasswordId('');
|
||||
};
|
||||
|
||||
// === CRUD для Interfaces ===
|
||||
@@ -836,6 +859,67 @@ function NetworkConfigManager() {
|
||||
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 ===
|
||||
const handleAddPool = () => {
|
||||
setEditingPool(getEmptyIpPool());
|
||||
@@ -1908,6 +1992,14 @@ function NetworkConfigManager() {
|
||||
<IconSparkles size={16} className="me-1" />
|
||||
Шаблон
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
onClick={() => setIpsecPasswordsListModalOpen(true)}
|
||||
title="Управление IPSec паролями"
|
||||
>
|
||||
<IconLock size={16} className="me-1" />
|
||||
IPSec пароли
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{activeTab === 'pools' && (
|
||||
@@ -2630,10 +2722,44 @@ function NetworkConfigManager() {
|
||||
name="type"
|
||||
type="select"
|
||||
value={editingInterface.type}
|
||||
onChange={(val) => setEditingInterface({ ...editingInterface, type: val })}
|
||||
onChange={(val) => setEditingInterface({ ...editingInterface, type: val, ipsecPasswordId: val !== 'IPSec' ? '' : editingInterface.ipsecPasswordId })}
|
||||
options={INTERFACE_TYPES}
|
||||
/>
|
||||
</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">
|
||||
<label className="form-label">Local IP</label>
|
||||
<div className="input-group">
|
||||
@@ -2787,6 +2913,7 @@ function NetworkConfigManager() {
|
||||
setTemplateNamePrefix('');
|
||||
setTemplateName1('');
|
||||
setTemplateName2('');
|
||||
setTemplateIpsecPasswordId('');
|
||||
}}
|
||||
onSubmit={handleCreateInterfaceFromTemplate}
|
||||
title="Создать интерфейс из шаблона"
|
||||
@@ -2892,9 +3019,43 @@ function NetworkConfigManager() {
|
||||
onChange={(e) => setTemplateName2(e.target.value)}
|
||||
placeholder={autoName2}
|
||||
/>
|
||||
<div className="form-text">Оставьте пустым для автогенерации</div>
|
||||
</div>
|
||||
<div className="form-text">Оставьте пустым для автогенерации </div>
|
||||
</div>
|
||||
{templateType === 'IPSec' && (
|
||||
<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-body">
|
||||
<h6 className="card-title mb-3">Предпросмотр интерфейса:</h6>
|
||||
@@ -2926,6 +3087,214 @@ function NetworkConfigManager() {
|
||||
</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 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 */}
|
||||
<FormModal
|
||||
show={poolModalOpen}
|
||||
|
||||
Reference in New Issue
Block a user