diff --git a/api-sandbox/app/index.html b/api-sandbox/app/index.html index a609007..8e949b0 100644 --- a/api-sandbox/app/index.html +++ b/api-sandbox/app/index.html @@ -5,36 +5,119 @@ API Debug Console -
-

API Debug Console

+
+
+
+

API Debug Console

+ +
+ + не авторизован +
-
- - +
+ + +
+ +
+ + +
+ +
+ + +
- -
- - + +
+

Result

+
+ +

Request

+
- -
- - - -
diff --git a/api-sandbox/app/main.js b/api-sandbox/app/main.js index 7bb099d..345b6f2 100644 --- a/api-sandbox/app/main.js +++ b/api-sandbox/app/main.js @@ -1,134 +1,262 @@ import $ from 'jquery' let apiSpec = null +let currentUser = null + +async function loadUserProfile() { + try { + const requestData = { + jsonrpc: '2.0', + method: 'Profile.get', + params: {}, + id: Date.now() + } + + const response = await fetch('http://localhost:8080/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(requestData) + }) + + const result = await response.json() + if (result.result) { + currentUser = result.result + $('#username').text(currentUser.name || 'неизвестный пользователь') + } else { + currentUser = null + $('#username').text('не авторизован') + } + } catch (error) { + console.error('Ошибка загрузки профиля:', error) + currentUser = null + $('#username').text('не авторизован') + } +} async function loadApiSpec() { - try { - const response = await fetch('/api.spec.json', { - method: 'GET', - headers: { - 'Cache-Control': 'no-cache', - 'Accept': 'application/json' - } - }) - if (!response.ok) { - throw new Error(`HTTP ${response.status}: ${response.statusText}`) + try { + const response = await fetch('/api.spec.json', { + method: 'GET', + headers: { + 'Cache-Control': 'no-cache', + 'Accept': 'application/json' + } + }) + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`) + } + apiSpec = await response.json() + console.log('API Spec loaded:', apiSpec) + return apiSpec + } catch (error) { + console.error('Ошибка загрузки API спецификации:', error) + $('#result').text('Ошибка загрузки API спецификации: ' + error.message) } - apiSpec = await response.json() - console.log('API Spec loaded:', apiSpec) - return apiSpec - } catch (error) { - console.error('Ошибка загрузки API спецификации:', error) - $('#result').text('Ошибка загрузки API спецификации: ' + error.message) - } } function populateServices() { - const $serviceSelect = $('#service') - $serviceSelect.empty() - $serviceSelect.append('') + const $serviceSelect = $('#service') + $serviceSelect.empty() + $serviceSelect.append('') - Object.keys(apiSpec).forEach(serviceName => { - $serviceSelect.append(``) - }) + Object.keys(apiSpec).forEach(serviceName => { + $serviceSelect.append(``) + }) } function populateMethods(serviceName) { - const $methodSelect = $('#method') - $methodSelect.empty() - $methodSelect.append('') + const $methodSelect = $('#method') + $methodSelect.empty() + $methodSelect.append('') - if (serviceName && apiSpec[serviceName] && apiSpec[serviceName].methods) { - apiSpec[serviceName].methods.forEach(method => { - $methodSelect.append(``) - }) - } + if (serviceName && apiSpec[serviceName] && apiSpec[serviceName].methods) { + apiSpec[serviceName].methods.forEach(method => { + $methodSelect.append(``) + }) + } } function createParamInputs(methodName, serviceName) { - const $paramsContainer = $('#params-container') - $paramsContainer.empty() + const $paramsContainer = $('#params-container') + $paramsContainer.empty() - if (!serviceName || !methodName) return + if (!serviceName || !methodName) return - const service = apiSpec[serviceName] - if (!service || !service.methods) return + const service = apiSpec[serviceName] + if (!service || !service.methods) return - const method = service.methods.find(m => m.name === methodName) - if (!method || !method.params) return + const method = service.methods.find(m => m.name === methodName) + if (!method || !method.params) return - method.params.forEach(param => { - const required = !param.optional ? ' (обязательно)' : '' - const $paramDiv = $(` -
- - -
- `) - $paramsContainer.append($paramDiv) - }) + method.params.forEach(param => { + const required = !param.optional ? ' (обязательно)' : ' (необязательно)' + const isOptional = param.optional + + const isObjectOrArray = param.type === 'object' || param.type === 'array' + const defaultValue = param.type === 'object' ? '{}' : (param.type === 'array' ? '[]' : '') + const inputElement = isObjectOrArray + ? `` + : `` + + if (isOptional) { + const $paramDiv = $(` +
+
+ +
+ + +
+
+ ${inputElement} +
+ `) + $paramsContainer.append($paramDiv) + + $(`#defined-${param.name}`).on('change', function() { + const $input = $(`#param-${param.name}`) + const isChecked = $(this).is(':checked') + $input.prop('disabled', !isChecked) + if (!isChecked) { + $input.val(isObjectOrArray ? defaultValue : '') + } + }) + + // Initially disable optional fields + $(`#param-${param.name}`).prop('disabled', true) + } else { + const $paramDiv = $(` +
+ + ${inputElement} +
+ `) + $paramsContainer.append($paramDiv) + } + }) } async function sendRequest() { - const serviceName = $('#service').val() - const methodName = $('#method').val() + const serviceName = $('#service').val() + const methodName = $('#method').val() - if (!serviceName || !methodName) { - $('#result').text('Выберите сервис и метод') - return - } - - const params = {} - $('.param input').each(function() { - const paramName = $(this).data('param') - const value = $(this).val() - if (value.trim() !== '') { - params[paramName] = value + if (!serviceName || !methodName) { + $('#result').text('Выберите сервис и метод') + return } - }) - const requestData = { - jsonrpc: '2.0', - method: `${serviceName}.${methodName}`, - params: params, - id: Date.now() - } + const params = {} + let parseError = null - $('#result').text('Отправка запроса...') - - try { - const response = await fetch('/api', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(requestData) + $('.param').each(function () { + const $checkbox = $(this).find('.defined-checkbox') + const $input = $(this).find('input[type="text"], textarea') + const paramName = $input.data('param') + const paramType = $input.attr('id').replace('param-', '') + + // Find parameter type from method definition + const service = apiSpec[serviceName] + const method = service.methods.find(m => m.name === methodName) + const paramDef = method.params.find(p => p.name === paramName) + const isObjectOrArray = paramDef && (paramDef.type === 'object' || paramDef.type === 'array') + + if ($checkbox.length > 0) { + // Optional parameter + if ($checkbox.is(':checked') && $input.val().trim() !== '') { + const value = $input.val().trim() + if (isObjectOrArray) { + try { + params[paramName] = JSON.parse(value) + } catch (e) { + parseError = `Ошибка парсинга JSON для поля "${paramName}": ${e.message}` + } + } else { + params[paramName] = value + } + } + } else { + // Required parameter + const value = $input.val().trim() + if (value !== '') { + if (isObjectOrArray) { + try { + params[paramName] = JSON.parse(value) + } catch (e) { + parseError = `Ошибка парсинга JSON для поля "${paramName}": ${e.message}` + } + } else { + params[paramName] = value + } + } + } }) - const result = await response.json() - $('#result').text(JSON.stringify(result, null, 2)) - } catch (error) { - $('#result').text(`Ошибка: ${error.message}`) - } + if (parseError) { + $('#result').text(parseError) + $('#request').text(JSON.stringify(requestData, null, 2)) + return + } + + const requestData = { + jsonrpc: '2.0', + method: `${serviceName}.${methodName}`, + params: params, + id: Date.now() + } + + $('#result').text('Отправка запроса...') + $('#request').text('') + + try { + const response = await fetch('http://localhost:8080/api', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + credentials: 'include', + body: JSON.stringify(requestData) + }) + + const result = await response.json() + $('#result').text(JSON.stringify(result, null, 2)) + $('#request').text(JSON.stringify(requestData, null, 2)) + + // Проверяем, нужно ли обновить профиль пользователя + if (result.result && ( + methodName.startsWith('authenticateBy') || + methodName === 'logout' + )) { + await loadUserProfile() + } + } catch (error) { + $('#result').text(`Ошибка: ${error.message}`) + $('#request').text(JSON.stringify(requestData, null, 2)) + } } $(document).ready(async () => { - await loadApiSpec() - if (apiSpec) { - populateServices() + await loadApiSpec() + if (apiSpec) { + populateServices() + + // Автоматически загружаем профиль пользователя + await loadUserProfile() - $('#service').on('change', function() { - const serviceName = $(this).val() - populateMethods(serviceName) - $('#params-container').empty() - }) + $('#service').on('change', function () { + const serviceName = $(this).val() + populateMethods(serviceName) + $('#params-container').empty() + }) - $('#method').on('change', function() { - const serviceName = $('#service').val() - const methodName = $(this).val() - createParamInputs(methodName, serviceName) - }) + $('#method').on('change', function () { + const serviceName = $('#service').val() + const methodName = $(this).val() + createParamInputs(methodName, serviceName) + }) - $('#send-btn').on('click', sendRequest) - } + $('#send-btn').on('click', sendRequest) + } }) \ No newline at end of file diff --git a/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java index 40ac0c2..3064901 100644 --- a/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java +++ b/api-spec-generator/src/main/java/ru/kirillius/XCP/ApiGenerator/SpecGenerator.java @@ -116,11 +116,11 @@ public class SpecGenerator { if (type == void.class || type == Void.class) return "void"; if (type.isArray() || Collection.class.isAssignableFrom(type) || type == ArrayNode.class) { - return "Array"; + return "array"; } if (JsonNode.class.isAssignableFrom(type)) { - return "Object"; + return "object"; } return type.getSimpleName(); diff --git a/api.spec.json b/api.spec.json index 6de2214..8eaf674 100644 --- a/api.spec.json +++ b/api.spec.json @@ -1,23 +1,86 @@ { - "Auth" : { - "name" : "Auth", + "Profile" : { + "name" : "Profile", "methods" : [ { - "name" : "generateToken", - "description" : "Generates a new API token and returns its details", - "return" : "Object", + "name" : "get", + "description" : "Get current user profile", + "return" : "void", + "accessLevel" : "Guest", + "params" : [ ] + }, { + "name" : "save", + "description" : "edit current user profile", + "return" : "boolean", "accessLevel" : "User", "params" : [ { - "name" : "permanent", - "type" : "boolean", - "description" : "If true, creates a token that never expires", - "optional" : true + "name" : "login", + "type" : "string", + "description" : "User login. Have to be unique", + "optional" : false }, { "name" : "name", "type" : "string", - "description" : "Display name for the token. If not provided, the User-Agent header will be used", + "description" : "User display name", + "optional" : false + }, { + "name" : "password", + "type" : "string", + "description" : "Change user password if defined", "optional" : true + }, { + "name" : "values", + "type" : "object", + "description" : "User custom values", + "optional" : false + } ] + } ] + }, + "UserManagement" : { + "name" : "UserManagement", + "methods" : [ { + "name" : "getById", + "description" : "Load user object by ID", + "return" : "object", + "accessLevel" : "Admin", + "params" : [ { + "name" : "id", + "type" : "number", + "description" : "User id", + "optional" : false } ] }, { + "name" : "remove", + "description" : "Remove user by ID", + "return" : "object", + "accessLevel" : "Admin", + "params" : [ { + "name" : "id", + "type" : "number", + "description" : "User id", + "optional" : false + } ] + }, { + "name" : "save", + "description" : "Get all users as list", + "return" : "array", + "accessLevel" : "Admin", + "params" : [ { + "name" : "user", + "type" : "object", + "description" : "User object to save", + "optional" : false + } ] + }, { + "name" : "getAll", + "description" : "Get all users as list", + "return" : "array", + "accessLevel" : "Admin", + "params" : [ ] + } ] + }, + "Auth" : { + "name" : "Auth", + "methods" : [ { "name" : "authenticateByPassword", "description" : "Authenticates a user using login and password. Returns true if authentication is successful.", "return" : "boolean", @@ -34,17 +97,27 @@ "optional" : false } ] }, { - "name" : "logout", - "description" : "Logs out the current user. Returns true if logout is successful, false if the user is not logged in", - "return" : "boolean", + "name" : "getTokens", + "description" : "Retrieves all API tokens associated with the current user", + "return" : "array", "accessLevel" : "User", "params" : [ ] }, { - "name" : "getTokens", - "description" : "Retrieves all API tokens associated with the current user", - "return" : "Array", + "name" : "generateToken", + "description" : "Generates a new API token and returns its details", + "return" : "object", "accessLevel" : "User", - "params" : [ ] + "params" : [ { + "name" : "permanent", + "type" : "boolean", + "description" : "If true, creates a token that never expires", + "optional" : true + }, { + "name" : "name", + "type" : "string", + "description" : "Display name for the token. If not provided, the User-Agent header will be used", + "optional" : true + } ] }, { "name" : "authenticateByToken", "description" : "Authenticates a user using an API token. Returns true if authentication is successful.", @@ -56,44 +129,12 @@ "description" : "API token string for authentication", "optional" : false } ] - } ] - }, - "Profile" : { - "name" : "Profile", - "methods" : [ { - "name" : "get", - "description" : "", - "return" : "void", + }, { + "name" : "logout", + "description" : "Logs out the current user. Returns true if logout is successful, false if the user is not logged in", + "return" : "boolean", "accessLevel" : "User", "params" : [ ] - }, { - "name" : "save", - "description" : "", - "return" : "void", - "accessLevel" : "User", - "params" : [ ] - } ] - }, - "UserManagement" : { - "name" : "UserManagement", - "methods" : [ { - "name" : "save", - "description" : "", - "return" : "void", - "accessLevel" : "Admin", - "params" : [ ] - }, { - "name" : "getAll", - "description" : "", - "return" : "void", - "accessLevel" : "Admin", - "params" : [ ] - }, { - "name" : "getById", - "description" : "", - "return" : "void", - "accessLevel" : "Admin", - "params" : [ ] } ] } } \ No newline at end of file diff --git a/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java b/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java index 9355f7a..af89c3f 100644 --- a/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java +++ b/api/src/main/java/ru/kirillius/XCP/Services/RepositoryService.java @@ -18,4 +18,5 @@ public interface RepositoryService extends Service { ObjectMapper getMapper(); + Class getEntityTypeByName(String entityName); } diff --git a/app/src/main/java/ru/kirillius/XCP/Application.java b/app/src/main/java/ru/kirillius/XCP/Application.java index ee4ed85..6483f67 100644 --- a/app/src/main/java/ru/kirillius/XCP/Application.java +++ b/app/src/main/java/ru/kirillius/XCP/Application.java @@ -8,13 +8,18 @@ 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.PersistenceEntity; +import ru.kirillius.XCP.Persistence.Repository; 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.RepositoryService; import ru.kirillius.XCP.Services.ServiceLoadPriority; import ru.kirillius.XCP.Services.WebService; import ru.kirillius.XCP.web.WebServiceImpl; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.ObjectNode; import java.io.IOException; import java.lang.reflect.InvocationTargetException; @@ -85,11 +90,38 @@ public class Application implements Context { shutdown(); throw new RuntimeException("Error loading services"); } + + checkDefaultInstall(); + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); ((WebServiceImpl) getService(WebService.class)).join(); } + private final static String DEFAULT_INSTALLER_FILE = "defaultEntities.json"; + + private void checkDefaultInstall() { + try (var stream = getClass().getClassLoader().getResourceAsStream(DEFAULT_INSTALLER_FILE)) { + var repositoryService = getService(RepositoryService.class); + var mapper = repositoryService.getMapper(); + var array = (ArrayNode) mapper.readTree(stream); + for (var node : array) { + var entry = (ObjectNode) node; + var typeName = entry.get("type").asString(); + var entityType = repositoryService.getEntityTypeByName(typeName); + @SuppressWarnings("unchecked") var repository = (Repository) repositoryService.getRepositoryForEntity(entityType); + var deserialized = repository.deserialize((ObjectNode) entry.get("entity")); + if (repository.get(deserialized.getUuid()) == null) { + log.warning("Installing default entity " + typeName + " (" + deserialized + ")."); + repository.save(deserialized); + } + } + + } catch (IOException e) { + throw new RuntimeException("Unable to load file " + DEFAULT_INSTALLER_FILE, e); + } + } + private void loadServices() { var servicesToLoad = List.of(RepositoryServiceImpl.class, WebServiceImpl.class); diff --git a/app/src/main/resources/defaultEntities.json b/app/src/main/resources/defaultEntities.json new file mode 100644 index 0000000..4c62b80 --- /dev/null +++ b/app/src/main/resources/defaultEntities.json @@ -0,0 +1,13 @@ +[ + { + "type": "User", + "entity": { + "login": "admin", + "name": "Administrator", + "role": "Admin", + "values": {}, + "uuid": "00000000-0000-0000-0000-000000000000", + "passwordHash": "$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ" + } + } +] \ No newline at end of file diff --git a/core/src/main/java/ru/kirillius/XCP/Security/Argon2HashUtility.java b/core/src/main/java/ru/kirillius/XCP/Security/Argon2HashUtility.java index 5943c4b..10d8885 100644 --- a/core/src/main/java/ru/kirillius/XCP/Security/Argon2HashUtility.java +++ b/core/src/main/java/ru/kirillius/XCP/Security/Argon2HashUtility.java @@ -2,12 +2,9 @@ package ru.kirillius.XCP.Security; import de.mkammerer.argon2.Argon2; import de.mkammerer.argon2.Argon2Factory; -import lombok.Getter; public class Argon2HashUtility implements HashUtility { - @Getter - private final static HashUtility instance = new Argon2HashUtility(); private final Argon2 argon2; diff --git a/core/src/test/java/ru/kirillius/XCP/Security/Argon2HashUtilityTest.java b/core/src/test/java/ru/kirillius/XCP/Security/Argon2HashUtilityTest.java new file mode 100644 index 0000000..44f5309 --- /dev/null +++ b/core/src/test/java/ru/kirillius/XCP/Security/Argon2HashUtilityTest.java @@ -0,0 +1,23 @@ +package ru.kirillius.XCP.Security; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; + +class Argon2HashUtilityTest { + + @Test + void hash() { + var util = new Argon2HashUtility(); + var hashed = util.hash("admin"); + assertThat(hashed).isNotNull().isNotEmpty().doesNotContain("admin").isEqualTo("$argon2id$v=19$m=65536,t=3,p=1$SBqQtx5adxoG53V0TgqmDw$zIy0Wiq53m9r/SOldtCXWXLWbvZuS0F3HHILxpUsLhQ"); + } + + @Test + void validate() { + var util = new Argon2HashUtility(); + var hashed = util.hash("admin"); + assertThat(util.validate("admin", hashed)).isTrue(); + } +} \ No newline at end of file diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractEntity.java b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractEntity.java new file mode 100644 index 0000000..bddae20 --- /dev/null +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/AbstractEntity.java @@ -0,0 +1,55 @@ +package ru.kirillius.XCP.Persistence; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.Getter; +import lombok.Setter; + +import java.util.Objects; +import java.util.UUID; + +@MappedSuperclass +public abstract class AbstractEntity implements PersistenceEntity { + + @Id + @Getter + @Setter + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonProperty + private long id = 0; + + @Getter + @Setter + @JsonProperty + @Column(unique = true, nullable = false, updatable = false) + private UUID uuid; + + @PrePersist + protected void prePersist() { + if (uuid == null) { + uuid = UUID.randomUUID(); + } + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null) { + return false; + } + if (o.getClass() != getClass()) { + return false; + } + + var that = (AbstractEntity) o; + 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/EntityReferenceDeserializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java index 6441015..18f3f83 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceDeserializer.java @@ -23,16 +23,11 @@ public class EntityReferenceDeserializer extends StdDeserializer) Class.forName(type)); - if (uuid != null) { - return new EntityReference(repository.get(UUID.fromString(uuid))); - } - return new EntityReference(repository.get(id)); - } catch (ClassNotFoundException e) { - throw new RuntimeException(e); + var repository = repositoryService.getRepositoryForEntity((Class) repositoryService.getEntityTypeByName(type)); + if (uuid != null) { + return new EntityReference(repository.get(UUID.fromString(uuid))); } + return new EntityReference(repository.get(id)); } } diff --git a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java index 9783f54..6f2c9d5 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/EntityReferenceSerializer.java @@ -22,7 +22,7 @@ public class EntityReferenceSerializer extends StdSerializer { } gen.writeStartObject(); var baseType = repositoryService.getEntityBaseType(value.getClass()); - gen.writeStringProperty("type", baseType.getName()); + gen.writeStringProperty("type", baseType.getSimpleName()); gen.writeNumberProperty("id", value.getId()); gen.writeStringProperty("uuid", value.getUuid().toString()); gen.writeEndObject(); 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 index ec463a9..981fe39 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/ApiTokenRepositoryImpl.java @@ -4,19 +4,13 @@ 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.*; 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 { @@ -37,17 +31,7 @@ public class ApiTokenRepositoryImpl extends AbstractRepository impleme @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; + public static class TokenEntity extends AbstractEntity implements ApiToken { @JsonProperty @Column @@ -79,15 +63,5 @@ public class ApiTokenRepositoryImpl extends AbstractRepository impleme 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 77a9ca0..b84d853 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 @@ -4,10 +4,9 @@ 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.Entities.Group; import ru.kirillius.XCP.Persistence.*; +import ru.kirillius.XCP.Persistence.Entities.Group; import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.ObjectNode; @@ -67,8 +66,6 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement super.save(entity); } - - @Entity @Table(name = "Groups") @Builder @@ -76,24 +73,12 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement @NoArgsConstructor @Getter @Setter - public static class GroupEntity implements Group { - - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @JsonProperty - private long id = 0; - - @JsonProperty - @Column(unique = true, nullable = false) - @UuidGenerator - private UUID uuid; + public static class GroupEntity extends AbstractEntity implements Group { @Column(nullable = false) @JsonProperty private String name = ""; - @Column(nullable = false) @JsonProperty @Getter @@ -140,9 +125,6 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement this.parent = (GroupEntity) parent; } - - - @Override public void setTags(TagCollection tags) { this.tags = tags.stream().map(t -> (TagRepositoryImpl.TagEntity) t).collect(Collectors.toSet()); @@ -163,15 +145,5 @@ public class GroupRepositoryImpl extends AbstractNodeRepository implement @ManyToMany(fetch = FetchType.EAGER) private Set tags = new HashSet<>(); - @Override - public boolean equals(Object o) { - if (!(o instanceof GroupEntity 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/InputRepositoryImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/Repositories/InputRepositoryImpl.java index cc6546d..49694e6 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 @@ -4,12 +4,11 @@ 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.Data.PollSettings; import ru.kirillius.XCP.Data.ValueTransformationChain; +import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Persistence.Entities.Group; import ru.kirillius.XCP.Persistence.Entities.Input; -import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Serialization.PollSettingsConverter; import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter; import tools.jackson.databind.annotation.JsonDeserialize; @@ -17,9 +16,7 @@ import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.ObjectNode; import java.util.HashSet; -import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; @EntityImplementation(InputRepositoryImpl.InputEntity.class) @@ -37,8 +34,6 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement super.save(entity); } - - @Entity @Table(name = "Inputs") @Builder @@ -46,17 +41,7 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement @NoArgsConstructor @Getter @Setter - public static class InputEntity implements Input { - - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @JsonProperty - private long id = 0; - - @JsonProperty - @Column(unique = true, nullable = false) - @UuidGenerator - private UUID uuid; + public static class InputEntity extends AbstractEntity implements Input { @Column(nullable = false) @JsonProperty @@ -116,17 +101,6 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement @ManyToMany(fetch = FetchType.EAGER) private Set tags = new HashSet<>(); - @Override - public boolean equals(Object o) { - if (!(o instanceof InputEntity that)) return false; - return Objects.equals(uuid, that.uuid); - } - - @Override - public int hashCode() { - return Objects.hashCode(uuid); - } - @Override public PollSettings getPollSettings() { return pollSettings; @@ -140,7 +114,7 @@ public class InputRepositoryImpl extends AbstractNodeRepository implement @Column(nullable = false) @JsonProperty @Convert(converter = PollSettingsConverter.class) - @JsonDeserialize(as = PollSettingsImpl.class) + @JsonDeserialize(as = PollSettingsImpl.class) private PollSettingsImpl pollSettings = new PollSettingsImpl(); @Column(nullable = false) 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 a73c67f..ad21f2f 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 @@ -4,19 +4,16 @@ 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.Data.ValueTransformationChain; +import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Persistence.Entities.Group; import ru.kirillius.XCP.Persistence.Entities.Output; -import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Serialization.ValueTransformationChainConverter; import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.ObjectNode; import java.util.HashSet; -import java.util.Objects; import java.util.Set; -import java.util.UUID; import java.util.stream.Collectors; @EntityImplementation(OutputRepositoryImpl.OutputEntity.class) @@ -34,8 +31,6 @@ public class OutputRepositoryImpl extends AbstractNodeRepository impleme super.save(entity); } - - @Entity @Table(name = "Outputs") @Builder @@ -43,16 +38,7 @@ public class OutputRepositoryImpl extends AbstractNodeRepository impleme @NoArgsConstructor @Getter @Setter - public static class OutputEntity implements Output { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @JsonProperty - private long id = 0; - - @JsonProperty - @Column(unique = true, nullable = false) - @UuidGenerator - private UUID uuid; + public static class OutputEntity extends AbstractEntity implements Output { @Column(nullable = false) @JsonProperty @@ -112,18 +98,6 @@ public class OutputRepositoryImpl extends AbstractNodeRepository impleme @ManyToMany(fetch = FetchType.EAGER) private Set tags = new HashSet<>(); - @Override - public boolean equals(Object o) { - if (!(o instanceof OutputEntity that)) return false; - return Objects.equals(uuid, that.uuid); - } - - @Override - public int hashCode() { - return Objects.hashCode(uuid); - } - - @Column(nullable = false) @JsonProperty @Getter @@ -136,6 +110,5 @@ public class OutputRepositoryImpl extends AbstractNodeRepository impleme @Convert(converter = ValueTransformationChainConverter.class) @Column(nullable = false) private ValueTransformationChain transformationChain = new ValueTransformationChain(); - } } 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 0299fda..756a6d6 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 @@ -1,9 +1,10 @@ package ru.kirillius.XCP.Persistence.Repositories; import com.fasterxml.jackson.annotation.JsonProperty; -import jakarta.persistence.*; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; import lombok.*; -import org.hibernate.annotations.UuidGenerator; import ru.kirillius.XCP.Commons.ResourceHandler; import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Persistence.Entities.Tag; @@ -11,8 +12,6 @@ import ru.kirillius.XCP.Persistence.Entities.Tag; import java.io.IOException; import java.util.Collection; import java.util.List; -import java.util.Objects; -import java.util.UUID; import java.util.regex.Pattern; import java.util.stream.Stream; @@ -50,12 +49,10 @@ public class TagRepositoryImpl extends AbstractRepository implements TagRep throw new RuntimeException("Unable to find tags by names " + names, e); } - - if (tags.size() != names.size()) { var foundNames = tags.stream().map(Tag::getName).toList(); names.forEach(tagName -> { - if(!foundNames.contains(tagName)) { + if (!foundNames.contains(tagName)) { var tag = create(); tag.setName(tagName); save(tag); @@ -84,16 +81,7 @@ public class TagRepositoryImpl extends AbstractRepository implements TagRep @NoArgsConstructor @Getter @Setter - public static class TagEntity implements Tag { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @JsonProperty - private long id = 0; - - @JsonProperty - @Column(unique = true, nullable = false) - @UuidGenerator - private UUID uuid; + public static class TagEntity extends AbstractEntity implements Tag { @Override public String toString() { @@ -126,16 +114,5 @@ public class TagRepositoryImpl extends AbstractRepository implements TagRep this.name = name; } - @Override - public boolean equals(Object o) { - if (!(o instanceof TagEntity tagEntity)) return false; - return Objects.equals(name, tagEntity.name); - } - - @Override - public int hashCode() { - return Objects.hashCode(name); - } - } } 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 76b79b7..a6b3a04 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 @@ -4,20 +4,14 @@ 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.Context; -import ru.kirillius.XCP.Persistence.AbstractRepository; -import ru.kirillius.XCP.Persistence.ContextReferencedEntity; +import ru.kirillius.XCP.Persistence.*; import ru.kirillius.XCP.Persistence.Entities.User; -import ru.kirillius.XCP.Persistence.EntityImplementation; -import ru.kirillius.XCP.Persistence.RepositoryServiceImpl; import ru.kirillius.XCP.Security.UserRole; import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.ObjectNode; import java.io.IOException; -import java.util.Objects; -import java.util.UUID; @EntityImplementation(UserRepositoryImpl.UserEntity.class) public class UserRepositoryImpl extends AbstractRepository implements UserRepository { @@ -42,23 +36,13 @@ public class UserRepositoryImpl extends AbstractRepository implements User @NoArgsConstructor @Getter @Setter - public static class UserEntity implements User, ContextReferencedEntity { + public static class UserEntity extends AbstractEntity implements User, ContextReferencedEntity { @Transient @JsonIgnore @Getter @Setter private Context context; - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @JsonProperty - private long id = 0; - - @JsonProperty - @Column(unique = true, nullable = false) - @UuidGenerator - private UUID uuid; - @Column(nullable = false, unique = true) @JsonProperty private String login = ""; @@ -82,6 +66,14 @@ public class UserRepositoryImpl extends AbstractRepository implements User @JsonProperty private ObjectNode values = JsonNodeFactory.instance.objectNode(); + @Override + protected void prePersist() { + super.prePersist(); + if (login == null || login.isEmpty()) { + login = "user" + System.currentTimeMillis(); + } + } + @Override public void setPassword(String password) { passwordHash = context.getSecurityManager().getHashUtility().hash(password); @@ -91,16 +83,5 @@ public class UserRepositoryImpl extends AbstractRepository implements User public boolean verifyPassword(String password) { return context.getSecurityManager().getHashUtility().validate(password, passwordHash); } - - @Override - public boolean equals(Object o) { - if (!(o instanceof UserEntity 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/RepositoryServiceImpl.java b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java index f963dcf..8abef39 100644 --- a/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java +++ b/database/src/main/java/ru/kirillius/XCP/Persistence/RepositoryServiceImpl.java @@ -220,4 +220,9 @@ public final class RepositoryServiceImpl implements RepositoryService { return repositoryEntityBindings.get(repositoryImplClass); } + @Override + public Class getEntityTypeByName(String entityName) { + return entityBaseBindings.values().stream().filter(e -> e.getSimpleName().equals(entityName)).findFirst().orElse(null); + } + } diff --git a/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java index 74e0e89..3282865 100644 --- a/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java +++ b/database/src/test/java/ru/kirillius/XCP/Persistence/RepositoryServiceImplTest.java @@ -56,7 +56,6 @@ class RepositoryServiceImplTest { @Setter private UUID uuid; - @Override public boolean equals(Object o) { if (!(o instanceof EntityImpl entity)) return false; @@ -187,4 +186,18 @@ class RepositoryServiceImplTest { } } + + @Test + public void TestGetByName() { + try (var service = instantiateTestService(List.of(RepoImpl.class))) { + var repository = service.getRepository(TestRepository.class); + var testEntity = repository.create(); + repository.save(List.of(testEntity)); + + var typeByName = service.getEntityTypeByName(TestEntity.class.getSimpleName()); + + assertThat(typeByName).isNotNull().isEqualTo(TestEntity.class); + + } + } } \ No newline at end of file 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 index 35c1e94..ceddc02 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/JSONRPC/JsonRpcServlet.java @@ -170,6 +170,13 @@ public class JsonRpcServlet extends HttpServlet { .build(); } + private JsonRpcResponse createErrorResponse(JsonRpcRequest request, JsonRpcErrorCode code, String message) { + return JsonRpcResponse.builder().jsonrpc("2.0") + .id(request == null ? -1L : request.getId()) + .error(new JsonRpcError(code, message)) + .build(); + } + private JsonRpcResponse processRequest(JsonRpcRequest request, CallContext callContext) { var split = request.getMethod().split(Pattern.quote("."), 2); if (split.length != 2) { @@ -188,14 +195,15 @@ public class JsonRpcServlet extends HttpServlet { if (callContext.getCurrentUser().getRole().getLevel() < methodBinding.getAccessLevel().getLevel()) { callContext.getResponse().setStatus(HttpServletResponse.SC_FORBIDDEN); - return createErrorResponse(request, JsonRpcErrorCode.INTERNAL_ERROR); + log.error("Forbidden JSON-RPC request: " + mapper.writerWithDefaultPrettyPrinter().writeValueAsString(request)); + return createErrorResponse(request, JsonRpcErrorCode.INTERNAL_ERROR, "Forbidden due to access level"); } try { var result = methodBinding.getMethod().invoke(methodBinding.getService(), callContext); - return JsonRpcResponse.builder().id(request.getId()).result(result).build(); + return JsonRpcResponse.builder().id(request.getId()).jsonrpc("2.0").result(result).build(); } catch (IllegalAccessException | InvocationTargetException e) { - log.error("Failed to process JSON-RPC request: " + mapper.valueToTree(request).toString(), e); + log.error("Failed to process JSON-RPC request: " + mapper.writerWithDefaultPrettyPrinter().valueToTree(request).toString(), e); return createErrorResponse(request, JsonRpcErrorCode.INVOCATION_ERROR); } 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 index 9d7437f..82b68b8 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/Profile.java @@ -11,7 +11,16 @@ import tools.jackson.databind.node.ObjectNode; public class Profile extends JsonRpcService { - @JsonRpcMethod(accessLevel = UserRole.User) + @JsonRpcMethod( + accessLevel = UserRole.User, + description = "edit current user profile", + parameters = { + @JsonRpcMethod.Parameter(name = "login", description = "User login. Have to be unique", type = String.class), + @JsonRpcMethod.Parameter(name = "name", description = "User display name", type = String.class), + @JsonRpcMethod.Parameter(name = "password", description = "Change user password if defined", type = String.class, optional = true), + @JsonRpcMethod.Parameter(name = "values", description = "User custom values", type = ObjectNode.class) + }, + returnType = boolean.class) public boolean save(CallContext call) { var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); var user = call.getCurrentUser(); @@ -49,7 +58,10 @@ public class Profile extends JsonRpcService { return true; } - @JsonRpcMethod(accessLevel = UserRole.User) + @JsonRpcMethod( + accessLevel = UserRole.Guest, + description = "Get current user profile" + ) public ObjectNode get(CallContext call) { var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); var user = call.getCurrentUser(); 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 index 7aeba40..473ce54 100644 --- a/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java +++ b/rpc/src/main/java/ru/kirillius/XCP/RPC/Services/UserManagement.java @@ -13,7 +13,14 @@ import tools.jackson.databind.node.ObjectNode; import java.io.IOException; public class UserManagement extends JsonRpcService { - @JsonRpcMethod(accessLevel = UserRole.Admin) + @JsonRpcMethod( + accessLevel = UserRole.Admin, + description = "Get all users as list", + parameters = { + + }, + returnType = ArrayNode.class + ) public ArrayNode getAll(CallContext call) throws IOException { var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); try (var handler = userRepository.getAll()) { @@ -21,18 +28,51 @@ public class UserManagement extends JsonRpcService { } } - @JsonRpcMethod(accessLevel = UserRole.Admin) + @JsonRpcMethod( + accessLevel = UserRole.Admin, + description = "Get all users as list", + parameters = { + @JsonRpcMethod.Parameter(name = "user", description = "User object to save", type = ObjectNode.class) + }, + returnType = ArrayNode.class + ) 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) + @JsonRpcMethod( + accessLevel = UserRole.Admin, + description = "Load user object by ID", + parameters = { + @JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class) + }, + returnType = ObjectNode.class + ) 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)); } + @JsonRpcMethod( + accessLevel = UserRole.Admin, + description = "Remove user by ID", + parameters = { + @JsonRpcMethod.Parameter(name = "id", description = "User id", type = int.class) + }, + returnType = ObjectNode.class + ) + public boolean remove(CallContext call) { + var userId = requireParam(call, "id", JsonNode::asLong); + var userRepository = call.getContext().getService(RepositoryService.class).getRepository(UserRepository.class); + var user = userRepository.get(userId); + if (user == null) { + return false; + } + userRepository.remove(user); + return true; + } + }