Compare commits
3 Commits
aa726d4653
...
b6489a5838
| Author | SHA1 | Date |
|---|---|---|
|
|
b6489a5838 | |
|
|
33a79a13d9 | |
|
|
f22eeb13ce |
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package ru.kirillius.pf.sdn;
|
||||||
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.SneakyThrows;
|
import lombok.SneakyThrows;
|
||||||
|
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
import ru.kirillius.pf.sdn.External.API.Components.FRR;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
import ru.kirillius.pf.sdn.External.API.Components.OVPN;
|
||||||
import ru.kirillius.pf.sdn.External.API.Components.TDNS;
|
import ru.kirillius.pf.sdn.External.API.Components.TDNS;
|
||||||
|
|
@ -9,7 +10,6 @@ import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||||
import ru.kirillius.pf.sdn.core.*;
|
import ru.kirillius.pf.sdn.core.*;
|
||||||
import ru.kirillius.pf.sdn.core.Component;
|
|
||||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||||
|
|
@ -21,6 +21,7 @@ import java.io.Closeable;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.atomic.AtomicBoolean;
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
@ -41,7 +42,7 @@ public class App implements Context, Closeable {
|
||||||
@Getter
|
@Getter
|
||||||
private final NetworkManager networkManager;
|
private final NetworkManager networkManager;
|
||||||
@Getter
|
@Getter
|
||||||
private Config config;
|
private volatile Config config;
|
||||||
@Getter
|
@Getter
|
||||||
private final AuthManager authManager;
|
private final AuthManager authManager;
|
||||||
@Getter
|
@Getter
|
||||||
|
|
@ -119,19 +120,29 @@ public class App implements Context, Closeable {
|
||||||
return List.of(FRR.class, OVPN.class, TDNS.class);
|
return List.of(FRR.class, OVPN.class, TDNS.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void unloadComponent(Component<?> component) {
|
||||||
|
SystemLogger.message("Unloading component: " + component.getClass().getSimpleName(), CTX);
|
||||||
|
try {
|
||||||
|
component.close();
|
||||||
|
} catch (IOException e) {
|
||||||
|
SystemLogger.error("Error on component unload", CTX, e);
|
||||||
|
} finally {
|
||||||
|
loadedComponents.remove(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadComponent(Class<? extends Component<?>> componentClass) {
|
||||||
|
SystemLogger.message("Loading component: " + componentClass.getSimpleName(), CTX);
|
||||||
|
var plugin = Component.loadPlugin(componentClass, this);
|
||||||
|
loadedComponents.add(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
public void initComponents() {
|
public void initComponents() {
|
||||||
var enabledPlugins = config.getEnabledComponents();
|
var enabledPlugins = config.getEnabledComponents();
|
||||||
|
|
||||||
(List.copyOf(loadedComponents)).forEach(plugin -> {
|
(List.copyOf(loadedComponents)).forEach(plugin -> {
|
||||||
if (!enabledPlugins.contains(plugin.getClass())) {
|
if (!enabledPlugins.contains(plugin.getClass())) {
|
||||||
SystemLogger.message("Unloading plugin: " + plugin.getClass().getSimpleName(), CTX);
|
unloadComponent(plugin);
|
||||||
try {
|
|
||||||
plugin.close();
|
|
||||||
} catch (IOException e) {
|
|
||||||
SystemLogger.error("Error on plugin unload", CTX, e);
|
|
||||||
} finally {
|
|
||||||
loadedComponents.remove(plugin);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
|
var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
|
||||||
|
|
@ -139,12 +150,31 @@ public class App implements Context, Closeable {
|
||||||
if (loadedClasses.contains(pluginClass)) {
|
if (loadedClasses.contains(pluginClass)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SystemLogger.message("Loading plugin: " + pluginClass.getSimpleName(), CTX);
|
loadComponent(pluginClass);
|
||||||
var plugin = Component.loadPlugin(pluginClass, this);
|
|
||||||
loadedComponents.add(plugin);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void reloadComponents(Class<? extends Component<?>>... classes) {
|
||||||
|
Arrays.stream(classes)
|
||||||
|
.forEach(componentClass -> {
|
||||||
|
loadedComponents.stream()
|
||||||
|
.filter(component -> componentClass.equals(component.getClass()))
|
||||||
|
.findFirst().ifPresent(this::unloadComponent);
|
||||||
|
loadComponent(componentClass);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JSONRPCServlet getRPC() {
|
||||||
|
if (server == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return server.getJSONRPC();
|
||||||
|
}
|
||||||
|
|
||||||
private void subscribe() {
|
private void subscribe() {
|
||||||
var eventsHandler = getEventsHandler();
|
var eventsHandler = getEventsHandler();
|
||||||
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
||||||
|
|
@ -158,7 +188,7 @@ public class App implements Context, Closeable {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Component<?> getPluginInstance(Class<? extends Component<?>> pluginClass) {
|
public Component<?> getComponentInstance(Class<? extends Component<?>> pluginClass) {
|
||||||
return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
|
return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,17 +24,24 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
||||||
|
|
||||||
public OVPN(Context context) {
|
public OVPN(Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
|
var RPC = context.getRPC();
|
||||||
|
if (RPC != null) {
|
||||||
|
RPC.addTargetInstance(OVPN.class, this);
|
||||||
|
subscription = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
subscription = context.getEventsHandler().getRPCInitEvent()
|
subscription = context.getEventsHandler().getRPCInitEvent()
|
||||||
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
|
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@JRPCMethod
|
@JRPCMethod
|
||||||
@ProtectedMethod
|
@ProtectedMethod
|
||||||
public void restartSystemService() {
|
public String restartSystemService() {
|
||||||
try (var shell = new ShellExecutor(config.shellConfig)) {
|
try (var shell = new ShellExecutor(config.shellConfig)) {
|
||||||
shell.executeCommand(new String[]{config.restartCommand});
|
return shell.executeCommand(new String[]{config.restartCommand});
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
SystemLogger.error("Error when trying to restart OVPN", CTX, e);
|
SystemLogger.error("Error when trying to restart OVPN", CTX, e);
|
||||||
|
return e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -53,20 +60,22 @@ public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() {
|
||||||
|
if (subscription != null) {
|
||||||
context.getEventsHandler().getRPCInitEvent().remove(subscription);
|
context.getEventsHandler().getRPCInitEvent().remove(subscription);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JSONSerializable
|
@JSONSerializable
|
||||||
public static class OVPNConfig {
|
public static class OVPNConfig {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private ShellExecutor.Config shellConfig = new ShellExecutor.Config();
|
private volatile ShellExecutor.Config shellConfig = new ShellExecutor.Config();
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String restartCommand = "rc-service openvpn restart";
|
private volatile String restartCommand = "rc-service openvpn restart";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -85,23 +85,23 @@ public class ShellExecutor implements Closeable {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private boolean useSSH = false;
|
private volatile boolean useSSH = false;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String host = "127.0.0.1";
|
private volatile String host = "127.0.0.1";
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int port = 22;
|
private volatile int port = 22;
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String username = "root";
|
private volatile String username = "root";
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String password = "securepassword";
|
private volatile String password = "securepassword";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package ru.kirillius.pf.sdn.web;
|
package ru.kirillius.pf.sdn.web;
|
||||||
|
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
||||||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||||
import org.eclipse.jetty.server.Server;
|
import org.eclipse.jetty.server.Server;
|
||||||
|
|
@ -34,6 +35,8 @@ public class HTTPServer extends Server {
|
||||||
|
|
||||||
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
private JSONRPCServlet JSONRPC = new JSONRPCServlet();
|
||||||
|
|
||||||
public HTTPServer(Context appContext) {
|
public HTTPServer(Context appContext) {
|
||||||
var config = appContext.getConfig();
|
var config = appContext.getConfig();
|
||||||
|
|
@ -46,7 +49,7 @@ public class HTTPServer extends Server {
|
||||||
|
|
||||||
this.addConnector(connector);
|
this.addConnector(connector);
|
||||||
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
||||||
var JSONRPC = new JSONRPCServlet();
|
|
||||||
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
||||||
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -77,4 +77,24 @@ 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));
|
||||||
|
context.reloadComponents(cls);
|
||||||
|
Object config1 = context.getConfig().getComponentsConfig().getConfig(cls);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -24,85 +24,85 @@ public class Config {
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int updateSubscriptionsInterval = 6;
|
private volatile int updateSubscriptionsInterval = 6;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private boolean cachingAS = true;
|
private volatile boolean cachingAS = true;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int updateASInterval = 12;
|
private volatile int updateASInterval = 12;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
private File loadedConfigFile = null;
|
private volatile File loadedConfigFile = null;
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String host = "0.0.0.0";
|
private volatile String host = "0.0.0.0";
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private File cacheDirectory = new File("./.cache");
|
private volatile File cacheDirectory = new File("./.cache");
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONArrayProperty(type = RepositoryConfig.class)
|
@JSONArrayProperty(type = RepositoryConfig.class)
|
||||||
private List<RepositoryConfig> subscriptions = Collections.emptyList();
|
private volatile List<RepositoryConfig> subscriptions = Collections.emptyList();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONArrayProperty(type = String.class)
|
@JSONArrayProperty(type = String.class)
|
||||||
private List<String> subscribedResources = Collections.emptyList();
|
private volatile List<String> subscribedResources = Collections.emptyList();
|
||||||
|
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty(required = false)
|
@JSONProperty(required = false)
|
||||||
private PluginConfigStorage pluginsConfig = new PluginConfigStorage();
|
private volatile ComponentConfigStorage componentsConfig = new ComponentConfigStorage();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONArrayProperty(type = Class.class, required = false)
|
@JSONArrayProperty(type = Class.class, required = false)
|
||||||
private List<Class<? extends Component<?>>> enabledComponents = new ArrayList<>();
|
private volatile List<Class<? extends Component<?>>> enabledComponents = new ArrayList<>();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String passwordSalt = UUID.randomUUID().toString();
|
private volatile String passwordSalt = UUID.randomUUID().toString();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private String passwordHash = "";
|
private volatile String passwordHash = "";
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int httpPort = 8081;
|
private volatile int httpPort = 8081;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private boolean mergeSubnets = true;
|
private volatile boolean mergeSubnets = true;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private int mergeSubnetsWithUsage = 51;
|
private volatile int mergeSubnetsWithUsage = 51;
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private NetworkResourceBundle customResources = new NetworkResourceBundle();
|
private volatile NetworkResourceBundle customResources = new NetworkResourceBundle();
|
||||||
|
|
||||||
@Setter
|
@Setter
|
||||||
@Getter
|
@Getter
|
||||||
@JSONProperty
|
@JSONProperty
|
||||||
private NetworkResourceBundle filteredResources = new NetworkResourceBundle();
|
private volatile NetworkResourceBundle filteredResources = new NetworkResourceBundle();
|
||||||
|
|
||||||
public static void store(Config config, File file) throws IOException {
|
public static void store(Config config, File file) throws IOException {
|
||||||
try (var fileInputStream = new FileOutputStream(file)) {
|
try (var fileInputStream = new FileOutputStream(file)) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package ru.kirillius.pf.sdn.core;
|
package ru.kirillius.pf.sdn.core;
|
||||||
|
|
||||||
|
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||||
|
|
@ -23,7 +24,7 @@ public interface Context {
|
||||||
|
|
||||||
UpdateManager getUpdateManager();
|
UpdateManager getUpdateManager();
|
||||||
|
|
||||||
Component<?> getPluginInstance(Class<? extends Component<?>> pluginClass);
|
Component<?> getComponentInstance(Class<? extends Component<?>> pluginClass);
|
||||||
|
|
||||||
void triggerRestart();
|
void triggerRestart();
|
||||||
|
|
||||||
|
|
@ -33,5 +34,9 @@ public interface Context {
|
||||||
|
|
||||||
void initComponents();
|
void initComponents();
|
||||||
|
|
||||||
|
void reloadComponents(Class<? extends Component<?>>... classes);
|
||||||
|
|
||||||
|
JSONRPCServlet getRPC();
|
||||||
|
|
||||||
Collection<Class<? extends Component<?>>> getComponentClasses();
|
Collection<Class<? extends Component<?>>> getComponentClasses();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
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,167 +0,0 @@
|
||||||
export const JSONRPC = {
|
|
||||||
url: "/jsonrpc/rpc",
|
|
||||||
__id: 1,
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param method
|
|
||||||
* @param params
|
|
||||||
* @returns Object
|
|
||||||
* @private
|
|
||||||
*/
|
|
||||||
__invoke: async function (method, params) {
|
|
||||||
const request = await JSONRPC.__performRequest(method, params);
|
|
||||||
|
|
||||||
if (!request.success) {
|
|
||||||
console.error(request.result);
|
|
||||||
throw new Error("Failed to invoke method " + method + " with params " + JSON.stringify(params));
|
|
||||||
}
|
|
||||||
|
|
||||||
return request.result;
|
|
||||||
},
|
|
||||||
__performRequest: async function (method, params) {
|
|
||||||
const __this = this;
|
|
||||||
const resp = await fetch(
|
|
||||||
__this.url,
|
|
||||||
{
|
|
||||||
method: "POST",
|
|
||||||
mode: "cors",
|
|
||||||
cache: "no-cache",
|
|
||||||
credentials: "include",
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json"
|
|
||||||
},
|
|
||||||
redirect: "follow",
|
|
||||||
referrerPolicy: "no-referrer",
|
|
||||||
body: JSON.stringify({
|
|
||||||
jsonrpc: '2.0',
|
|
||||||
method: method,
|
|
||||||
params: params,
|
|
||||||
id: __this.__id++
|
|
||||||
})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
const success = resp.status === 200;
|
|
||||||
const result = (success ? (await resp.json()).result : {
|
|
||||||
"error": true,
|
|
||||||
"code": resp.status,
|
|
||||||
"status": resp.statusText,
|
|
||||||
"body": await resp.text()
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
"result": result,
|
|
||||||
"success": success
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const TYPES = {};
|
|
||||||
JSONRPC.SubscriptionManager = {};
|
|
||||||
|
|
||||||
JSONRPC.SubscriptionManager.getSubscribedResources=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.SubscriptionManager::getSubscribedResources", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.SubscriptionManager.getAvailableResources=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.SubscriptionManager::getAvailableResources", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.SubscriptionManager.setSubscribedResources=async function(resources){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.SubscriptionManager::setSubscribedResources", {"resources":resources});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.SubscriptionManager.isUpdating=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.SubscriptionManager::isUpdating", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.SubscriptionManager.triggerUpdate=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.SubscriptionManager::triggerUpdate", {});
|
|
||||||
};
|
|
||||||
JSONRPC.Auth = {};
|
|
||||||
|
|
||||||
JSONRPC.Auth.createToken=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::createToken", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.listTokens=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::listTokens", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.removeToken=async function(token){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::removeToken", {"token":token});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.createAPIToken=async function(description){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::createAPIToken", {"description":description});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.startSessionByPassword=async function(password){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::startSessionByPassword", {"password":password});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.startSessionByToken=async function(token){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::startSessionByToken", {"token":token});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.isAuthenticated=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::isAuthenticated", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.Auth.logout=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.Auth::logout", {});
|
|
||||||
};
|
|
||||||
JSONRPC.System = {};
|
|
||||||
|
|
||||||
JSONRPC.System.shutdown=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::shutdown", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.getEnabledComponents=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::getEnabledComponents", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.setEnabledComponents=async function(components){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::setEnabledComponents", {"components":components});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.isConfigChanged=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::isConfigChanged", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.hasUpdates=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::hasUpdates", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.getAvailableComponents=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::getAvailableComponents", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.getConfig=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::getConfig", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.System.restart=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.System::restart", {});
|
|
||||||
};
|
|
||||||
JSONRPC.NetworkManager = {};
|
|
||||||
|
|
||||||
JSONRPC.NetworkManager.getOutputResources=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.NetworkManager::getOutputResources", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.NetworkManager.isUpdating=async function(){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.NetworkManager::isUpdating", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.NetworkManager.triggerUpdate=async function(ignoreCache){
|
|
||||||
return await JSONRPC.__invoke("web.RPC.NetworkManager::triggerUpdate", {"ignoreCache":ignoreCache});
|
|
||||||
};
|
|
||||||
JSONRPC.OVPN = {};
|
|
||||||
|
|
||||||
JSONRPC.OVPN.restartSystemService=async function(){
|
|
||||||
return await JSONRPC.__invoke("ru.kirillius.pf.sdn.External.API.Components.OVPN::restartSystemService", {});
|
|
||||||
};
|
|
||||||
|
|
||||||
JSONRPC.OVPN.getManagedRoutes=async function(){
|
|
||||||
return await JSONRPC.__invoke("ru.kirillius.pf.sdn.External.API.Components.OVPN::getManagedRoutes", {});
|
|
||||||
};
|
|
||||||
|
|
@ -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() {
|
||||||
|
|
@ -9,8 +10,10 @@ export function renderLoginForm() {
|
||||||
<div id="login-container" class="full-screen flex-center">
|
<div id="login-container" class="full-screen flex-center">
|
||||||
<div class="card" style="width: 380px;">
|
<div class="card" style="width: 380px;">
|
||||||
<h2 style="margin-top: 0; color: var(--color-text);">Авторизация</h2>
|
<h2 style="margin-top: 0; color: var(--color-text);">Авторизация</h2>
|
||||||
<form id="login-form">
|
<nav id="main-nav">
|
||||||
<div class="form-group">
|
${sidebarHtml}
|
||||||
|
<a href="#" id="logout-btn" class="menu-item logout-btn">Выход</a>
|
||||||
|
</nav>
|
||||||
<label for="password-input">Пароль</label>
|
<label for="password-input">Пароль</label>
|
||||||
<input type="password" id="password-input" class="form-control" placeholder="Введите пароль">
|
<input type="password" id="password-input" class="form-control" placeholder="Введите пароль">
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -36,12 +39,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 +64,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('');
|
||||||
|
|
@ -68,8 +75,10 @@ export function renderDashboard() {
|
||||||
<div id="dashboard-container" style="display: flex; min-height: 100vh;">
|
<div id="dashboard-container" style="display: flex; min-height: 100vh;">
|
||||||
<div id="sidebar" class="sidebar">
|
<div id="sidebar" class="sidebar">
|
||||||
<h2 class="sidebar-title">SDN Control</h2>
|
<h2 class="sidebar-title">SDN Control</h2>
|
||||||
<nav id="main-nav">${sidebarHtml}</nav>
|
<nav id="main-nav">
|
||||||
|
${sidebarHtml}
|
||||||
<a href="#" id="logout-btn" class="menu-item logout-btn">Выход</a>
|
<a href="#" id="logout-btn" class="menu-item logout-btn">Выход</a>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<main id="content-area" class="content-area">
|
<main id="content-area" class="content-area">
|
||||||
|
|
@ -91,7 +100,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');
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,158 @@
|
||||||
// src/pages/APITokens.js
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
import { APITokensPage } from '../ui/api_tokens.js';
|
let tokens = [];
|
||||||
|
|
||||||
export const APITokens = APITokensPage;
|
async function loadTokens() {
|
||||||
|
try {
|
||||||
|
tokens = await JSONRPC.Auth.listTokens();
|
||||||
|
tokens = tokens || [];
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при загрузке токенов:', e);
|
||||||
|
tokens = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTokenList() {
|
||||||
|
const $listContainer = $('#api-token-container');
|
||||||
|
|
||||||
|
const createButtonHtml = `
|
||||||
|
<button id="create-token-btn" class="btn-primary" style="width: 200px; margin-bottom: 30px;">
|
||||||
|
Создать токен API
|
||||||
|
</button>
|
||||||
|
<div id="token-status-message" class="success-message" style="display: none;"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (tokens.length === 0) {
|
||||||
|
$listContainer.html(createButtonHtml + '<p>Активных API токенов не найдено.</p>');
|
||||||
|
attachEventHandlers();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHtml = tokens.map(item => {
|
||||||
|
const displayToken = item.token ?
|
||||||
|
`${item.token.substring(0, 8)}...${item.token.substring(item.token.length - 4)}` :
|
||||||
|
'Неизвестный токен';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="token-item" data-full-token="${item.token}">
|
||||||
|
<div class="token-description">${item.description}</div>
|
||||||
|
<div class="token-details">
|
||||||
|
<span class="token-value">Ключ: ${displayToken}</span>
|
||||||
|
<button class="delete-token-btn btn-icon" data-token="${item.token}" title="Удалить токен">
|
||||||
|
<span class="delete-icon">✖</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
${createButtonHtml}
|
||||||
|
<ul class="token-list">${listHtml}</ul>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$listContainer.html(html);
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showCreateTokenModal() {
|
||||||
|
const description = prompt('Введите описание для нового API токена (например, "Токен для Telegram бота"):');
|
||||||
|
|
||||||
|
if (description === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description.trim() === '') {
|
||||||
|
alert('Описание токена не может быть пустым.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
createToken(description.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createToken(description) {
|
||||||
|
const $btn = $('#create-token-btn');
|
||||||
|
const $message = $('#token-status-message');
|
||||||
|
|
||||||
|
$message.hide().removeClass('error-message').addClass('success-message');
|
||||||
|
$btn.prop('disabled', true).text('Создание...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newTokenValue = await JSONRPC.Auth.createAPIToken(description);
|
||||||
|
|
||||||
|
if (!newTokenValue) {
|
||||||
|
throw new Error('Сервер не вернул токен.');
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt('ВАЖНО: Ваш новый токен API. Сохраните его, он больше не будет показан!', newTokenValue);
|
||||||
|
|
||||||
|
await loadTokens();
|
||||||
|
renderTokenList();
|
||||||
|
|
||||||
|
$message.text(`Токен "${description}" успешно создан.`).show();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка создания токена:', e);
|
||||||
|
$message.removeClass('success-message').addClass('error-message').text('Ошибка при создании токена.').show();
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false).text('Создать токен API');
|
||||||
|
setTimeout(() => $message.fadeOut(), 8000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteToken(token) {
|
||||||
|
if (!confirm('Вы уверены, что хотите удалить этот API токен?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $item = $(`li.token-item[data-full-token="${token}"]`);
|
||||||
|
$item.addClass('deleting').css('opacity', 0.5);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.Auth.removeToken(token);
|
||||||
|
tokens = tokens.filter(item => item.token !== token);
|
||||||
|
renderTokenList();
|
||||||
|
|
||||||
|
const $message = $('#token-status-message');
|
||||||
|
$message.text('Токен успешно удален.').addClass('success-message').show();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка удаления токена:', e);
|
||||||
|
$item.removeClass('deleting').css('opacity', 1);
|
||||||
|
const $message = $('#token-status-message');
|
||||||
|
$message.removeClass('success-message').addClass('error-message').text('Ошибка при удалении токена.').show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$('#create-token-btn').off('click').on('click', showCreateTokenModal);
|
||||||
|
|
||||||
|
$('#api-token-container').off('click', '.delete-token-btn').on('click', '.delete-token-btn', function () {
|
||||||
|
const tokenToDelete = $(this).data('token');
|
||||||
|
if (tokenToDelete) {
|
||||||
|
deleteToken(tokenToDelete);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const APITokens = {
|
||||||
|
render: () => `
|
||||||
|
<h1 class="page-title">API Токены</h1>
|
||||||
|
<div id="api-token-container">
|
||||||
|
<p>Загрузка токенов...</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadTokens();
|
||||||
|
if (success) {
|
||||||
|
renderTokenList();
|
||||||
|
} else {
|
||||||
|
$('#api-token-container').html('<p class="error-message">Не удалось загрузить токены. Проверьте соединение с сервером.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
tokens = [];
|
||||||
|
$('#api-token-container').off();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,5 +1,107 @@
|
||||||
// src/pages/Components.js
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
import { ComponentsPage } from '../ui/components.js';
|
let availableComponents = [];
|
||||||
|
let enabledComponents = [];
|
||||||
|
|
||||||
export const Components = ComponentsPage;
|
function getShortName(fullName) {
|
||||||
|
const lastDotIndex = fullName.lastIndexOf('.');
|
||||||
|
if (lastDotIndex > -1 && lastDotIndex < fullName.length - 1) {
|
||||||
|
return fullName.substring(lastDotIndex + 1);
|
||||||
|
}
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadComponentData() {
|
||||||
|
try {
|
||||||
|
enabledComponents = await JSONRPC.System.getEnabledComponents();
|
||||||
|
availableComponents = await JSONRPC.System.getAvailableComponents();
|
||||||
|
availableComponents.sort();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при загрузке данных компонентов:', e);
|
||||||
|
availableComponents = [];
|
||||||
|
enabledComponents = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderComponentList() {
|
||||||
|
const $listContainer = $('#component-list-container');
|
||||||
|
|
||||||
|
if (availableComponents.length === 0) {
|
||||||
|
$listContainer.html('<p>Нет доступных системных компонентов.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHtml = availableComponents.map(fullName => {
|
||||||
|
const shortName = getShortName(fullName);
|
||||||
|
const isChecked = enabledComponents.includes(fullName);
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="subscription-item">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="component-checkbox" value="${fullName}" ${isChecked ? 'checked' : ''}>
|
||||||
|
<span class="resource-key">${shortName}</span>
|
||||||
|
<span class="resource-details" style="margin-left: 10px;">(${fullName})</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<ul class="subscription-list">${listHtml}</ul>
|
||||||
|
<button id="save-components-btn" class="btn-primary" style="width: 250px; margin-top: 30px;">Сохранить</button>
|
||||||
|
<div id="component-status-message" class="error-message" style="display: none;"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$listContainer.html(html);
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$('#save-components-btn').on('click', async function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
const $message = $('#component-status-message');
|
||||||
|
$message.hide().removeClass('success-message').addClass('error-message');
|
||||||
|
|
||||||
|
const newEnabledComponents = $('.component-checkbox:checked').map(function () {
|
||||||
|
return $(this).val();
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Сохранение...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.System.setEnabledComponents(newEnabledComponents);
|
||||||
|
enabledComponents = newEnabledComponents;
|
||||||
|
$message.text('Настройки компонентов успешно сохранены!').addClass('success-message').show();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка сохранения компонентов:', e);
|
||||||
|
$message.text('Ошибка при сохранении компонентов.').show();
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false).text('Сохранить');
|
||||||
|
setTimeout(() => $message.fadeOut(), 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Components = {
|
||||||
|
render: () => `
|
||||||
|
<h1 class="page-title">Управление Компонентами</h1>
|
||||||
|
<div id="component-list-container">
|
||||||
|
<p>Загрузка доступных компонентов...</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadComponentData();
|
||||||
|
if (success) {
|
||||||
|
renderComponentList();
|
||||||
|
} else {
|
||||||
|
$('#component-list-container').html('<p class="error-message">Не удалось загрузить данные. Проверьте соединение с сервером.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
availableComponents = [];
|
||||||
|
enabledComponents = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,237 @@
|
||||||
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
|
const OVPN_COMPONENT_NAME = 'ru.kirillius.pf.sdn.External.API.Components.OVPN';
|
||||||
|
const DEFAULT_PORT = 22;
|
||||||
|
|
||||||
|
const FIELD_IDS = {
|
||||||
|
host: 'ovpn-host',
|
||||||
|
port: 'ovpn-port',
|
||||||
|
username: 'ovpn-username',
|
||||||
|
password: 'ovpn-password',
|
||||||
|
useSSH: 'ovpn-use-ssh',
|
||||||
|
restartCommand: 'ovpn-restart-command',
|
||||||
|
saveButton: 'save-ovpn-btn',
|
||||||
|
restartButton: 'restart-ovpn-btn',
|
||||||
|
status: 'ovpn-status-message',
|
||||||
|
container: 'ovpn-config-container'
|
||||||
|
};
|
||||||
|
|
||||||
|
const SELECTORS = {
|
||||||
|
container: `#${FIELD_IDS.container}`,
|
||||||
|
host: `#${FIELD_IDS.host}`,
|
||||||
|
port: `#${FIELD_IDS.port}`,
|
||||||
|
username: `#${FIELD_IDS.username}`,
|
||||||
|
password: `#${FIELD_IDS.password}`,
|
||||||
|
useSSH: `#${FIELD_IDS.useSSH}`,
|
||||||
|
restartCommand: `#${FIELD_IDS.restartCommand}`,
|
||||||
|
saveButton: `#${FIELD_IDS.saveButton}`,
|
||||||
|
restartButton: `#${FIELD_IDS.restartButton}`,
|
||||||
|
status: `#${FIELD_IDS.status}`
|
||||||
|
};
|
||||||
|
|
||||||
|
let currentConfig = {};
|
||||||
|
let statusTimeoutId = null;
|
||||||
|
|
||||||
|
const getStatusElement = () => $(SELECTORS.status);
|
||||||
|
|
||||||
|
function clearStatus() {
|
||||||
|
const $status = getStatusElement();
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusTimeoutId) {
|
||||||
|
clearTimeout(statusTimeoutId);
|
||||||
|
statusTimeoutId = null;
|
||||||
|
}
|
||||||
|
$status.stop(true, true).hide().text('').removeClass('success-message error-message');
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStatus(message, type) {
|
||||||
|
const $status = getStatusElement();
|
||||||
|
if (!$status.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (statusTimeoutId) {
|
||||||
|
clearTimeout(statusTimeoutId);
|
||||||
|
}
|
||||||
|
$status
|
||||||
|
.removeClass('success-message error-message')
|
||||||
|
.addClass(type === 'success' ? 'success-message' : 'error-message')
|
||||||
|
.text(message)
|
||||||
|
.show();
|
||||||
|
|
||||||
|
statusTimeoutId = window.setTimeout(() => {
|
||||||
|
$status.fadeOut();
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runAction($button, pendingText, action, messages) {
|
||||||
|
if (!$button.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalText = $button.text();
|
||||||
|
$button.prop('disabled', true).text(pendingText);
|
||||||
|
clearStatus();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await action();
|
||||||
|
if (messages?.success) {
|
||||||
|
const successMessage = typeof messages.success === 'function'
|
||||||
|
? messages.success(result)
|
||||||
|
: messages.success;
|
||||||
|
if (successMessage) {
|
||||||
|
updateStatus(successMessage, 'success');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(messages?.log || 'Ошибка выполнения действия OVPN:', error);
|
||||||
|
if (messages?.error) {
|
||||||
|
updateStatus(messages.error, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$button.prop('disabled', false).text(originalText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadConfig() {
|
||||||
|
try {
|
||||||
|
const fullConfig = await JSONRPC.System.getComponentConfig(OVPN_COMPONENT_NAME);
|
||||||
|
currentConfig = fullConfig || {};
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при загрузке конфига OVPN:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOVPNForm() {
|
||||||
|
const $container = $(SELECTORS.container);
|
||||||
|
const shellConfig = currentConfig.shellConfig || {};
|
||||||
|
|
||||||
|
$container.html(`
|
||||||
|
<div class="component-config-form">
|
||||||
|
<h3 class="config-section-title">Конфигурация Shell (ShellConfig)</h3>
|
||||||
|
<div class="form-group checkbox-group">
|
||||||
|
<input type="checkbox" id="${FIELD_IDS.useSSH}" ${shellConfig.useSSH ? 'checked' : ''}>
|
||||||
|
<label for="${FIELD_IDS.useSSH}" style="margin-bottom: 0;">Использовать SSH</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.host}">Хост</label>
|
||||||
|
<input type="text" id="${FIELD_IDS.host}" class="form-control" value="${shellConfig.host || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.port}">Порт</label>
|
||||||
|
<input type="number" id="${FIELD_IDS.port}" class="form-control" value="${shellConfig.port || DEFAULT_PORT}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.username}">Имя пользователя</label>
|
||||||
|
<input type="text" id="${FIELD_IDS.username}" class="form-control" value="${shellConfig.username || ''}">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.password}">Пароль</label>
|
||||||
|
<input type="password" id="${FIELD_IDS.password}" class="form-control" placeholder="Оставьте пустым для сохранения текущего пароля">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="config-section-title" style="margin-top: 40px;">Перезапуск сервиса</h3>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="${FIELD_IDS.restartCommand}">Команда для перезапуска</label>
|
||||||
|
<input type="text" id="${FIELD_IDS.restartCommand}" class="form-control" value="${currentConfig.restartCommand || 'systemctl restart openvpn@server'}">
|
||||||
|
<small class="hint-text">Команда, которая будет выполнена через Shell для перезапуска сервиса OVPN.</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="action-buttons">
|
||||||
|
<button id="${FIELD_IDS.saveButton}" class="btn-primary" style="width: 200px;">Сохранить Конфигурацию</button>
|
||||||
|
<button id="${FIELD_IDS.restartButton}" class="btn-secondary" style="width: 200px; margin-left: 20px;">Перезапустить Сервис</button>
|
||||||
|
</div>
|
||||||
|
<div id="${FIELD_IDS.status}" class="error-message" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectConfigFromForm() {
|
||||||
|
const shellConfig = currentConfig.shellConfig || {};
|
||||||
|
const newPassword = $(SELECTORS.password).val();
|
||||||
|
const useSSH = $(SELECTORS.useSSH).prop('checked');
|
||||||
|
|
||||||
|
return {
|
||||||
|
shellConfig: {
|
||||||
|
useSSH,
|
||||||
|
host: $(SELECTORS.host).val(),
|
||||||
|
port: parseInt($(SELECTORS.port).val(), 10) || DEFAULT_PORT,
|
||||||
|
username: $(SELECTORS.username).val(),
|
||||||
|
password: newPassword ? newPassword : (shellConfig.password || ''),
|
||||||
|
},
|
||||||
|
restartCommand: $(SELECTORS.restartCommand).val()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleSave() {
|
||||||
|
const $button = $(SELECTORS.saveButton);
|
||||||
|
await runAction($button, 'Сохранение...', async () => {
|
||||||
|
const newConfig = collectConfigFromForm();
|
||||||
|
await JSONRPC.System.setComponentConfig(OVPN_COMPONENT_NAME, newConfig);
|
||||||
|
currentConfig = newConfig;
|
||||||
|
}, {
|
||||||
|
success: 'Конфигурация OVPN успешно сохранена.',
|
||||||
|
error: 'Ошибка при сохранении конфигурации.',
|
||||||
|
log: 'Ошибка сохранения конфига OVPN'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRestart() {
|
||||||
|
const $button = $(SELECTORS.restartButton);
|
||||||
|
await runAction($button, 'Перезапуск...', async () => {
|
||||||
|
return JSONRPC.OVPN.restartSystemService();
|
||||||
|
}, {
|
||||||
|
success: (result) => result || 'Сервис OVPN успешно перезапущен.',
|
||||||
|
error: 'Ошибка при перезапуске сервиса.',
|
||||||
|
log: 'Ошибка перезапуска OVPN'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$(SELECTORS.saveButton).off('click').on('click', handleSave);
|
||||||
|
$(SELECTORS.restartButton).off('click').on('click', handleRestart);
|
||||||
|
$(SELECTORS.useSSH).off('change').on('change', toggleSSHFields);
|
||||||
|
toggleSSHFields();
|
||||||
|
}
|
||||||
|
|
||||||
|
function detachEventHandlers() {
|
||||||
|
$(SELECTORS.saveButton).off('click');
|
||||||
|
$(SELECTORS.restartButton).off('click');
|
||||||
|
$(SELECTORS.useSSH).off('change');
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleSSHFields() {
|
||||||
|
const enabled = $(SELECTORS.useSSH).prop('checked');
|
||||||
|
$(SELECTORS.host).prop('disabled', !enabled);
|
||||||
|
$(SELECTORS.port).prop('disabled', !enabled);
|
||||||
|
$(SELECTORS.username).prop('disabled', !enabled);
|
||||||
|
$(SELECTORS.password).prop('disabled', !enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const OVPNConfig = {
|
||||||
|
render: () => `
|
||||||
|
<h1 class="page-title">Настройка OVPN</h1>
|
||||||
|
<div id="${FIELD_IDS.container}">
|
||||||
|
<p>Загрузка конфигурации...</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadConfig();
|
||||||
|
if (success) {
|
||||||
|
renderOVPNForm();
|
||||||
|
} else {
|
||||||
|
$(SELECTORS.container).html('<p class="error-message">Не удалось загрузить конфигурацию OVPN.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
detachEventHandlers();
|
||||||
|
clearStatus();
|
||||||
|
currentConfig = {};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -1,22 +1,179 @@
|
||||||
// src/pages/Statistics.js
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
import { renderStatisticsPage, stopPolling } from '../ui/statistics.js';
|
const POLLING_INTERVAL = 5000;
|
||||||
|
let updateInterval = null;
|
||||||
|
|
||||||
|
const $content = () => $('#statistics-content');
|
||||||
|
|
||||||
|
const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => {
|
||||||
|
const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500';
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="stat-card">
|
||||||
|
<h3>${title}</h3>
|
||||||
|
<p class="status-line">
|
||||||
|
Статус: <span class="${statusClass}">${status}</span>
|
||||||
|
</p>
|
||||||
|
<button id="${buttonId}" class="btn-secondary" ${isUpdating ? 'disabled' : ''}>
|
||||||
|
${isUpdating ? 'Обновление...' : buttonLabel}
|
||||||
|
</button>
|
||||||
|
${resourceStatsHtml}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResourceStatsHtml = (resources) => {
|
||||||
|
return `
|
||||||
|
<div class="resource-group">
|
||||||
|
<h4>Ресурсы:</h4>
|
||||||
|
<div class="resource-row">
|
||||||
|
${createResourceBadge("Домены", resources.domains.length)}
|
||||||
|
${createResourceBadge("ASN", resources.ASN.length)}
|
||||||
|
${createResourceBadge("Подсети", resources.subnets.length)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const createResourceBadge = (title, count) => {
|
||||||
|
return `
|
||||||
|
<div class="resource-badge">
|
||||||
|
<span class="resource-count small-count">${count}</span>
|
||||||
|
<span class="resource-title">${title}</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getSubscriptionCount() {
|
||||||
|
try {
|
||||||
|
const subscribedResources = await JSONRPC.SubscriptionManager.getSubscribedResources();
|
||||||
|
return (subscribedResources && subscribedResources.length) || 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при получении subscribedResources:', e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getNetworkResources() {
|
||||||
|
try {
|
||||||
|
const resources = await JSONRPC.NetworkManager.getOutputResources();
|
||||||
|
return {
|
||||||
|
domains: resources.domains || [],
|
||||||
|
ASN: resources.ASN || [],
|
||||||
|
subnets: resources.subnets || []
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при получении ресурсов:', e);
|
||||||
|
return { domains: [], ASN: [], subnets: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderSystemAndManagerStatus() {
|
||||||
|
const dataHtml = [];
|
||||||
|
|
||||||
|
const hasUpdates = await JSONRPC.System.hasUpdates();
|
||||||
|
const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating();
|
||||||
|
const isNetUpdating = await JSONRPC.NetworkManager.isUpdating();
|
||||||
|
const subCount = await getSubscriptionCount();
|
||||||
|
const networkResources = await getNetworkResources();
|
||||||
|
|
||||||
|
dataHtml.push(createStatusCard(
|
||||||
|
'Обновление ПО',
|
||||||
|
hasUpdates ? 'Обнаружено' : 'Нет обновлений',
|
||||||
|
'update-software-btn',
|
||||||
|
'Проверить',
|
||||||
|
false
|
||||||
|
));
|
||||||
|
|
||||||
|
const subStatsHtml = `<div class="resource-group" style="margin-top: 20px;">
|
||||||
|
<h4>Подписки:</h4>
|
||||||
|
<div class="resource-row">
|
||||||
|
${createResourceBadge("Активно", subCount)}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
dataHtml.push(createStatusCard(
|
||||||
|
'Менеджер Подписок',
|
||||||
|
isSubUpdating ? 'Обновляется' : 'Активен',
|
||||||
|
'update-sub-btn',
|
||||||
|
'Обновить',
|
||||||
|
isSubUpdating,
|
||||||
|
subStatsHtml
|
||||||
|
));
|
||||||
|
|
||||||
|
const netResourcesHtml = createResourceStatsHtml(networkResources);
|
||||||
|
dataHtml.push(createStatusCard(
|
||||||
|
'Менеджер Сетей',
|
||||||
|
isNetUpdating ? 'Обновляется' : 'Активен',
|
||||||
|
'update-net-btn',
|
||||||
|
'Обновить',
|
||||||
|
isNetUpdating,
|
||||||
|
netResourcesHtml
|
||||||
|
));
|
||||||
|
|
||||||
|
$content().html(`<div class="stats-grid status-grid">${dataHtml.join('')}</div>`);
|
||||||
|
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function stopPolling() {
|
||||||
|
if (updateInterval) {
|
||||||
|
console.log('clear interval');
|
||||||
|
clearInterval(updateInterval);
|
||||||
|
updateInterval = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$('#update-sub-btn').off('click').on('click', async function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
$btn.prop('disabled', true).text('Обновление...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.SubscriptionManager.triggerUpdate();
|
||||||
|
alert('Обновление подписок запущено!');
|
||||||
|
renderSystemAndManagerStatus();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка при запуске обновления подписок!');
|
||||||
|
$btn.prop('disabled', false).text('Обновить');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#update-net-btn').off('click').on('click', async function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
$btn.prop('disabled', true).text('Обновление...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.NetworkManager.triggerUpdate(true);
|
||||||
|
alert('Обновление сетей запущено!');
|
||||||
|
renderSystemAndManagerStatus();
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка при запуске обновления сетей!');
|
||||||
|
$btn.prop('disabled', false).text('Обновить');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#update-software-btn').off('click').on('click', function () {
|
||||||
|
alert('Проверка обновлений ПО запущена...');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function mountStatisticsPage() {
|
||||||
|
stopPolling();
|
||||||
|
await renderSystemAndManagerStatus();
|
||||||
|
console.log('start interval');
|
||||||
|
updateInterval = setInterval(renderSystemAndManagerStatus, POLLING_INTERVAL);
|
||||||
|
}
|
||||||
|
|
||||||
export const StatisticsPage = {
|
export const StatisticsPage = {
|
||||||
render: () => {
|
render: () => `
|
||||||
return `
|
|
||||||
<h1 class="page-title">Статистика</h1>
|
<h1 class="page-title">Статистика</h1>
|
||||||
<div id="statistics-content">
|
<div id="statistics-content">
|
||||||
Загрузка данных...
|
Загрузка данных...
|
||||||
</div>
|
</div>
|
||||||
`;
|
`,
|
||||||
},
|
|
||||||
mount: () => {
|
mount: () => {
|
||||||
// Вызывает renderStatisticsPage, который запускает опрос
|
mountStatisticsPage();
|
||||||
renderStatisticsPage();
|
|
||||||
},
|
},
|
||||||
// 🔥 ВСТРОЕННАЯ ФУНКЦИЯ UNMOUNT: Роутер использует StatisticsPage.unmount
|
|
||||||
unmount: stopPolling
|
unmount: stopPolling
|
||||||
};
|
};
|
||||||
|
|
||||||
// 🔥 УДАЛЕН ЭКСПОРТ { stopPolling }; из этого файла.
|
|
||||||
|
|
@ -1,6 +1,110 @@
|
||||||
// src/pages/Subscriptions.js
|
import $ from 'jquery';
|
||||||
|
import { JSONRPC } from '@/json-rpc.js';
|
||||||
|
|
||||||
import { SubscriptionsPage } from '../ui/subscriptions.js';
|
let availableResources = {};
|
||||||
|
let subscribedKeys = [];
|
||||||
|
|
||||||
// Просто реэкспортируем объект для роутера
|
async function loadSubscriptionData() {
|
||||||
export const Subscriptions = SubscriptionsPage;
|
try {
|
||||||
|
subscribedKeys = await JSONRPC.SubscriptionManager.getSubscribedResources();
|
||||||
|
availableResources = await JSONRPC.SubscriptionManager.getAvailableResources();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка при загрузке данных подписок:', e);
|
||||||
|
availableResources = {};
|
||||||
|
subscribedKeys = [];
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubscriptionList() {
|
||||||
|
const $listContainer = $('#subscription-list-container');
|
||||||
|
const availableKeys = Object.keys(availableResources).sort();
|
||||||
|
|
||||||
|
if (availableKeys.length === 0) {
|
||||||
|
$listContainer.html('<p>Нет доступных ресурсов для подписки.</p>');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const listHtml = availableKeys.map(key => {
|
||||||
|
const details = availableResources[key];
|
||||||
|
const isChecked = subscribedKeys.includes(key);
|
||||||
|
const description = details.description || 'Нет описания';
|
||||||
|
const detailText = `
|
||||||
|
Доменов: ${details.domains ? details.domains.length : 0},
|
||||||
|
ASN: ${details.ASN ? details.ASN.length : 0},
|
||||||
|
Подсетей: ${details.subnets ? details.subnets.length : 0}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<li class="subscription-item">
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" class="subscription-checkbox" value="${key}" ${isChecked ? 'checked' : ''}>
|
||||||
|
<div class="resource-info">
|
||||||
|
<div class="resource-main-line">
|
||||||
|
<span class="resource-description">${description}</span>
|
||||||
|
<span class="resource-key">(${key})</span>
|
||||||
|
</div>
|
||||||
|
<div class="resource-details">${detailText}</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
const html = `
|
||||||
|
<ul class="subscription-list">${listHtml}</ul>
|
||||||
|
<button id="save-subscriptions-btn" class="btn-primary" style="width: 250px; margin-top: 30px;">Сохранить</button>
|
||||||
|
<div id="subscription-status-message" class="error-message" style="display: none;"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
$listContainer.html(html);
|
||||||
|
attachEventHandlers();
|
||||||
|
}
|
||||||
|
|
||||||
|
function attachEventHandlers() {
|
||||||
|
$('#save-subscriptions-btn').on('click', async function () {
|
||||||
|
const $btn = $(this);
|
||||||
|
const $message = $('#subscription-status-message');
|
||||||
|
$message.hide().removeClass('success-message').addClass('error-message');
|
||||||
|
|
||||||
|
const newSubscribedKeys = $('.subscription-checkbox:checked').map(function () {
|
||||||
|
return $(this).val();
|
||||||
|
}).get();
|
||||||
|
|
||||||
|
$btn.prop('disabled', true).text('Сохранение...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
await JSONRPC.SubscriptionManager.setSubscribedResources(newSubscribedKeys);
|
||||||
|
subscribedKeys = newSubscribedKeys;
|
||||||
|
$message.text('Настройки подписок успешно сохранены!').addClass('success-message').show();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Ошибка сохранения подписок:', e);
|
||||||
|
$message.text('Ошибка при сохранении подписок.').show();
|
||||||
|
} finally {
|
||||||
|
$btn.prop('disabled', false).text('Сохранить');
|
||||||
|
setTimeout(() => $message.fadeOut(), 5000);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Subscriptions = {
|
||||||
|
render: () => `
|
||||||
|
<h1 class="page-title">Управление Подписками</h1>
|
||||||
|
<div id="subscription-list-container">
|
||||||
|
<p>Загрузка доступных ресурсов...</p>
|
||||||
|
</div>
|
||||||
|
`,
|
||||||
|
mount: async () => {
|
||||||
|
const success = await loadSubscriptionData();
|
||||||
|
if (success) {
|
||||||
|
renderSubscriptionList();
|
||||||
|
} else {
|
||||||
|
$('#subscription-list-container').html('<p class="error-message">Не удалось загрузить данные. Проверьте соединение с сервером.</p>');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unmount: () => {
|
||||||
|
availableResources = {};
|
||||||
|
subscribedKeys = [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -138,8 +138,9 @@ html.dark-theme, body {
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
.logout-btn {
|
.logout-btn {
|
||||||
margin-top: auto; /* Прижимает к низу */
|
margin-top: 15px;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
|
padding-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-area {
|
.content-area {
|
||||||
|
|
@ -409,3 +410,57 @@ 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);
|
||||||
|
max-width: 520px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
@ -1,205 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import { JSONRPC } from '@/json-rpc.js';
|
|
||||||
|
|
||||||
// --- Глобальное состояние ---
|
|
||||||
let tokens = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Загружает текущий список токенов.
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
async function loadTokens() {
|
|
||||||
try {
|
|
||||||
// Запрос списка токенов
|
|
||||||
tokens = await JSONRPC.Auth.listTokens();
|
|
||||||
tokens = tokens || [];
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Ошибка при загрузке токенов:", e);
|
|
||||||
tokens = [];
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Рендерит список токенов и кнопки управления.
|
|
||||||
*/
|
|
||||||
function renderTokenList() {
|
|
||||||
const $listContainer = $('#api-token-container');
|
|
||||||
|
|
||||||
// Кнопка "Создать токен"
|
|
||||||
const createButtonHtml = `
|
|
||||||
<button id="create-token-btn" class="btn-primary" style="width: 200px; margin-bottom: 30px;">
|
|
||||||
Создать токен API
|
|
||||||
</button>
|
|
||||||
<div id="token-status-message" class="success-message" style="display: none;"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
if (tokens.length === 0) {
|
|
||||||
$listContainer.html(createButtonHtml + '<p>Активных API токенов не найдено.</p>');
|
|
||||||
attachEventHandlers();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let listHtml = tokens.map(item => {
|
|
||||||
// Мы НЕ показываем полный токен, показываем только его начало/конец для идентификации
|
|
||||||
const displayToken = item.token ?
|
|
||||||
`${item.token.substring(0, 8)}...${item.token.substring(item.token.length - 4)}` :
|
|
||||||
'Неизвестный токен';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<li class="token-item" data-full-token="${item.token}">
|
|
||||||
<div class="token-description">${item.description}</div>
|
|
||||||
<div class="token-details">
|
|
||||||
<span class="token-value">Ключ: ${displayToken}</span>
|
|
||||||
<button class="delete-token-btn btn-icon" data-token="${item.token}" title="Удалить токен">
|
|
||||||
<span class="delete-icon">✖</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
${createButtonHtml}
|
|
||||||
<ul class="token-list">${listHtml}</ul>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$listContainer.html(html);
|
|
||||||
attachEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Отображает модальное окно для создания токена.
|
|
||||||
*/
|
|
||||||
function showCreateTokenModal() {
|
|
||||||
// Используем простое модальное окно jQuery для минимализма
|
|
||||||
const description = prompt("Введите описание для нового API токена (например, 'Токен для Telegram бота'):");
|
|
||||||
|
|
||||||
if (description === null) {
|
|
||||||
return; // Пользователь нажал Отмена
|
|
||||||
}
|
|
||||||
|
|
||||||
if (description.trim() === "") {
|
|
||||||
alert("Описание токена не может быть пустым.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
createToken(description.trim());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Логика создания токена и обновления UI.
|
|
||||||
* @param {string} description - Описание токена.
|
|
||||||
*/
|
|
||||||
async function createToken(description) {
|
|
||||||
const $btn = $('#create-token-btn');
|
|
||||||
const $message = $('#token-status-message');
|
|
||||||
|
|
||||||
$message.hide().removeClass('error-message').addClass('success-message');
|
|
||||||
$btn.prop('disabled', true).text('Создание...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newTokenValue = await JSONRPC.Auth.createAPIToken(description);
|
|
||||||
|
|
||||||
if (!newTokenValue) {
|
|
||||||
throw new Error("Сервер не вернул токен.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Показываем токен пользователю ОДИН раз
|
|
||||||
prompt("ВАЖНО: Ваш новый токен API. Сохраните его, он больше не будет показан!", newTokenValue);
|
|
||||||
|
|
||||||
// Перезагружаем список
|
|
||||||
await loadTokens();
|
|
||||||
renderTokenList();
|
|
||||||
|
|
||||||
$message.text(`Токен "${description}" успешно создан.`).show();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка создания токена:', e);
|
|
||||||
$message.removeClass('success-message').addClass('error-message').text('Ошибка при создании токена.').show();
|
|
||||||
} finally {
|
|
||||||
$btn.prop('disabled', false).text('Создать токен API');
|
|
||||||
setTimeout(() => $message.fadeOut(), 8000);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Логика удаления токена.
|
|
||||||
* @param {string} token - Токен для удаления.
|
|
||||||
*/
|
|
||||||
async function deleteToken(token) {
|
|
||||||
if (!confirm("Вы уверены, что хотите удалить этот API токен?")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $item = $(`li.token-item[data-full-token="${token}"]`);
|
|
||||||
$item.addClass('deleting').css('opacity', 0.5); // Визуальная обратная связь
|
|
||||||
|
|
||||||
try {
|
|
||||||
await JSONRPC.Auth.removeToken(token);
|
|
||||||
|
|
||||||
// Удаляем из локального состояния
|
|
||||||
tokens = tokens.filter(item => item.token !== token);
|
|
||||||
|
|
||||||
// Перерисовываем список
|
|
||||||
renderTokenList();
|
|
||||||
|
|
||||||
const $message = $('#token-status-message');
|
|
||||||
$message.text('Токен успешно удален.').addClass('success-message').show();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка удаления токена:', e);
|
|
||||||
$item.removeClass('deleting').css('opacity', 1); // Восстанавливаем, если ошибка
|
|
||||||
const $message = $('#token-status-message');
|
|
||||||
$message.removeClass('success-message').addClass('error-message').text('Ошибка при удалении токена.').show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Прикрепление обработчиков событий.
|
|
||||||
*/
|
|
||||||
function attachEventHandlers() {
|
|
||||||
$('#create-token-btn').off('click').on('click', showCreateTokenModal);
|
|
||||||
|
|
||||||
// Делегирование для кнопок удаления (они перерисовываются)
|
|
||||||
$('#api-token-container').off('click', '.delete-token-btn').on('click', '.delete-token-btn', function() {
|
|
||||||
const tokenToDelete = $(this).data('token');
|
|
||||||
if (tokenToDelete) {
|
|
||||||
deleteToken(tokenToDelete);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// --- Функции для роутера ---
|
|
||||||
|
|
||||||
export const APITokensPage = {
|
|
||||||
render: () => {
|
|
||||||
return `
|
|
||||||
<h1 class="page-title">API Токены</h1>
|
|
||||||
<div id="api-token-container">
|
|
||||||
<p>Загрузка токенов...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
mount: async () => {
|
|
||||||
const success = await loadTokens();
|
|
||||||
if (success) {
|
|
||||||
renderTokenList();
|
|
||||||
} else {
|
|
||||||
$('#api-token-container').html('<p class="error-message">Не удалось загрузить токены. Проверьте соединение с сервером.</p>');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmount: () => {
|
|
||||||
tokens = [];
|
|
||||||
$('#api-token-container').off(); // Удаляем все обработчики
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import { JSONRPC } from '@/json-rpc.js';
|
|
||||||
|
|
||||||
// --- Глобальное состояние для страницы ---
|
|
||||||
let availableComponents = [];
|
|
||||||
let enabledComponents = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @private
|
|
||||||
* Извлекает короткое имя компонента, беря часть строки после последней точки.
|
|
||||||
* @param {string} fullName - Полное имя компонента (напр., ru.kirillius.pf.sdn.API).
|
|
||||||
* @returns {string} Короткое имя (напр., API).
|
|
||||||
*/
|
|
||||||
function getShortName(fullName) {
|
|
||||||
// Находим позицию последней точки
|
|
||||||
const lastDotIndex = fullName.lastIndexOf('.');
|
|
||||||
|
|
||||||
// Если точка найдена, возвращаем подстроку после неё.
|
|
||||||
// Если точки нет (или она в конце), возвращаем полное имя.
|
|
||||||
if (lastDotIndex > -1 && lastDotIndex < fullName.length - 1) {
|
|
||||||
return fullName.substring(lastDotIndex + 1);
|
|
||||||
}
|
|
||||||
return fullName;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Загружает все необходимые данные для страницы (без изменений) */
|
|
||||||
async function loadComponentData() {
|
|
||||||
try {
|
|
||||||
enabledComponents = await JSONRPC.System.getEnabledComponents();
|
|
||||||
availableComponents = await JSONRPC.System.getAvailableComponents();
|
|
||||||
availableComponents.sort();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Ошибка при загрузке данных компонентов:", e);
|
|
||||||
availableComponents = [];
|
|
||||||
enabledComponents = [];
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Рендеринг HTML списка компонентов */
|
|
||||||
function renderComponentList() {
|
|
||||||
const $listContainer = $('#component-list-container');
|
|
||||||
|
|
||||||
if (availableComponents.length === 0) {
|
|
||||||
$listContainer.html('<p>Нет доступных системных компонентов.</p>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let listHtml = availableComponents.map(fullName => {
|
|
||||||
// 🔥 Применяем новую функцию для отображения
|
|
||||||
const shortName = getShortName(fullName);
|
|
||||||
|
|
||||||
// Проверяем, включен ли компонент
|
|
||||||
const isChecked = enabledComponents.includes(fullName); // Проверка по полному имени!
|
|
||||||
|
|
||||||
return `
|
|
||||||
<li class="subscription-item">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" class="component-checkbox" value="${fullName}" ${isChecked ? 'checked' : ''}>
|
|
||||||
<span class="resource-key">${shortName}</span>
|
|
||||||
<span class="resource-details" style="margin-left: 10px;">(${fullName})</span>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<ul class="subscription-list">${listHtml}</ul>
|
|
||||||
<button id="save-components-btn" class="btn-primary" style="width: 250px; margin-top: 30px;">Сохранить</button>
|
|
||||||
<div id="component-status-message" class="error-message" style="display: none;"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$listContainer.html(html);
|
|
||||||
attachEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Обработчик кнопки "Сохранить" (без изменений, т.к. работаем с полными именами из value) */
|
|
||||||
function attachEventHandlers() {
|
|
||||||
$('#save-components-btn').on('click', async function() {
|
|
||||||
const $btn = $(this);
|
|
||||||
const $message = $('#component-status-message');
|
|
||||||
$message.hide().removeClass('success-message').addClass('error-message');
|
|
||||||
|
|
||||||
// Собираем все отмеченные ключи компонентов (они содержат полные имена)
|
|
||||||
const newEnabledComponents = $('.component-checkbox:checked').map(function() {
|
|
||||||
return $(this).val();
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
$btn.prop('disabled', true).text('Сохранение...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await JSONRPC.System.setEnabledComponents(newEnabledComponents);
|
|
||||||
|
|
||||||
enabledComponents = newEnabledComponents;
|
|
||||||
|
|
||||||
$message.text('Настройки компонентов успешно сохранены!').addClass('success-message').show();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка сохранения компонентов:', e);
|
|
||||||
$message.text('Ошибка при сохранении компонентов.').show();
|
|
||||||
} finally {
|
|
||||||
$btn.prop('disabled', false).text('Сохранить');
|
|
||||||
setTimeout(() => $message.fadeOut(), 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Функции для роутера (без изменений) ---
|
|
||||||
|
|
||||||
export const ComponentsPage = {
|
|
||||||
render: () => {
|
|
||||||
return `
|
|
||||||
<h1 class="page-title">Управление Компонентами</h1>
|
|
||||||
<div id="component-list-container">
|
|
||||||
<p>Загрузка доступных компонентов...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
mount: async () => {
|
|
||||||
const success = await loadComponentData();
|
|
||||||
if (success) {
|
|
||||||
renderComponentList();
|
|
||||||
} else {
|
|
||||||
$('#component-list-container').html('<p class="error-message">Не удалось загрузить данные. Проверьте соединение с сервером.</p>');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmount: () => {
|
|
||||||
availableComponents = [];
|
|
||||||
enabledComponents = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -1,198 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import { JSONRPC } from '@/json-rpc.js';
|
|
||||||
|
|
||||||
const $content = () => $('#statistics-content');
|
|
||||||
|
|
||||||
// 🔥 Переменная для хранения ID интервала опроса
|
|
||||||
let updateInterval = null;
|
|
||||||
const POLLING_INTERVAL = 5000; // 5 секунд
|
|
||||||
|
|
||||||
// --- Утилитарные функции для рендеринга UI-блоков ---
|
|
||||||
|
|
||||||
/** Создает HTML-карточку для отображения статуса и ресурсов */
|
|
||||||
const createStatusCard = (title, status, buttonId, buttonLabel, isUpdating, resourceStatsHtml = '') => {
|
|
||||||
const statusClass = status === 'Обновляется' || status === 'Обнаружено' ? 'text-yellow-500' : 'text-green-500';
|
|
||||||
|
|
||||||
return `
|
|
||||||
<div class="stat-card">
|
|
||||||
<h3>${title}</h3>
|
|
||||||
<p class="status-line">
|
|
||||||
Статус: <span class="${statusClass}">${status}</span>
|
|
||||||
</p>
|
|
||||||
<button id="${buttonId}" class="btn-secondary" ${isUpdating ? 'disabled' : ''}>
|
|
||||||
${isUpdating ? 'Обновление...' : buttonLabel}
|
|
||||||
</button>
|
|
||||||
${resourceStatsHtml}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Создает HTML-блок для отображения ресурсов сети */
|
|
||||||
const createResourceStatsHtml = (resources) => {
|
|
||||||
return `
|
|
||||||
<div class="resource-group">
|
|
||||||
<h4>Ресурсы:</h4>
|
|
||||||
<div class="resource-row">
|
|
||||||
${createResourceBadge("Домены", resources.domains.length)}
|
|
||||||
${createResourceBadge("ASN", resources.ASN.length)}
|
|
||||||
${createResourceBadge("Подсети", resources.subnets.length)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Создает HTML-бэйдж для отдельного ресурса (с уменьшенным шрифтом) */
|
|
||||||
const createResourceBadge = (title, count) => {
|
|
||||||
return `
|
|
||||||
<div class="resource-badge">
|
|
||||||
<span class="resource-count small-count">${count}</span>
|
|
||||||
<span class="resource-title">${title}</span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Основные функции API и DOM ---
|
|
||||||
|
|
||||||
/** Получение количества активных подписок. */
|
|
||||||
async function getSubscriptionCount() {
|
|
||||||
try {
|
|
||||||
// Используем обновленный метод getSubscribedResources
|
|
||||||
let subscribedResources = await JSONRPC.SubscriptionManager.getSubscribedResources();
|
|
||||||
return (subscribedResources && subscribedResources.length) || 0;
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Ошибка при получении subscribedResources:", e);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Запрос данных о ресурсах сети. */
|
|
||||||
async function getNetworkResources() {
|
|
||||||
try {
|
|
||||||
let res = await JSONRPC.NetworkManager.getOutputResources();
|
|
||||||
// Используем ключи, как они приходят с API: domains, ASN, subnets
|
|
||||||
return {
|
|
||||||
domains: res.domains || [],
|
|
||||||
ASN: res.ASN || [],
|
|
||||||
subnets: res.subnets || []
|
|
||||||
};
|
|
||||||
} catch(e) {
|
|
||||||
console.error("Ошибка при получении ресурсов:", e);
|
|
||||||
return { domains: [], ASN: [], subnets: [] };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 2. Запрос и рендеринг данных о ПО и Статусах */
|
|
||||||
async function renderSystemAndManagerStatus() {
|
|
||||||
const dataHtml = [];
|
|
||||||
|
|
||||||
// --- Получение данных ---
|
|
||||||
const hasUpdates = await JSONRPC.System.hasUpdates();
|
|
||||||
const isSubUpdating = await JSONRPC.SubscriptionManager.isUpdating();
|
|
||||||
const isNetUpdating = await JSONRPC.NetworkManager.isUpdating();
|
|
||||||
const subCount = await getSubscriptionCount();
|
|
||||||
const networkResources = await getNetworkResources();
|
|
||||||
|
|
||||||
// 1. Информация об обновлении ПО
|
|
||||||
dataHtml.push(createStatusCard(
|
|
||||||
"Обновление ПО",
|
|
||||||
hasUpdates ? "Обнаружено" : "Нет обновлений",
|
|
||||||
"update-software-btn",
|
|
||||||
"Проверить",
|
|
||||||
false
|
|
||||||
));
|
|
||||||
|
|
||||||
// 2. Статус менеджера подписок (добавляем статистику)
|
|
||||||
const subStatsHtml = `<div class="resource-group" style="margin-top: 20px;">
|
|
||||||
<h4>Подписки:</h4>
|
|
||||||
<div class="resource-row">
|
|
||||||
${createResourceBadge("Активно", subCount)}
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
dataHtml.push(createStatusCard(
|
|
||||||
"Менеджер Подписок",
|
|
||||||
isSubUpdating ? "Обновляется" : "Активен",
|
|
||||||
"update-sub-btn",
|
|
||||||
"Обновить",
|
|
||||||
isSubUpdating,
|
|
||||||
subStatsHtml
|
|
||||||
));
|
|
||||||
|
|
||||||
// 3. Статус менеджера сетей (ВКЛЮЧАЕМ РЕСУРСЫ)
|
|
||||||
const netResourcesHtml = createResourceStatsHtml(networkResources);
|
|
||||||
dataHtml.push(createStatusCard(
|
|
||||||
"Менеджер Сетей",
|
|
||||||
isNetUpdating ? "Обновляется" : "Активен",
|
|
||||||
"update-net-btn",
|
|
||||||
"Обновить",
|
|
||||||
isNetUpdating,
|
|
||||||
netResourcesHtml
|
|
||||||
));
|
|
||||||
|
|
||||||
// Обновляем DOM один раз
|
|
||||||
$content().html(`<div class="stats-grid status-grid">${dataHtml.join('')}</div>`);
|
|
||||||
|
|
||||||
// Прикрепляем обработчики событий после рендеринга
|
|
||||||
attachEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Главная функция монтирования ---
|
|
||||||
export async function renderStatisticsPage() {
|
|
||||||
// 1. Очищаем старый интервал (на случай, если он был)
|
|
||||||
stopPolling();
|
|
||||||
|
|
||||||
// 2. Первичный рендеринг
|
|
||||||
await renderSystemAndManagerStatus();
|
|
||||||
|
|
||||||
console.log("start interval")
|
|
||||||
// 3. Запускаем опрос каждые 5 секунд
|
|
||||||
updateInterval = setInterval(renderSystemAndManagerStatus, POLLING_INTERVAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 🔥 Остановка интервала опроса при уходе со страницы */
|
|
||||||
export function stopPolling() {
|
|
||||||
if (updateInterval) {
|
|
||||||
console.log("clear interval")
|
|
||||||
clearInterval(updateInterval);
|
|
||||||
updateInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Прикрепление обработчиков к кнопкам */
|
|
||||||
function attachEventHandlers() {
|
|
||||||
// Обработчик для менеджера подписок
|
|
||||||
$('#update-sub-btn').off('click').on('click', async function() {
|
|
||||||
const $btn = $(this);
|
|
||||||
$btn.prop('disabled', true).text('Обновление...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await JSONRPC.SubscriptionManager.triggerUpdate();
|
|
||||||
alert("Обновление подписок запущено!");
|
|
||||||
renderSystemAndManagerStatus(); // Обновляем немедленно
|
|
||||||
} catch(e) {
|
|
||||||
alert("Ошибка при запуске обновления подписок!");
|
|
||||||
$btn.prop('disabled', false).text('Обновить');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Обработчик для менеджера сетей
|
|
||||||
$('#update-net-btn').off('click').on('click', async function() {
|
|
||||||
const $btn = $(this);
|
|
||||||
$btn.prop('disabled', true).text('Обновление...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Передаем аргумент true в triggerUpdate
|
|
||||||
await JSONRPC.NetworkManager.triggerUpdate(true);
|
|
||||||
alert("Обновление сетей запущено!");
|
|
||||||
renderSystemAndManagerStatus(); // Обновляем немедленно
|
|
||||||
} catch(e) {
|
|
||||||
alert("Ошибка при запуске обновления сетей!");
|
|
||||||
$btn.prop('disabled', false).text('Обновить');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Кнопка проверки обновлений ПО
|
|
||||||
$('#update-software-btn').off('click').on('click', function() {
|
|
||||||
alert("Проверка обновлений ПО запущена...");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
import $ from 'jquery';
|
|
||||||
import { JSONRPC } from '@/json-rpc.js';
|
|
||||||
|
|
||||||
// --- Глобальное состояние для страницы ---
|
|
||||||
let availableResources = {};
|
|
||||||
let subscribedKeys = [];
|
|
||||||
|
|
||||||
/** Загружает все необходимые данные для страницы */
|
|
||||||
async function loadSubscriptionData() {
|
|
||||||
try {
|
|
||||||
subscribedKeys = await JSONRPC.SubscriptionManager.getSubscribedResources();
|
|
||||||
availableResources = await JSONRPC.SubscriptionManager.getAvailableResources();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Ошибка при загрузке данных подписок:", e);
|
|
||||||
availableResources = {};
|
|
||||||
subscribedKeys = [];
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Рендеринг HTML списка */
|
|
||||||
function renderSubscriptionList() {
|
|
||||||
const $listContainer = $('#subscription-list-container');
|
|
||||||
const availableKeys = Object.keys(availableResources).sort();
|
|
||||||
|
|
||||||
if (availableKeys.length === 0) {
|
|
||||||
$listContainer.html('<p>Нет доступных ресурсов для подписки.</p>');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let listHtml = availableKeys.map(key => {
|
|
||||||
const details = availableResources[key];
|
|
||||||
const isChecked = subscribedKeys.includes(key);
|
|
||||||
|
|
||||||
// 🔥 Обновлено: Используем description и меняем структуру
|
|
||||||
const description = details.description || 'Нет описания';
|
|
||||||
|
|
||||||
// Формируем строку с деталями (теперь на новой строке)
|
|
||||||
const detailText = `
|
|
||||||
Доменов: ${details.domains ? details.domains.length : 0},
|
|
||||||
ASN: ${details.ASN ? details.ASN.length : 0},
|
|
||||||
Подсетей: ${details.subnets ? details.subnets.length : 0}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return `
|
|
||||||
<li class="subscription-item">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" class="subscription-checkbox" value="${key}" ${isChecked ? 'checked' : ''}>
|
|
||||||
<div class="resource-info">
|
|
||||||
<div class="resource-main-line">
|
|
||||||
<span class="resource-description">${description}</span>
|
|
||||||
<span class="resource-key">(${key})</span>
|
|
||||||
</div>
|
|
||||||
<div class="resource-details">${detailText}</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</li>
|
|
||||||
`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const html = `
|
|
||||||
<ul class="subscription-list">${listHtml}</ul>
|
|
||||||
<button id="save-subscriptions-btn" class="btn-primary" style="width: 250px; margin-top: 30px;">Сохранить</button>
|
|
||||||
<div id="subscription-status-message" class="error-message" style="display: none;"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
$listContainer.html(html);
|
|
||||||
attachEventHandlers();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Обработчик кнопки "Сохранить" (остается без изменений) */
|
|
||||||
function attachEventHandlers() {
|
|
||||||
$('#save-subscriptions-btn').on('click', async function() {
|
|
||||||
const $btn = $(this);
|
|
||||||
const $message = $('#subscription-status-message');
|
|
||||||
$message.hide().removeClass('success-message').addClass('error-message');
|
|
||||||
|
|
||||||
const newSubscribedKeys = $('.subscription-checkbox:checked').map(function() {
|
|
||||||
return $(this).val();
|
|
||||||
}).get();
|
|
||||||
|
|
||||||
$btn.prop('disabled', true).text('Сохранение...');
|
|
||||||
|
|
||||||
try {
|
|
||||||
await JSONRPC.SubscriptionManager.setSubscribedResources(newSubscribedKeys);
|
|
||||||
|
|
||||||
subscribedKeys = newSubscribedKeys;
|
|
||||||
|
|
||||||
$message.text('Настройки подписок успешно сохранены!').addClass('success-message').show();
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Ошибка сохранения подписок:', e);
|
|
||||||
$message.text('Ошибка при сохранении подписок.').show();
|
|
||||||
} finally {
|
|
||||||
$btn.prop('disabled', false).text('Сохранить');
|
|
||||||
setTimeout(() => $message.fadeOut(), 5000);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ... (SubscriptionsPage, loadSubscriptionData остаются без изменений) ...
|
|
||||||
|
|
||||||
export const SubscriptionsPage = {
|
|
||||||
render: () => {
|
|
||||||
return `
|
|
||||||
<h1 class="page-title">Управление Подписками</h1>
|
|
||||||
<div id="subscription-list-container">
|
|
||||||
<p>Загрузка доступных ресурсов...</p>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
mount: async () => {
|
|
||||||
const success = await loadSubscriptionData();
|
|
||||||
if (success) {
|
|
||||||
renderSubscriptionList();
|
|
||||||
} else {
|
|
||||||
$('#subscription-list-container').html('<p class="error-message">Не удалось загрузить данные. Проверьте соединение с сервером.</p>');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
unmount: () => {
|
|
||||||
availableResources = {};
|
|
||||||
subscribedKeys = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
@ -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