Промежуточный коммит
This commit is contained in:
parent
f22eeb13ce
commit
33a79a13d9
|
|
@ -42,3 +42,4 @@ build/
|
||||||
/web-server/src/main/webui/src/json-rpc.js
|
/web-server/src/main/webui/src/json-rpc.js
|
||||||
/.cache/
|
/.cache/
|
||||||
ovpn-connector.json
|
ovpn-connector.json
|
||||||
|
/webui/src/json-rpc.js
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,21 @@ public class System implements RPC {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
|
@ProtectedMethod
|
||||||
|
@JRPCMethod
|
||||||
|
public JSONObject getComponentConfig(@JRPCArgument(name = "component") String componentName) throws ClassNotFoundException {
|
||||||
|
var config = context.getConfig().getComponentsConfig().getConfig((Class) Class.forName(componentName));
|
||||||
|
return JSONUtility.serializeStructure(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
|
@ProtectedMethod
|
||||||
|
@JRPCMethod
|
||||||
|
public void setComponentConfig(@JRPCArgument(name = "component") String componentName, @JRPCArgument(name = "config") JSONObject config) throws ClassNotFoundException {
|
||||||
|
var cls = (Class) Class.forName(componentName);
|
||||||
|
var configClass = Component.getConfigClass(cls);
|
||||||
|
context.getConfig().getComponentsConfig().setConfig(cls, JSONUtility.deserializeStructure(config, configClass));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ public abstract class AbstractComponent<CT> implements Component<CT> {
|
||||||
|
|
||||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||||
public AbstractComponent(Context context) {
|
public AbstractComponent(Context context) {
|
||||||
config = (CT) context.getConfig().getPluginsConfig().getConfig((Class) getClass());
|
config = (CT) context.getConfig().getComponentsConfig().getConfig((Class) getClass());
|
||||||
this.context = context;
|
this.context = context;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,15 +12,15 @@ import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@JSONSerializable(PluginConfigStorage.Serializer.class)
|
@JSONSerializable(ComponentConfigStorage.Serializer.class)
|
||||||
public class PluginConfigStorage {
|
public class ComponentConfigStorage {
|
||||||
|
|
||||||
public final static class Serializer implements JSONSerializer<PluginConfigStorage> {
|
public final static class Serializer implements JSONSerializer<ComponentConfigStorage> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object serialize(PluginConfigStorage pluginConfigStorage) throws SerializationException {
|
public Object serialize(ComponentConfigStorage componentConfigStorage) throws SerializationException {
|
||||||
var json = new JSONObject();
|
var json = new JSONObject();
|
||||||
pluginConfigStorage.configs.forEach((key, value) -> {
|
componentConfigStorage.configs.forEach((key, value) -> {
|
||||||
json.put(key.getName(), JSONUtility.serializeStructure(value));
|
json.put(key.getName(), JSONUtility.serializeStructure(value));
|
||||||
});
|
});
|
||||||
return json;
|
return json;
|
||||||
|
|
@ -28,16 +28,16 @@ public class PluginConfigStorage {
|
||||||
|
|
||||||
@SuppressWarnings({"rawtypes", "unchecked"})
|
@SuppressWarnings({"rawtypes", "unchecked"})
|
||||||
@Override
|
@Override
|
||||||
public PluginConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
|
public ComponentConfigStorage deserialize(Object o, Class<?> aClass) throws SerializationException {
|
||||||
var loader = getClass().getClassLoader();
|
var loader = getClass().getClassLoader();
|
||||||
var json = (JSONObject) o;
|
var json = (JSONObject) o;
|
||||||
var storage = new PluginConfigStorage();
|
var storage = new ComponentConfigStorage();
|
||||||
json.keySet().forEach(key -> {
|
json.keySet().forEach(key -> {
|
||||||
try {
|
try {
|
||||||
var pluginClass = loader.loadClass(key);
|
var pluginClass = loader.loadClass(key);
|
||||||
var value = json.getJSONObject(key);
|
var value = json.getJSONObject(key);
|
||||||
var configClass = Component.getConfigClass((Class) pluginClass);
|
var configClass = Component.getConfigClass((Class) pluginClass);
|
||||||
storage.configs.put((Class)pluginClass, JSONUtility.deserializeStructure(value, configClass));
|
storage.configs.put((Class) pluginClass, JSONUtility.deserializeStructure(value, configClass));
|
||||||
} catch (ClassNotFoundException e) {
|
} catch (ClassNotFoundException e) {
|
||||||
throw new RuntimeException(e);
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
@ -50,12 +50,18 @@ public class PluginConfigStorage {
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
public <CT> CT getConfig(Class<? extends Component<CT>> pluginClass) {
|
public <CT> CT getConfig(Class<? extends Component<CT>> componentClass) {
|
||||||
if (!configs.containsKey(pluginClass)) {
|
if (!configs.containsKey(componentClass)) {
|
||||||
var configClass = Component.getConfigClass(pluginClass);
|
var configClass = Component.getConfigClass(componentClass);
|
||||||
var instance = configClass.getConstructor().newInstance();
|
var instance = configClass.getConstructor().newInstance();
|
||||||
configs.put(pluginClass, instance);
|
configs.put(componentClass, instance);
|
||||||
}
|
}
|
||||||
return (CT) configs.get(pluginClass);
|
return (CT) configs.get(componentClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SneakyThrows
|
||||||
|
public <CT> void setConfig(Class<? extends Component<CT>> componentClass, CT config) {
|
||||||
|
var configClass = Component.getConfigClass(componentClass);
|
||||||
|
configs.put(componentClass, config);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +62,7 @@ public class Config {
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty(required = false)
|
@JSONProperty(required = false)
|
||||||
private PluginConfigStorage pluginsConfig = new PluginConfigStorage();
|
private ComponentConfigStorage componentsConfig = new ComponentConfigStorage();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
|
|
|
||||||
2
pom.xml
2
pom.xml
|
|
@ -91,7 +91,7 @@
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ru.kirillius</groupId>
|
<groupId>ru.kirillius</groupId>
|
||||||
<artifactId>json-rpc-servlet</artifactId>
|
<artifactId>json-rpc-servlet</artifactId>
|
||||||
<version>2.1.5.0</version>
|
<version>2.1.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
|
||||||
|
|
@ -15,9 +15,7 @@
|
||||||
"jquery": "^3.7.1"
|
"jquery": "^3.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^6.0.1",
|
|
||||||
"sass": "^1.93.2",
|
"sass": "^1.93.2",
|
||||||
"vite": "^7.1.7",
|
"vite": "^7.1.7"
|
||||||
"vite-plugin-vue-devtools": "^8.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,36 @@
|
||||||
import { renderLoginForm, renderDashboard } from './ui.js';
|
import { renderLoginForm, renderDashboard } from './ui.js';
|
||||||
import { checkAuthOnStartup } from './auth.js'; // 🔥 Теперь импортируем здесь
|
import { checkAuthStatus } from './auth.js';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
// Глобальный статус приложения
|
let isAuthenticated = false;
|
||||||
export let isAuthenticated = false;
|
let enabledComponents = [];
|
||||||
|
|
||||||
// Главная функция инициализации
|
async function loadEnabledComponents() {
|
||||||
export async function initApp() {
|
try {
|
||||||
// 1. Проверяем авторизацию
|
enabledComponents = await JSONRPC.System.getEnabledComponents();
|
||||||
const isInitialAuth = await checkAuthOnStartup();
|
} catch(e) {
|
||||||
setAuthenticated(isInitialAuth); // Устанавливаем состояние и рендерим
|
console.error("Ошибка при загрузке включенных компонентов:", e);
|
||||||
|
enabledComponents = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Функция для смены состояния (например, после успешного логина/выхода)
|
export async function initApp() {
|
||||||
export function setAuthenticated(status) {
|
const authSuccess = await checkAuthStatus();
|
||||||
isAuthenticated = status;
|
setAuthenticated(authSuccess);
|
||||||
|
}
|
||||||
|
|
||||||
if (status) {
|
export async function setAuthenticated(status) {
|
||||||
|
isAuthenticated = status;
|
||||||
|
if (isAuthenticated) {
|
||||||
|
// Сначала загружаем компоненты
|
||||||
|
await loadEnabledComponents();
|
||||||
renderDashboard();
|
renderDashboard();
|
||||||
} else {
|
} else {
|
||||||
renderLoginForm();
|
renderLoginForm();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Новый экспорт для доступа роутера к списку компонентов
|
||||||
|
export function getEnabledComponents() {
|
||||||
|
return enabledComponents;
|
||||||
}
|
}
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
|
// src/modules/auth.js
|
||||||
|
|
||||||
import { getAuthToken, setAuthToken, removeAuthToken } from '../utils/cookies.js';
|
import { getAuthToken, setAuthToken, removeAuthToken } from '../utils/cookies.js';
|
||||||
|
|
||||||
// ⚠️ ОБНОВИ ПУТЬ! Этот импорт должен указывать на твой модуль для работы с RPC.
|
|
||||||
// Предполагается, что JSONRPC.Auth содержит методы:
|
// Предполагается, что JSONRPC.Auth содержит методы:
|
||||||
// - isAuthenticated()
|
// - isAuthenticated()
|
||||||
// - startSessionByToken(token)
|
// - startSessionByToken(token)
|
||||||
|
|
@ -11,11 +12,13 @@ import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @private
|
* @public
|
||||||
* Проверяет, активна ли сессия (через API) или сохранен ли токен в cookies.
|
* Проверяет, активна ли сессия (через API) или сохранен ли токен в cookies.
|
||||||
|
*
|
||||||
|
* 🔥 ПЕРЕИМЕНОВАНА для соответствия импорту в app.js!
|
||||||
* @returns {Promise<boolean>}
|
* @returns {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
export async function checkAuthOnStartup() {
|
export async function checkAuthStatus() {
|
||||||
try {
|
try {
|
||||||
// 1. Проверяем активную сессию (самый надежный способ)
|
// 1. Проверяем активную сессию (самый надежный способ)
|
||||||
const isAuthenticated = await JSONRPC.Auth.isAuthenticated();
|
const isAuthenticated = await JSONRPC.Auth.isAuthenticated();
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,26 @@
|
||||||
// src/modules/router.js
|
// src/modules/router.js
|
||||||
|
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
// 🔥 Импортируем только объект StatisticsPage
|
|
||||||
import { StatisticsPage } from '../pages/Statistics.js';
|
import { StatisticsPage } from '../pages/Statistics.js';
|
||||||
import { Subscriptions } from '../pages/Subscriptions.js';
|
import { Subscriptions } from '../pages/Subscriptions.js';
|
||||||
import { Components } from '../pages/Components.js';
|
import { Components } from '../pages/Components.js';
|
||||||
import { APITokens } from '../pages/APITokens.js';
|
import { APITokens } from '../pages/APITokens.js';
|
||||||
|
import { getEnabledComponents } from './app.js';
|
||||||
|
import { OVPNConfig } from '../pages/OVPN.js';
|
||||||
|
|
||||||
|
|
||||||
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
||||||
let currentRouteHash = '';
|
let currentRouteHash = '';
|
||||||
|
|
||||||
|
|
||||||
// 1. Определение меню и путей
|
// 1. Определение ВСЕХ возможных пунктов меню
|
||||||
const menuItems = [
|
const allMenuItems = [
|
||||||
{ label: 'Статистика', path: 'stats' },
|
{ label: 'Статистика', path: 'stats', component: null },
|
||||||
{ label: 'Подписки', path: 'subscriptions' },
|
{ label: 'Подписки', path: 'subscriptions', component: null },
|
||||||
{ label: 'Настройки', path: 'settings' },
|
{ label: 'Настройки', path: 'settings', component: null },
|
||||||
{ label: 'Компоненты', path: 'components' },
|
{ label: 'Компоненты', path: 'components', component: null },
|
||||||
{ label: 'API', path: 'api' },
|
{ label: 'API', path: 'api', component: null },
|
||||||
|
{ label: 'Настройка OVPN', path: 'ovpn', component: 'ru.kirillius.pf.sdn.External.API.Components.OVPN' },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 2. Определение страниц
|
// 2. Определение страниц
|
||||||
|
|
@ -25,7 +28,6 @@ const routes = {
|
||||||
'#stats': {
|
'#stats': {
|
||||||
render: StatisticsPage.render,
|
render: StatisticsPage.render,
|
||||||
mount: StatisticsPage.mount,
|
mount: StatisticsPage.mount,
|
||||||
// 🔥 Используем unmount из объекта страницы
|
|
||||||
unmount: StatisticsPage.unmount
|
unmount: StatisticsPage.unmount
|
||||||
},
|
},
|
||||||
'#subscriptions': {
|
'#subscriptions': {
|
||||||
|
|
@ -47,22 +49,38 @@ const routes = {
|
||||||
render: APITokens.render,
|
render: APITokens.render,
|
||||||
mount: APITokens.mount,
|
mount: APITokens.mount,
|
||||||
unmount: APITokens.unmount
|
unmount: APITokens.unmount
|
||||||
|
},
|
||||||
|
'#ovpn': {
|
||||||
|
render: OVPNConfig.render,
|
||||||
|
mount: OVPNConfig.mount,
|
||||||
|
unmount: OVPNConfig.unmount
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 3. Функция рендеринга страницы
|
// 🔥 Убедитесь, что здесь НЕТ слова 'export'
|
||||||
|
function getFilteredMenuItems() {
|
||||||
|
const enabled = getEnabledComponents();
|
||||||
|
|
||||||
|
return allMenuItems.filter(item => {
|
||||||
|
if (item.component === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return enabled.includes(item.component);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Функция рендеринга страницы (без изменений)
|
||||||
export function renderPage(hash) {
|
export function renderPage(hash) {
|
||||||
const $contentArea = $('#content-area');
|
const $contentArea = $('#content-area');
|
||||||
const key = hash.startsWith('#') ? hash : '#' + hash;
|
const key = hash.startsWith('#') ? hash : '#' + hash;
|
||||||
|
|
||||||
// 🔥 НОВАЯ ПРОВЕРКА: Если страница уже открыта, просто обновляем меню и выходим
|
// Если страница уже открыта, просто обновляем меню и выходим
|
||||||
if (currentRouteHash === key) {
|
if (currentRouteHash === key) {
|
||||||
$('.menu-item').removeClass('active');
|
$('.menu-item').removeClass('active');
|
||||||
$(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active');
|
$(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Определяем, какой хеш был активным до этого. Используем 'stats' как дефолт
|
|
||||||
const previousKey = currentRouteHash || '#stats';
|
const previousKey = currentRouteHash || '#stats';
|
||||||
|
|
||||||
// Шаг 1: Если мы меняем страницу, и предыдущая страница имеет unmount, вызываем его
|
// Шаг 1: Если мы меняем страницу, и предыдущая страница имеет unmount, вызываем его
|
||||||
|
|
@ -100,5 +118,5 @@ $(window).on('hashchange', function() {
|
||||||
renderPage(window.location.hash);
|
renderPage(window.location.hash);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Экспорт menuItems для построения сайдбара в ui.js
|
// 🔥 Оставляем ТОЛЬКО ОДИН экспорт в конце файла
|
||||||
export { menuItems };
|
export { getFilteredMenuItems };
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import { handleLogin, handleLogout } from './auth.js';
|
import { handleLogin, handleLogout } from './auth.js';
|
||||||
import { setAuthenticated } from './app.js';
|
import { setAuthenticated } from './app.js';
|
||||||
import { renderPage, menuItems } from './router.js';
|
import { renderPage, getFilteredMenuItems } from './router.js';
|
||||||
|
|
||||||
|
|
||||||
// Функция рендеринга формы авторизации
|
// Функция рендеринга формы авторизации
|
||||||
export function renderLoginForm() {
|
export function renderLoginForm() {
|
||||||
|
|
@ -36,12 +37,14 @@ export function renderLoginForm() {
|
||||||
const $button = $('#login-button');
|
const $button = $('#login-button');
|
||||||
const $error = $('#login-error');
|
const $error = $('#login-error');
|
||||||
const password = $('#password-input').val();
|
const password = $('#password-input').val();
|
||||||
|
// Получение флага rememberMe
|
||||||
const rememberMe = $('#remember-me').prop('checked');
|
const rememberMe = $('#remember-me').prop('checked');
|
||||||
|
|
||||||
$error.hide().text('');
|
$error.hide().text('');
|
||||||
$button.prop('disabled', true).text('Вход...');
|
$button.prop('disabled', true).text('Вход...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// Передача флага rememberMe
|
||||||
const success = await handleLogin(password, rememberMe);
|
const success = await handleLogin(password, rememberMe);
|
||||||
if (success) {
|
if (success) {
|
||||||
setAuthenticated(true);
|
setAuthenticated(true);
|
||||||
|
|
@ -59,7 +62,9 @@ export function renderLoginForm() {
|
||||||
|
|
||||||
// Функция рендеринга рабочего стола (Dashboard)
|
// Функция рендеринга рабочего стола (Dashboard)
|
||||||
export function renderDashboard() {
|
export function renderDashboard() {
|
||||||
// Используем menuItems из router.js для построения сайдбара
|
// Используем функцию из роутера для получения актуального списка меню
|
||||||
|
const menuItems = getFilteredMenuItems();
|
||||||
|
|
||||||
const sidebarHtml = menuItems.map(item => {
|
const sidebarHtml = menuItems.map(item => {
|
||||||
return `<a href="#${item.path}" class="menu-item" data-path="${item.path}">${item.label}</a>`;
|
return `<a href="#${item.path}" class="menu-item" data-path="${item.path}">${item.label}</a>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
@ -91,7 +96,6 @@ export function renderDashboard() {
|
||||||
|
|
||||||
// Прикрепляем события для меню (роутер)
|
// Прикрепляем события для меню (роутер)
|
||||||
$('#main-nav').on('click', '.menu-item', function(e) {
|
$('#main-nav').on('click', '.menu-item', function(e) {
|
||||||
// Предотвращаем стандартный переход, чтобы обработать его через JS
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
const path = $(this).data('path');
|
const path = $(this).data('path');
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
// src/pages/OVPN.js
|
||||||
|
|
||||||
|
import { OVPNConfigPage } from '../ui/ovpn.js';
|
||||||
|
|
||||||
|
export const OVPNConfig = OVPNConfigPage;
|
||||||
|
|
@ -409,3 +409,55 @@ html.dark-theme, body {
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/* --- Стили для формы конфигурации (OVPN) --- */
|
||||||
|
|
||||||
|
.component-config-form {
|
||||||
|
background-color: var(--color-bg-card);
|
||||||
|
padding: 30px;
|
||||||
|
border-radius: var(--border-radius);
|
||||||
|
border: 1px solid var(--color-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-section-title {
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
padding-bottom: 8px;
|
||||||
|
margin-bottom: 25px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-text {
|
||||||
|
display: block;
|
||||||
|
margin-top: 5px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding-top: 20px;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn-secondary {
|
||||||
|
/* Дополнительная настройка, чтобы соответствовать ширине primary */
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,167 @@
|
||||||
|
// src/ui/ovpn.js
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
|
const OVPN_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.OVPN';
|
||||||
|
|
||||||
|
// --- Глобальное состояние ---
|
||||||
|
let config = {};
|
||||||
|
|
||||||
|
/** Загрузка конфигурации OVPN */
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const fullConfig = await JSONRPC.System.getComponentConfig(OVPN_COMPONENT_NAME);
|
||||||
|
config = fullConfig || {};
|
||||||
|
return true;
|
||||||
|
} catch(e) {
|
||||||
|
console.error("Ошибка при загрузке конфига OVPN:", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Рендеринг формы */
|
||||||
|
function renderOVPNForm() {
|
||||||
|
const $container = $('#ovpn-config-container');
|
||||||
|
|
||||||
|
// Деструктурируем для удобства
|
||||||
|
const shellConfig = config.shellConfig || {};
|
||||||
|
|
||||||
|
const formHtml = `
|
||||||
|
<div class="component-config-form">
|
||||||
|
<h3 class="config-section-title">Конфигурация Shell (ShellConfig)</h3>
|
||||||
|
<div class="form-grid">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ovpn-host">Host (Хост)</label>
|
||||||
|
<input type="text" id="ovpn-host" class="form-control" value="${shellConfig.host || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ovpn-port">Port (Порт)</label>
|
||||||
|
<input type="number" id="ovpn-port" class="form-control" value="${shellConfig.port || 22}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ovpn-username">Username (Имя пользователя)</label>
|
||||||
|
<input type="text" id="ovpn-username" class="form-control" value="${shellConfig.username || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ovpn-password">Password (Пароль)</label>
|
||||||
|
<input type="password" id="ovpn-password" class="form-control" placeholder="Оставьте пустым для сохранения текущего пароля">
|
||||||
|
</div>
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="ovpn-use-ssh" ${shellConfig.useSSH ? 'checked' : ''}>
|
||||||
|
<label for="ovpn-use-ssh" style="margin-bottom: 0;">Использовать SSH</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="config-section-title" style="margin-top: 40px;">Команда перезапуска</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="ovpn-restart-command">Команда перезапуска сервиса</label>
|
||||||
|
<input type="text" id="ovpn-restart-command" class="form-control" value="${config.restartCommand || 'systemctl restart openvpn@server'}">
|
||||||
|
<small class="hint-text">Команда, которая будет выполнена через Shell для перезапуска сервиса OVPN.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="save-ovpn-btn" class="btn-primary" style="width: 200px;">Сохранить Конфигурацию</button>
|
||||||
|
<button id="restart-ovpn-btn" class="btn-secondary" style="width: 200px; margin-left: 20px;">Перезапустить Сервис</button>
|
||||||
|
</div>
|
||||||
|
<div id="ovpn-status-message" class="error-message" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$container.html(formHtml);
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сбор данных из формы */
|
||||||
|
function collectConfigFromForm() {
|
||||||
|
// Получаем текущий пароль, если поле ввода пустое (чтобы не отправлять пустую строку)
|
||||||
|
const newPassword = $('#ovpn-password').val();
|
||||||
|
const oldPassword = config.shellConfig ? config.shellConfig.password : '';
|
||||||
|
|
||||||
|
const newConfig = {
|
||||||
|
shellConfig: {
|
||||||
|
useSSH: $('#ovpn-use-ssh').prop('checked'),
|
||||||
|
host: $('#ovpn-host').val(),
|
||||||
|
port: parseInt($('#ovpn-port').val()) || 22,
|
||||||
|
username: $('#ovpn-username').val(),
|
||||||
|
// Если пароль не введен, используем старый пароль, иначе - новый
|
||||||
|
password: newPassword ? newPassword : oldPassword,
|
||||||
|
},
|
||||||
|
restartCommand: $('#ovpn-restart-command').val()
|
||||||
|
};
|
||||||
|
return newConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Сохранение конфига */
|
||||||
|
async function saveConfig() {
|
||||||
|
const newConfig = collectConfigFromForm();
|
||||||
|
const $btn = $('#save-ovpn-btn');
|
||||||
|
const $message = $('#ovpn-status-message');
|
||||||
|
|
||||||
|
$message.hide().removeClass('success-message').addClass('error-message');
|
||||||
|
$btn.prop('disabled', true).text('Сохранение...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.System.setComponentConfig(OVPN_COMPONENT_NAME, newConfig);
|
||||||
|
config = newConfig; // Обновляем локальное состояние
|
||||||
|
$message.text('Конфигурация OVPN успешно сохранена.').addClass('success-message').show();
|
||||||
|
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Ошибка сохранения конфига:', e);
|
||||||
|
$message.text('Ошибка при сохранении конфигурации.').show();
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false).text('Сохранить Конфигурацию');
|
||||||
|
setTimeout(() => $message.fadeOut(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Перезапуск сервиса */
|
||||||
|
async function restartService() {
|
||||||
|
const $btn = $('#restart-ovpn-btn');
|
||||||
|
const $message = $('#ovpn-status-message');
|
||||||
|
|
||||||
|
$message.hide().removeClass('success-message').addClass('error-message');
|
||||||
|
$btn.prop('disabled', true).text('Перезапуск...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.OVPN.restartSystemService();
|
||||||
|
$message.text('Сервис OVPN успешно перезапущен.').addClass('success-message').show();
|
||||||
|
} catch(e) {
|
||||||
|
console.error('Ошибка перезапуска:', e);
|
||||||
|
$message.text('Ошибка при перезапуске сервиса.').show();
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false).text('Перезапустить Сервис');
|
||||||
|
setTimeout(() => $message.fadeOut(), 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/** Прикрепление обработчиков */
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$('#save-ovpn-btn').off('click').on('click', saveConfig);
|
||||||
|
$('#restart-ovpn-btn').off('click').on('click', restartService);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Объект страницы для роутера ---
|
||||||
|
export const OVPNConfigPage = {
|
||||||
|
render: () => {
|
||||||
|
return `
|
||||||
|
<h1 class="page-title">Настройка OVPN</h1>
|
||||||
|
<div id="ovpn-config-container">
|
||||||
|
<p>Загрузка конфигурации...</p>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadConfig();
|
||||||
|
if (success) {
|
||||||
|
renderOVPNForm();
|
||||||
|
} else {
|
||||||
|
$('#ovpn-config-container').html('<p class="error-message">Не удалось загрузить конфигурацию OVPN.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
config = {};
|
||||||
|
$('#ovpn-config-container').off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -3,15 +3,10 @@ import { fileURLToPath, URL } from 'node:url'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import vue from '@vitejs/plugin-vue'
|
|
||||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [],
|
||||||
vue(),
|
|
||||||
vueDevTools(),
|
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue