большой рефакторинг + вэбка
This commit is contained in:
parent
32e65a7742
commit
aa726d4653
|
|
@ -39,3 +39,6 @@ build/
|
|||
/.idea/
|
||||
/.mvn/
|
||||
/config.json
|
||||
/web-server/src/main/webui/src/json-rpc.js
|
||||
/.cache/
|
||||
ovpn-connector.json
|
||||
|
|
|
|||
32
app/pom.xml
32
app/pom.xml
|
|
@ -25,7 +25,6 @@
|
|||
</dependency>
|
||||
|
||||
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
|
|
@ -40,6 +39,37 @@
|
|||
<version>0.39.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-server -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>12.0.12</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>3.5.0</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>java</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<mainClass>ru.kirillius.json.rpc.CodeGeneration.AutoGenerationUtility</mainClass>
|
||||
<arguments>
|
||||
<argument>--b=${basedir}</argument>
|
||||
<argument>${project.basedir}/../webui/src/json-rpc.js</argument>
|
||||
</arguments>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
|
@ -1,48 +1,182 @@
|
|||
package ru.kirillius.pf.sdn;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
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.TDNS;
|
||||
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||
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.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
import ru.kirillius.pf.sdn.core.Util.Wait;
|
||||
import ru.kirillius.pf.sdn.web.HTTPServer;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.Collections;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class App extends AppContext {
|
||||
public class App implements Context, Closeable {
|
||||
private final static File configFile = new File("config.json");
|
||||
protected final static String CTX = App.class.getSimpleName();
|
||||
|
||||
static {
|
||||
SystemLogger.initializeLogging(Level.INFO, List.of(InMemoryLogHandler.class));
|
||||
SystemLogger.setExceptionDumping(true);
|
||||
}
|
||||
|
||||
private final AtomicBoolean shouldRestart = new AtomicBoolean(false);
|
||||
private final AtomicBoolean running = new AtomicBoolean(true);
|
||||
|
||||
@Getter
|
||||
private final NetworkManager networkManager;
|
||||
@Getter
|
||||
private Config config;
|
||||
@Getter
|
||||
private final AuthManager authManager;
|
||||
@Getter
|
||||
private final HTTPServer server;
|
||||
@Getter
|
||||
private final ASInfoService ASInfoService;
|
||||
@Getter
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
@Getter
|
||||
private final UpdateManager updateManager;
|
||||
@Getter
|
||||
private final TokenStorage tokenStorage;
|
||||
@Getter
|
||||
private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
|
||||
|
||||
private final List<Component<?>> loadedComponents = new ArrayList<>();
|
||||
|
||||
@SneakyThrows
|
||||
public App(File configFile) {
|
||||
super(configFile);
|
||||
|
||||
|
||||
getSubscriptionManager().triggerUpdate();
|
||||
|
||||
|
||||
try {
|
||||
config = Config.load(configFile);
|
||||
} catch (IOException e) {
|
||||
config = new Config();
|
||||
try {
|
||||
Config.store(config, configFile);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
SystemLogger.initializeLogging(Level.INFO, Collections.emptyList());
|
||||
authManager = new AuthManager(this);
|
||||
ASInfoService = new ASInfoService();
|
||||
ASInfoService.setProvider(new HEInfoProvider(this));
|
||||
networkManager = new NetworkManager(this);
|
||||
networkManager.getInputResources().add(config.getCustomResources());
|
||||
subscriptionManager = new SubscriptionManager(this);
|
||||
updateManager = new UpdateManager(this);
|
||||
tokenStorage = new TokenStorage(this);
|
||||
subscribe();
|
||||
updateManager.start();
|
||||
initComponents();
|
||||
server = new HTTPServer(this);
|
||||
|
||||
if (config.getPasswordHash() == null || config.getPasswordHash().isEmpty()) {
|
||||
SystemLogger.error("There is no password for admin. Setting default password: admin", CTX);
|
||||
getAuthManager().updatePassword("admin");
|
||||
}
|
||||
getSubscriptionManager().triggerUpdate();
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
var restart = false;
|
||||
do {
|
||||
try (var app = new App(configFile)) {
|
||||
Wait.when(app.running::get);
|
||||
restart = app.shouldRestart.get();
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Unhandled error", CTX, e);
|
||||
}
|
||||
} while (restart);
|
||||
}
|
||||
|
||||
try (App app = new App(configFile)) {
|
||||
app.getEventsHandler().getNetworkManagerUpdateEvent().add(new EventListener<NetworkResourceBundle>() {
|
||||
@Override
|
||||
public void invoke(NetworkResourceBundle bundle) throws Exception {
|
||||
SystemLogger.message("Network resource bundle updated.", CTX);
|
||||
public void triggerRestart() {
|
||||
SystemLogger.message("Restarting app", CTX);
|
||||
running.set(false);
|
||||
shouldRestart.set(true);
|
||||
}
|
||||
|
||||
public void triggerShutdown() {
|
||||
SystemLogger.message("Shutting down app", CTX);
|
||||
running.set(false);
|
||||
}
|
||||
|
||||
public Collection<Class<? extends Component<?>>> getComponentClasses() {
|
||||
return List.of(FRR.class, OVPN.class, TDNS.class);
|
||||
}
|
||||
|
||||
public void initComponents() {
|
||||
var enabledPlugins = config.getEnabledComponents();
|
||||
|
||||
(List.copyOf(loadedComponents)).forEach(plugin -> {
|
||||
if (!enabledPlugins.contains(plugin.getClass())) {
|
||||
SystemLogger.message("Unloading plugin: " + plugin.getClass().getSimpleName(), CTX);
|
||||
try {
|
||||
plugin.close();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error on plugin unload", CTX, e);
|
||||
} finally {
|
||||
loadedComponents.remove(plugin);
|
||||
}
|
||||
}
|
||||
});
|
||||
while (true) {
|
||||
Thread.yield();
|
||||
var loadedClasses = loadedComponents.stream().map(plugin -> plugin.getClass()).toList();
|
||||
enabledPlugins.forEach(pluginClass -> {
|
||||
if (loadedClasses.contains(pluginClass)) {
|
||||
return;
|
||||
}
|
||||
SystemLogger.message("Loading plugin: " + pluginClass.getSimpleName(), CTX);
|
||||
var plugin = Component.loadPlugin(pluginClass, this);
|
||||
loadedComponents.add(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
private void subscribe() {
|
||||
var eventsHandler = getEventsHandler();
|
||||
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
||||
var manager = getNetworkManager();
|
||||
var inputResources = getNetworkManager().getInputResources();
|
||||
inputResources.clear();
|
||||
inputResources.add(config.getCustomResources());
|
||||
inputResources.add(bundle);
|
||||
manager.triggerUpdate(false);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component<?> getPluginInstance(Class<? extends Component<?>> pluginClass) {
|
||||
return loadedComponents.stream().filter(plugin -> plugin.getClass().equals(pluginClass)).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
loadedComponents.forEach(plugin -> {
|
||||
try {
|
||||
plugin.close();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
SystemLogger.error("Error closing plugin", CTX, e);
|
||||
}
|
||||
});
|
||||
ASInfoService.close();
|
||||
networkManager.close();
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Error stopping server", CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,106 +0,0 @@
|
|||
package ru.kirillius.pf.sdn;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import ru.kirillius.pf.sdn.External.API.HEInfoProvider;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Config;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.ContextEventsHandler;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Plugin;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AppContext implements Context, Closeable {
|
||||
|
||||
protected final static String CTX = AppContext.class.getSimpleName();
|
||||
|
||||
public AppContext(File configFile) {
|
||||
try {
|
||||
config = Config.load(configFile);
|
||||
} catch (IOException e) {
|
||||
config = new Config();
|
||||
try {
|
||||
Config.store(config, configFile);
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
authManager = new AuthManager(this);
|
||||
server = new Server();
|
||||
ASInfoService = new ASInfoService();
|
||||
ASInfoService.setProvider(new HEInfoProvider(this));
|
||||
networkManager = new NetworkManager(this);
|
||||
networkManager.getInputResources().add(config.getCustomResources());
|
||||
subscriptionManager = new SubscriptionManager(this);
|
||||
subscribe();
|
||||
initPlugins();
|
||||
}
|
||||
|
||||
private void initPlugins() {
|
||||
config.getEnabledPlugins().forEach(pluginClass -> {
|
||||
var plugin = Plugin.loadPlugin(pluginClass, this);
|
||||
loadedPlugins.add(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
private final List<Plugin<?>> loadedPlugins = new ArrayList<>();
|
||||
|
||||
private void subscribe() {
|
||||
var eventsHandler = getEventsHandler();
|
||||
eventsHandler.getSubscriptionsUpdateEvent().add(bundle -> {
|
||||
var manager = getNetworkManager();
|
||||
var inputResources = getNetworkManager().getInputResources();
|
||||
inputResources.clear();
|
||||
inputResources.add(config.getCustomResources());
|
||||
inputResources.add(bundle);
|
||||
manager.triggerUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Getter
|
||||
private final NetworkManager networkManager;
|
||||
@Getter
|
||||
private Config config;
|
||||
@Getter
|
||||
private final AuthManager authManager;
|
||||
@Getter
|
||||
private final Server server;
|
||||
@Getter
|
||||
private final ASInfoService ASInfoService;
|
||||
@Getter
|
||||
private final SubscriptionManager subscriptionManager;
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
loadedPlugins.forEach(plugin -> {
|
||||
try {
|
||||
plugin.close();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error closing plugin", CTX, e);
|
||||
}
|
||||
});
|
||||
ASInfoService.close();
|
||||
networkManager.close();
|
||||
try {
|
||||
server.stop();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final ContextEventsHandler EventsHandler = new ContextEventsHandler();
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
package ru.kirillius.pf.sdn.External.API.Components;
|
||||
|
||||
import lombok.*;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.json.JSONArrayProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.pf.sdn.core.AbstractPlugin;
|
||||
import ru.kirillius.pf.sdn.External.API.ShellExecutor;
|
||||
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.IPv4Subnet;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
|
|
@ -17,13 +18,13 @@ import java.util.List;
|
|||
import java.util.function.Consumer;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
public final class FRRPlugin extends AbstractPlugin<FRRPlugin.FRRConfig> {
|
||||
public final class FRR extends AbstractComponent<FRR.FRRConfig> {
|
||||
private final static String SUBNET_PATTERN = "{%subnet}";
|
||||
private final static String GW_PATTERN = "{%gateway}";
|
||||
private final static String CTX = FRRPlugin.class.getSimpleName();
|
||||
private final static String CTX = FRR.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
public FRRPlugin(Context context) {
|
||||
public FRR(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getSubnets()));
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package ru.kirillius.pf.sdn.External.API.Components;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.External.API.ShellExecutor;
|
||||
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public final class OVPN extends AbstractComponent<OVPN.OVPNConfig> {
|
||||
private final static String CTX = OVPN.class.getSimpleName();
|
||||
private final EventListener<JSONRPCServlet> subscription;
|
||||
|
||||
public OVPN(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getRPCInitEvent()
|
||||
.add(servlet -> servlet.addTargetInstance(OVPN.class, OVPN.this));
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void restartSystemService() {
|
||||
try (var shell = new ShellExecutor(config.shellConfig)) {
|
||||
shell.executeCommand(new String[]{config.restartCommand});
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Error when trying to restart OVPN", CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONArray getManagedRoutes() {
|
||||
var array = new JSONArray();
|
||||
context.getNetworkManager().getOutputResources().getSubnets().stream().map(subnet -> {
|
||||
var json = new JSONObject();
|
||||
json.put("address", subnet.getAddress());
|
||||
json.put("mask", IPv4Util.maskToString(IPv4Util.calculateMask(subnet.getPrefixLength())));
|
||||
return json;
|
||||
}).forEach(array::put);
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
context.getEventsHandler().getRPCInitEvent().remove(subscription);
|
||||
}
|
||||
|
||||
@JSONSerializable
|
||||
public static class OVPNConfig {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private ShellExecutor.Config shellConfig = new ShellExecutor.Config();
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private String restartCommand = "rc-service openvpn restart";
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
package ru.kirillius.pf.sdn.External.API;
|
||||
package ru.kirillius.pf.sdn.External.API.Components;
|
||||
|
||||
import lombok.*;
|
||||
import ru.kirillius.java.utils.events.EventListener;
|
||||
import ru.kirillius.json.JSONArrayProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.pf.sdn.core.AbstractPlugin;
|
||||
import ru.kirillius.pf.sdn.External.API.TDNSAPI;
|
||||
import ru.kirillius.pf.sdn.core.AbstractComponent;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
|
@ -15,12 +16,12 @@ import java.util.ArrayList;
|
|||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class TechnitiumPlugin extends AbstractPlugin<TechnitiumPlugin.TechnitiumConfig> {
|
||||
public final class TDNS extends AbstractComponent<TDNS.TechnitiumConfig> {
|
||||
|
||||
private final static String CTX = TechnitiumPlugin.class.getSimpleName();
|
||||
private final static String CTX = TDNS.class.getSimpleName();
|
||||
private final EventListener<NetworkResourceBundle> subscription;
|
||||
|
||||
public TechnitiumPlugin(Context context) {
|
||||
public TDNS(Context context) {
|
||||
super(context);
|
||||
subscription = context.getEventsHandler().getNetworkManagerUpdateEvent().add(bundle -> updateSubnets(bundle.getDomains()));
|
||||
}
|
||||
|
|
@ -28,10 +29,10 @@ public final class TechnitiumPlugin extends AbstractPlugin<TechnitiumPlugin.Tech
|
|||
private void updateSubnets(List<String> domains) {
|
||||
for (var instance : config.instances) {
|
||||
SystemLogger.message("Updating zones on DNS server " + instance.server, CTX);
|
||||
try (var api = new TechnitiumDNSAPI(instance.server, instance.token)) {
|
||||
try (var api = new TDNSAPI(instance.server, instance.token)) {
|
||||
var existingForwardZones = api.getZones().stream()
|
||||
.filter(zoneResponse -> zoneResponse.getType() == TechnitiumDNSAPI.ZoneType.Forwarder)
|
||||
.map(TechnitiumDNSAPI.ZoneResponse::getName)
|
||||
.filter(zoneResponse -> zoneResponse.getType() == TDNSAPI.ZoneType.Forwarder)
|
||||
.map(TDNSAPI.ZoneResponse::getName)
|
||||
.collect(Collectors.toCollection(ArrayList::new));
|
||||
existingForwardZones.forEach(zoneName -> {
|
||||
if (!domains.contains(zoneName)) {
|
||||
|
|
@ -16,12 +16,12 @@ import java.net.http.HttpResponse;
|
|||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class TechnitiumDNSAPI implements Closeable {
|
||||
public class TDNSAPI implements Closeable {
|
||||
private final String server;
|
||||
private final String authToken;
|
||||
private final HttpClient httpClient;
|
||||
|
||||
public TechnitiumDNSAPI(String server, String authToken) {
|
||||
public TDNSAPI(String server, String authToken) {
|
||||
this.server = server;
|
||||
httpClient = HttpClient.newBuilder().build();
|
||||
this.authToken = authToken;
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
package ru.kirillius.pf.sdn;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.logging.Handler;
|
||||
import java.util.logging.LogRecord;
|
||||
|
||||
public class InMemoryLogHandler extends Handler {
|
||||
private final static Queue<String> queue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final static DateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US);
|
||||
|
||||
private String format(LogRecord logRecord) {
|
||||
return "[" + dateFormat.format(new Date(logRecord.getMillis())) + "][" + logRecord.getLevel().getName() + "] " + logRecord.getMessage().trim();
|
||||
}
|
||||
|
||||
public static Collection<String> getMessages() {
|
||||
return Collections.unmodifiableCollection(queue);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(LogRecord logRecord) {
|
||||
if (queue.size() >= 1000) {
|
||||
queue.poll();
|
||||
}
|
||||
queue.add(format(logRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ import org.eclipse.jetty.ee10.servlet.DefaultServlet;
|
|||
import org.eclipse.jetty.ee10.servlet.ServletContextHandler;
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import org.eclipse.jetty.server.ServerConnector;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
import ru.kirillius.pf.sdn.web.RPC.AuthRPC;
|
||||
import ru.kirillius.pf.sdn.web.RPC.Auth;
|
||||
import ru.kirillius.pf.sdn.web.RPC.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.web.RPC.RPC;
|
||||
import ru.kirillius.pf.sdn.web.RPC.SubscriptionManager;
|
||||
import ru.kirillius.pf.sdn.web.RPC.System;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
|
|
@ -23,8 +25,6 @@ public class HTTPServer extends Server {
|
|||
private final static String LOG_CONTEXT = HTTPServer.class.getSimpleName();
|
||||
private final static String TOKEN_HEADER = "X-Auth-token";
|
||||
|
||||
private final JSONRPCServlet JSONRPC = new JSONRPCServlet();
|
||||
|
||||
private String getResourceBase() throws MalformedURLException {
|
||||
var resourceFile = getClass().getClassLoader().getResource("htdocs/index.html");
|
||||
return new URL(Objects.requireNonNull(resourceFile).getProtocol(), resourceFile.getHost(), resourceFile.getPath()
|
||||
|
|
@ -32,26 +32,35 @@ public class HTTPServer extends Server {
|
|||
.toString();
|
||||
}
|
||||
|
||||
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(AuthRPC.class);
|
||||
private final static Set<Class<? extends RPC>> RPCHandlerTypes = Set.of(Auth.class, NetworkManager.class, SubscriptionManager.class, System.class);
|
||||
|
||||
|
||||
public HTTPServer(Context appContext, int port, @Nullable String host) throws Exception {
|
||||
|
||||
public HTTPServer(Context appContext) {
|
||||
var config = appContext.getConfig();
|
||||
var connector = new ServerConnector(this);
|
||||
connector.setPort(port);
|
||||
connector.setPort(config.getHttpPort());
|
||||
var host = config.getHost();
|
||||
if (host != null && !host.equals(ANY_HOST)) {
|
||||
connector.setHost(host);
|
||||
}
|
||||
|
||||
this.addConnector(connector);
|
||||
ServletContextHandler servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
||||
var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS);
|
||||
var JSONRPC = new JSONRPCServlet();
|
||||
servletContext.addServlet(JSONRPC, JSONRPCServlet.CONTEXT_PATH);
|
||||
var holder = servletContext.addServlet(DefaultServlet.class, "/");
|
||||
try {
|
||||
holder.setInitParameter("resourceBase", getResourceBase());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
this.setHandler(servletContext);
|
||||
|
||||
try {
|
||||
start();
|
||||
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Error starting HTTPServer", e);
|
||||
}
|
||||
|
||||
JSONRPC.addRequestHandler((request, response, call) -> {
|
||||
var authManager = appContext.getAuthManager();
|
||||
|
|
@ -87,5 +96,13 @@ public class HTTPServer extends Server {
|
|||
(throwable.getRequestData() == null ? "" : throwable.getRequestData().toString()) +
|
||||
" has failed with error", LOG_CONTEXT, throwable.getError());
|
||||
});
|
||||
|
||||
try {
|
||||
appContext.getEventsHandler().getRPCInitEvent().invoke(JSONRPC);
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Error on RPC init event", CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
private final static String CTX = HTTPServer.class.getSimpleName();
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package ru.kirillius.pf.sdn.web.RPC;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCContext;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.json.rpc.Servlet.CallContext;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class Auth implements RPC {
|
||||
private final Context context;
|
||||
|
||||
public Auth(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray listTokens() {
|
||||
return JSONUtility.serializeCollection(context.getTokenStorage().getTokens(), AuthToken.class, null);
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void removeToken(@JRPCArgument(name = "token") String token) {
|
||||
context.getTokenStorage().remove(new AuthToken(token));
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public String createAPIToken(@JRPCArgument(name = "description") String description) {
|
||||
var token = new AuthToken();
|
||||
token.setDescription(description);
|
||||
context.getTokenStorage().add(token);
|
||||
return token.getToken();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public String createToken(@JRPCContext CallContext call) {
|
||||
var UA = call.getRequest().getHeader("User-Agent");
|
||||
if (UA == null) {
|
||||
UA = "Unknown user agent";
|
||||
}
|
||||
var authManager = context.getAuthManager();
|
||||
var token = authManager.createToken(UA +" "+ new Date());
|
||||
authManager.setSessionToken(call.getSession(), token);
|
||||
return token.getToken();
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
public boolean startSessionByPassword(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
if (authManager.validatePassword(password)) {
|
||||
authManager.setSessionAuthState(call.getSession(), true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
public boolean startSessionByToken(@JRPCArgument(name = "token") String token, @JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
if (authManager.validateToken(token)) {
|
||||
authManager.setSessionAuthState(call.getSession(), true);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
public boolean isAuthenticated(@JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
return authManager.getSessionAuthState(call.getSession());
|
||||
}
|
||||
|
||||
|
||||
@JRPCMethod
|
||||
public void logout(@JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
authManager.setSessionAuthState(call.getSession(), false);
|
||||
var token = authManager.getSessionToken(call.getSession());
|
||||
if (token != null) {
|
||||
authManager.invalidateToken(token);
|
||||
authManager.setSessionToken(call.getSession(), null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package ru.kirillius.pf.sdn.web.RPC;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
public class NetworkManager implements RPC {
|
||||
private final Context context;
|
||||
|
||||
public NetworkManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public boolean isUpdating() {
|
||||
return context.getNetworkManager().isUpdatingNow();
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void triggerUpdate(@JRPCArgument(name = "ignoreCache") boolean ignoreCache) {
|
||||
context.getNetworkManager().triggerUpdate(ignoreCache);
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONObject getOutputResources() {
|
||||
return JSONUtility.serializeStructure(context.getNetworkManager().getOutputResources());
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package ru.kirillius.pf.sdn.web.RPC;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SubscriptionManager implements RPC {
|
||||
private final Context context;
|
||||
|
||||
public SubscriptionManager(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public boolean isUpdating() {
|
||||
return context.getSubscriptionManager().isUpdatingNow();
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void triggerUpdate() {
|
||||
context.getSubscriptionManager().triggerUpdate();
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONArray getSubscribedResources() {
|
||||
return JSONUtility.serializeCollection(context.getConfig().getSubscribedResources(), String.class, null);
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public JSONObject getAvailableResources() {
|
||||
return JSONUtility.serializeMap(context.getSubscriptionManager().getAvailableResources(), String.class, NetworkResourceBundle.class, null, null);
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
@ProtectedMethod
|
||||
public void setSubscribedResources(@JRPCArgument(name = "resources") JSONArray subscribedResources) {
|
||||
context.getConfig().setSubscribedResources(JSONUtility.deserializeCollection(subscribedResources, String.class, null).collect(Collectors.toList()));
|
||||
triggerUpdate();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
package ru.kirillius.pf.sdn.web.RPC;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.pf.sdn.core.Component;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class System implements RPC {
|
||||
private final Context context;
|
||||
|
||||
public System(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void restart() {
|
||||
context.triggerRestart();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void shutdown() {
|
||||
context.triggerShutdown();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONObject getConfig() {
|
||||
return JSONUtility.serializeStructure(context.getConfig());
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public boolean isConfigChanged() {
|
||||
return context.getConfig().isModified();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public boolean hasUpdates() {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray getEnabledComponents() {
|
||||
return JSONUtility.serializeCollection(context.getConfig().getEnabledComponents().stream().map(Class::getName).toList(), String.class, null);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public void setEnabledComponents(@JRPCArgument(name = "components") JSONArray components) {
|
||||
context.getConfig().setEnabledComponents(JSONUtility.deserializeCollection(components, String.class, null).map(s -> {
|
||||
try {
|
||||
return (Class<? extends Component<?>>) Class.forName(s);
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException("Unable to load Component class " + s, e);
|
||||
}
|
||||
}).collect(Collectors.toList()));
|
||||
context.initComponents();
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public JSONArray getAvailableComponents() {
|
||||
return JSONUtility.serializeCollection(context.getComponentClasses().stream().map(Class::getName).toList(), String.class, null);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Title</title>
|
||||
</head>
|
||||
<body>
|
||||
test ok
|
||||
</body>
|
||||
</html>
|
||||
19
core/pom.xml
19
core/pom.xml
|
|
@ -11,5 +11,24 @@
|
|||
|
||||
<artifactId>core</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ru.kirillius.utils</groupId>
|
||||
<artifactId>common-logging</artifactId>
|
||||
<version>1.3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-jdk14</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
public abstract class AbstractPlugin<CT> implements Plugin<CT> {
|
||||
public abstract class AbstractComponent<CT> implements Component<CT> {
|
||||
protected final CT config;
|
||||
protected final Context context;
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public AbstractPlugin(Context context) {
|
||||
public AbstractComponent(Context context) {
|
||||
config = (CT) context.getConfig().getPluginsConfig().getConfig((Class) getClass());
|
||||
this.context = context;
|
||||
}
|
||||
|
|
@ -3,13 +3,17 @@ package ru.kirillius.pf.sdn.core.Auth;
|
|||
import jakarta.servlet.http.HttpSession;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.HashUtil;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
public class AuthManager {
|
||||
private final Context context;
|
||||
public final static String SESSION_AUTH_KEY = "auth";
|
||||
public final static String SESSION_TOKEN = "token";
|
||||
private final static String CTX = AuthManager.class.getSimpleName();
|
||||
private final Context context;
|
||||
|
||||
public AuthManager(Context context) {
|
||||
this.context = context;
|
||||
|
|
@ -22,22 +26,27 @@ public class AuthManager {
|
|||
|
||||
public void updatePassword(String pass) {
|
||||
var config = context.getConfig();
|
||||
config.setPasswordSalt(UUID.randomUUID().toString());
|
||||
config.setPasswordHash(
|
||||
HashUtil.hash(pass, config.getPasswordSalt())
|
||||
);
|
||||
}
|
||||
|
||||
public AuthToken createToken(String description) {
|
||||
var config = context.getConfig();
|
||||
var token = new AuthToken();
|
||||
token.setDescripton(description);
|
||||
config.getTokens().add(token);
|
||||
config.update();
|
||||
token.setDescription(description);
|
||||
var tokenStorage = context.getTokenStorage();
|
||||
tokenStorage.add(token);
|
||||
try {
|
||||
tokenStorage.store();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Failed to save token storage", CTX, e);
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
public boolean validateToken(AuthToken token) {
|
||||
return context.getConfig().getTokens().contains(token);
|
||||
return context.getTokenStorage().contains(token);
|
||||
}
|
||||
|
||||
public void setSessionAuthState(HttpSession session, boolean state) {
|
||||
|
|
@ -57,9 +66,16 @@ public class AuthManager {
|
|||
}
|
||||
|
||||
public void invalidateToken(AuthToken token) {
|
||||
var config = context.getConfig();
|
||||
config.getTokens().remove(token);
|
||||
config.update();
|
||||
var tokenStorage = context.getTokenStorage();
|
||||
if (tokenStorage.contains(token)) {
|
||||
tokenStorage.remove(token);
|
||||
try {
|
||||
tokenStorage.store();
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Failed to save token storage", CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ public class AuthToken {
|
|||
@Setter
|
||||
@Getter
|
||||
@JSONProperty
|
||||
private String descripton = "untitled";
|
||||
private String description = "untitled";
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
package ru.kirillius.pf.sdn.core.Auth;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
||||
public class TokenStorage {
|
||||
private final File file;
|
||||
|
||||
public TokenStorage(Context context) {
|
||||
file = new File(context.getConfig().getCacheDirectory(), "tokens.json");
|
||||
if (file.exists()) {
|
||||
try (var stream = new FileInputStream(file)) {
|
||||
var json = new JSONArray(new JSONTokener(stream));
|
||||
tokens = JSONUtility.deserializeCollection(json, AuthToken.class, null).collect(Collectors.toList());
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to read token storage", e);
|
||||
}
|
||||
} else {
|
||||
tokens = new ArrayList<>();
|
||||
}
|
||||
}
|
||||
|
||||
private final List<AuthToken> tokens;
|
||||
|
||||
public Collection<AuthToken> getTokens() {
|
||||
synchronized (tokens) {
|
||||
return Collections.unmodifiableList(tokens);
|
||||
}
|
||||
}
|
||||
|
||||
public void add(AuthToken... what) {
|
||||
synchronized (tokens) {
|
||||
Collections.addAll(tokens, what);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(AuthToken what) {
|
||||
synchronized (tokens) {
|
||||
return tokens.contains(what);
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(AuthToken... what) {
|
||||
synchronized (tokens) {
|
||||
Arrays.stream(what).forEach(tokens::remove);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void store() throws IOException {
|
||||
try (var fileInputStream = new FileOutputStream(file)) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
|
||||
writer.write(JSONUtility.serializeCollection(tokens, AuthToken.class, null).toString());
|
||||
writer.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -5,16 +5,16 @@ import lombok.SneakyThrows;
|
|||
import java.io.Closeable;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
|
||||
public interface Plugin<CT> extends Closeable {
|
||||
public interface Component<CT> extends Closeable {
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> Class<T> getConfigClass(Class<? extends Plugin<T>> pluginClass) {
|
||||
static <T> Class<T> getConfigClass(Class<? extends Component<T>> pluginClass) {
|
||||
var genericSuperclass = (ParameterizedType) pluginClass.getGenericSuperclass();
|
||||
var typeArguments = genericSuperclass.getActualTypeArguments();
|
||||
return (Class<T>) typeArguments[0];
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
static <T extends Plugin<?>> T loadPlugin(Class<T> pluginClass, Context context) {
|
||||
static <T extends Component<?>> T loadPlugin(Class<T> pluginClass, Context context) {
|
||||
return pluginClass.getConstructor(Context.class).newInstance(context);
|
||||
}
|
||||
}
|
||||
|
|
@ -5,8 +5,10 @@ import lombok.NoArgsConstructor;
|
|||
import lombok.Setter;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.*;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||
import ru.kirillius.json.JSONArrayProperty;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.RepositoryConfig;
|
||||
|
||||
|
|
@ -19,6 +21,21 @@ import java.util.UUID;
|
|||
@NoArgsConstructor
|
||||
@JSONSerializable
|
||||
public class Config {
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private int updateSubscriptionsInterval = 6;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private boolean cachingAS = true;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@JSONProperty
|
||||
private int updateASInterval = 12;
|
||||
|
||||
@Getter
|
||||
private File loadedConfigFile = null;
|
||||
@Setter
|
||||
|
|
@ -31,11 +48,6 @@ public class Config {
|
|||
@JSONProperty
|
||||
private File cacheDirectory = new File("./.cache");
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONArrayProperty(type = AuthToken.class)
|
||||
private List<AuthToken> tokens = Collections.emptyList();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
@JSONArrayProperty(type = RepositoryConfig.class)
|
||||
|
|
@ -55,7 +67,7 @@ public class Config {
|
|||
@Setter
|
||||
@Getter
|
||||
@JSONArrayProperty(type = Class.class, required = false)
|
||||
private List<Class<? extends Plugin<?>>> enabledPlugins = new ArrayList<>();
|
||||
private List<Class<? extends Component<?>>> enabledComponents = new ArrayList<>();
|
||||
|
||||
@Setter
|
||||
@Getter
|
||||
|
|
@ -92,14 +104,6 @@ public class Config {
|
|||
@JSONProperty
|
||||
private NetworkResourceBundle filteredResources = new NetworkResourceBundle();
|
||||
|
||||
public void update() {
|
||||
try {
|
||||
store(this, loadedConfigFile);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void store(Config config, File file) throws IOException {
|
||||
try (var fileInputStream = new FileOutputStream(file)) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(fileInputStream))) {
|
||||
|
|
@ -122,9 +126,14 @@ public class Config {
|
|||
var json = new JSONObject(new JSONTokener(stream));
|
||||
var config = deserialize(json);
|
||||
config.loadedConfigFile = file;
|
||||
config.initialJSON = json;
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
private JSONObject initialJSON = new JSONObject();
|
||||
|
||||
public boolean isModified() {
|
||||
return !initialJSON.equals(serialize(this));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,21 +1,37 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import org.eclipse.jetty.server.Server;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthManager;
|
||||
import ru.kirillius.pf.sdn.core.Auth.TokenStorage;
|
||||
import ru.kirillius.pf.sdn.core.Networking.ASInfoService;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkManager;
|
||||
import ru.kirillius.pf.sdn.core.Subscription.SubscriptionManager;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
public interface Context {
|
||||
Config getConfig();
|
||||
|
||||
AuthManager getAuthManager();
|
||||
|
||||
Server getServer();
|
||||
|
||||
ASInfoService getASInfoService();
|
||||
|
||||
NetworkManager getNetworkManager();
|
||||
|
||||
ContextEventsHandler getEventsHandler();
|
||||
|
||||
SubscriptionManager getSubscriptionManager();
|
||||
|
||||
UpdateManager getUpdateManager();
|
||||
|
||||
Component<?> getPluginInstance(Class<? extends Component<?>> pluginClass);
|
||||
|
||||
void triggerRestart();
|
||||
|
||||
void triggerShutdown();
|
||||
|
||||
TokenStorage getTokenStorage();
|
||||
|
||||
void initComponents();
|
||||
|
||||
Collection<Class<? extends Component<?>>> getComponentClasses();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package ru.kirillius.pf.sdn.core;
|
|||
import lombok.Getter;
|
||||
import ru.kirillius.java.utils.events.ConcurrentEventHandler;
|
||||
import ru.kirillius.java.utils.events.EventHandler;
|
||||
import ru.kirillius.json.rpc.Servlet.JSONRPCServlet;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
|
||||
public final class ContextEventsHandler {
|
||||
|
|
@ -10,4 +11,6 @@ public final class ContextEventsHandler {
|
|||
private final EventHandler<NetworkResourceBundle> networkManagerUpdateEvent = new ConcurrentEventHandler<>();
|
||||
@Getter
|
||||
private final EventHandler<NetworkResourceBundle> subscriptionsUpdateEvent = new ConcurrentEventHandler<>();
|
||||
@Getter
|
||||
private final EventHandler<JSONRPCServlet> RPCInitEvent = new ConcurrentEventHandler<>();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,28 +1,42 @@
|
|||
package ru.kirillius.pf.sdn.core.Networking;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Util.IPv4Util;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.io.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.*;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public class NetworkManager implements Closeable {
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
private final static String CTX = NetworkManager.class.getSimpleName();
|
||||
private final Context context;
|
||||
private final File cacheFile;
|
||||
|
||||
public NetworkManager(Context context) {
|
||||
this.context = context;
|
||||
cacheFile = new File(context.getConfig().getCacheDirectory(), "as-cache.json");
|
||||
if (cacheFile.exists() && context.getConfig().isCachingAS()) {
|
||||
SystemLogger.message("Loading as cache file", CTX);
|
||||
try (var is = new FileInputStream(cacheFile)) {
|
||||
var json = new JSONObject(new JSONTokener(is));
|
||||
json.keySet().forEach(key -> {
|
||||
var as = Integer.parseInt(key);
|
||||
prefixCache.put(as, JSONUtility.deserializeCollection(json.getJSONArray(key), IPv4Subnet.class, null).toList());
|
||||
});
|
||||
} catch (Exception e) {
|
||||
SystemLogger.error("Failed to load as cache file " + cacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final AtomicReference<Future<?>> updateProcess = new AtomicReference<>();
|
||||
private final AtomicBoolean cacheInvalid = new AtomicBoolean(true);
|
||||
@Getter
|
||||
private final NetworkResourceBundle inputResources = new NetworkResourceBundle();
|
||||
@Getter
|
||||
|
|
@ -35,7 +49,7 @@ public class NetworkManager implements Closeable {
|
|||
|
||||
private final Map<Integer, List<IPv4Subnet>> prefixCache = new ConcurrentHashMap<>();
|
||||
|
||||
public void triggerUpdate() {
|
||||
public void triggerUpdate(boolean ignoreCache) {
|
||||
if (isUpdatingNow()) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -48,8 +62,23 @@ public class NetworkManager implements Closeable {
|
|||
var asn = new ArrayList<>(inputResources.getASN());
|
||||
asn.removeAll(filteredResources.getASN());
|
||||
|
||||
if (cacheInvalid.get()) {
|
||||
fetchPrefixes(asn);
|
||||
var asnToFetch = new ArrayList<>(asn);
|
||||
if (!ignoreCache) {
|
||||
asnToFetch.removeAll(prefixCache.keySet());
|
||||
}
|
||||
|
||||
fetchPrefixes(asnToFetch);
|
||||
|
||||
if (config.isCachingAS()) {
|
||||
try (var os = new FileOutputStream(cacheFile)) {
|
||||
var json = new JSONObject();
|
||||
prefixCache.forEach((key, asnList) -> {
|
||||
json.put(String.valueOf(key), JSONUtility.serializeCollection(asnList, IPv4Subnet.class, null));
|
||||
});
|
||||
os.write(json.toString().getBytes());
|
||||
} catch (IOException e) {
|
||||
SystemLogger.error("Unable to write file " + cacheFile.getPath(), CTX, e);
|
||||
}
|
||||
}
|
||||
|
||||
var subnets = new HashSet<>(inputResources.getSubnets());
|
||||
|
|
@ -100,14 +129,10 @@ public class NetworkManager implements Closeable {
|
|||
}
|
||||
|
||||
|
||||
public void invalidateCache() {
|
||||
cacheInvalid.set(true);
|
||||
}
|
||||
|
||||
private void fetchPrefixes(List<Integer> systems) {
|
||||
var service = context.getASInfoService();
|
||||
systems.forEach(as -> {
|
||||
SystemLogger.message("Fetching AS" + as + " prefixes...", CTX);
|
||||
var service = context.getASInfoService();
|
||||
var future = service.getPrefixes(as);
|
||||
|
||||
while (!future.isDone() && !future.isCancelled()) {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ public class PluginConfigStorage {
|
|||
try {
|
||||
var pluginClass = loader.loadClass(key);
|
||||
var value = json.getJSONObject(key);
|
||||
var configClass = Plugin.getConfigClass((Class) pluginClass);
|
||||
var configClass = Component.getConfigClass((Class) pluginClass);
|
||||
storage.configs.put((Class)pluginClass, JSONUtility.deserializeStructure(value, configClass));
|
||||
} catch (ClassNotFoundException e) {
|
||||
throw new RuntimeException(e);
|
||||
|
|
@ -46,13 +46,13 @@ public class PluginConfigStorage {
|
|||
}
|
||||
}
|
||||
|
||||
private final Map<Class<? extends Plugin<?>>, Object> configs = new ConcurrentHashMap<>();
|
||||
private final Map<Class<? extends Component<?>>, Object> configs = new ConcurrentHashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@SneakyThrows
|
||||
public <CT> CT getConfig(Class<? extends Plugin<CT>> pluginClass) {
|
||||
public <CT> CT getConfig(Class<? extends Component<CT>> pluginClass) {
|
||||
if (!configs.containsKey(pluginClass)) {
|
||||
var configClass = Plugin.getConfigClass(pluginClass);
|
||||
var configClass = Component.getConfigClass(pluginClass);
|
||||
var instance = configClass.getConstructor().newInstance();
|
||||
configs.put(pluginClass, instance);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,9 +3,11 @@ package ru.kirillius.pf.sdn.core.Subscription;
|
|||
import lombok.Getter;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.core.Networking.NetworkResourceBundle;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
|
|
@ -34,12 +36,17 @@ public class SubscriptionManager implements Closeable {
|
|||
return future != null && !future.isDone() && !future.isCancelled();
|
||||
}
|
||||
|
||||
@Getter
|
||||
private final Map<String, NetworkResourceBundle> availableResources = new ConcurrentHashMap<>();
|
||||
|
||||
|
||||
public synchronized void triggerUpdate() {
|
||||
if (isUpdatingNow()) {
|
||||
return;
|
||||
}
|
||||
updateProcess.set(executor.submit(() -> {
|
||||
var available = new HashMap<String, NetworkResourceBundle>();
|
||||
|
||||
var bundle = new NetworkResourceBundle();
|
||||
|
||||
var config = context.getConfig();
|
||||
|
|
@ -61,20 +68,24 @@ public class SubscriptionManager implements Closeable {
|
|||
if (subscribedResources.contains(resourceName)) {
|
||||
bundle.add(resources.get(key));
|
||||
}
|
||||
available.put(resourceName, resources.get(key));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
availableResources.clear();
|
||||
availableResources.putAll(available);
|
||||
outputResources.clear();
|
||||
outputResources.add(bundle);
|
||||
try {
|
||||
context.getEventsHandler().getSubscriptionsUpdateEvent().invoke(outputResources);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e); //FIXME to LOG
|
||||
SystemLogger.error("Error on update event", CTX, e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
private final static String CTX = SubscriptionManager.class.getSimpleName();
|
||||
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
package ru.kirillius.pf.sdn.core;
|
||||
|
||||
import lombok.SneakyThrows;
|
||||
import ru.kirillius.pf.sdn.core.Util.Wait;
|
||||
import ru.kirillius.utils.logging.SystemLogger;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
|
||||
public class UpdateManager implements Closeable {
|
||||
private final Thread updateThread;
|
||||
|
||||
public UpdateManager(Context context) {
|
||||
this.context = context;
|
||||
updateThread = new Thread(new ThreadWorker());
|
||||
}
|
||||
|
||||
public void start() {
|
||||
updateThread.start();
|
||||
}
|
||||
|
||||
private final Context context;
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
updateThread.interrupt();
|
||||
}
|
||||
|
||||
private final static String CTX = UpdateManager.class.getSimpleName();
|
||||
|
||||
private class ThreadWorker implements Runnable {
|
||||
|
||||
@SneakyThrows
|
||||
@Override
|
||||
public void run() {
|
||||
var uptime = 0L;
|
||||
var config = context.getConfig();
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
Thread.sleep(Duration.ofMinutes(1));
|
||||
uptime++;
|
||||
|
||||
|
||||
if (config.getUpdateSubscriptionsInterval() > 0 && uptime % (config.getUpdateSubscriptionsInterval()*60L) == 0) {
|
||||
SystemLogger.message("Updating subscriptions", CTX);
|
||||
var subscriptionManager = context.getSubscriptionManager();
|
||||
subscriptionManager.triggerUpdate();
|
||||
Wait.until(subscriptionManager::isUpdatingNow);
|
||||
Wait.when(subscriptionManager::isUpdatingNow);
|
||||
}
|
||||
|
||||
if (config.getUpdateASInterval() > 0 && uptime % (config.getUpdateASInterval() * 60L) == 0) {
|
||||
SystemLogger.message("Updating cached AS", CTX);
|
||||
var networkManager = context.getNetworkManager();
|
||||
networkManager.triggerUpdate(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -57,6 +57,14 @@ public class IPv4Util {
|
|||
return ((ipLong >> 24) & 0xFF) + "." + ((ipLong >> 16) & 0xFF) + "." + ((ipLong >> 8) & 0xFF) + "." + (ipLong & 0xFF);
|
||||
}
|
||||
|
||||
public static String maskToString(long maskLong) {
|
||||
return String.format("%d.%d.%d.%d",
|
||||
(maskLong >> 24) & 0xff,
|
||||
(maskLong >> 16) & 0xff,
|
||||
(maskLong >> 8) & 0xff,
|
||||
maskLong & 0xff);
|
||||
}
|
||||
|
||||
public interface SummarisationResult {
|
||||
List<IPv4Subnet> getResult();
|
||||
|
||||
|
|
@ -97,7 +105,7 @@ public class IPv4Util {
|
|||
}
|
||||
if (parent.overlaps(subnet)) {
|
||||
overlapped.add(subnet);
|
||||
if(source.contains(subnet)) {
|
||||
if (source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
}
|
||||
|
|
@ -135,10 +143,10 @@ public class IPv4Util {
|
|||
result.remove(subnet);
|
||||
result.remove(next);
|
||||
result.add(largerSubnet);
|
||||
if(source.contains(subnet)) {
|
||||
if (source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
if(source.contains(next)) {
|
||||
if (source.contains(next)) {
|
||||
mergedSubnets.add(next);
|
||||
}
|
||||
i++;
|
||||
|
|
@ -215,7 +223,7 @@ public class IPv4Util {
|
|||
if (100.0 * used.get() / candidate.count() >= usePercentage) {
|
||||
//подсеть подходит под критерий
|
||||
overlapped.forEach(subnet -> {
|
||||
if(source.contains(subnet)) {
|
||||
if (source.contains(subnet)) {
|
||||
mergedSubnets.add(subnet);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
package ru.kirillius.pf.sdn.core.Util;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public final class Wait {
|
||||
private Wait() {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
public static void until(Supplier<Boolean> condition) throws InterruptedException {
|
||||
if (condition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (!condition.get() && !Thread.currentThread().isInterrupted()) {
|
||||
Thread.sleep(Duration.ofSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
public static void when(Supplier<Boolean> condition) throws InterruptedException {
|
||||
if (condition == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (condition.get() && !Thread.currentThread().isInterrupted()) {
|
||||
Thread.sleep(Duration.ofSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>ovpn-connector</artifactId>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>24</maven.compiler.source>
|
||||
<maven.compiler.target>24</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>1.4</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>module-info.class</exclude>
|
||||
<exclude>JDOMAbout*class</exclude>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
<transformers>
|
||||
<transformer
|
||||
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||
<manifestEntries>
|
||||
<Main-Class>ru.kirillius.pf.sdn.External.API.ovpn.connector.App</Main-Class>
|
||||
</manifestEntries>
|
||||
</transformer>
|
||||
</transformers>
|
||||
<shadedArtifactAttached>true</shadedArtifactAttached> <!-- Make the shaded artifact not the main one -->
|
||||
<shadedClassifierName>shaded</shadedClassifierName> <!-- set the suffix to the shaded jar -->
|
||||
</configuration>
|
||||
</plugin>
|
||||
|
||||
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
90
ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java
vendored
Normal file
90
ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/App.java
vendored
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package ru.kirillius.pf.sdn.External.API.ovpn.connector;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
import org.json.JSONTokener;
|
||||
import ru.kirillius.json.JSONUtility;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.ConnectException;
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
import java.time.Duration;
|
||||
|
||||
public class App {
|
||||
private static Config loadConfig() {
|
||||
var configFile = new File("ovpn-connector.json");
|
||||
if (!configFile.exists()) {
|
||||
throw new RuntimeException("Config file " + configFile.getAbsolutePath() + " does not exist");
|
||||
}
|
||||
try (var stream = new FileInputStream(configFile)) {
|
||||
return JSONUtility.deserializeStructure(new JSONObject(new JSONTokener(stream)), Config.class);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to load config file", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void pushRoutes(JSONArray routes, File pushFile) {
|
||||
try (var writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(pushFile)))) {
|
||||
for (var i = 0; i < routes.length(); i++) {
|
||||
var entry = routes.getJSONObject(i);
|
||||
var route = "push \"route " + entry.getString("address") + " " + entry.getString("mask") + "\"\n";
|
||||
writer.write(route);
|
||||
}
|
||||
writer.flush();
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Failed to write PUSH file", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
try {
|
||||
var config = loadConfig();
|
||||
|
||||
if (args.length < 1) {
|
||||
throw new RuntimeException("OVPN Push file is not specified in ARGS");
|
||||
}
|
||||
var pushFile = new File(args[0]);
|
||||
var routes = sendRequest(config);
|
||||
pushRoutes(routes, pushFile);
|
||||
System.exit(0);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace(System.err);
|
||||
System.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private static JSONArray sendRequest(Config config) throws IOException, InterruptedException {
|
||||
var json = new JSONObject();
|
||||
json.put("jsonrpc", "2.0");
|
||||
json.put("method", "ru.kirillius.pf.sdn.External.API.OVPNPlugin::getManagedRoutes");
|
||||
json.put("params", new JSONObject());
|
||||
json.put("id", 1);
|
||||
|
||||
|
||||
try (var httpClient = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)).build()) {
|
||||
// Создаем HTTP запрос
|
||||
var request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(config.getHost() + "/jsonrpc/rpc"))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-Auth-token", config.getToken())
|
||||
.POST(HttpRequest.BodyPublishers.ofString(json.toString()))
|
||||
.timeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
// Отправляем запрос и получаем ответ
|
||||
var response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
|
||||
|
||||
// Проверяем статус ответа
|
||||
if (response.statusCode() != 200) {
|
||||
throw new RuntimeException("HTTP error code: " + response.statusCode() + " body: " + response.body());
|
||||
}
|
||||
|
||||
return new JSONArray(new JSONObject(response.body()).getJSONArray("result"));
|
||||
} catch (ConnectException e) {
|
||||
throw new RuntimeException("Unable to connect to remote server", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
16
ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java
vendored
Normal file
16
ovpn-connector/src/main/java/ru/kirillius/pf/sdn/External/API/ovpn/connector/Config.java
vendored
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
package ru.kirillius.pf.sdn.External.API.ovpn.connector;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import ru.kirillius.json.JSONProperty;
|
||||
import ru.kirillius.json.JSONSerializable;
|
||||
|
||||
@JSONSerializable
|
||||
@Getter
|
||||
@Setter
|
||||
public class Config {
|
||||
@JSONProperty
|
||||
private String token = "NO_TOKEN_IS_SET";
|
||||
@JSONProperty
|
||||
private String host = "http://127.0.0.1:8080";
|
||||
}
|
||||
76
pom.xml
76
pom.xml
|
|
@ -9,10 +9,11 @@
|
|||
<version>0.1.0.0</version>
|
||||
<packaging>pom</packaging>
|
||||
<modules>
|
||||
<module>web-client</module>
|
||||
<module>core</module>
|
||||
<module>web-server</module>
|
||||
|
||||
<module>app</module>
|
||||
<module>ovpn-connector</module>
|
||||
|
||||
</modules>
|
||||
|
||||
<properties>
|
||||
|
|
@ -21,6 +22,30 @@
|
|||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
</properties>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.8.1</version>
|
||||
<configuration>
|
||||
<source>${maven.compiler.source}</source>
|
||||
<target>${maven.compiler.target}</target>
|
||||
|
||||
<annotationProcessorPaths>
|
||||
<path>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.40</version>
|
||||
</path>
|
||||
</annotationProcessorPaths>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
|
|
@ -53,46 +78,6 @@
|
|||
<scope>test</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.javatuples</groupId>
|
||||
<artifactId>javatuples</artifactId>
|
||||
<version>1.2</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.cronutils</groupId>
|
||||
<artifactId>cron-utils</artifactId>
|
||||
<version>9.2.0</version>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>json-rpc-servlet</artifactId>
|
||||
<version>2.1.4.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ru.kirillius.utils</groupId>
|
||||
<artifactId>common-logging</artifactId>
|
||||
<version>1.3.0.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-jdk14</artifactId>
|
||||
<version>2.0.9</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jetty/jetty-server -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jetty</groupId>
|
||||
<artifactId>jetty-server</artifactId>
|
||||
<version>12.0.12</version>
|
||||
</dependency>
|
||||
|
||||
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
|
||||
<dependency>
|
||||
|
|
@ -102,6 +87,13 @@
|
|||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
|
||||
<dependency>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>json-rpc-servlet</artifactId>
|
||||
<version>2.1.5.0</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>web-client</artifactId>
|
||||
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>pf-sdn</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>web-server</artifactId>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>ru.kirillius</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
<version>0.1.0.0</version>
|
||||
<scope>compile</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
|
|
@ -1,52 +0,0 @@
|
|||
package ru.kirillius.pf.sdn.web.RPC;
|
||||
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCArgument;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCContext;
|
||||
import ru.kirillius.json.rpc.Annotations.JRPCMethod;
|
||||
import ru.kirillius.json.rpc.Servlet.CallContext;
|
||||
import ru.kirillius.pf.sdn.core.Auth.AuthToken;
|
||||
import ru.kirillius.pf.sdn.core.Context;
|
||||
import ru.kirillius.pf.sdn.web.ProtectedMethod;
|
||||
|
||||
public class AuthRPC implements RPC {
|
||||
private final Context context;
|
||||
|
||||
public AuthRPC(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public AuthToken getAuthToken(@JRPCContext CallContext call) {
|
||||
return context.getAuthManager().getSessionToken(call.getSession());
|
||||
}
|
||||
|
||||
@ProtectedMethod
|
||||
@JRPCMethod
|
||||
public AuthToken rememberCurrentUser(@JRPCContext CallContext call) {
|
||||
var UA = call.getRequest().getHeader("User-Agent");
|
||||
if (UA == null) {
|
||||
UA = "Unknown user agent";
|
||||
}
|
||||
var authManager = context.getAuthManager();
|
||||
var token = authManager.createToken(UA);
|
||||
authManager.setSessionToken(call.getSession(), token);
|
||||
return token;
|
||||
}
|
||||
|
||||
@JRPCMethod
|
||||
public boolean auth(@JRPCArgument(name = "password") String password, @JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
if (authManager.validatePassword(password)) {
|
||||
authManager.setSessionAuthState(call.getSession(), true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@JRPCMethod
|
||||
public void logout(@JRPCContext CallContext call) {
|
||||
var authManager = context.getAuthManager();
|
||||
authManager.setSessionAuthState(call.getSession(), false);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
# webui-vue
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
|
||||
## Recommended Browser Setup
|
||||
|
||||
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
|
||||
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
|
||||
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
|
||||
- Firefox:
|
||||
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
|
||||
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
npm install
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Compile and Minify for Production
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
```
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en" class="dark-theme">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SDN Control</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<div id="loader" style="height: 100vh; display: flex; justify-content: center; align-items: center; color: #fff;">
|
||||
Загрузка...
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
"name": "webui",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || >=22.12.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"jquery": "^3.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^6.0.1",
|
||||
"sass": "^1.93.2",
|
||||
"vite": "^7.1.7",
|
||||
"vite-plugin-vue-devtools": "^8.0.2"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,167 @@
|
|||
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", {});
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
// src/main.js
|
||||
|
||||
import './styles/app.css';
|
||||
import $ from 'jquery';
|
||||
window.$ = window.jQuery = $;
|
||||
|
||||
// 1. Импорт JSONRPC по алиасу (предполагаем, что алиас настроен в vite.config.js)
|
||||
import { JSONRPC } from '@/json-rpc.js';
|
||||
|
||||
// 🔥 Указываем URL сервера, как ты и сделал
|
||||
JSONRPC.url = "http://localhost:8080" + JSONRPC.url;
|
||||
|
||||
// 2. Импорт главного модуля приложения
|
||||
import { initApp } from './modules/app.js';
|
||||
|
||||
// Запуск приложения после загрузки DOM
|
||||
$(document).ready(() => {
|
||||
$('#loader').remove();
|
||||
initApp();
|
||||
});
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { renderLoginForm, renderDashboard } from './ui.js';
|
||||
import { checkAuthOnStartup } from './auth.js'; // 🔥 Теперь импортируем здесь
|
||||
|
||||
// Глобальный статус приложения
|
||||
export let isAuthenticated = false;
|
||||
|
||||
// Главная функция инициализации
|
||||
export async function initApp() {
|
||||
// 1. Проверяем авторизацию
|
||||
const isInitialAuth = await checkAuthOnStartup();
|
||||
setAuthenticated(isInitialAuth); // Устанавливаем состояние и рендерим
|
||||
}
|
||||
|
||||
// Функция для смены состояния (например, после успешного логина/выхода)
|
||||
export function setAuthenticated(status) {
|
||||
isAuthenticated = status;
|
||||
|
||||
if (status) {
|
||||
renderDashboard();
|
||||
} else {
|
||||
renderLoginForm();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { getAuthToken, setAuthToken, removeAuthToken } from '../utils/cookies.js';
|
||||
|
||||
// ⚠️ ОБНОВИ ПУТЬ! Этот импорт должен указывать на твой модуль для работы с RPC.
|
||||
// Предполагается, что JSONRPC.Auth содержит методы:
|
||||
// - isAuthenticated()
|
||||
// - startSessionByToken(token)
|
||||
// - startSessionByPassword(password)
|
||||
// - createToken()
|
||||
// - logout()
|
||||
import { JSONRPC } from '@/json-rpc.js';
|
||||
|
||||
|
||||
/**
|
||||
* @private
|
||||
* Проверяет, активна ли сессия (через API) или сохранен ли токен в cookies.
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function checkAuthOnStartup() {
|
||||
try {
|
||||
// 1. Проверяем активную сессию (самый надежный способ)
|
||||
const isAuthenticated = await JSONRPC.Auth.isAuthenticated();
|
||||
if (isAuthenticated) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 2. Сессии нет, ищем токен в Cookie
|
||||
const token = getAuthToken();
|
||||
if (token) {
|
||||
console.log("Токен найден, пытаемся стартовать сессию...");
|
||||
// 3. Пытаемся стартовать сессию по токену
|
||||
const success = await JSONRPC.Auth.startSessionByToken(token);
|
||||
if (success) {
|
||||
console.log("Сессия успешно восстановлена по токену.");
|
||||
return true;
|
||||
} else {
|
||||
// Токен недействителен или просрочен
|
||||
console.warn("Токен недействителен, удаляем его.");
|
||||
removeAuthToken();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Нет ни сессии, ни токена
|
||||
return false;
|
||||
|
||||
} catch (err) {
|
||||
console.error('Ошибка при стартовой проверке аутентификации:', err);
|
||||
// В случае ошибки сети/бэкенда, предполагаем, что не авторизованы
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Обрабатывает вход по паролю.
|
||||
* @param {string} password - Пароль пользователя.
|
||||
* @param {boolean} rememberMe - Флаг, указывающий, нужно ли сохранить токен.
|
||||
* @returns {Promise<boolean>} - true, если вход успешен.
|
||||
*/
|
||||
export async function handleLogin(password, rememberMe) {
|
||||
// 1. Пытаемся стартовать сессию по паролю
|
||||
const success = await JSONRPC.Auth.startSessionByPassword(password);
|
||||
|
||||
if (success) {
|
||||
if (rememberMe) {
|
||||
// 2. Успех: если "запомнить меня", создаем и сохраняем токен
|
||||
const token = await JSONRPC.Auth.createToken();
|
||||
setAuthToken(token);
|
||||
console.log("Токен создан и сохранен.");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @public
|
||||
* Обрабатывает выход из системы.
|
||||
*/
|
||||
export async function handleLogout() {
|
||||
try {
|
||||
// 1. Завершаем сессию на стороне сервера
|
||||
await JSONRPC.Auth.logout();
|
||||
} catch(e) {
|
||||
console.warn("Ошибка при завершении сессии на сервере (возможно, уже была неактивна).", e);
|
||||
}
|
||||
|
||||
// 2. Удаляем токен из Cookie
|
||||
removeAuthToken();
|
||||
console.log("Выход выполнен, токен удален.");
|
||||
|
||||
// Обработчик в ui.js сменит состояние приложения
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
// src/modules/router.js
|
||||
|
||||
import $ from 'jquery';
|
||||
// 🔥 Импортируем только объект StatisticsPage
|
||||
import { StatisticsPage } from '../pages/Statistics.js';
|
||||
import { Subscriptions } from '../pages/Subscriptions.js';
|
||||
import { Components } from '../pages/Components.js';
|
||||
import { APITokens } from '../pages/APITokens.js';
|
||||
|
||||
// Переменная для отслеживания текущего активного хеша (для корректного unmount)
|
||||
let currentRouteHash = '';
|
||||
|
||||
|
||||
// 1. Определение меню и путей
|
||||
const menuItems = [
|
||||
{ label: 'Статистика', path: 'stats' },
|
||||
{ label: 'Подписки', path: 'subscriptions' },
|
||||
{ label: 'Настройки', path: 'settings' },
|
||||
{ label: 'Компоненты', path: 'components' },
|
||||
{ label: 'API', path: 'api' },
|
||||
];
|
||||
|
||||
// 2. Определение страниц
|
||||
const routes = {
|
||||
'#stats': {
|
||||
render: StatisticsPage.render,
|
||||
mount: StatisticsPage.mount,
|
||||
// 🔥 Используем unmount из объекта страницы
|
||||
unmount: StatisticsPage.unmount
|
||||
},
|
||||
'#subscriptions': {
|
||||
render: Subscriptions.render,
|
||||
mount: Subscriptions.mount,
|
||||
unmount: Subscriptions.unmount
|
||||
},
|
||||
'#settings': {
|
||||
render: () => '<h1 class="page-title">Системные Настройки</h1><p>Конфигурация сети, пользователя и безопасности.</p>',
|
||||
mount: () => {},
|
||||
unmount: () => {}
|
||||
},
|
||||
'#components': {
|
||||
render: Components.render,
|
||||
mount: Components.mount,
|
||||
unmount: Components.unmount
|
||||
},
|
||||
'#api': {
|
||||
render: APITokens.render,
|
||||
mount: APITokens.mount,
|
||||
unmount: APITokens.unmount
|
||||
}
|
||||
};
|
||||
|
||||
// 3. Функция рендеринга страницы
|
||||
export function renderPage(hash) {
|
||||
const $contentArea = $('#content-area');
|
||||
const key = hash.startsWith('#') ? hash : '#' + hash;
|
||||
|
||||
// 🔥 НОВАЯ ПРОВЕРКА: Если страница уже открыта, просто обновляем меню и выходим
|
||||
if (currentRouteHash === key) {
|
||||
$('.menu-item').removeClass('active');
|
||||
$(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active');
|
||||
return;
|
||||
}
|
||||
|
||||
// Определяем, какой хеш был активным до этого. Используем 'stats' как дефолт
|
||||
const previousKey = currentRouteHash || '#stats';
|
||||
|
||||
// Шаг 1: Если мы меняем страницу, и предыдущая страница имеет unmount, вызываем его
|
||||
if (previousKey !== key && routes[previousKey] && routes[previousKey].unmount) {
|
||||
routes[previousKey].unmount();
|
||||
}
|
||||
|
||||
if (routes[key]) {
|
||||
// Рендерим HTML
|
||||
$contentArea.html(routes[key].render());
|
||||
|
||||
// Вызываем функцию монтирования
|
||||
routes[key].mount();
|
||||
|
||||
// Обновляем активный пункт меню
|
||||
$('.menu-item').removeClass('active');
|
||||
$(`.menu-item[data-path="${key.substring(1)}"]`).addClass('active');
|
||||
|
||||
// Обновляем hash в адресной строке
|
||||
if (window.location.hash !== key) {
|
||||
history.pushState(null, null, key);
|
||||
}
|
||||
|
||||
// Шаг 2: Успешно обновили страницу, сохраняем новый хеш
|
||||
currentRouteHash = key;
|
||||
|
||||
} else {
|
||||
// Если путь не найден, перенаправляем на "Статистику"
|
||||
window.location.hash = 'stats';
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Обработчик изменения хеша в браузере (для навигации по истории)
|
||||
$(window).on('hashchange', function() {
|
||||
renderPage(window.location.hash);
|
||||
});
|
||||
|
||||
// Экспорт menuItems для построения сайдбара в ui.js
|
||||
export { menuItems };
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import $ from 'jquery';
|
||||
import { handleLogin, handleLogout } from './auth.js';
|
||||
import { setAuthenticated } from './app.js';
|
||||
import { renderPage, menuItems } from './router.js';
|
||||
|
||||
// Функция рендеринга формы авторизации
|
||||
export function renderLoginForm() {
|
||||
const html = `
|
||||
<div id="login-container" class="full-screen flex-center">
|
||||
<div class="card" style="width: 380px;">
|
||||
<h2 style="margin-top: 0; color: var(--color-text);">Авторизация</h2>
|
||||
<form id="login-form">
|
||||
<div class="form-group">
|
||||
<label for="password-input">Пароль</label>
|
||||
<input type="password" id="password-input" class="form-control" placeholder="Введите пароль">
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="display: flex; align-items: center;">
|
||||
<input type="checkbox" id="remember-me" style="margin-right: 10px;">
|
||||
<label for="remember-me" style="margin-bottom: 0; font-weight: normal; cursor: pointer;">Запомнить меня</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" id="login-button" class="btn-primary">Войти</button>
|
||||
<div id="login-error" class="error-message" style="display: none;"></div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#app').html(html);
|
||||
|
||||
// Прикрепление событий jQuery
|
||||
$('#login-form').on('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const $button = $('#login-button');
|
||||
const $error = $('#login-error');
|
||||
const password = $('#password-input').val();
|
||||
const rememberMe = $('#remember-me').prop('checked');
|
||||
|
||||
$error.hide().text('');
|
||||
$button.prop('disabled', true).text('Вход...');
|
||||
|
||||
try {
|
||||
const success = await handleLogin(password, rememberMe);
|
||||
if (success) {
|
||||
setAuthenticated(true);
|
||||
} else {
|
||||
$error.text('Ошибка входа: Неверный пароль.').show();
|
||||
}
|
||||
} catch (err) {
|
||||
$error.text('Произошла ошибка при попытке входа.').show();
|
||||
} finally {
|
||||
$button.prop('disabled', false).text('Войти');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Функция рендеринга рабочего стола (Dashboard)
|
||||
export function renderDashboard() {
|
||||
// Используем menuItems из router.js для построения сайдбара
|
||||
const sidebarHtml = menuItems.map(item => {
|
||||
return `<a href="#${item.path}" class="menu-item" data-path="${item.path}">${item.label}</a>`;
|
||||
}).join('');
|
||||
|
||||
const html = `
|
||||
<div id="dashboard-container" style="display: flex; min-height: 100vh;">
|
||||
<div id="sidebar" class="sidebar">
|
||||
<h2 class="sidebar-title">SDN Control</h2>
|
||||
<nav id="main-nav">${sidebarHtml}</nav>
|
||||
<a href="#" id="logout-btn" class="menu-item logout-btn">Выход</a>
|
||||
</div>
|
||||
|
||||
<main id="content-area" class="content-area">
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
|
||||
$('#app').html(html);
|
||||
|
||||
// Прикрепляем события для выхода
|
||||
$('#logout-btn').on('click', async function(e) {
|
||||
e.preventDefault();
|
||||
await handleLogout();
|
||||
setAuthenticated(false);
|
||||
});
|
||||
|
||||
// Запускаем роутер с текущим хешем (или 'stats' по умолчанию)
|
||||
renderPage(window.location.hash || '#stats');
|
||||
|
||||
// Прикрепляем события для меню (роутер)
|
||||
$('#main-nav').on('click', '.menu-item', function(e) {
|
||||
// Предотвращаем стандартный переход, чтобы обработать его через JS
|
||||
e.preventDefault();
|
||||
|
||||
const path = $(this).data('path');
|
||||
if (path) {
|
||||
window.location.hash = path;
|
||||
renderPage(path);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// src/pages/APITokens.js
|
||||
|
||||
import { APITokensPage } from '../ui/api_tokens.js';
|
||||
|
||||
export const APITokens = APITokensPage;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
// src/pages/Components.js
|
||||
|
||||
import { ComponentsPage } from '../ui/components.js';
|
||||
|
||||
export const Components = ComponentsPage;
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
// src/pages/Statistics.js
|
||||
|
||||
import { renderStatisticsPage, stopPolling } from '../ui/statistics.js';
|
||||
|
||||
export const StatisticsPage = {
|
||||
render: () => {
|
||||
return `
|
||||
<h1 class="page-title">Статистика</h1>
|
||||
<div id="statistics-content">
|
||||
Загрузка данных...
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
mount: () => {
|
||||
// Вызывает renderStatisticsPage, который запускает опрос
|
||||
renderStatisticsPage();
|
||||
},
|
||||
// 🔥 ВСТРОЕННАЯ ФУНКЦИЯ UNMOUNT: Роутер использует StatisticsPage.unmount
|
||||
unmount: stopPolling
|
||||
};
|
||||
|
||||
// 🔥 УДАЛЕН ЭКСПОРТ { stopPolling }; из этого файла.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// src/pages/Subscriptions.js
|
||||
|
||||
import { SubscriptionsPage } from '../ui/subscriptions.js';
|
||||
|
||||
// Просто реэкспортируем объект для роутера
|
||||
export const Subscriptions = SubscriptionsPage;
|
||||
|
|
@ -0,0 +1,411 @@
|
|||
/* --- Сброс и Тёмные Переменные --- */
|
||||
:root {
|
||||
/* Основная цветовая палитра */
|
||||
--color-primary: #3b82f6; /* Синий (Акцент) */
|
||||
--color-primary-dark: #2563eb;
|
||||
--color-text: #e5e7eb; /* Светлый текст */
|
||||
--color-text-secondary: #9ca3af; /* Второстепенный текст */
|
||||
--color-bg-main: #111827; /* Очень тёмный фон */
|
||||
--color-bg-card: #1f2937; /* Фон карточек и сайдбара */
|
||||
--color-border: #374151; /* Границы */
|
||||
--color-error: #ef4444; /* Красный */
|
||||
|
||||
--font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
--border-radius: 6px;
|
||||
}
|
||||
|
||||
/* Применяем переменные к тёмной теме */
|
||||
html.dark-theme, body {
|
||||
background-color: var(--color-bg-main);
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* --- Общие Классы UI и Формы (для Авторизации) --- */
|
||||
|
||||
/* Контейнеры */
|
||||
.flex-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.full-screen {
|
||||
min-height: 100vh;
|
||||
}
|
||||
.card {
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 30px;
|
||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.2), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* Форма и Поля */
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 10px;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-bg-main);
|
||||
color: var(--color-text);
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
font-size: 16px;
|
||||
}
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0 0 0 1px var(--color-primary);
|
||||
}
|
||||
|
||||
/* Кнопка */
|
||||
.btn-primary {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.btn-primary:hover:not(:disabled) {
|
||||
background-color: var(--color-primary-dark);
|
||||
}
|
||||
.btn-primary:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error-message {
|
||||
color: var(--color-error);
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
.success-message {
|
||||
color: #10b981; /* Зеленый */
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
|
||||
/* --- Стили Dashboard (Сайдбар и Общая Структура) --- */
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
background-color: var(--color-bg-card);
|
||||
border-right: 1px solid var(--color-border);
|
||||
flex-shrink: 0;
|
||||
padding: 20px 0;
|
||||
display: flex; /* Для прибивания кнопки выхода вниз */
|
||||
flex-direction: column;
|
||||
}
|
||||
.sidebar-title {
|
||||
color: var(--color-primary);
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
/* Стили меню */
|
||||
.menu-item {
|
||||
display: block;
|
||||
padding: 12px 20px;
|
||||
color: var(--color-text-secondary);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
font-size: 15px;
|
||||
}
|
||||
.menu-item:hover {
|
||||
background-color: #2e3a4e; /* Чуть светлее, чем сайдбар */
|
||||
color: var(--color-text);
|
||||
}
|
||||
.menu-item.active {
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
}
|
||||
.logout-btn {
|
||||
margin-top: auto; /* Прижимает к низу */
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.content-area {
|
||||
flex-grow: 1;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.page-title {
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 15px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
||||
/* --- Стили для страницы Статистики --- */
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Сетка для статусов (3 колонки) */
|
||||
.status-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: var(--color-bg-card);
|
||||
border-radius: var(--border-radius);
|
||||
padding: 20px;
|
||||
border: 1px solid var(--color-border);
|
||||
transition: box-shadow 0.3s;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.stat-card h3 {
|
||||
margin-top: 0;
|
||||
font-size: 18px;
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.status-line {
|
||||
font-size: 14px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.text-yellow-500 {
|
||||
color: #f59e0b; /* Желтый для "обновляется" */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.text-green-500 {
|
||||
color: #10b981; /* Зеленый для "активен" */
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* Кнопка вторичная (для обновления) */
|
||||
.btn-secondary {
|
||||
padding: 8px 15px;
|
||||
background-color: transparent;
|
||||
color: var(--color-primary);
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--border-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-secondary:hover:not(:disabled) {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
|
||||
.btn-secondary:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
color: var(--color-text-secondary);
|
||||
border-color: var(--color-border);
|
||||
}
|
||||
|
||||
/* --- Стили для страницы Статистики (Дополнение: Ресурсы) --- */
|
||||
|
||||
.resource-group {
|
||||
border-top: 1px solid var(--color-border);
|
||||
padding-top: 15px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.resource-group h4 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.resource-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.resource-badge {
|
||||
text-align: center;
|
||||
padding: 10px;
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-bg-main); /* Более темный фон для выделения */
|
||||
flex-grow: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Класс для размера чисел в блоках (50% от исходного размера) */
|
||||
.resource-count {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin-bottom: 3px;
|
||||
font-size: 20px; /* Базовый размер для подписок */
|
||||
}
|
||||
|
||||
/* Корректировка для уменьшения размера */
|
||||
.resource-count.small-count {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
.resource-title {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
|
||||
/* --- Стили для страницы Подписок --- */
|
||||
|
||||
.subscription-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 20px 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.subscription-item {
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.subscription-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.subscription-item label {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Выравниваем элементы по верху */
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.subscription-checkbox {
|
||||
margin-right: 15px;
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.resource-info {
|
||||
display: flex;
|
||||
flex-direction: column; /* Обертка для двух строк */
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.resource-main-line {
|
||||
/* Первая строка: Описание (Ключ) */
|
||||
display: flex;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.resource-description {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 🔥 ОБНОВЛЕНО: Сделаем resource-key (используется для shortName) крупнее */
|
||||
.resource-key {
|
||||
font-weight: 600; /* Жирный шрифт */
|
||||
color: var(--color-text); /* Основной цвет текста */
|
||||
font-size: 16px; /* Крупнее, чтобы лучше читалось */
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.resource-details {
|
||||
/* Вторая строка: Детали / или в случае компонентов - полное имя */
|
||||
font-size: 13px;
|
||||
color: var(--color-text-secondary);
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* --- Стили для страницы API Токенов --- */
|
||||
|
||||
.token-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--border-radius);
|
||||
background-color: var(--color-bg-card);
|
||||
}
|
||||
|
||||
.token-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.token-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.token-description {
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.token-details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Кнопка-иконка для удаления */
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--color-error);
|
||||
padding: 5px;
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.btn-icon:hover {
|
||||
background-color: rgba(239, 68, 68, 0.1); /* Легкий красный фон при наведении */
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
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(); // Удаляем все обработчики
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,134 @@
|
|||
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 = [];
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
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("Проверка обновлений ПО запущена...");
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,126 @@
|
|||
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 = [];
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
// src/utils/cookies.js
|
||||
|
||||
const TOKEN_KEY = 'sdn_auth_token';
|
||||
|
||||
/**
|
||||
* Получает токен авторизации из куки.
|
||||
* @returns {string|null} Токен или null, если не найден.
|
||||
*/
|
||||
export function getAuthToken() {
|
||||
const nameEQ = TOKEN_KEY + "=";
|
||||
const ca = document.cookie.split(';');
|
||||
for(let i=0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') c = c.substring(1, c.length);
|
||||
if (c.indexOf(nameEQ) === 0) {
|
||||
return c.substring(nameEQ.length, c.length);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает токен авторизации в куки на 1 год.
|
||||
* @param {string} token - Токен для сохранения.
|
||||
*/
|
||||
export function setAuthToken(token) {
|
||||
const expirationDate = new Date();
|
||||
// Устанавливаем срок действия на 1 год
|
||||
expirationDate.setTime(expirationDate.getTime() + (365 * 24 * 60 * 60 * 1000));
|
||||
const expires = "expires=" + expirationDate.toUTCString();
|
||||
|
||||
// Используем secure и samesite=Lax (рекомендуется для современных браузеров)
|
||||
// path=/ делает токен доступным для всего сайта
|
||||
document.cookie = `${TOKEN_KEY}=${token};${expires};path=/;samesite=Lax`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет токен авторизации.
|
||||
*/
|
||||
export function removeAuthToken() {
|
||||
document.cookie = `${TOKEN_KEY}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
// 🔥 ДОБАВЛЯЕМ ИМПОРТ node:path
|
||||
import path from 'node:path'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
},
|
||||
},
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// 🔥 ДОБАВЛЯЕМ ЭТОТ БЛОК! Явно разрешаем доступ к node_modules
|
||||
server: {
|
||||
fs: {
|
||||
allow: [
|
||||
// Позволяем доступ к корневой папке проекта (для src)
|
||||
'.',
|
||||
// Позволяем доступ к папке node_modules, чтобы Vite мог найти стили
|
||||
path.resolve(__dirname, 'node_modules')
|
||||
]
|
||||
}
|
||||
}
|
||||
// -----------------------------------------------------------------
|
||||
})
|
||||
Loading…
Reference in New Issue