From 898a170d7b9ff403e5f18be6ae24d583fd306aea Mon Sep 17 00:00:00 2001 From: kirillius Date: Mon, 12 Jan 2026 10:14:17 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20Ap?= =?UTF-8?q?iToken=20entity=20=D0=B4=D0=BB=D1=8F=20=D0=B0=D0=B2=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D0=B8,=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=B4=D0=BB=D1=8F=20RPC?= =?UTF-8?q?,=20=D1=80=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D0=BB=20ConfigManager,=20=D0=BB=D0=BE=D0=B3=D0=B3=D0=B8=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B8=20RPC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/pom.xml | 6 - .../java/ru/kirillius/XCP/Commons/Config.java | 3 +- .../kirillius/XCP/Commons/ConfigManager.java | 4 +- .../ru/kirillius/XCP/Commons/Context.java | 7 + .../ru/kirillius/XCP/Logging/LogLevel.java | 7 + .../ru/kirillius/XCP/Logging/LogMessage.java | 9 + .../java/ru/kirillius/XCP/Logging/Logger.java | 15 ++ .../kirillius/XCP/Logging/LoggingSystem.java | 14 ++ .../XCP/Persistence/Entities/ApiToken.java | 27 +++ .../XCP/Persistence/PersistenceException.java | 15 ++ .../Repositories/ApiTokenRepository.java | 10 + .../kirillius/XCP/Persistence/Repository.java | 12 +- .../RepositoryService.java | 8 +- .../XCP/Services/ServiceLoadPriority.java | 12 ++ .../ru/kirillius/XCP/Services/WebService.java | 6 + app/pom.xml | 52 +++++ .../java/ru/kirillius/XCP/Application.java | 142 ++++++++++++++ .../XCP/Security/ConfigManagerImpl.java | 80 ++++++++ .../XCP/Security/SecurityManagerImpl.java | 9 + database/pom.xml | 6 +- .../XCP/Persistence/AbstractRepository.java | 32 +-- .../EntityReferenceDeserializer.java | 4 +- .../Repositories/ApiTokenRepositoryImpl.java | 93 +++++++++ .../Repositories/GroupRepositoryImpl.java | 4 +- .../Repositories/InputRepositoryImpl.java | 4 +- .../Repositories/OutputRepositoryImpl.java | 4 +- .../Repositories/TagRepositoryImpl.java | 4 +- .../Repositories/UserRepositoryImpl.java | 2 +- .../Persistence/RepositoryServiceImpl.java | 23 ++- .../ApiTokenRepositoryImplTest.java | 27 +++ .../GenericNodeRepositoryTest.java | 16 +- .../Repositories/GenericRepositoryTest.java | 26 +-- .../Repositories/GroupRepositoryImplTest.java | 26 +-- .../Repositories/InputRepositoryImplTest.java | 8 +- .../OutputRepositoryImplTest.java | 8 +- .../Repositories/TagRepositoryImplTest.java | 10 +- .../Repositories/UserRepositoryImplTest.java | 30 ++- .../RepositoryServiceImplTest.java | 34 ++-- logging/pom.xml | 39 ++++ .../kirillius/XCP/Logging/LogHandlerImpl.java | 90 +++++++++ .../ru/kirillius/XCP/Logging/LoggerImpl.java | 36 ++++ .../XCP/Logging/LoggingSystemImpl.java | 50 +++++ pom.xml | 11 +- rpc/pom.xml | 60 ++++++ .../XCP/RPC/JSONRPC/CallContext.java | 19 ++ .../XCP/RPC/JSONRPC/JsonRpcError.java | 33 ++++ .../XCP/RPC/JSONRPC/JsonRpcErrorCode.java | 44 +++++ .../XCP/RPC/JSONRPC/JsonRpcMethod.java | 14 ++ .../XCP/RPC/JSONRPC/JsonRpcRequest.java | 19 ++ .../XCP/RPC/JSONRPC/JsonRpcResponse.java | 15 ++ .../XCP/RPC/JSONRPC/JsonRpcService.java | 33 ++++ .../XCP/RPC/JSONRPC/JsonRpcServlet.java | 184 ++++++++++++++++++ .../ru/kirillius/XCP/RPC/Services/Auth.java | 120 ++++++++++++ .../kirillius/XCP/RPC/Services/Profile.java | 63 ++++++ .../XCP/RPC/Services/UserManagement.java | 38 ++++ web-server/pom.xml | 34 ++++ .../ru/kirillius/XCP/web/WebServiceImpl.java | 72 +++++++ .../src/main/resources/htdocs/index.html | 10 + 58 files changed, 1663 insertions(+), 120 deletions(-) create mode 100644 api/src/main/java/ru/kirillius/XCP/Logging/LogLevel.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Logging/LogMessage.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Logging/Logger.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Logging/LoggingSystem.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceException.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepository.java rename api/src/main/java/ru/kirillius/XCP/{Persistence => Services}/RepositoryService.java (73%) create mode 100644 api/src/main/java/ru/kirillius/XCP/Services/ServiceLoadPriority.java create mode 100644 api/src/main/java/ru/kirillius/XCP/Services/WebService.java create mode 100644 app/pom.xml create mode 100644 app/src/main/java/ru/kirillius/XCP/Application.java create mode 100644 core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java create mode 100644 core/src/main/java/ru/kirillius/XCP/Security/SecurityManagerImpl.java create mode 100644 database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java create mode 100644 database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImplTest.java create mode 100644 logging/pom.xml create mode 100644 logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java create mode 100644 logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java create mode 100644 logging/src/main/java/ru/kirillius/XCP/Logging/LoggingSystemImpl.java create mode 100644 rpc/pom.xml create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/CallContext.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcError.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcErrorCode.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcRequest.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcResponse.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcService.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java create mode 100644 rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java create mode 100644 web-server/pom.xml create mode 100644 web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java create mode 100644 web-server/src/main/resources/htdocs/index.html diff --git a/api/pom.xml b/api/pom.xml index afe8d2c..de32b0b 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -17,11 +17,5 @@ java-events 1.1.0.0 - - - tools.jackson.core - jackson-databind - 3.0.3 - \ No newline at end of file diff --git a/api/src/main/java/ru/kirillius/XCP/Commons/Config.java b/api/src/main/java/ru/kirillius/XCP/Commons/Config.java index 154c954..b992cde 100644 --- a/api/src/main/java/ru/kirillius/XCP/Commons/Config.java +++ b/api/src/main/java/ru/kirillius/XCP/Commons/Config.java @@ -2,7 +2,8 @@ package ru.kirillius.XCP.Commons; import java.io.File; -public interface Config { +public interface +Config { File getLoadedConfigFile(); diff --git a/api/src/main/java/ru/kirillius/XCP/Commons/ConfigManager.java b/api/src/main/java/ru/kirillius/XCP/Commons/ConfigManager.java index 251358e..453364f 100644 --- a/api/src/main/java/ru/kirillius/XCP/Commons/ConfigManager.java +++ b/api/src/main/java/ru/kirillius/XCP/Commons/ConfigManager.java @@ -1,12 +1,14 @@ package ru.kirillius.XCP.Commons; +import java.io.File; import java.io.IOException; public interface ConfigManager { + File getConfigFile(); boolean isExist(); - Config load(); + Config load() throws IOException; Config create(); diff --git a/api/src/main/java/ru/kirillius/XCP/Commons/Context.java b/api/src/main/java/ru/kirillius/XCP/Commons/Context.java index af8995d..15f5ccc 100644 --- a/api/src/main/java/ru/kirillius/XCP/Commons/Context.java +++ b/api/src/main/java/ru/kirillius/XCP/Commons/Context.java @@ -1,7 +1,10 @@ package ru.kirillius.XCP.Commons; +import ru.kirillius.XCP.Logging.LoggingSystem; import ru.kirillius.XCP.Security.SecurityManager; +import java.util.List; + public interface Context { Config getConfig(); @@ -12,4 +15,8 @@ public interface Context { void shutdown(); SecurityManager getSecurityManager(); + + LoggingSystem getLoggingSystem(); + + List getLaunchArgs(); } diff --git a/api/src/main/java/ru/kirillius/XCP/Logging/LogLevel.java b/api/src/main/java/ru/kirillius/XCP/Logging/LogLevel.java new file mode 100644 index 0000000..6c347e9 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Logging/LogLevel.java @@ -0,0 +1,7 @@ +package ru.kirillius.XCP.Logging; + +public enum LogLevel { + INFO, + ERROR, + WARNING +} diff --git a/api/src/main/java/ru/kirillius/XCP/Logging/LogMessage.java b/api/src/main/java/ru/kirillius/XCP/Logging/LogMessage.java new file mode 100644 index 0000000..2624589 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Logging/LogMessage.java @@ -0,0 +1,9 @@ +package ru.kirillius.XCP.Logging; + +import lombok.Builder; + +import java.time.Instant; + +@Builder +public record LogMessage(LogLevel level, Instant date, String message) { +} diff --git a/api/src/main/java/ru/kirillius/XCP/Logging/Logger.java b/api/src/main/java/ru/kirillius/XCP/Logging/Logger.java new file mode 100644 index 0000000..c1eb81f --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Logging/Logger.java @@ -0,0 +1,15 @@ +package ru.kirillius.XCP.Logging; + +public interface Logger { + + + void info(String message); + + void warning(String message); + + void error(String message); + + void error(String message, Throwable error); + + void error(Throwable error); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Logging/LoggingSystem.java b/api/src/main/java/ru/kirillius/XCP/Logging/LoggingSystem.java new file mode 100644 index 0000000..ac7266f --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Logging/LoggingSystem.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.Logging; + +import ru.kirillius.java.utils.events.EventHandler; + +import java.io.Closeable; + +public interface LoggingSystem extends Closeable { + + Logger createLogger(Class clazz); + + Logger createLogger(String name); + + EventHandler getEventHandler(); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java new file mode 100644 index 0000000..41845a7 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Entities/ApiToken.java @@ -0,0 +1,27 @@ +package ru.kirillius.XCP.Persistence.Entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import ru.kirillius.XCP.Persistence.PersistenceEntity; + +import java.time.Instant; +import java.util.Date; + +public interface ApiToken extends PersistenceEntity { + + User getUser(); + + void setUser(User user); + + String getName(); + + void setName(String name); + + Date getExpirationDate(); + + void setExpirationDate(Date expirationDate); + + @JsonIgnore + default boolean isExpired() { + return getExpirationDate().toInstant().isBefore(Instant.now()); + } +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceException.java b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceException.java new file mode 100644 index 0000000..aed661b --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/PersistenceException.java @@ -0,0 +1,15 @@ +package ru.kirillius.XCP.Persistence; + +public class PersistenceException extends RuntimeException { + public PersistenceException(String message) { + super(message); + } + + public PersistenceException(String message, Throwable cause) { + super(message, cause); + } + + public PersistenceException(Throwable cause) { + super(cause); + } +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepository.java new file mode 100644 index 0000000..94b7627 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepository.java @@ -0,0 +1,10 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import ru.kirillius.XCP.Commons.StreamHandler; +import ru.kirillius.XCP.Persistence.Entities.ApiToken; +import ru.kirillius.XCP.Persistence.Entities.User; +import ru.kirillius.XCP.Persistence.Repository; + +public interface ApiTokenRepository extends Repository { + StreamHandler getByUser(User user); +} diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/Repository.java b/api/src/main/java/ru/kirillius/XCP/Persistence/Repository.java index c95254b..014f579 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/Repository.java +++ b/api/src/main/java/ru/kirillius/XCP/Persistence/Repository.java @@ -11,23 +11,23 @@ import java.util.UUID; public interface Repository { E create(); - E load(long id); + E get(long id); - E load(UUID uuid); + E get(UUID uuid); - StreamHandler load(Collection ids); + StreamHandler get(Collection ids); StreamHandler search(String query, Collection queryParameters); EventBindings events(); - StreamHandler loadAll(); + StreamHandler getAll(); long getCount(); - void store(E entity); + void save(E entity); - void store(Collection entities); + void save(Collection entities); void remove(E entity); diff --git a/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java b/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java similarity index 73% rename from api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java rename to api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java index 58010c5..9355f7a 100644 --- a/api/src/main/java/ru/kirillius/XCP/Persistence/RepositoryService.java +++ b/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java @@ -1,6 +1,9 @@ -package ru.kirillius.XCP.Persistence; +package ru.kirillius.XCP.Services; import ru.kirillius.XCP.Commons.Service; +import ru.kirillius.XCP.Persistence.PersistenceEntity; +import ru.kirillius.XCP.Persistence.Repository; +import tools.jackson.databind.ObjectMapper; public interface RepositoryService extends Service { Repository getRepositoryForEntity(Class entityType); @@ -8,8 +11,11 @@ public interface RepositoryService extends Service { > R getRepository(Class repositoryType); Class getEntityBaseType(Class entityClass); + Class> getRepositoryBaseType(Class> repositoryClass); + Class getRepositoryEntityType(Class> repositoryClass); + ObjectMapper getMapper(); } diff --git a/api/src/main/java/ru/kirillius/XCP/Services/ServiceLoadPriority.java b/api/src/main/java/ru/kirillius/XCP/Services/ServiceLoadPriority.java new file mode 100644 index 0000000..29ba312 --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Services/ServiceLoadPriority.java @@ -0,0 +1,12 @@ +package ru.kirillius.XCP.Services; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface ServiceLoadPriority { + int value() default 1000; +} diff --git a/api/src/main/java/ru/kirillius/XCP/Services/WebService.java b/api/src/main/java/ru/kirillius/XCP/Services/WebService.java new file mode 100644 index 0000000..040c7af --- /dev/null +++ b/api/src/main/java/ru/kirillius/XCP/Services/WebService.java @@ -0,0 +1,6 @@ +package ru.kirillius.XCP.Services; + +import ru.kirillius.XCP.Commons.Service; + +public interface WebService extends Service { +} diff --git a/app/pom.xml b/app/pom.xml new file mode 100644 index 0000000..889abd2 --- /dev/null +++ b/app/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + app + + + 21 + 21 + UTF-8 + + + + ru.kirillius + api + 1.0.0.0 + compile + + + ru.kirillius + core + 1.0.0.0 + compile + + + ru.kirillius + logging + 1.0.0.0 + compile + + + ru.kirillius + database + 1.0.0.0 + compile + + + ru.kirillius + web-server + 1.0.0.0 + compile + + + + \ No newline at end of file diff --git a/app/src/main/java/ru/kirillius/XCP/Application.java b/app/src/main/java/ru/kirillius/XCP/Application.java new file mode 100644 index 0000000..7827ed5 --- /dev/null +++ b/app/src/main/java/ru/kirillius/XCP/Application.java @@ -0,0 +1,142 @@ +package ru.kirillius.XCP; + +import lombok.Getter; +import ru.kirillius.XCP.Commons.Config; +import ru.kirillius.XCP.Commons.ConfigManager; +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.XCP.Commons.Service; +import ru.kirillius.XCP.Logging.Logger; +import ru.kirillius.XCP.Logging.LoggingSystem; +import ru.kirillius.XCP.Logging.LoggingSystemImpl; +import ru.kirillius.XCP.Persistence.RepositoryServiceImpl; +import ru.kirillius.XCP.Security.ConfigManagerImpl; +import ru.kirillius.XCP.Security.SecurityManager; +import ru.kirillius.XCP.Security.SecurityManagerImpl; +import ru.kirillius.XCP.Services.ServiceLoadPriority; +import ru.kirillius.XCP.Services.WebService; +import ru.kirillius.XCP.web.WebServiceImpl; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Getter +public class Application implements Context { + @Getter + private final SecurityManager securityManager; + @Getter + private final LoggingSystem loggingSystem; + private final Logger log; + @Getter + private final List launchArgs; + @Getter + private final Config config; + @Getter + private final ConfigManager configManager; + private final Map, Service> services = new ConcurrentHashMap<>(); + + public Application(String[] args) { + launchArgs = Arrays.stream(args).toList(); + loggingSystem = new LoggingSystemImpl(this); + log = loggingSystem.createLogger(Application.class); + configManager = new ConfigManagerImpl(this); + if (configManager.isExist()) { + try { + config = configManager.load(); + log.info("Loaded config file: " + configManager.getConfigFile().getAbsolutePath()); + } catch (IOException e) { + log.error("Error loading config file " + configManager.getConfigFile(), e); + throw new RuntimeException(e); + } + } else { + log.warning("Unable to find config file " + configManager.getConfigFile().getAbsolutePath() + ". Using default values."); + config = configManager.create(); + } + securityManager = new SecurityManagerImpl(); + try { + loadServices(); + } catch (Throwable throwable) { + log.error(throwable); + shutdown(); + throw new RuntimeException("Error loading services"); + } + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); + + ((WebServiceImpl) getService(WebService.class)).join(); + } + + private void loadServices() { + var servicesToLoad = List.of(RepositoryServiceImpl.class, WebServiceImpl.class); + + servicesToLoad.stream().sorted(Comparator.comparingInt(aClass -> { + var order = aClass.getAnnotation(ServiceLoadPriority.class); + return order == null ? 100000 : order.value(); + })).forEach(aClass -> { + log.info("Loading service " + aClass.getSimpleName()); + try { + var constructor = aClass.getConstructor(); + try { + var service = constructor.newInstance(); + try { + service.initialize(this); + @SuppressWarnings("unchecked") var facade = (Class) Arrays.stream(aClass.getInterfaces()) + .filter(Service.class::isAssignableFrom) + .findFirst(). + orElseThrow(() -> new ClassCastException("Unable to get service interface from class " + aClass.getSimpleName())); + services.put(facade, service); + } catch (Throwable e) { + try { + service.close(); + } catch (IOException ex) { + e.addSuppressed(ex); + } + throw new RuntimeException("Failed to start service " + aClass.getSimpleName(), e); + } + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Failed to instantiate service " + aClass.getSimpleName(), e); + } + + } catch (NoSuchMethodException e) { + throw new RuntimeException("Failed to find default constructor of service " + aClass.getSimpleName(), e); + } + }); + } + + @SuppressWarnings("unchecked") + @Override + public S getService(Class serviceClass) { + return (S) services.get(serviceClass); + } + + @Override + public void shutdown() { + try { + services.forEach((serviceClass, service) -> { + try { + log.info("Shutting down service " + serviceClass.getSimpleName()); + service.close(); + } catch (IOException e) { + log.error("Error shutting down service " + serviceClass.getSimpleName(), e); + } + }); + loggingSystem.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static void main(String[] args) { + try { + new Application(args); + } catch (Throwable e) { + System.err.println("Error starting application"); + e.printStackTrace(System.err); + System.exit(1); + } + } + +} diff --git a/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java b/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java new file mode 100644 index 0000000..13eabce --- /dev/null +++ b/core/src/main/java/ru/kirillius/XCP/Security/ConfigManagerImpl.java @@ -0,0 +1,80 @@ +package ru.kirillius.XCP.Security; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import ru.kirillius.XCP.Commons.Config; +import ru.kirillius.XCP.Commons.ConfigManager; +import ru.kirillius.XCP.Commons.Context; +import tools.jackson.databind.ObjectMapper; + +import java.io.*; + +public final class ConfigManagerImpl implements ConfigManager { + private final Context context; + private final static String DEFAULT_CONFIG_PATH = "./xcp.conf"; + private final static String DEFAULT_DB_PATH = "./xcpdata"; + private final ObjectMapper mapper = new ObjectMapper(); + + public ConfigManagerImpl(Context context) { + this.context = context; + configFile = findConfigFile(); + } + + private File findConfigFile() { + return new File(context.getLaunchArgs().stream().filter(a -> a.startsWith("--config=")).findFirst().orElse(DEFAULT_CONFIG_PATH)); + } + + @Getter + private final File configFile; + + @Override + public boolean isExist() { + return configFile.exists(); + } + + @Override + public Config load() throws IOException { + try (var stream = new FileInputStream(configFile)) { + return mapper.readValue(stream, ConfigImpl.class); + } + } + + @Override + public Config create() { + var config = new ConfigImpl(); + config.loadedConfigFile = configFile; + return config; + } + + @Override + public void save(Config config) throws IOException { + try (var stream = new FileOutputStream(configFile)) { + mapper.writeValue(stream, config); + } + } + + @NoArgsConstructor + private final static class ConfigImpl implements Config { + @Getter + @JsonIgnore + private File loadedConfigFile; + + @Getter + @Setter + @JsonProperty + private String host = "127.0.0.1"; + + @Getter + @Setter + @JsonProperty + private File databaseFile = new File(DEFAULT_CONFIG_PATH); + @Getter + @Setter + @JsonProperty + private int httpPort = 8080; + + } +} diff --git a/core/src/main/java/ru/kirillius/XCP/Security/SecurityManagerImpl.java b/core/src/main/java/ru/kirillius/XCP/Security/SecurityManagerImpl.java new file mode 100644 index 0000000..6ed839c --- /dev/null +++ b/core/src/main/java/ru/kirillius/XCP/Security/SecurityManagerImpl.java @@ -0,0 +1,9 @@ +package ru.kirillius.XCP.Security; + +import lombok.Getter; + +public class SecurityManagerImpl implements SecurityManager { + @Getter + private final HashUtility hashUtility = new Argon2HashUtility(); + +} diff --git a/database/pom.xml b/database/pom.xml index e33ac8a..38bb21c 100644 --- a/database/pom.xml +++ b/database/pom.xml @@ -30,11 +30,7 @@ hibernate-c3p0 7.1.10.Final - - tools.jackson.core - jackson-databind - 3.0.3 - + ru.kirillius api diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java index 252dce5..85fac7b 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractRepository.java @@ -37,14 +37,14 @@ public abstract class AbstractRepository implements public E create() { try { var constructor = entityImplementationClass.getConstructor(); - var instance= constructor.newInstance(); - if(instance instanceof ContextReferencedEntity referencedEntity) { + var instance = constructor.newInstance(); + if (instance instanceof ContextReferencedEntity referencedEntity) { referencedEntity.setContext(repositoryService.getContext()); } return instance; } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) { - throw new RuntimeException("Unable to instantiate entity", e); + throw new PersistenceException("Unable to instantiate entity", e); } } @@ -54,25 +54,25 @@ public abstract class AbstractRepository implements } @Override - public E load(UUID uuid) { + public E get(UUID uuid) { try (var query = buildQueryParametrized("where uuid = ?1", uuid)) { return query.get().findFirst().orElse(null); } catch (IOException e) { - throw new RuntimeException(e); + throw new PersistenceException(e); } } @Override - public E load(long id) { + public E get(long id) { try (var query = buildQueryParametrized("where id = ?1", id)) { return query.get().findFirst().orElse(null); } catch (IOException e) { - throw new RuntimeException(e); + throw new PersistenceException(e); } } @Override - public StreamHandler load(Collection ids) { + public StreamHandler get(Collection ids) { if (ids != null && !ids.isEmpty()) { return buildQueryParametrized("where id IN (" + joinIdentifiers(ids) + ")"); } else { @@ -86,7 +86,7 @@ public abstract class AbstractRepository implements } @Override - public StreamHandler loadAll() { + public StreamHandler getAll() { return buildQueryParametrized("order by id"); } @@ -104,7 +104,7 @@ public abstract class AbstractRepository implements } @Override - public void store(E entity) { + public void save(E entity) { var session = repositoryService.openSession(); var transaction = session.beginTransaction(); try { @@ -116,21 +116,21 @@ public abstract class AbstractRepository implements transaction.commit(); } catch (Exception e) { transaction.rollback(); - throw new RuntimeException(e); + throw new PersistenceException("Unable to save entity", e); } try { eventBindings.entityStored().invoke(entity); } catch (Exception e) { - throw new RuntimeException(e); + throw new PersistenceException("Something went wrong on entity save event", e); } finally { session.close(); } } @Override - public void store(Collection entities) { - entities.forEach(this::store); + public void save(Collection entities) { + entities.forEach(this::save); } @Override @@ -142,13 +142,13 @@ public abstract class AbstractRepository implements transaction.commit(); } catch (Exception e) { transaction.rollback(); - throw new RuntimeException(e); + throw new PersistenceException("Unable to remove entity", e); } try { eventBindings.entityRemoved().invoke(entity); } catch (Exception e) { - throw new RuntimeException(e); + throw new PersistenceException("Something went wrong on entity deletion", e); } finally { session.close(); } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java index 220df2b..6441015 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java @@ -27,9 +27,9 @@ public class EntityReferenceDeserializer extends StdDeserializer) Class.forName(type)); if (uuid != null) { - return new EntityReference(repository.load(UUID.fromString(uuid))); + return new EntityReference(repository.get(UUID.fromString(uuid))); } - return new EntityReference(repository.load(id)); + return new EntityReference(repository.get(id)); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java new file mode 100644 index 0000000..ec463a9 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java @@ -0,0 +1,93 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.*; +import org.hibernate.annotations.UuidGenerator; +import ru.kirillius.XCP.Commons.StreamHandler; +import ru.kirillius.XCP.Persistence.AbstractRepository; +import ru.kirillius.XCP.Persistence.Entities.ApiToken; +import ru.kirillius.XCP.Persistence.Entities.User; +import ru.kirillius.XCP.Persistence.EntityImplementation; +import ru.kirillius.XCP.Persistence.EntityReference; +import ru.kirillius.XCP.Persistence.RepositoryServiceImpl; + +import java.util.Date; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +@EntityImplementation(ApiTokenRepositoryImpl.TokenEntity.class) +public class ApiTokenRepositoryImpl extends AbstractRepository implements ApiTokenRepository { + + public ApiTokenRepositoryImpl(RepositoryServiceImpl repositoryService) { + super(repositoryService); + } + + @Override + public StreamHandler getByUser(User user) { + return search("WHERE user = ?1", List.of(user)); + } + + @Entity + @Table(name = "Tokens") + @Builder + @AllArgsConstructor + @NoArgsConstructor + @Getter + @Setter + public static class TokenEntity implements ApiToken { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonProperty + private long id = 0; + + @JsonProperty + @Column(unique = true, nullable = false) + @UuidGenerator + private UUID uuid; + + @JsonProperty + @Column + private Date expirationDate; + + @JsonProperty + @Column(nullable = false) + private String name; + + @ManyToOne(fetch = FetchType.EAGER) + @JsonIgnore + private UserRepositoryImpl.UserEntity user; + + @JsonProperty("user") + EntityReference getUserReference() { + return new EntityReference(getUser()); + } + + @JsonProperty("user") + void setUserReference(EntityReference entityReference) { + user = entityReference == null ? null : (UserRepositoryImpl.UserEntity) entityReference.get(); + } + + public User getUser() { + return user; + } + + public void setUser(User parent) { + this.user = (UserRepositoryImpl.UserEntity) parent; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TokenEntity that)) return false; + return Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hashCode(uuid); + } + } +} diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java index 0f5d9cf..b87620f 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/GroupRepositoryImpl.java @@ -57,14 +57,14 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement } @Override - public void store(Group entity) { + public void save(Group entity) { if (entity != null && entity.getParent() == null) { var root = getRoot(); if (root != null && !root.equals(entity)) { throw new IllegalStateException("Root group already exists"); } } - super.store(entity); + super.save(entity); } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java index e6f7210..2d77e49 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java @@ -30,11 +30,11 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement } @Override - public void store(Input entity) { + public void save(Input entity) { if (entity != null && entity.getParent() == null) { throw new IllegalStateException("Saving inputs without group is prohibited"); } - super.store(entity); + super.save(entity); } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java index 3b97722..aa85291 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/OutputRepositoryImpl.java @@ -27,11 +27,11 @@ public class OutputRepositoryImpl extends AbstractNodeRepository impleme } @Override - public void store(Output entity) { + public void save(Output entity) { if (entity != null && entity.getParent() == null) { throw new IllegalStateException("Saving outputs without group is prohibited"); } - super.store(entity); + super.save(entity); } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java index 728c96f..0299fda 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/TagRepositoryImpl.java @@ -32,7 +32,7 @@ public class TagRepositoryImpl extends AbstractRepository implements TagRep } else { var tag = create(); tag.setName(name); - store(tag); + save(tag); return tag; } @@ -58,7 +58,7 @@ public class TagRepositoryImpl extends AbstractRepository implements TagRep if(!foundNames.contains(tagName)) { var tag = create(); tag.setName(tagName); - store(tag); + save(tag); tags.add(tag); } }); diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java index c415984..e6fc0da 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/UserRepositoryImpl.java @@ -59,7 +59,7 @@ public class UserRepositoryImpl extends AbstractRepository implements User @UuidGenerator private UUID uuid; - @Column(nullable = false) + @Column(nullable = false, unique = true) @JsonProperty private String login = ""; diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java index 4ecc39b..f963dcf 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java @@ -7,15 +7,20 @@ import org.hibernate.SessionFactory; import org.hibernate.cfg.Configuration; import org.hibernate.type.Type; import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.XCP.Persistence.Repositories.*; +import ru.kirillius.XCP.Services.RepositoryService; +import ru.kirillius.XCP.Services.ServiceLoadPriority; import tools.jackson.databind.ObjectMapper; import tools.jackson.databind.json.JsonMapper; import java.lang.reflect.InvocationTargetException; import java.util.Arrays; import java.util.Collection; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +@ServiceLoadPriority(0) public final class RepositoryServiceImpl implements RepositoryService { @Getter private ObjectMapper mapper = new ObjectMapper(); @@ -51,6 +56,20 @@ public final class RepositoryServiceImpl implements RepositoryService { registerClasses(); } + public RepositoryServiceImpl() { + managedRepositoryClasses = List.of( + ApiTokenRepositoryImpl.class, + GroupRepositoryImpl.class, + InputRepositoryImpl.class, + OutputRepositoryImpl.class, + TagRepositoryImpl.class, + UserRepositoryImpl.class + ); + configuration = new Configuration(); + configuration.configure(); + registerClasses(); + } + private void registerClasses() { managedRepositoryClasses.forEach(aClass -> { var implementation = aClass.getAnnotation(EntityImplementation.class); @@ -81,7 +100,9 @@ public final class RepositoryServiceImpl implements RepositoryService { public void close() { repositoryBindings.clear(); entityBindings.clear(); - sessionFactory.close(); + if (sessionFactory != null) { + sessionFactory.close(); + } } private volatile boolean initialized; diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImplTest.java new file mode 100644 index 0000000..00b75d4 --- /dev/null +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImplTest.java @@ -0,0 +1,27 @@ +package ru.kirillius.XCP.Persistence.Repositories; + +import ru.kirillius.XCP.Persistence.Entities.ApiToken; +import ru.kirillius.XCP.Services.RepositoryService; + +import java.util.List; +import java.util.UUID; + +import static ru.kirillius.XCP.Persistence.TestEnvironment.instantiateTestService; + +class ApiTokenRepositoryImplTest extends GenericRepositoryTest { + @Override + protected RepositoryService spawnRepositoryService() { + var service = instantiateTestService(List.of(repositoryClass, UserRepositoryImpl.class)); + var userRepository = service.getRepository(UserRepository.class); + var user = userRepository.create(); + userRepository.save(user); + return service; + } + + @Override + protected void modify(ApiToken entity, RepositoryService service) { + entity.setName("test" + UUID.randomUUID()); + var user = service.getRepository(UserRepository.class).get(1); + entity.setUser(user); + } +} \ No newline at end of file diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericNodeRepositoryTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericNodeRepositoryTest.java index 0a33bc7..2c5e2ec 100644 --- a/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericNodeRepositoryTest.java +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/Repositories/GenericNodeRepositoryTest.java @@ -20,25 +20,25 @@ abstract class GenericNodeRepositoryTest(); for (var i = 0; i < 20; i++) { var child = repository.create(); child.setParent(anotherParent); children.add(child); - repository.store(child); + repository.save(child); } try (var handler = repository.getChildrenOf(root)) { @@ -65,22 +65,22 @@ class GroupRepositoryImplTest extends GenericNodeRepositoryTest(); for (var i = 0; i < 10; i++) { var child = repository.create(); child.setParent(anotherParent); children.add(child); - repository.store(child); + repository.save(child); var subchild = repository.create(); subchild.setParent(child); - repository.store(subchild); + repository.save(subchild); children.add(subchild); } @@ -101,10 +101,10 @@ class GroupRepositoryImplTest extends GenericNodeRepositoryTest { + @Test + void saveSameLogin() throws IOException { + + try (var service = spawnRepositoryService()) { + var repository = service.getRepository(UserRepository.class); + + var user = repository.create(); + user.setLogin("test"); + repository.save(user); + + user = repository.create(); + user.setLogin("test"); + try { + repository.save(user); + throw new AssertionError("Should have thrown an exception"); + } catch (Throwable t) { + assertThat(t).isInstanceOf(PersistenceException.class); + } + } + } + @Test void getByLogin() throws IOException { var correct = "correctlogin"; @@ -19,15 +41,15 @@ class UserRepositoryImplTest extends GenericRepositoryTest + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + logging + + + 21 + 21 + UTF-8 + + + + ru.kirillius + api + 1.0.0.0 + compile + + + + org.slf4j + slf4j-api + 2.0.9 + + + org.slf4j + slf4j-jdk14 + 2.0.9 + + + + \ No newline at end of file diff --git a/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java b/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java new file mode 100644 index 0000000..0a0ee03 --- /dev/null +++ b/logging/src/main/java/ru/kirillius/XCP/Logging/LogHandlerImpl.java @@ -0,0 +1,90 @@ +package ru.kirillius.XCP.Logging; + +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.java.utils.events.EventHandler; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +public class LogHandlerImpl extends Handler { + private final EventHandler eventHandler; + private final boolean debugging; + + public LogHandlerImpl(LoggingSystem loggingSystem, Context context) { + eventHandler = loggingSystem.getEventHandler(); + debugging = context.getLaunchArgs().contains("--debug"); + } + + private final static SimpleDateFormat dateFormat = new SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.US); + + private String format(LogRecord logRecord) { + var date = new Date(logRecord.getMillis()); + var builder = new StringBuilder(); + builder.append("["); + builder.append(dateFormat.format(date)); + builder.append("]["); + builder.append(convertLevel(logRecord.getLevel())); + builder.append("] "); + builder.append(logRecord.getMessage().trim()); + var thrown = logRecord.getThrown(); + if (thrown != null) { + builder.append("\nError thrown ").append(thrown.getClass().getSimpleName()).append(":").append(thrown.getMessage()); + if (debugging) { + builder.append("\nStack trace:\n"); + + try (var writer = new StringWriter()) { + try (var printWriter = new PrintWriter(writer)) { + thrown.printStackTrace(printWriter); + builder.append(writer); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + return builder.toString(); + } + + @Override + public synchronized void publish(LogRecord logRecord) { + //TODO сделать асинхронным чтобы не лочить треды + print(logRecord); + var message = LogMessage.builder().message(logRecord.getMessage()).level(convertLevel(logRecord.getLevel())).date(logRecord.getInstant()).build(); + try { + eventHandler.invoke(message); + } catch (Exception e) { + print(new LogRecord(Level.SEVERE, "Unhandled error in logger event listener: " + e.getClass().getSimpleName() + ": " + e.getMessage())); + } + } + + private void print(LogRecord logRecord) { + System.out.println(format(logRecord)); + } + + public LogLevel convertLevel(Level level) { + if (level == Level.SEVERE) { + return LogLevel.ERROR; + } + if (level == Level.WARNING) { + return LogLevel.WARNING; + } + return LogLevel.INFO; + } + + @Override + public void flush() { + + } + + @Override + public void close() throws SecurityException { + + } +} diff --git a/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java new file mode 100644 index 0000000..d683901 --- /dev/null +++ b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggerImpl.java @@ -0,0 +1,36 @@ +package ru.kirillius.XCP.Logging; + +import java.util.logging.Level; + +public class LoggerImpl extends java.util.logging.Logger implements Logger { + private final static String SEPARATOR = ": "; + + public LoggerImpl(String name) { + super(name, null); + } + + @Override + public void info(String message) { + super.info(getName() + SEPARATOR + message); + } + + @Override + public void warning(String message) { + super.warning(getName() + SEPARATOR + message); + } + + @Override + public void error(String message) { + severe(getName() + SEPARATOR + message); + } + + @Override + public void error(String message, Throwable error) { + log(Level.SEVERE, getName() + SEPARATOR + message, error); + } + + @Override + public void error(Throwable error) { + log(Level.SEVERE, getName() + SEPARATOR + "Thrown error", error); + } +} diff --git a/logging/src/main/java/ru/kirillius/XCP/Logging/LoggingSystemImpl.java b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggingSystemImpl.java new file mode 100644 index 0000000..2bbc718 --- /dev/null +++ b/logging/src/main/java/ru/kirillius/XCP/Logging/LoggingSystemImpl.java @@ -0,0 +1,50 @@ +package ru.kirillius.XCP.Logging; + +import lombok.Getter; +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.java.utils.events.ConcurrentEventHandler; +import ru.kirillius.java.utils.events.EventHandler; + +import java.util.Arrays; +import java.util.logging.Handler; +import java.util.logging.Level; +import java.util.logging.LogManager; + +public class LoggingSystemImpl implements LoggingSystem { + private final java.util.logging.Logger rootLogger; + private final Handler handler; + + @Override + public Logger createLogger(Class cls) { + var logger = new LoggerImpl(cls.getSimpleName()); + LogManager.getLogManager().addLogger(logger); + return logger; + } + + @Override + public Logger createLogger(String name) { + var logger = new LoggerImpl(name); + LogManager.getLogManager().addLogger(logger); + return logger; + } + + public LoggingSystemImpl(Context context) { + eventHandler = new ConcurrentEventHandler<>(); + var logManager = LogManager.getLogManager(); + rootLogger = logManager.getLogger(""); + rootLogger.setLevel(Level.INFO); + handler = new LogHandlerImpl(this, context); + Arrays.stream(rootLogger.getHandlers()).forEach(rootLogger::removeHandler); + rootLogger.addHandler(handler); + } + + @Getter + private final EventHandler eventHandler; + + @Override + public void close() { + if (handler != null) { + rootLogger.removeHandler(handler); + } + } +} diff --git a/pom.xml b/pom.xml index 2b447f6..7304f74 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,10 @@ api database core + rpc + web-server + app + logging @@ -108,11 +112,10 @@ 1.3.0.0 - - org.javassist - javassist - 3.29.2-GA + tools.jackson.core + jackson-databind + 3.0.3 diff --git a/rpc/pom.xml b/rpc/pom.xml new file mode 100644 index 0000000..217189c --- /dev/null +++ b/rpc/pom.xml @@ -0,0 +1,60 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + rpc + + + 21 + 21 + UTF-8 + + + + + + + + + + + + + + + + + org.eclipse.jetty + jetty-server + 12.1.5 + + + + + org.eclipse.jetty.ee10 + jetty-ee10-servlet + 12.1.5 + + + org.eclipse.jetty.ee10 + jetty-ee10-webapp + 12.1.5 + + + ru.kirillius + api + 1.0.0.0 + compile + + + + + + \ No newline at end of file diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/CallContext.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/CallContext.java new file mode 100644 index 0000000..045d210 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/CallContext.java @@ -0,0 +1,19 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.XCP.Persistence.Entities.User; +import tools.jackson.databind.node.ObjectNode; + +public interface CallContext { + Context getContext(); + + HttpServletRequest getRequest(); + + HttpServletResponse getResponse(); + + ObjectNode getParams(); + + User getCurrentUser(); +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcError.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcError.java new file mode 100644 index 0000000..f7fbe6e --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcError.java @@ -0,0 +1,33 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +public class JsonRpcError { + public JsonRpcError(JsonRpcErrorCode code) { + this.code = code; + } + + public JsonRpcError(JsonRpcErrorCode code, String message) { + this.code = code; + this.messageInternal = message; + } + + @JsonIgnore + + private final JsonRpcErrorCode code; + + @JsonProperty(value = "error") + public int getError() { + return code.getCode(); + } + + @JsonProperty(value = "message") + public String getMessage() { + return messageInternal == null ? code.getMessage() : messageInternal; + } + + @JsonIgnore + private String messageInternal; + +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcErrorCode.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcErrorCode.java new file mode 100644 index 0000000..784260b --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcErrorCode.java @@ -0,0 +1,44 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +public enum JsonRpcErrorCode { + + // Standard JSON-RPC 2.0 errors + PARSE_ERROR(-32700, "Parse error: Invalid JSON was received by the server. An error occurred on the server while parsing the JSON text."), + INVALID_REQUEST(-32600, "Invalid Request: The JSON sent is not a valid Request object."), + METHOD_NOT_FOUND(-32601, "Method not found: The method does not exist / is not available."), + INVALID_PARAMS(-32602, "Invalid params: Invalid method parameter(s)."), + INTERNAL_ERROR(-32603, "Internal error: Internal JSON-RPC error."), + + // Implementation-specific errors + INVOCATION_ERROR(-32000, "Invocation error: The target object threw an exception (CommonErrorData)."), + NO_MARSHALED_OBJECT_FOUND(-32001, "No marshaled object found: The request was made for a remotely marshaled object that does not exist or has been removed."), + RESPONSE_SERIALIZATION_FAILURE(-32003, "Response serialization failure: The response could not be serialized as intended."), + INVOCATION_ERROR_WITH_EXCEPTION(-32004, "Invocation error with exception: The target object threw an exception (ISerializable)."), + REQUEST_CANCELED(-32800, "Request canceled: The execution of the server method was aborted due to a cancellation request from the client."); + + private final int code; + private final String message; + + JsonRpcErrorCode(int code, String message) { + this.code = code; + this.message = message; + } + + public int getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public static JsonRpcErrorCode fromCode(int code) { + for (var error : JsonRpcErrorCode.values()) { + if (error.code == code) { + return error; + } + } + return INTERNAL_ERROR; + } + +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java new file mode 100644 index 0000000..5fd636d --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcMethod.java @@ -0,0 +1,14 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import ru.kirillius.XCP.Security.UserRole; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.METHOD) +public @interface JsonRpcMethod { + UserRole accessLevel() default UserRole.Admin; +} \ No newline at end of file diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcRequest.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcRequest.java new file mode 100644 index 0000000..1210d75 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcRequest.java @@ -0,0 +1,19 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Getter; +import lombok.Setter; +import tools.jackson.databind.node.ObjectNode; + +@Getter +@Setter +public class JsonRpcRequest { + @JsonProperty + private String jsonrpc = "2.0"; + @JsonProperty + private String method; + @JsonProperty + private ObjectNode params; + @JsonProperty + private long id; +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcResponse.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcResponse.java new file mode 100644 index 0000000..183c46b --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcResponse.java @@ -0,0 +1,15 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import lombok.*; + +@Getter +@Setter +@Builder +@AllArgsConstructor +public class JsonRpcResponse { + private String jsonrpc = "2.0"; + private Object result; + private JsonRpcError error; + private long id; + +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcService.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcService.java new file mode 100644 index 0000000..95252a1 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcService.java @@ -0,0 +1,33 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import tools.jackson.databind.JsonNode; + +import java.util.Optional; +import java.util.function.Function; + +public abstract class JsonRpcService { + protected JsonNode requireParam(CallContext context, String paramName) { + var params = context.getParams(); + if (params.has(paramName)) { + return params.get(paramName); + } + throw new IllegalArgumentException(String.format("Missing required parameter '%s'", paramName)); + } + + protected T requireParam(CallContext context, String paramName, Function whenFound) { + return whenFound.apply(requireParam(context, paramName)); + } + + protected Optional getParam(CallContext context, String paramName, Function whenFound) { + var node = getParam(context, paramName); + return node.map(whenFound); + } + + protected Optional getParam(CallContext context, String paramName) { + var params = context.getParams(); + if (params.has(paramName)) { + return Optional.of(params.get(paramName)); + } + return Optional.empty(); + } +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java new file mode 100644 index 0000000..3327318 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java @@ -0,0 +1,184 @@ +package ru.kirillius.XCP.RPC.JSONRPC; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.SneakyThrows; +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.XCP.Logging.Logger; +import ru.kirillius.XCP.Persistence.Entities.User; +import ru.kirillius.XCP.Persistence.Repositories.ApiTokenRepository; +import ru.kirillius.XCP.Persistence.Repositories.UserRepository; +import ru.kirillius.XCP.Services.RepositoryService; +import ru.kirillius.XCP.Security.UserRole; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.regex.Pattern; + +public class JsonRpcServlet extends HttpServlet { + private final Context context; + private final ObjectMapper mapper; + private final Logger log; + + public JsonRpcServlet(Context context) { + this.context = context; + var repositoryService = context.getService(RepositoryService.class); + mapper = repositoryService.getMapper(); + userRepository = repositoryService.getRepository(UserRepository.class); + log = context.getLoggingSystem().createLogger(JsonRpcServlet.class); + } + + @SafeVarargs + @SneakyThrows + public final void registerRpcService(Class... serviceClasses) { + + for (var cls : serviceClasses) { + var constructor = cls.getConstructor(); + var instance = (JsonRpcService) constructor.newInstance(); + var methodBindings = new ConcurrentHashMap(); + + for (var method : cls.getDeclaredMethods()) { + if (Modifier.isPublic(method.getModifiers()) && method.isAnnotationPresent(JsonRpcMethod.class)) { + methodBindings.put(method.getName(), new MethodBinding(instance, method, method.getAnnotation(JsonRpcMethod.class).accessLevel())); + } + } + + bindings.put(cls.getSimpleName(), methodBindings); + } + } + + @AllArgsConstructor + @Getter + private static final class MethodBinding { + private final JsonRpcService service; + private final Method method; + private UserRole accessLevel; + } + + private final Map> bindings = new ConcurrentHashMap<>(); + public final static String SESSION_ATTR = "ActiveUser"; + + private final UserRepository userRepository; + + private User authorize(HttpServletRequest httpServletRequest) { + User user = null; + + var session = httpServletRequest.getSession(false); + + //try auth by session + if (session != null) { + user = (User) session.getAttribute(SESSION_ATTR); + } + + //try auth by token + if (user == null) { + var authHeader = httpServletRequest.getHeader("X-Auth-Token"); + if (authHeader != null) { + var tokenRepository = context.getService(RepositoryService.class).getRepository(ApiTokenRepository.class); + var token = tokenRepository.get(UUID.fromString(authHeader)); + if (token != null && token.getExpirationDate() != null && token.isExpired()) { + tokenRepository.remove(token); + } else if (token != null) { + user = token.getUser(); + if (session == null) { + session = httpServletRequest.getSession(); + } + session.setAttribute(SESSION_ATTR, user); + } + } + } + + if (user == null) { + user = userRepository.create(); + user.setName("Guest"); + user.setRole(UserRole.Guest); + } + + return user; + } + + @Override + protected void doPost(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) throws ServletException, IOException { + JsonRpcRequest jsonRpcRequest = null; + JsonRpcResponse jsonRpcResponse = null; + + httpServletResponse.setStatus(HttpServletResponse.SC_OK); + httpServletResponse.setContentType("application/json;charset=UTF-8"); + httpServletResponse.setHeader("Access-Control-Allow-Origin", "*"); + httpServletResponse.setCharacterEncoding("UTF-8"); + + var user = authorize(httpServletRequest); + + try { + jsonRpcRequest = mapper.readValue(httpServletRequest.getReader(), JsonRpcRequest.class); + var callContext = new CallContextImpl(httpServletRequest, httpServletResponse, context, jsonRpcRequest.getParams(), user); + jsonRpcResponse = processRequest(jsonRpcRequest, callContext); + } catch (Exception e) { + httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); + jsonRpcResponse = createErrorResponse(jsonRpcRequest, JsonRpcErrorCode.PARSE_ERROR); + log.error("Failed to parse JRPC request", e); + } + + mapper.writeValue(httpServletResponse.getWriter(), jsonRpcResponse); + } + + private JsonRpcResponse createErrorResponse(JsonRpcRequest request, JsonRpcErrorCode code) { + return JsonRpcResponse.builder() + .id(request == null ? -1L : request.getId()) + .error(new JsonRpcError(code)) + .build(); + } + + private JsonRpcResponse processRequest(JsonRpcRequest request, CallContext callContext) { + var split = request.getMethod().split(Pattern.quote("."), 2); + if (split.length != 2) { + return createErrorResponse(request, JsonRpcErrorCode.METHOD_NOT_FOUND); + } + + var bindingMap = bindings.get(split[0]); + if (bindingMap == null) { + return createErrorResponse(request, JsonRpcErrorCode.METHOD_NOT_FOUND); + } + + var methodBinding = bindingMap.get(split[1]); + if (methodBinding == null) { + return createErrorResponse(request, JsonRpcErrorCode.METHOD_NOT_FOUND); + } + + if (callContext.getCurrentUser().getRole().getLevel() < methodBinding.getAccessLevel().getLevel()) { + callContext.getResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); + return createErrorResponse(request, JsonRpcErrorCode.INTERNAL_ERROR); + } + + try { + var result = methodBinding.getMethod().invoke(methodBinding.getService(), callContext); + return JsonRpcResponse.builder().id(request.getId()).result(result).build(); + } catch (IllegalAccessException | InvocationTargetException e) { + log.error("Failed to process JSON-RPC request: " + mapper.valueToTree(request).toString(), e); + return createErrorResponse(request, JsonRpcErrorCode.INVOCATION_ERROR); + } + + } + + @Getter + @AllArgsConstructor + private static final class CallContextImpl implements CallContext { + private final HttpServletRequest request; + private final HttpServletResponse response; + private final Context context; + private final ObjectNode params; + private final User currentUser; + } + +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java new file mode 100644 index 0000000..34dbe98 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Auth.java @@ -0,0 +1,120 @@ +package ru.kirillius.XCP.RPC.Services; + +import ru.kirillius.XCP.Persistence.Repositories.ApiTokenRepository; +import ru.kirillius.XCP.Persistence.Repositories.UserRepository; +import ru.kirillius.XCP.Services.RepositoryService; +import ru.kirillius.XCP.RPC.JSONRPC.CallContext; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet; +import ru.kirillius.XCP.Security.UserRole; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.Date; +import java.util.UUID; + +public class Auth extends JsonRpcService { + + @JsonRpcMethod(accessLevel = UserRole.Guest) + public boolean authenticateByPassword(CallContext call) { + var login = requireParam(call, "login", JsonNode::asString); + var passwd = requireParam(call, "password", JsonNode::asString); + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + + var user = userRepository.getByLogin(login); + if (user == null) { + return false; + } + if (!user.verifyPassword(passwd)) { + return false; + } + + var session = call.getRequest().getSession(); + session.setAttribute(JsonRpcServlet.SESSION_ATTR, user); + return true; + } + + + @JsonRpcMethod(accessLevel = UserRole.User) + public ArrayNode getTokens(CallContext call) throws IOException { + var tokenRepository = call.getContext().getService(RepositoryService.class).getRepository(ApiTokenRepository.class); + var tokens = JsonNodeFactory.instance.arrayNode(); + try (var handler = tokenRepository.getByUser(call.getCurrentUser())) { + handler.get().map(token -> { + var json = tokenRepository.serialize(token); + json.remove("uuid"); + var uuid = token.getUuid().toString(); + json.put("token", uuid.substring(0, 4) + "..." + uuid.substring(uuid.length() - 4)); + return json; + }).forEach(tokens::add); + } + return tokens; + } + + @JsonRpcMethod(accessLevel = UserRole.User) + public ObjectNode generateToken(CallContext call) { + var repositoryService = call.getContext().getService(RepositoryService.class); + var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class); + var params = call.getParams(); + var permanent = params.has("permanent") && params.get("permanent").asBoolean(); + var token = tokenRepository.create(); + var name = params.has("name") ? params.get("name").asString() : null; + + if (name == null) { + var header = call.getRequest().getHeader("User-Agent"); + name = header != null ? header : "Unnamed token"; + } + + token.setExpirationDate(permanent ? null : Date.from(Instant.now().plus(30, ChronoUnit.DAYS))); + token.setName(name); + + tokenRepository.save(token); + + return tokenRepository.serialize(token); + } + + @JsonRpcMethod(accessLevel = UserRole.Guest) + public boolean authenticateByToken(CallContext call) throws IllegalAccessException { + var repositoryService = call.getContext().getService(RepositoryService.class); + var tokenRepository = repositoryService.getRepository(ApiTokenRepository.class); + var token = tokenRepository.get(UUID.fromString( + requireParam(call, "token", JsonNode::asString) + )); + if (token == null) { + return false; + } + + if (token.isExpired()) { + tokenRepository.remove(token); + return false; + } + + var user = token.getUser(); + if (user == null) { + return false; + } + + var session = call.getRequest().getSession(); + session.setAttribute(JsonRpcServlet.SESSION_ATTR, user); + return true; + } + + @JsonRpcMethod(accessLevel = UserRole.User) + public boolean logout(CallContext call) { + ru.kirillius.XCP.Persistence.Entities.User user = null; + + var session = call.getRequest().getSession(false); + if (session != null) { + user = (ru.kirillius.XCP.Persistence.Entities.User) session.getAttribute(JsonRpcServlet.SESSION_ATTR); + session.setAttribute(JsonRpcServlet.SESSION_ATTR, null); + } + + return session != null && user != null; + } +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java new file mode 100644 index 0000000..9d7437f --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java @@ -0,0 +1,63 @@ +package ru.kirillius.XCP.RPC.Services; + +import ru.kirillius.XCP.Persistence.Repositories.UserRepository; +import ru.kirillius.XCP.Services.RepositoryService; +import ru.kirillius.XCP.RPC.JSONRPC.CallContext; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService; +import ru.kirillius.XCP.Security.UserRole; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ObjectNode; + +public class Profile extends JsonRpcService { + + @JsonRpcMethod(accessLevel = UserRole.User) + public boolean save(CallContext call) { + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + var user = call.getCurrentUser(); + + var login = requireParam(call, "login", JsonNode::asString); + var name = requireParam(call, "name", JsonNode::asString); + var passwordOptional = getParam(call, "password", JsonNode::asString); + var values = requireParam(call, "values", n -> (ObjectNode) n); + + if (!user.getLogin().equals(login) && userRepository.getByLogin(login) != null) { + throw new RuntimeException("Login is already in use"); + } + + if (login.isBlank()) { + throw new RuntimeException("Login is blank"); + } + + if (name.isBlank()) { + throw new RuntimeException("Name is blank"); + } + + user.setLogin(login); + user.setName(name); + + if (passwordOptional.isPresent()) { + var password = passwordOptional.get(); + if (password.isBlank()) { + throw new RuntimeException("Password is blank"); + } + user.setPassword(password); + } + + user.setValues(values); + userRepository.save(user); + return true; + } + + @JsonRpcMethod(accessLevel = UserRole.User) + public ObjectNode get(CallContext call) { + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + var user = call.getCurrentUser(); + + var json = userRepository.serialize(user); + json.remove("passwordHash"); + + return json; + } + +} diff --git a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java new file mode 100644 index 0000000..7aeba40 --- /dev/null +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java @@ -0,0 +1,38 @@ +package ru.kirillius.XCP.RPC.Services; + +import ru.kirillius.XCP.Persistence.Repositories.UserRepository; +import ru.kirillius.XCP.Services.RepositoryService; +import ru.kirillius.XCP.RPC.JSONRPC.CallContext; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcMethod; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcService; +import ru.kirillius.XCP.Security.UserRole; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; + +import java.io.IOException; + +public class UserManagement extends JsonRpcService { + @JsonRpcMethod(accessLevel = UserRole.Admin) + public ArrayNode getAll(CallContext call) throws IOException { + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + try (var handler = userRepository.getAll()) { + return userRepository.serialize(handler.get().toList()); + } + } + + @JsonRpcMethod(accessLevel = UserRole.Admin) + public void save(CallContext call) { + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + var user = userRepository.deserialize((ObjectNode) requireParam(call, "user")); + userRepository.save(user); + } + + @JsonRpcMethod(accessLevel = UserRole.Admin) + public ObjectNode getById(CallContext call) { + var userId = requireParam(call, "id", JsonNode::asLong); + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + return userRepository.serialize(userRepository.get(userId)); + } + +} diff --git a/web-server/pom.xml b/web-server/pom.xml new file mode 100644 index 0000000..99e34f7 --- /dev/null +++ b/web-server/pom.xml @@ -0,0 +1,34 @@ + + + 4.0.0 + + ru.kirillius + XCP + 1.0.0.0 + + + web-server + + + 21 + 21 + UTF-8 + + + + ru.kirillius + api + 1.0.0.0 + compile + + + ru.kirillius + rpc + 1.0.0.0 + compile + + + + \ No newline at end of file diff --git a/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java new file mode 100644 index 0000000..8a5de83 --- /dev/null +++ b/web-server/src/main/java/ru/kirillius/XCP/web/WebServiceImpl.java @@ -0,0 +1,72 @@ +package ru.kirillius.XCP.web; + +import org.eclipse.jetty.ee10.servlet.DefaultServlet; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.util.resource.ResourceFactory; +import ru.kirillius.XCP.Commons.Context; +import ru.kirillius.XCP.RPC.JSONRPC.JsonRpcServlet; +import ru.kirillius.XCP.RPC.Services.Auth; +import ru.kirillius.XCP.RPC.Services.Profile; +import ru.kirillius.XCP.RPC.Services.UserManagement; +import ru.kirillius.XCP.Services.ServiceLoadPriority; +import ru.kirillius.XCP.Services.WebService; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.Objects; + +@ServiceLoadPriority(100) +public class WebServiceImpl implements WebService { + private Server server; + + @Override + public void initialize(Context context) { + if (server != null) { + throw new IllegalStateException("Server already started"); + } + var jsonRpc = new JsonRpcServlet(context); + jsonRpc.registerRpcService( + UserManagement.class, + Auth.class, + Profile.class + ); + var config = context.getConfig(); + server = new Server(new InetSocketAddress(config.getHost(), config.getHttpPort())); + var servletContext = new ServletContextHandler("/", ServletContextHandler.SESSIONS); + servletContext.addServlet(new ServletHolder(jsonRpc), "/api/*"); + servletContext.addServlet(DefaultServlet.class, "/"); + var resourceFactory = ResourceFactory.root(); + + try { + var resource = resourceFactory.newResource(Objects.requireNonNull(getClass().getClassLoader().getResource("htdocs/")).toURI()); + servletContext.setBaseResource(resource); + } catch (Exception e) { + throw new RuntimeException("Unable to determine path to htdocs directory", e); + } + + server.setHandler(servletContext); + } + + @Override + public void close() throws IOException { + if (server != null) { + try { + server.stop(); + } catch (Exception e) { + throw new IOException("Failed to stop web server", e); + } finally { + server = null; + } + } + } + + public void join() { + try { + server.join(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/web-server/src/main/resources/htdocs/index.html b/web-server/src/main/resources/htdocs/index.html new file mode 100644 index 0000000..566549b --- /dev/null +++ b/web-server/src/main/resources/htdocs/index.html @@ -0,0 +1,10 @@ + + + + + Title + + + + + \ No newline at end of file