diff --git a/backlib/.gitignore b/backlib/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/backlib/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/backlib/pom.xml b/backlib/pom.xml
new file mode 100644
index 0000000..914ce6f
--- /dev/null
+++ b/backlib/pom.xml
@@ -0,0 +1,164 @@
+
+ 4.0.0
+ net.miarma.api
+ backlib
+ 1.2.0
+
+
+ 23
+ 23
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+ MiarmaGit
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+
+
+
+ gitea
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+ gitea
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+
+
+
+
+ io.vertx
+ vertx-core
+ 4.5.13
+
+
+
+
+ io.vertx
+ vertx-web
+ 4.5.13
+
+
+
+
+ io.vertx
+ vertx-web-client
+ 4.5.13
+
+
+
+
+ io.vertx
+ vertx-mysql-client
+ 4.5.13
+
+
+
+
+ io.vertx
+ vertx-mail-client
+ 4.5.16
+
+
+
+
+ io.vertx
+ vertx-redis-client
+ 4.5.16
+
+
+
+
+ com.google.code.gson
+ gson
+ 2.12.1
+
+
+
+
+ org.mindrot
+ jbcrypt
+ 0.4
+
+
+
+
+ com.auth0
+ java-jwt
+ 4.5.0
+
+
+
+
+ org.slf4j
+ slf4j-api
+ 2.0.12
+
+
+
+ ch.qos.logback
+ logback-classic
+ 1.5.13
+
+
+
+
+ com.fasterxml.jackson.datatype
+ jackson-datatype-jsr310
+ 2.18.3
+
+
+
+
+ com.sun.mail
+ jakarta.mail
+ 2.0.1
+
+
+
+
+ com.github.eduardomcb
+ discord-webhook
+ 1.0.0
+
+
+
+
+ BackLib
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ net.miarma.backlib.MainVerticle
+
+
+
+
+
+
+
+
+
+
diff --git a/backlib/src/main/java/net/miarma/api/backlib/ConfigManager.java b/backlib/src/main/java/net/miarma/api/backlib/ConfigManager.java
new file mode 100644
index 0000000..3aab338
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/ConfigManager.java
@@ -0,0 +1,146 @@
+package net.miarma.api.backlib;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Properties;
+
+/**
+ * Gestión de toda la configuración de la aplicación.
+ * Se encarga de cargar, guardar y proporcionar acceso a las propiedades de configuración.
+ * Proporciona métodos para obtener la URL de la base de datos, directorios de archivos,
+ * y propiedades específicas como host, puerto, etc.
+ *
+ * Esta clase sigue el patron Singleton para asegurar una sola instancia.
+ * @author José Manuel Amador Gallardo
+ */
+public class ConfigManager {
+ private static ConfigManager instance;
+ private final File configFile;
+ private final Properties config;
+ private static final String CONFIG_FILE_NAME = "config.properties";
+
+ private ConfigManager() {
+ String path = getBaseDir() + CONFIG_FILE_NAME;
+ this.configFile = new File(path);
+ this.config = new Properties();
+ }
+
+ public static synchronized ConfigManager getInstance() {
+ if (instance == null) {
+ instance = new ConfigManager();
+ }
+ return instance;
+ }
+
+ public void loadConfig() {
+ try (FileInputStream fis = new FileInputStream(configFile);
+ InputStreamReader isr = new InputStreamReader(fis, StandardCharsets.UTF_8)) {
+ config.load(isr);
+ } catch (IOException e) {
+ Constants.LOGGER.error("Error loading configuration file: ", e);
+ }
+ }
+ public File getConfigFile() {
+ return configFile;
+ }
+
+ public String getJdbcUrl() {
+ return String.format("%s://%s:%s/%s",
+ config.getProperty("db.protocol"),
+ config.getProperty("db.host"),
+ config.getProperty("db.port"),
+ config.getProperty("db.name"));
+ }
+
+ public String getHost() {
+ return this.getStringProperty("inet.host");
+ }
+
+ public String getHomeDir() {
+ if (isDocker()) {
+ return "/data/";
+ }
+ return getOS() == OSType.WINDOWS ?
+ "C:/Users/" + System.getProperty("user.name") + "/" :
+ System.getProperty("user.home").contains("root") ? "/root/" :
+ "/home/" + System.getProperty("user.name") + "/";
+ }
+
+ public String getBaseDir() {
+ if (isDocker()) {
+ return getHomeDir() + ".config/";
+ }
+ return getHomeDir() + (getOS() == OSType.WINDOWS ? ".miarmacoreapi/" :
+ getOS() == OSType.LINUX ? ".config/miarmacoreapi/" :
+ ".contaminus/");
+ }
+
+ public String getFilesDir(String context) {
+ if (config.getProperty("files.dir") != null) {
+ return config.getProperty("files.dir");
+ }
+ if (isDocker()) {
+ return "/files/" + context + "/";
+ }
+ return getOS() == OSType.WINDOWS ?
+ System.getProperty("user.home") + "\\" + "Documents\\" + context + "\\" :
+ "/var/www/files/" + context + "/";
+ }
+
+ public String getModsDir() {
+ return getFilesDir("miarmacraft") + "mods/";
+ }
+
+ public String getWebRoot() {
+ if (config.getProperty("web.root") != null) {
+ return config.getProperty("web.root");
+ }
+ return getBaseDir() + "webroot/";
+ }
+
+
+ public static OSType getOS() {
+ String os = System.getProperty("os.name").toLowerCase();
+ if (os.contains("win")) {
+ return OSType.WINDOWS;
+ } else if (os.contains("nix") || os.contains("nux")) {
+ return OSType.LINUX;
+ } else {
+ return OSType.INVALID_OS;
+ }
+ }
+
+ public static boolean isDocker() {
+ return Boolean.parseBoolean(System.getenv("RUNNING_IN_DOCKER"));
+ }
+
+ public String getStringProperty(String key) {
+ return config.getProperty(key);
+ }
+
+ public int getIntProperty(String key) {
+ String value = config.getProperty(key);
+ return value != null ? Integer.parseInt(value) : 10;
+ }
+
+ public boolean getBooleanProperty(String key) {
+ return Boolean.parseBoolean(config.getProperty(key));
+ }
+
+ public void setProperty(String key, String value) {
+ config.setProperty(key, value);
+ saveConfig();
+ }
+
+ private void saveConfig() {
+ try (FileOutputStream fos = new FileOutputStream(configFile)) {
+ config.store(fos, "Configuration for: " + Constants.APP_NAME);
+ } catch (IOException e) {
+ Constants.LOGGER.error("Error saving configuration file: ", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/Constants.java b/backlib/src/main/java/net/miarma/api/backlib/Constants.java
new file mode 100644
index 0000000..94ccf06
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/Constants.java
@@ -0,0 +1,469 @@
+package net.miarma.api.backlib;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+
+import io.vertx.core.json.JsonObject;
+import net.miarma.api.backlib.gson.APIDontReturnExclusionStrategy;
+import net.miarma.api.backlib.gson.JsonObjectTypeAdapter;
+import net.miarma.api.backlib.gson.LocalDateTimeAdapter;
+import net.miarma.api.backlib.gson.ValuableEnumDeserializer;
+import net.miarma.api.backlib.gson.ValuableEnumTypeAdapter;
+import net.miarma.api.backlib.interfaces.IUserRole;
+
+/**
+ * Clase que contiene constantes y enumeraciones utilizadas en la API de MiarmaCore.
+ * @author José Manuel Amador Gallardo
+ */
+public class Constants {
+ public static final String APP_NAME = "MiarmaCoreAPI";
+ public static final String BASE_PREFIX = "/api";
+ public static final String CORE_PREFIX = BASE_PREFIX + "/core/v1"; // tabla de usuarios central
+ public static final String AUTH_PREFIX = "/auth/v1";
+ public static final String HUERTOS_PREFIX = BASE_PREFIX + "/huertos/v1";
+ public static final String MMC_PREFIX = BASE_PREFIX + "/mmc/v1";
+ public static final String CINE_PREFIX = BASE_PREFIX + "/cine/v1";
+ public static final String MPASTE_PREFIX = BASE_PREFIX + "/mpaste/v1";
+
+ public static final String AUTH_EVENT_BUS = "auth.eventbus";
+ public static final String CORE_EVENT_BUS = "core.eventbus";
+ public static final String HUERTOS_EVENT_BUS = "huertos.eventbus";
+ public static final String MMC_EVENT_BUS = "mmc.eventbus";
+ public static final String CINE_EVENT_BUS = "cine.eventbus";
+ public static final String MPASTE_EVENT_BUS = "mpaste.eventbus";
+
+ public static final List HUERTOS_ALLOWED_FOLDERS =
+ List.of("INBOX", "Drafts", "Sent", "Spam", "Trash");
+
+ public static final Logger LOGGER = LoggerFactory.getLogger(Constants.APP_NAME);
+
+ public static final Gson GSON = new GsonBuilder()
+ .registerTypeAdapter(LocalDateTime.class, new LocalDateTimeAdapter())
+ .registerTypeAdapter(JsonObject.class, new JsonObjectTypeAdapter())
+ .registerTypeHierarchyAdapter(ValuableEnum.class, new ValuableEnumTypeAdapter())
+ .registerTypeAdapter(CoreUserGlobalStatus.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(CoreUserRole.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosUserType.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosUserStatus.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosUserRole.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosRequestStatus.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosRequestType.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosPaymentType.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosPaymentFrequency.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(HuertosAnnouncePriority.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(MMCUserStatus.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(MMCUserRole.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(CoreFileContext.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(MMCModStatus.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(CineUserRole.class, new ValuableEnumDeserializer())
+ .registerTypeAdapter(CineUserStatus.class, new ValuableEnumDeserializer())
+ .addSerializationExclusionStrategy(new APIDontReturnExclusionStrategy())
+ .create();
+
+ public enum CoreUserRole implements IUserRole {
+ USER(0),
+ ADMIN(1);
+
+ private final int value;
+
+ CoreUserRole(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static CoreUserRole fromInt(int i) {
+ for (CoreUserRole role : values()) {
+ if (role.value == i) return role;
+ }
+ throw new IllegalArgumentException("Invalid CoreUserRole value: " + i);
+ }
+ }
+
+
+ public enum CoreUserGlobalStatus implements ValuableEnum {
+ INACTIVE(0),
+ ACTIVE(1);
+
+ private final int value;
+
+ CoreUserGlobalStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static CoreUserGlobalStatus fromInt(int i) {
+ for (CoreUserGlobalStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid CoreUserGlobalStatus value: " + i);
+ }
+ }
+
+ public enum CoreFileContext implements ValuableEnum {
+ CORE(0),
+ HUERTOS(1),
+ MMC(2),
+ CINE(3);
+
+ private final int value;
+
+ CoreFileContext(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public String toCtxString() {
+ return switch(this) {
+ case CORE -> "core";
+ case HUERTOS -> "huertos";
+ case MMC -> "miarmacraft";
+ case CINE -> "cine";
+ };
+ }
+
+ public static CoreFileContext fromInt(int i) {
+ for (CoreFileContext context : values()) {
+ if (context.value == i) return context;
+ }
+ throw new IllegalArgumentException("Invalid CoreFileContext value: " + i);
+ }
+ }
+
+
+ public enum HuertosUserRole implements IUserRole {
+ USER(0),
+ ADMIN(1),
+ DEV(2);
+
+ private final int value;
+
+ HuertosUserRole(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosUserRole fromInt(int i) {
+ for (HuertosUserRole role : values()) {
+ if (role.value == i) return role;
+ }
+ throw new IllegalArgumentException("Invalid HuertosUserRole value: " + i);
+ }
+ }
+
+ public enum HuertosUserType implements ValuableEnum {
+ WAIT_LIST(0),
+ MEMBER(1),
+ WITH_GREENHOUSE(2),
+ COLLABORATOR(3),
+ DEVELOPER(5),
+ SUBSIDY(4);
+
+ private final int value;
+
+ HuertosUserType(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosUserType fromInt(int i) {
+ for (HuertosUserType type : values()) {
+ if (type.value == i) return type;
+ }
+ throw new IllegalArgumentException("Invalid HuertosUserType value: " + i);
+ }
+ }
+
+
+ public enum HuertosUserStatus implements ValuableEnum {
+ INACTIVE(0),
+ ACTIVE(1);
+
+ private final int value;
+
+ HuertosUserStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosUserStatus fromInt(int i) {
+ for (HuertosUserStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid HuertosUserStatus value: " + i);
+ }
+ }
+
+
+ public enum HuertosRequestStatus implements ValuableEnum {
+ PENDING(0),
+ APPROVED(1),
+ REJECTED(2);
+
+ private final int value;
+
+ HuertosRequestStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosRequestStatus fromInt(int i) {
+ for (HuertosRequestStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid HuertoRequestStatus value: " + i);
+ }
+ }
+
+ public enum HuertosPaymentType implements ValuableEnum {
+ BANK(0),
+ CASH(1);
+
+ private final int value;
+
+ HuertosPaymentType(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosPaymentType fromInt(int i) {
+ for (HuertosPaymentType type : values()) {
+ if (type.value == i) return type;
+ }
+ throw new IllegalArgumentException("Invalid HuertoPaymentType value: " + i);
+ }
+ }
+
+ public enum HuertosRequestType implements ValuableEnum {
+ REGISTER(0),
+ UNREGISTER(1),
+ ADD_COLLABORATOR(2),
+ REMOVE_COLLABORATOR(3),
+ ADD_GREENHOUSE(4),
+ REMOVE_GREENHOUSE(5);
+
+ private final int value;
+
+ HuertosRequestType(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosRequestType fromInt(int i) {
+ for (HuertosRequestType type : values()) {
+ if (type.value == i) return type;
+ }
+ throw new IllegalArgumentException("Invalid HuertoRequestType value: " + i);
+ }
+ }
+
+ public enum HuertosAnnouncePriority implements ValuableEnum {
+ LOW(0),
+ MEDIUM(1),
+ HIGH(2);
+
+ private final int value;
+
+ HuertosAnnouncePriority(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosAnnouncePriority fromInt(int i) {
+ for (HuertosAnnouncePriority priority : values()) {
+ if (priority.value == i) return priority;
+ }
+ throw new IllegalArgumentException("Invalid HuertoAnnouncePriority value: " + i);
+ }
+ }
+
+ public enum HuertosPaymentFrequency implements ValuableEnum {
+ BIYEARLY(0),
+ YEARLY(1);
+
+ private final int value;
+
+ HuertosPaymentFrequency(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static HuertosPaymentFrequency fromInt(int i) {
+ for (HuertosPaymentFrequency frequency : values()) {
+ if (frequency.value == i) return frequency;
+ }
+ throw new IllegalArgumentException("Invalid HuertoPaymentFrequency value: " + i);
+ }
+ }
+
+
+ public enum MMCUserRole implements IUserRole {
+ PLAYER(0),
+ ADMIN(1);
+
+ private final int value;
+
+ MMCUserRole(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static MMCUserRole fromInt(int i) {
+ for (MMCUserRole role : values()) {
+ if (role.value == i) return role;
+ }
+ throw new IllegalArgumentException("Invalid MMCUserRole value: " + i);
+ }
+ }
+
+
+ public enum MMCUserStatus implements ValuableEnum {
+ INACTIVE(0),
+ ACTIVE(1);
+
+ private final int value;
+
+ MMCUserStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static MMCUserStatus fromInt(int i) {
+ for (MMCUserStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid MMCUserStatus value: " + i);
+ }
+ }
+
+ public enum MMCModStatus implements ValuableEnum {
+ ACTIVE(0),
+ INACTIVE(1);
+
+ private final int value;
+
+ MMCModStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static MMCModStatus fromInt(int i) {
+ for (MMCModStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid MiarmacraftModStatus value: " + i);
+ }
+ }
+
+ public enum CineUserStatus implements ValuableEnum {
+ ACTIVE(1),
+ INACTIVE(0);
+
+ private final int value;
+
+ CineUserStatus(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static CineUserStatus fromInt(int i) {
+ for (CineUserStatus status : values()) {
+ if (status.value == i) return status;
+ }
+ throw new IllegalArgumentException("Invalid CineUserStatus value: " + i);
+ }
+ }
+
+ public enum CineUserRole implements IUserRole {
+ USER(0),
+ ADMIN(1);
+
+ private final int value;
+
+ CineUserRole(int value) {
+ this.value = value;
+ }
+
+ @Override
+ public int getValue() {
+ return value;
+ }
+
+ public static CineUserRole fromInt(int i) {
+ for (CineUserRole role : values()) {
+ if (role.value == i) return role;
+ }
+ throw new IllegalArgumentException("Invalid CineUserRole value: " + i);
+ }
+ }
+
+ // Private constructor to prevent instantiation
+ private Constants() {
+ throw new AssertionError("Utility class cannot be instantiated.");
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/LogAccumulator.java b/backlib/src/main/java/net/miarma/api/backlib/LogAccumulator.java
new file mode 100644
index 0000000..a9eb4f6
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/LogAccumulator.java
@@ -0,0 +1,25 @@
+package net.miarma.api.backlib;
+
+import org.slf4j.Logger;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class LogAccumulator {
+ private static final List LOGS = Collections.synchronizedList(new ArrayList<>());
+ private static final AtomicInteger COUNTER = new AtomicInteger(0);
+
+ public static void add(String message) {
+ LOGS.add(LogEntry.of(COUNTER.getAndIncrement(), message));
+ }
+
+ public static void flushToLogger(Logger logger) {
+ LOGS.stream()
+ .sorted(Comparator.comparingInt(LogEntry::order))
+ .forEach(entry -> logger.info(entry.message()));
+ LOGS.clear();
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/LogEntry.java b/backlib/src/main/java/net/miarma/api/backlib/LogEntry.java
new file mode 100644
index 0000000..a3fe2e3
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/LogEntry.java
@@ -0,0 +1,7 @@
+package net.miarma.api.backlib;
+
+public record LogEntry(int order, String message) {
+ public static LogEntry of(int order, String message) {
+ return new LogEntry(order, message);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/OSType.java b/backlib/src/main/java/net/miarma/api/backlib/OSType.java
new file mode 100644
index 0000000..ded6952
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/OSType.java
@@ -0,0 +1,9 @@
+package net.miarma.api.backlib;
+
+/**
+ * Enum que representa los diferentes tipos de sistemas operativos soportados
+ * @author José Manuel Amador Gallardo
+ */
+public enum OSType {
+ LINUX, WINDOWS, INVALID_OS
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/ValuableEnum.java b/backlib/src/main/java/net/miarma/api/backlib/ValuableEnum.java
new file mode 100644
index 0000000..4b1acdb
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/ValuableEnum.java
@@ -0,0 +1,9 @@
+package net.miarma.api.backlib;
+
+/**
+ * Interfaz que define un enum con un valor entero asociado
+ * @author José Manuel Amador Gallardo
+ */
+public interface ValuableEnum {
+ int getValue();
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/annotations/APIDontReturn.java b/backlib/src/main/java/net/miarma/api/backlib/annotations/APIDontReturn.java
new file mode 100644
index 0000000..7335883
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/annotations/APIDontReturn.java
@@ -0,0 +1,16 @@
+package net.miarma.api.backlib.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Esta anotación se utiliza para indicar que un campo no debe ser incluido en la respuesta de la API.
+ * Se aplica a campos de clases o interfaces y está disponible en tiempo de ejecución.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+@Target(ElementType.FIELD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface APIDontReturn {}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/annotations/Table.java b/backlib/src/main/java/net/miarma/api/backlib/annotations/Table.java
new file mode 100644
index 0000000..085c7b9
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/annotations/Table.java
@@ -0,0 +1,18 @@
+package net.miarma.api.backlib.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Anotación para definir el nombre de una tabla en la base de datos.
+ * Se utiliza para mapear una clase a una tabla específica.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Table {
+ String value();
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/dao/FileDAO.java b/backlib/src/main/java/net/miarma/api/backlib/core/dao/FileDAO.java
new file mode 100644
index 0000000..739a232
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/dao/FileDAO.java
@@ -0,0 +1,148 @@
+package net.miarma.api.backlib.core.dao;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.backlib.http.QueryFilters;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.backlib.core.entities.FileEntity;
+
+import java.util.List;
+import java.util.Map;
+
+public class FileDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public FileDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ return getAll(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(FileEntity.class)
+ .where(Map.of("file_id", id.toString()))
+ .build();
+
+ db.executeOne(query, FileEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAll(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(FileEntity.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, FileEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getUserFiles(Integer userId) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(FileEntity.class)
+ .where(Map.of("uploaded_by", userId.toString()))
+ .build();
+
+ db.execute(query, FileEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future insert(FileEntity file) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.insert(file).build();
+
+ db.executeOne(query, FileEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future upsert(FileEntity file, String... conflictKeys) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.upsert(file, conflictKeys).build();
+
+ db.executeOne(query, FileEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future update(FileEntity file) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.update(file).build();
+
+ db.executeOne(query, FileEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future exists(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(FileEntity.class)
+ .where(Map.of("file_id", id.toString()))
+ .build();
+
+ db.executeOne(query, FileEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future delete(Integer id) {
+ Promise promise = Promise.promise();
+ FileEntity file = new FileEntity();
+ file.setFile_id(id);
+
+ String query = QueryBuilder.delete(file).build();
+
+ db.executeOne(query, FileEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/dao/UserDAO.java b/backlib/src/main/java/net/miarma/api/backlib/core/dao/UserDAO.java
new file mode 100644
index 0000000..be6b6ea
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/dao/UserDAO.java
@@ -0,0 +1,163 @@
+package net.miarma.api.backlib.core.dao;
+
+import java.util.List;
+import java.util.Map;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.core.entities.UserEntity;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.backlib.http.QueryFilters;
+import net.miarma.api.backlib.http.QueryParams;
+
+public class UserDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public UserDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ return getAll(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(UserEntity.class)
+ .where(Map.of("user_id", id.toString()))
+ .build();
+
+ db.executeOne(query, UserEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAll(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(UserEntity.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, UserEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future getByEmail(String email) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(UserEntity.class)
+ .where(Map.of("email", email))
+ .build();
+
+ db.executeOne(query, UserEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future getByUserName(String userName) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(UserEntity.class)
+ .where(Map.of("user_name", userName))
+ .build();
+
+ db.executeOne(query, UserEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future insert(UserEntity user) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.insert(user).build();
+
+ db.executeOne(query, UserEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future upsert(UserEntity userEntity, String... conflictKeys) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.upsert(userEntity, conflictKeys).build();
+
+ db.executeOne(query, UserEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future update(UserEntity user) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.update(user).build();
+
+ db.executeOne(query, UserEntity.class,
+ _ -> promise.complete(user),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future delete(Integer id) {
+ Promise promise = Promise.promise();
+ UserEntity user = new UserEntity();
+ user.setUser_id(id);
+
+ String query = QueryBuilder.delete(user).build();
+
+ db.executeOne(query, UserEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future exists(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(UserEntity.class)
+ .where(Map.of("user_id", id.toString()))
+ .build();
+
+ db.executeOne(query, UserEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/entities/FileEntity.java b/backlib/src/main/java/net/miarma/api/backlib/core/entities/FileEntity.java
new file mode 100644
index 0000000..d2abe45
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/entities/FileEntity.java
@@ -0,0 +1,83 @@
+package net.miarma.api.backlib.core.entities;
+
+import io.vertx.sqlclient.Row;
+import net.miarma.api.backlib.Constants.CoreFileContext;
+import net.miarma.api.backlib.annotations.Table;
+import net.miarma.api.backlib.db.AbstractEntity;
+
+import java.time.LocalDateTime;
+
+@Table("files")
+public class FileEntity extends AbstractEntity {
+ private Integer file_id;
+ private String file_name;
+ private String file_path;
+ private String mime_type;
+ private Integer uploaded_by;
+ private CoreFileContext context;
+ private LocalDateTime uploaded_at;
+
+ public FileEntity() {
+ super();
+ }
+
+ public FileEntity(Row row) {
+ super(row);
+ }
+
+ public Integer getFile_id() {
+ return file_id;
+ }
+
+ public void setFile_id(Integer file_id) {
+ this.file_id = file_id;
+ }
+
+ public String getFile_name() {
+ return file_name;
+ }
+
+ public void setFile_name(String file_name) {
+ this.file_name = file_name;
+ }
+
+ public String getFile_path() {
+ return file_path;
+ }
+
+ public void setFile_path(String file_path) {
+ this.file_path = file_path;
+ }
+
+ public String getMime_type() {
+ return mime_type;
+ }
+
+ public void setMime_type(String mime_type) {
+ this.mime_type = mime_type;
+ }
+
+ public Integer getUploaded_by() {
+ return uploaded_by;
+ }
+
+ public void setUploaded_by(Integer uploaded_by) {
+ this.uploaded_by = uploaded_by;
+ }
+
+ public CoreFileContext getContext() {
+ return context;
+ }
+
+ public void setContext(CoreFileContext context) {
+ this.context = context;
+ }
+
+ public LocalDateTime getUploaded_at() {
+ return uploaded_at;
+ }
+
+ public void setUploaded_at(LocalDateTime uploaded_at) {
+ this.uploaded_at = uploaded_at;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/entities/UserEntity.java b/backlib/src/main/java/net/miarma/api/backlib/core/entities/UserEntity.java
new file mode 100644
index 0000000..33a7e0e
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/entities/UserEntity.java
@@ -0,0 +1,63 @@
+package net.miarma.api.backlib.core.entities;
+
+import java.time.LocalDateTime;
+
+import io.vertx.sqlclient.Row;
+import net.miarma.api.backlib.Constants.CoreUserGlobalStatus;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.annotations.APIDontReturn;
+import net.miarma.api.backlib.annotations.Table;
+import net.miarma.api.backlib.db.AbstractEntity;
+import net.miarma.api.backlib.interfaces.IUser;
+
+@Table("users")
+public class UserEntity extends AbstractEntity implements IUser {
+ private Integer user_id;
+ private String user_name;
+ private String email;
+ private String display_name;
+ @APIDontReturn
+ private String password;
+ private String avatar;
+ private CoreUserGlobalStatus global_status;
+ private CoreUserRole role;
+ private LocalDateTime created_at;
+ private LocalDateTime updated_at;
+
+ public UserEntity() { }
+ public UserEntity(Row row) { super(row); }
+
+ public Integer getUser_id() { return user_id; }
+ public void setUser_id(Integer user_id) { this.user_id = user_id; }
+ public String getUser_name() { return user_name; }
+ public void setUser_name(String user_name) { this.user_name = user_name; }
+ public String getEmail() { return email; }
+ public void setEmail(String email) { this.email = email; }
+ public String getDisplay_name() { return display_name; }
+ public void setDisplay_name(String display_name) { this.display_name = display_name; }
+ public String getPassword() { return password; }
+ public void setPassword(String password) { this.password = password; }
+ public String getAvatar() { return avatar; }
+ public void setAvatar(String avatar) { this.avatar = avatar; }
+ public CoreUserGlobalStatus getGlobal_status() { return global_status; }
+ public void setGlobal_status(CoreUserGlobalStatus global_status) { this.global_status = global_status; }
+ public CoreUserRole getGlobal_role() { return role; }
+ public void setGlobal_role(CoreUserRole role) { this.role = role; }
+ public LocalDateTime getCreated_at() { return created_at; }
+ public void setCreated_at(LocalDateTime created_at) { this.created_at = created_at; }
+ public LocalDateTime getUpdated_at() { return updated_at; }
+ public void setUpdated_at(LocalDateTime updated_at) { this.updated_at = updated_at; }
+
+ public static UserEntity from(IUser user) {
+ UserEntity entity = new UserEntity();
+ entity.setUser_id(user.getUser_id());
+ entity.setUser_name(user.getUser_name());
+ entity.setDisplay_name(user.getDisplay_name());
+ entity.setEmail(user.getEmail());
+ entity.setPassword(user.getPassword());
+ entity.setAvatar(user.getAvatar());
+ entity.setGlobal_status(user.getGlobal_status());
+ entity.setGlobal_role(user.getGlobal_role());
+ return entity;
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileDataHandler.java b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileDataHandler.java
new file mode 100644
index 0000000..332ff39
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileDataHandler.java
@@ -0,0 +1,101 @@
+package net.miarma.api.backlib.core.handlers;
+
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.FileUpload;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.Constants.CoreFileContext;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.backlib.core.entities.FileEntity;
+import net.miarma.api.backlib.core.services.FileService;
+import net.miarma.api.backlib.util.JsonUtil;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+
+@SuppressWarnings("unused")
+public class FileDataHandler {
+
+ private final FileService fileService;
+
+ public FileDataHandler(Pool pool) {
+ this.fileService = new FileService(pool);
+ }
+
+ public void getAll(RoutingContext ctx) {
+ QueryParams params = QueryParams.from(ctx);
+
+ fileService.getAll(params)
+ .onSuccess(files -> JsonUtil.sendJson(ctx, ApiStatus.OK, files))
+ .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
+ }
+
+ public void getById(RoutingContext ctx) {
+ Integer fileId = Integer.parseInt(ctx.pathParam("file_id"));
+
+ fileService.getById(fileId)
+ .onSuccess(file -> JsonUtil.sendJson(ctx, ApiStatus.OK, file))
+ .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
+ }
+
+ public void create(RoutingContext ctx) {
+ try {
+ String fileName = ctx.request().getFormAttribute("file_name");
+ String mimeType = ctx.request().getFormAttribute("mime_type");
+ int uploadedBy = Integer.parseInt(ctx.request().getFormAttribute("uploaded_by"));
+ int contextValue = Integer.parseInt(ctx.request().getFormAttribute("context"));
+
+ FileUpload upload = ctx.fileUploads().stream()
+ .filter(f -> f.name().equals("file"))
+ .findFirst()
+ .orElseThrow(() -> new RuntimeException("Archivo no encontrado"));
+
+ Buffer buffer = ctx.vertx().fileSystem().readFileBlocking(upload.uploadedFileName());
+ byte[] fileBinary = buffer.getBytes();
+
+ FileEntity file = new FileEntity();
+ file.setFile_name(fileName);
+ file.setMime_type(mimeType);
+ file.setUploaded_by(uploadedBy);
+ file.setContext(CoreFileContext.fromInt(contextValue));
+
+ fileService.create(file, fileBinary)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.CREATED, result))
+ .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
+ } catch (Exception e) {
+ Constants.LOGGER.error(e.getMessage(), e);
+ JsonUtil.sendJson(ctx, ApiStatus.INTERNAL_SERVER_ERROR, null, e.getMessage());
+ }
+ }
+
+
+ public void update(RoutingContext ctx) {
+ FileEntity file = Constants.GSON.fromJson(ctx.body().asString(), FileEntity.class);
+
+ fileService.update(file)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.OK, result))
+ .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
+ }
+
+ public void delete(RoutingContext ctx) {
+ Integer fileId = Integer.parseInt(ctx.pathParam("file_id"));
+ JsonObject body = ctx.body().asJsonObject();
+ String filePath = body.getString("file_path");
+
+ try {
+ Files.deleteIfExists(Paths.get(filePath));
+ } catch (IOException e) {
+ Constants.LOGGER.error(e.getMessage(), e);
+ JsonUtil.sendJson(ctx, ApiStatus.INTERNAL_SERVER_ERROR, null, e.getMessage());
+ return;
+ }
+
+ fileService.delete(fileId)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null))
+ .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileLogicHandler.java b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileLogicHandler.java
new file mode 100644
index 0000000..ab10c87
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/FileLogicHandler.java
@@ -0,0 +1,67 @@
+package net.miarma.api.backlib.core.handlers;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.security.JWTManager;
+import net.miarma.api.backlib.util.JsonUtil;
+
+public class FileLogicHandler {
+
+ private final Vertx vertx;
+
+ public FileLogicHandler(Vertx vertx) {
+ this.vertx = vertx;
+ }
+
+ private boolean validateAuth(RoutingContext ctx, JsonObject request) {
+ String authHeader = ctx.request().getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Unauthorized");
+ return false;
+ }
+
+ String token = authHeader.substring(7);
+ int userId = JWTManager.getInstance().getUserId(token);
+
+ if (userId <= 0) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token");
+ return false;
+ }
+
+ request.put("userId", userId);
+ return true;
+ }
+
+ public void getUserFiles(RoutingContext ctx) {
+ JsonObject request = new JsonObject().put("action", "getUserFiles");
+ if (!validateAuth(ctx, request)) return;
+
+ vertx.eventBus().request(Constants.CORE_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.NOT_FOUND, null, "The user has no files");
+ }
+ });
+ }
+
+ public void downloadFile(RoutingContext ctx) {
+ JsonObject request = new JsonObject()
+ .put("action", "downloadFile")
+ .put("fileId", Integer.parseInt(ctx.pathParam("file_id")));
+
+ if (!validateAuth(ctx, request)) return;
+
+ vertx.eventBus().request(Constants.CORE_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.NOT_FOUND, null, "Error downloading file");
+ }
+ });
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/handlers/ScreenshotHandler.java b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/ScreenshotHandler.java
new file mode 100644
index 0000000..b238252
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/ScreenshotHandler.java
@@ -0,0 +1,44 @@
+package net.miarma.api.backlib.core.handlers;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.ext.web.client.WebClient;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.util.JsonUtil;
+
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+
+public class ScreenshotHandler {
+
+ private final WebClient webClient;
+
+ public ScreenshotHandler(Vertx vertx) {
+ this.webClient = WebClient.create(vertx);
+ }
+
+ public void getScreenshot(RoutingContext ctx) {
+ String url = ctx.request().getParam("url");
+
+ if (url == null || url.isEmpty()) {
+ ctx.response().setStatusCode(400).end("URL parameter is required");
+ return;
+ }
+
+ String encodedUrl = URLEncoder.encode(url, StandardCharsets.UTF_8);
+ String microserviceUrl = "http://screenshoter:7000/screenshot?url=" + encodedUrl;
+
+ webClient.getAbs(microserviceUrl)
+ .send(ar -> {
+ if (ar.succeeded()) {
+ ctx.response()
+ .putHeader("Content-Type", "image/png")
+ .end(ar.result().body());
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.INTERNAL_SERVER_ERROR, null, "Could not generate the screenshot");
+ }
+ });
+
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserDataHandler.java b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserDataHandler.java
new file mode 100644
index 0000000..c99512f
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserDataHandler.java
@@ -0,0 +1,70 @@
+package net.miarma.api.backlib.core.handlers;
+
+import io.vertx.ext.web.RoutingContext;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.backlib.util.JsonUtil;
+import net.miarma.api.backlib.core.entities.UserEntity;
+import net.miarma.api.backlib.core.services.UserService;
+
+@SuppressWarnings("unused")
+public class UserDataHandler {
+
+ private final UserService userService;
+
+ public UserDataHandler(Pool pool) {
+ this.userService = new UserService(pool);
+ }
+
+ public void getAll(RoutingContext ctx) {
+ QueryParams params = QueryParams.from(ctx);
+
+ userService.getAll(params)
+ .onSuccess(users -> JsonUtil.sendJson(ctx, ApiStatus.OK, users)).onFailure(err -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ JsonUtil.sendJson(ctx, status, null, err.getMessage());
+ });
+ }
+
+ public void getById(RoutingContext ctx) {
+ Integer userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ userService.getById(userId)
+ .onSuccess(user -> JsonUtil.sendJson(ctx, ApiStatus.OK, user)).onFailure(err -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ JsonUtil.sendJson(ctx, status, null, err.getMessage());
+ });
+ }
+
+ public void create(RoutingContext ctx) {
+ UserEntity user = Constants.GSON.fromJson(ctx.body().asString(), UserEntity.class);
+
+ userService.register(user)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.CREATED, result)).onFailure(err -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ JsonUtil.sendJson(ctx, status, null, err.getMessage());
+ });
+ }
+
+ public void update(RoutingContext ctx) {
+ UserEntity user = Constants.GSON.fromJson(ctx.body().asString(), UserEntity.class);
+
+ userService.update(user)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, result)).onFailure(err -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ JsonUtil.sendJson(ctx, status, null, err.getMessage());
+ });
+ }
+
+ public void delete(RoutingContext ctx) {
+ Integer userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ userService.delete(userId)
+ .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, result)).onFailure(err -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ JsonUtil.sendJson(ctx, status, null, err.getMessage());
+ });
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserLogicHandler.java b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserLogicHandler.java
new file mode 100644
index 0000000..90e96f2
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/handlers/UserLogicHandler.java
@@ -0,0 +1,286 @@
+package net.miarma.api.backlib.core.handlers;
+
+import com.auth0.jwt.interfaces.DecodedJWT;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.security.JWTManager;
+import net.miarma.api.backlib.util.EventBusUtil;
+import net.miarma.api.backlib.util.JsonUtil;
+import net.miarma.api.backlib.core.entities.UserEntity;
+
+public class UserLogicHandler {
+
+ private final Vertx vertx;
+
+ public UserLogicHandler(Vertx vertx) {
+ this.vertx = vertx;
+ }
+
+ public void login(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "login")
+ .put("email", body.getString("email", null))
+ .put("userName", body.getString("userName", null))
+ .put("password", body.getString("password"))
+ .put("keepLoggedIn", body.getBoolean("keepLoggedIn", false));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonObject result = (JsonObject) ar.result().body();
+ result.put("tokenTime", System.currentTimeMillis());
+ JsonUtil.sendJson(ctx, ApiStatus.OK, result);
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void loginValidate(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "loginValidate")
+ .put("userId", body.getInteger("userId"))
+ .put("password", body.getString("password"));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void register(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "register")
+ .put("userName", body.getString("userName"))
+ .put("email", body.getString("email"))
+ .put("displayName", body.getString("displayName"))
+ .put("password", body.getString("password"));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.CREATED, null);
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void changePassword(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "changePassword")
+ .put("userId", body.getInteger("userId"))
+ .put("newPassword", body.getString("newPassword"));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, true, "Updated");
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void validateToken(RoutingContext ctx) {
+ String authHeader = ctx.request().getHeader("Authorization");
+
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ String token = authHeader.substring(7);
+
+ JsonObject request = new JsonObject()
+ .put("action", "validateToken")
+ .put("token", token);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded() && Boolean.TRUE.equals(ar.result().body())) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, true, "Valid token");
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, false, "Invalid token");
+ }
+ });
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.BAD_REQUEST, null, "Missing or invalid Authorization header");
+ }
+ }
+
+ public void refreshToken(RoutingContext ctx) {
+ String tokenHeader = ctx.request().getHeader("Authorization");
+
+ if (tokenHeader == null || !tokenHeader.startsWith("Bearer ")) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Missing or invalid Authorization header");
+ return;
+ }
+
+ String token = tokenHeader.substring("Bearer ".length());
+ JWTManager jwt = JWTManager.getInstance();
+
+ try {
+ DecodedJWT decoded = jwt.decodeWithoutVerification(token);
+ int userId = decoded.getClaim("userId").asInt();
+ if (userId == -1) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token");
+ return;
+ }
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, new JsonObject()
+ .put("action", "getUserById")
+ .put("userId", userId), ar -> {
+
+ if (ar.succeeded()) {
+ JsonObject userJson = (JsonObject) ar.result().body();
+ UserEntity user = Constants.GSON.fromJson(userJson.encode(), UserEntity.class);
+ String newToken = jwt.generateToken(user.getUser_name(), user.getUser_id(), user.getGlobal_role(), false);
+
+ JsonUtil.sendJson(ctx, ApiStatus.OK, new JsonObject().put("token", newToken));
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "User not found");
+ }
+ });
+
+ } catch (Exception e) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token");
+ }
+ }
+
+
+
+ public void getInfo(RoutingContext ctx) {
+ String authHeader = ctx.request().getHeader("Authorization");
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Unauthorized");
+ return;
+ }
+
+ String token = authHeader.substring(7);
+ int userId = net.miarma.api.backlib.security.JWTManager.getInstance().getUserId(token);
+
+ if (userId <= 0) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token");
+ return;
+ }
+
+ JsonObject request = new JsonObject()
+ .put("action", "getInfo")
+ .put("userId", userId);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void exists(RoutingContext ctx) {
+ int userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ JsonObject request = new JsonObject()
+ .put("action", "userExists")
+ .put("userId", userId);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void getStatus(RoutingContext ctx) {
+ int userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ JsonObject request = new JsonObject()
+ .put("action", "getStatus")
+ .put("userId", userId);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void getRole(RoutingContext ctx) {
+ int userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ JsonObject request = new JsonObject()
+ .put("action", "getRole")
+ .put("userId", userId);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void getAvatar(RoutingContext ctx) {
+ int userId = Integer.parseInt(ctx.pathParam("user_id"));
+
+ JsonObject request = new JsonObject()
+ .put("action", "getAvatar")
+ .put("userId", userId);
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void updateStatus(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "updateStatus")
+ .put("userId", body.getInteger("userId"))
+ .put("status", body.getInteger("status"));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null);
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+
+ public void updateRole(RoutingContext ctx) {
+ JsonObject body = ctx.body().asJsonObject();
+
+ JsonObject request = new JsonObject()
+ .put("action", "updateRole")
+ .put("userId", body.getInteger("userId"))
+ .put("role", body.getInteger("role"));
+
+ vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> {
+ if (ar.succeeded()) {
+ JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null);
+ } else {
+ EventBusUtil.handleReplyError(ctx, ar.cause());
+ }
+ });
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/services/FileService.java b/backlib/src/main/java/net/miarma/api/backlib/core/services/FileService.java
new file mode 100644
index 0000000..1b2dde1
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/services/FileService.java
@@ -0,0 +1,124 @@
+package net.miarma.api.backlib.core.services;
+
+import io.vertx.core.Future;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.OSType;
+import net.miarma.api.backlib.exceptions.NotFoundException;
+import net.miarma.api.backlib.exceptions.ValidationException;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.backlib.core.dao.FileDAO;
+import net.miarma.api.backlib.core.entities.FileEntity;
+import net.miarma.api.backlib.core.validators.FileValidator;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.List;
+
+public class FileService {
+
+ private final FileDAO fileDAO;
+ private final FileValidator fileValidator;
+
+ public FileService(Pool pool) {
+ this.fileDAO = new FileDAO(pool);
+ this.fileValidator = new FileValidator();
+ }
+
+ public Future> getAll(QueryParams params) {
+ return fileDAO.getAll(params);
+ }
+
+ public Future getById(Integer id) {
+ return fileDAO.getById(id).compose(file -> {
+ if (file == null) {
+ return Future.failedFuture(new NotFoundException("File not found with id: " + id));
+ }
+ return Future.succeededFuture(file);
+ });
+ }
+
+ public Future> getUserFiles(Integer userId) {
+ return fileDAO.getUserFiles(userId);
+ }
+
+ public Future create(FileEntity file, byte[] fileBinary) {
+ return fileValidator.validate(file, fileBinary.length).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+
+ String dir = ConfigManager.getInstance()
+ .getFilesDir(file.getContext().toCtxString());
+
+ String pathString = dir + file.getFile_name();
+ Path filePath = Paths.get(dir + file.getFile_name());
+ file.setFile_path(ConfigManager.getOS() == OSType.WINDOWS ?
+ pathString.replace("\\", "\\\\") : pathString);
+
+ try {
+ Files.write(filePath, fileBinary);
+ } catch (IOException e) {
+ Constants.LOGGER.error("Error writing file to disk: ", e);
+ return Future.failedFuture(e);
+ }
+
+ return fileDAO.insert(file);
+ });
+ }
+
+ public Future downloadFile(Integer fileId) {
+ return getById(fileId);
+ }
+
+ public Future update(FileEntity file) {
+ return fileValidator.validate(file).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+
+ return fileDAO.update(file);
+ });
+ }
+
+ public Future upsert(FileEntity file) {
+ return fileValidator.validate(file).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+
+ return fileDAO.upsert(file, "file_id");
+ });
+ }
+
+ public Future delete(Integer fileId) {
+ return getById(fileId).compose(file -> {
+ String dir = ConfigManager.getInstance()
+ .getFilesDir(file.getContext().toCtxString());
+
+ String filePath = dir + file.getFile_name();
+ Path path = Paths.get(filePath);
+
+ try {
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ Constants.LOGGER.error("Error deleting file from disk: ", e);
+ return Future.failedFuture(e);
+ }
+
+ return fileDAO.delete(fileId).compose(deleted -> {
+ if (!deleted) {
+ return Future.failedFuture(new NotFoundException("File not found with id: " + fileId));
+ }
+ return Future.succeededFuture(true);
+ });
+ });
+ }
+
+ public Future exists(Integer fileId) {
+ return fileDAO.exists(fileId);
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/services/UserService.java b/backlib/src/main/java/net/miarma/api/backlib/core/services/UserService.java
new file mode 100644
index 0000000..ab1407c
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/services/UserService.java
@@ -0,0 +1,209 @@
+package net.miarma.api.backlib.core.services;
+
+import java.util.List;
+
+import io.vertx.core.Future;
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.Constants.CoreUserGlobalStatus;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.exceptions.AlreadyExistsException;
+import net.miarma.api.backlib.exceptions.BadRequestException;
+import net.miarma.api.backlib.exceptions.ForbiddenException;
+import net.miarma.api.backlib.exceptions.NotFoundException;
+import net.miarma.api.backlib.exceptions.UnauthorizedException;
+import net.miarma.api.backlib.exceptions.ValidationException;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.backlib.security.JWTManager;
+import net.miarma.api.backlib.security.PasswordHasher;
+import net.miarma.api.backlib.core.dao.UserDAO;
+import net.miarma.api.backlib.core.entities.UserEntity;
+import net.miarma.api.backlib.core.validators.UserValidator;
+
+public class UserService {
+
+ private final UserDAO userDAO;
+ private final UserValidator userValidator;
+
+ public UserService(Pool pool) {
+ this.userDAO = new UserDAO(pool);
+ this.userValidator = new UserValidator();
+ }
+
+ /* AUTHENTICATION */
+
+ public Future login(String emailOrUsername, String plainPassword, boolean keepLoggedIn) {
+ return getByEmail(emailOrUsername).compose(user -> {
+ if (user == null) {
+ return getByUserName(emailOrUsername).compose(user2 -> {
+ if (user2 == null) {
+ return Future.succeededFuture(null);
+ }
+ return Future.succeededFuture(user2);
+ });
+ }
+ return Future.succeededFuture(user);
+ }).compose(user -> {
+
+ if (user == null) {
+ return Future.failedFuture(new BadRequestException("Invalid credentials"));
+ }
+
+ if (user.getGlobal_status() != Constants.CoreUserGlobalStatus.ACTIVE) {
+ return Future.failedFuture(new ForbiddenException("User is not active"));
+ }
+
+ if (!PasswordHasher.verify(plainPassword, user.getPassword())) {
+ return Future.failedFuture(new BadRequestException("Invalid credentials"));
+ }
+
+ JWTManager jwtManager = JWTManager.getInstance();
+ String token = jwtManager.generateToken(user.getUser_name(), user.getUser_id(), user.getGlobal_role(), keepLoggedIn);
+
+ JsonObject response = new JsonObject()
+ .put("token", token)
+ .put("loggedUser", new JsonObject(user.encode()));
+
+ return Future.succeededFuture(response);
+ });
+ }
+
+ public Future loginValidate(Integer userId, String password) {
+ return getById(userId).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found"));
+ }
+ if (!PasswordHasher.verify(password, user.getPassword())) {
+ return Future.failedFuture(new BadRequestException("Invalid credentials"));
+ }
+ JsonObject response = new JsonObject()
+ .put("valid", true);
+ return Future.succeededFuture(response);
+ });
+ }
+
+ public Future register(UserEntity user) {
+ return getByEmail(user.getEmail()).compose(existing -> {
+ if (existing != null) {
+ return Future.failedFuture(new AlreadyExistsException("Email already exists"));
+ }
+
+ user.setPassword(PasswordHasher.hash(user.getPassword()));
+ user.setGlobal_role(CoreUserRole.USER);
+ user.setGlobal_status(CoreUserGlobalStatus.ACTIVE);
+
+ return userValidator.validate(user).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+ return userDAO.insert(user);
+ });
+ });
+ }
+
+ public Future changePassword(int userId, String newPassword) {
+ return getById(userId).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found"));
+ }
+
+ user.setPassword(PasswordHasher.hash(newPassword));
+ return userDAO.update(user);
+ });
+ }
+
+ public Future validateToken(String token) {
+ JWTManager jwtManager = JWTManager.getInstance();
+ return jwtManager.isValid(token) ?
+ Future.succeededFuture(true) :
+ Future.failedFuture(new UnauthorizedException("Invalid token"));
+ }
+
+ /* USERS OPERATIONS */
+
+ public Future> getAll(QueryParams params) {
+ return userDAO.getAll(params);
+ }
+
+ public Future getById(Integer id) {
+ return userDAO.getById(id).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found in the database"));
+ }
+ return Future.succeededFuture(user);
+ });
+ }
+
+ public Future getByEmail(String email) {
+ return userDAO.getByEmail(email);
+ }
+
+ public Future getByUserName(String userName) {
+ return userDAO.getByUserName(userName);
+ }
+
+ public Future updateRole(Integer userId, CoreUserRole role) {
+ return getById(userId).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found in the database"));
+ }
+ user.setGlobal_role(role);
+ return userDAO.update(user);
+ });
+ }
+
+ public Future updateStatus(Integer userId, CoreUserGlobalStatus status) {
+ return getById(userId).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found in the database"));
+ }
+ user.setGlobal_status(status);
+ return userDAO.update(user);
+ });
+ }
+
+ /* CRUD OPERATIONS */
+
+ public Future create(UserEntity user) {
+ return userValidator.validate(user).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+ return userDAO.insert(user);
+ });
+ }
+
+ public Future update(UserEntity user) {
+ return userValidator.validate(user).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+ if (user.getPassword() == null || user.getPassword().isEmpty()) {
+ user.setPassword(null);
+ }
+ return userDAO.update(user);
+ });
+ }
+
+ public Future upsert(UserEntity user) {
+ return userValidator.validate(user).compose(validation -> {
+ if (!validation.isValid()) {
+ return Future.failedFuture(new ValidationException(Constants.GSON.toJson(validation.getErrors())));
+ }
+ if (user.getPassword() != null && !user.getPassword().isEmpty()) {
+ user.setPassword(PasswordHasher.hash(user.getPassword()));
+ }
+ return userDAO.upsert(user, "user_id", "email", "user_name");
+ });
+ }
+
+ public Future delete(Integer id) {
+ return getById(id).compose(user -> {
+ if (user == null) {
+ return Future.failedFuture(new NotFoundException("User not found in the database"));
+ }
+ return userDAO.delete(id);
+ });
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/validators/FileValidator.java b/backlib/src/main/java/net/miarma/api/backlib/core/validators/FileValidator.java
new file mode 100644
index 0000000..3190f95
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/validators/FileValidator.java
@@ -0,0 +1,84 @@
+package net.miarma.api.backlib.core.validators;
+
+import io.vertx.core.Future;
+import net.miarma.api.backlib.validation.ValidationResult;
+import net.miarma.api.backlib.core.entities.FileEntity;
+
+public class FileValidator {
+
+ public Future validate(FileEntity file, int size) {
+ ValidationResult result = new ValidationResult();
+
+ if (file == null) {
+ return Future.succeededFuture(result.addError("file", "El archivo no puede ser nulo"));
+ }
+
+ if (file.getFile_name() == null || file.getFile_name().isBlank()) {
+ result.addError("file_name", "El nombre del archivo es obligatorio");
+ }
+
+ if (file.getMime_type() == null || file.getMime_type().isBlank()) {
+ result.addError("mime_type", "El tipo MIME es obligatorio");
+ }
+
+ if (file.getContext() == null) {
+ result.addError("context", "El contexto del archivo es obligatorio");
+ }
+
+ if (file.getUploaded_by() == null || file.getUploaded_by() <= 0) {
+ result.addError("uploaded_by", "El ID del usuario que subió el archivo es obligatorio y debe ser válido");
+ }
+
+ if (size <= 0) {
+ result.addError("size", "El archivo debe pesar más de 0 bytes");
+ }
+
+ if (file.getFile_name() != null && file.getFile_name().length() > 255) {
+ result.addError("file_name", "El nombre del archivo es demasiado largo");
+ }
+
+ if (size > 10485760) { // 10 MB limit
+ result.addError("size", "El archivo no puede pesar más de 10 MB");
+ }
+
+ return Future.succeededFuture(result);
+ }
+
+ public Future validate(FileEntity file) {
+ ValidationResult result = new ValidationResult();
+
+ if (file == null) {
+ return Future.succeededFuture(result.addError("file", "File cannot be null"));
+ }
+
+ if (file.getFile_name() == null || file.getFile_name().isBlank()) {
+ result.addError("file_name", "File name is required");
+ }
+
+ if (file.getFile_path() == null || file.getFile_path().isBlank()) {
+ result.addError("file_path", "File path is required");
+ }
+
+ if (file.getMime_type() == null || file.getMime_type().isBlank()) {
+ result.addError("mime_type", "MIME type is required");
+ }
+
+ if (file.getContext() == null) {
+ result.addError("context", "File context is required");
+ }
+
+ if (file.getUploaded_by() == null || file.getUploaded_by() <= 0) {
+ result.addError("uploaded_by", "Uploader user ID is required and must be valid");
+ }
+
+ if (file.getFile_name() != null && file.getFile_name().length() > 255) {
+ result.addError("file_name", "File name is too long");
+ }
+
+ if (file.getFile_path() != null && file.getFile_path().length() > 255) {
+ result.addError("file_path", "File path is too long");
+ }
+
+ return Future.succeededFuture(result);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/core/validators/UserValidator.java b/backlib/src/main/java/net/miarma/api/backlib/core/validators/UserValidator.java
new file mode 100644
index 0000000..ff42903
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/core/validators/UserValidator.java
@@ -0,0 +1,44 @@
+package net.miarma.api.backlib.core.validators;
+
+import io.vertx.core.Future;
+import net.miarma.api.backlib.validation.ValidationResult;
+import net.miarma.api.backlib.core.entities.UserEntity;
+
+public class UserValidator {
+
+ public Future validate(UserEntity user) {
+ ValidationResult result = new ValidationResult();
+
+ if (user == null) {
+ return Future.succeededFuture(result.addError("user", "El usuario no puede ser nulo"));
+ }
+
+ if (user.getUser_name() == null || user.getUser_name().isBlank()) {
+ result.addError("user_name", "El nombre de usuario es obligatorio");
+ }
+
+ if (user.getDisplay_name() == null || user.getDisplay_name().isBlank()) {
+ result.addError("display_name", "El nombre para mostrar es obligatorio");
+ }
+
+ if (user.getEmail() != null && !user.getEmail().isBlank()) {
+ if (!user.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
+ result.addError("email", "El correo electrónico no es válido");
+ }
+ }
+
+ if (user.getPassword() == null || user.getPassword().isBlank()) {
+ result.addError("password", "La contraseña es obligatoria");
+ }
+
+ if (user.getGlobal_status() == null) {
+ result.addError("global_status", "El estado global del usuario es obligatorio");
+ }
+
+ if (user.getGlobal_role() == null) {
+ result.addError("role", "El rol del usuario es obligatorio");
+ }
+
+ return Future.succeededFuture(result);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/db/AbstractEntity.java b/backlib/src/main/java/net/miarma/api/backlib/db/AbstractEntity.java
new file mode 100644
index 0000000..b5bb1b3
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/db/AbstractEntity.java
@@ -0,0 +1,160 @@
+package net.miarma.api.backlib.db;
+
+import io.vertx.core.json.JsonObject;
+import io.vertx.sqlclient.Row;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.ValuableEnum;
+import net.miarma.api.backlib.annotations.APIDontReturn;
+
+import java.lang.reflect.Field;
+
+/**
+ * Clase base para todas las entidades persistentes del sistema.
+ *
+ * Proporciona utilidades para:
+ *
+ * - Construir una entidad a partir de una fila de base de datos ({@link Row})
+ * - Serializar una entidad a {@link JsonObject}
+ * - Generar una representación en texto
+ *
+ *
+ * Los campos se mapean por reflexión, lo que permite extender fácilmente las entidades
+ * sin necesidad de escribir lógica de parsing repetitiva.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public abstract class AbstractEntity {
+
+ /**
+ * Constructor por defecto. Requerido para instanciación sin datos.
+ */
+ public AbstractEntity() {}
+
+ /**
+ * Constructor que inicializa los campos de la entidad a partir de una fila de base de datos.
+ *
+ * @param row Fila SQL proporcionada por Vert.x.
+ */
+ public AbstractEntity(Row row) {
+ populateFromRow(row);
+ }
+
+ /**
+ * Rellena los campos del objeto usando reflexión a partir de una {@link Row} de Vert.x.
+ * Se soportan tipos básicos (String, int, boolean, etc.), enums con método estático {@code fromInt(int)},
+ * y {@link java.math.BigDecimal} (a través del tipo {@code Numeric} de Vert.x).
+ *
+ * Si un tipo no está soportado, se registra un error en el log y se ignora ese campo.
+ *
+ * @param row Fila de datos de la que extraer los valores.
+ */
+ private void populateFromRow(Row row) {
+ Field[] fields = this.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ try {
+ field.setAccessible(true);
+ Class> type = field.getType();
+ String name = field.getName();
+
+ Object value;
+ if (type.isEnum()) {
+ Integer intValue = row.getInteger(name);
+ if (intValue != null) {
+ try {
+ var method = type.getMethod("fromInt", int.class);
+ value = method.invoke(null, intValue);
+ } catch (Exception e) {
+ value = null;
+ }
+ } else {
+ value = null;
+ }
+ } else {
+ value = switch (type.getSimpleName()) {
+ case "Integer", "int" -> row.getInteger(name);
+ case "String" -> row.getString(name);
+ case "double", "Double" -> row.getDouble(name);
+ case "long", "Long" -> row.getLong(name);
+ case "boolean", "Boolean" -> row.getBoolean(name);
+ case "LocalDateTime" -> row.getLocalDateTime(name);
+ case "BigDecimal" -> {
+ try {
+ var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(name));
+ yield numeric != null ? numeric.bigDecimalValue() : null;
+ } catch (Exception e) {
+ yield null;
+ }
+ }
+ default -> {
+ Constants.LOGGER.error("Type not supported yet: {} for field {}", type.getName(), name);
+ yield null;
+ }
+ };
+
+ }
+
+ field.set(this, value);
+ } catch (Exception e) {
+ Constants.LOGGER.error("Error populating field {}: {}", field.getName(), e.getMessage());
+ }
+ }
+ }
+
+ /**
+ * Codifica esta entidad como un objeto JSON, omitiendo los campos anotados con {@link APIDontReturn}.
+ *
+ *
Si un campo implementa {@link ValuableEnum}, se usará su valor en lugar del nombre del enum.
+ *
+ * @return Representación JSON de esta entidad.
+ */
+ public String encode() {
+ JsonObject json = new JsonObject();
+ Class> clazz = this.getClass();
+
+ while (clazz != null) {
+ for (Field field : clazz.getDeclaredFields()) {
+ if (field.isAnnotationPresent(APIDontReturn.class)) continue;
+
+ field.setAccessible(true);
+ try {
+ Object value = field.get(this);
+
+ if (value instanceof ValuableEnum ve) {
+ json.put(field.getName(), ve.getValue());
+ } else {
+ json.put(field.getName(), value);
+ }
+ } catch (IllegalAccessException e) {
+ Constants.LOGGER.error("Error accessing field {}: {}", field.getName(), e.getMessage());
+ }
+ }
+ clazz = clazz.getSuperclass();
+ }
+
+ return json.encode();
+ }
+
+ /**
+ * Devuelve una representación en texto de la entidad, mostrando todos los campos y sus valores.
+ *
+ * Útil para logs y debugging.
+ *
+ * @return Cadena de texto con el nombre de la clase y todos los campos.
+ */
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(this.getClass().getSimpleName()).append(" [ ");
+ Field[] fields = this.getClass().getDeclaredFields();
+ for (Field field : fields) {
+ field.setAccessible(true);
+ try {
+ sb.append(field.getName()).append("= ").append(field.get(this)).append(", ");
+ } catch (IllegalAccessException e) {
+ Constants.LOGGER.error("Error stringing field {}: {}", field.getName(), e.getMessage());
+ }
+ }
+ sb.append("]");
+ return sb.toString();
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/db/DataAccessObject.java b/backlib/src/main/java/net/miarma/api/backlib/db/DataAccessObject.java
new file mode 100644
index 0000000..5930719
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/db/DataAccessObject.java
@@ -0,0 +1,73 @@
+package net.miarma.api.backlib.db;
+
+import io.vertx.core.Future;
+
+import java.util.List;
+
+/**
+ * Interfaz genérica para operaciones CRUD básicas en una base de datos,
+ * adaptada al modelo asincrónico de Vert.x usando {@link Future}.
+ *
+ * @param Tipo de la entidad gestionada.
+ * @param Tipo del identificador único de la entidad.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public interface DataAccessObject {
+
+ /**
+ * Recupera todos los registros de la entidad.
+ *
+ * @return Un {@link Future} que contiene una lista con todas las entidades encontradas.
+ */
+ Future> getAll();
+
+ /**
+ * Recupera una entidad por su identificador.
+ *
+ * @param id Identificador de la entidad.
+ * @return Un {@link Future} que contiene la entidad, o falla si no se encuentra.
+ */
+ Future getById(ID id);
+
+ /**
+ * Inserta una nueva entidad en la base de datos.
+ *
+ * @param t Entidad a insertar.
+ * @return Un {@link Future} que contiene la entidad insertada, posiblemente con su ID asignado.
+ */
+ Future insert(T t);
+
+ /**
+ * Inserta o actualiza una entidad en la base de datos.
+ * Si la entidad ya existe, se actualiza; si no, se inserta como nueva.
+ *
+ * @param t Entidad a insertar o actualizar.
+ * @return Un {@link Future} que contiene la entidad insertada o actualizada.
+ */
+ Future upsert(T t, String... conflictKeys);
+
+ /**
+ * Actualiza una entidad existente.
+ *
+ * @param t Entidad con los datos actualizados.
+ * @return Un {@link Future} que contiene la entidad actualizada.
+ */
+ Future update(T t);
+
+ /**
+ * Elimina una entidad por su identificador.
+ *
+ * @param id Identificador de la entidad a eliminar.
+ * @return Un {@link Future} que indica si la operación fue exitosa.
+ */
+ Future delete(ID id);
+
+ /**
+ * Comprueba si existe una entidad con el identificador proporcionado.
+ *
+ * @param id Identificador a comprobar.
+ * @return Un {@link Future} que contiene {@code true} si existe, o {@code false} si no.
+ */
+ Future exists(ID id);
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseManager.java b/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseManager.java
new file mode 100644
index 0000000..400a48f
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseManager.java
@@ -0,0 +1,131 @@
+package net.miarma.api.backlib.db;
+
+import io.vertx.core.Future;
+import io.vertx.core.Handler;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.Row;
+import io.vertx.sqlclient.RowSet;
+import net.miarma.api.backlib.Constants;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Gestor centralizado de acceso a la base de datos utilizando Vert.x SQL Client.
+ *
+ *
+ * Esta clase sigue el patron Singleton para asegurar una sola instancia.
+ *
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class DatabaseManager {
+
+ private static DatabaseManager instance;
+ private final Pool pool;
+
+ /**
+ * Constructor privado para seguir el patrón Singleton.
+ *
+ * @param pool el pool de conexiones proporcionado por Vert.x
+ */
+ private DatabaseManager(Pool pool) {
+ this.pool = pool;
+ }
+
+ /**
+ * Devuelve la instancia única de {@link DatabaseManager}. Si no existe, la crea.
+ *
+ * @param pool el pool de conexiones a reutilizar
+ * @return la instancia singleton de DatabaseManager
+ */
+ public static synchronized DatabaseManager getInstance(Pool pool) {
+ if (instance == null) {
+ instance = new DatabaseManager(pool);
+ }
+ return instance;
+ }
+
+ /**
+ * Devuelve el pool de conexiones actual.
+ *
+ * @return el pool de conexiones
+ */
+ public Pool getPool() {
+ return pool;
+ }
+
+ /**
+ * Realiza una consulta simple para verificar que la conexión con la base de datos funciona.
+ *
+ * @return un {@link Future} que representa el resultado de la consulta
+ */
+ public Future> testConnection() {
+ return pool.query("SELECT 1").execute();
+ }
+
+ /**
+ * Ejecuta una consulta SQL que devuelve múltiples resultados y los convierte en objetos de tipo {@code T}.
+ *
+ * @param query la consulta SQL a ejecutar
+ * @param clazz clase del objeto a instanciar desde cada fila del resultado
+ * @param onSuccess callback que se ejecuta si la consulta fue exitosa
+ * @param onFailure callback que se ejecuta si ocurre un error
+ * @param tipo del objeto a devolver
+ * @return un {@link Future} con la lista de resultados convertidos
+ */
+ public Future> execute(String query, Class clazz, Handler> onSuccess,
+ Handler onFailure) {
+ return pool.query(query).execute().map(rows -> {
+ List results = new ArrayList<>();
+ for (Row row : rows) {
+ try {
+ Constructor constructor = clazz.getConstructor(Row.class);
+ results.add(constructor.newInstance(row));
+ } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
+ | InvocationTargetException e) {
+ Constants.LOGGER.error("Error instantiating class: {}", e.getMessage());
+ }
+ }
+ return results;
+ }).onComplete(ar -> {
+ if (ar.succeeded()) {
+ onSuccess.handle(ar.result());
+ } else {
+ onFailure.handle(ar.cause());
+ }
+ });
+ }
+
+ /**
+ * Ejecuta una consulta SQL que devuelve como máximo una fila y la convierte en un objeto de tipo {@code T}.
+ *
+ * @param query la consulta SQL a ejecutar
+ * @param clazz clase del objeto a instanciar desde la fila del resultado
+ * @param onSuccess callback que se ejecuta si la consulta fue exitosa
+ * @param onFailure callback que se ejecuta si ocurre un error
+ * @param tipo del objeto a devolver
+ * @return un {@link Future} con el objeto instanciado, o null si no hay resultados
+ */
+ public Future executeOne(String query, Class clazz, Handler onSuccess, Handler onFailure) {
+ return pool.query(query).execute().map(rows -> {
+ for (Row row : rows) {
+ try {
+ Constructor constructor = clazz.getConstructor(Row.class);
+ return constructor.newInstance(row);
+ } catch (Exception e) {
+ Constants.LOGGER.error("Error instantiating class: {}", e.getMessage());
+ }
+ }
+ return null; // Si no hay filas
+ }).onComplete(ar -> {
+ if (ar.succeeded()) {
+ onSuccess.handle(ar.result());
+ } else {
+ onFailure.handle(ar.cause());
+ }
+ });
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseProvider.java b/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseProvider.java
new file mode 100644
index 0000000..d1dc777
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/db/DatabaseProvider.java
@@ -0,0 +1,49 @@
+package net.miarma.api.backlib.db;
+
+import io.vertx.core.Vertx;
+import io.vertx.mysqlclient.MySQLConnectOptions;
+import io.vertx.sqlclient.Pool;
+import io.vertx.sqlclient.PoolOptions;
+import net.miarma.api.backlib.ConfigManager;
+
+/**
+ * Factoría de {@link Pool} para conexiones MySQL usando Vert.x.
+ *
+ *
+ * Se apoya en {@link ConfigManager} para extraer la configuración de la BBDD
+ * (host, puerto, nombre, usuario y contraseña) y crea un pool con un tamaño
+ * máximo de 10 conexiones.
+ *
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class DatabaseProvider {
+
+ /**
+ * Crea y configura un pool de conexiones MySQL.
+ *
+ * @param vertx instancia principal de Vert.x
+ * @param config gestor de configuración con las propiedades necesarias:
+ *
+ * - db.port – puerto del servidor MySQL
+ * - db.host – host o IP
+ * - db.name – nombre de la base de datos
+ * - db.user – usuario de la base
+ * - db.password – contraseña del usuario
+ *
+ * @return un {@link Pool} listo para usarse en consultas Vert.x
+ */
+ public static Pool createPool(Vertx vertx, ConfigManager config) {
+ MySQLConnectOptions connectOptions = new MySQLConnectOptions()
+ .setPort(config.getIntProperty("db.port"))
+ .setHost(config.getStringProperty("db.host"))
+ .setDatabase(config.getStringProperty("db.name"))
+ .setUser(config.getStringProperty("db.user"))
+ .setPassword(config.getStringProperty("db.password"));
+
+ PoolOptions poolOptions = new PoolOptions()
+ .setMaxSize(10);
+
+ return Pool.pool(vertx, connectOptions, poolOptions);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/db/QueryBuilder.java b/backlib/src/main/java/net/miarma/api/backlib/db/QueryBuilder.java
new file mode 100644
index 0000000..4409868
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/db/QueryBuilder.java
@@ -0,0 +1,516 @@
+package net.miarma.api.backlib.db;
+
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.annotations.Table;
+
+import java.lang.reflect.Field;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Clase utilitaria para construir queries SQL dinámicamente mediante reflexión,
+ * usando entidades anotadas con {@link Table}.
+ *
+ * Soporta operaciones SELECT, INSERT, UPDATE (con y sin valores nulos), y UPSERT.
+ * También permite aplicar filtros desde un mapa o directamente desde un objeto.
+ *
+ * ¡Ojo! No ejecuta la query, solo la construye.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class QueryBuilder {
+ private final StringBuilder query;
+ private String sort;
+ private String order;
+ private String limit;
+ private Class> entityClass;
+
+ public QueryBuilder() {
+ this.query = new StringBuilder();
+ }
+
+ /**
+ * Obtiene el nombre de la tabla desde la anotación @Table de la clase dada.
+ */
+ private static String getTableName(Class clazz) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("Class cannot be null");
+ }
+
+ if (clazz.isAnnotationPresent(Table.class)) {
+ Table annotation = clazz.getAnnotation(Table.class);
+ return annotation.value();
+ }
+ throw new IllegalArgumentException("Class does not have @Table annotation");
+ }
+
+ /**
+ * Devuelve la consulta SQL construida hasta el momento.
+ */
+ public String getQuery() {
+ return query.toString();
+ }
+
+ /**
+ * Extrae el valor de un campo, manejando enums y tipos especiales.
+ * Si es un Enum y tiene getValue(), lo usa; si no, devuelve el name().
+ * Si es un LocalDateTime, lo convierte a String en formato SQL.
+ */
+ private static Object extractValue(Object fieldValue) {
+ if (fieldValue instanceof Enum>) {
+ try {
+ var method = fieldValue.getClass().getMethod("getValue");
+ return method.invoke(fieldValue);
+ } catch (Exception e) {
+ return ((Enum>) fieldValue).name();
+ }
+ }
+
+ if (fieldValue instanceof LocalDateTime ldt) {
+ return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+ }
+
+ return fieldValue;
+ }
+
+ /**
+ * Escapa los caracteres especiales en una cadena para evitar inyecciones SQL.
+ * @param value the string value to escape
+ * @return the escaped string
+ */
+ private static String escapeSql(String value) {
+ return value.replace("'", "''");
+ }
+
+ /**
+ * Construye una consulta SELECT para la clase dada, con columnas opcionales.
+ * @param clazz the entity class to query
+ * @param columns optional columns to select; if empty, selects all columns
+ * @return the current QueryBuilder instance
+ * @param the type of the entity class
+ */
+ public static QueryBuilder select(Class clazz, String... columns) {
+ if (clazz == null) {
+ throw new IllegalArgumentException("Class cannot be null");
+ }
+
+ QueryBuilder qb = new QueryBuilder();
+ qb.entityClass = clazz;
+ String tableName = getTableName(clazz);
+
+ qb.query.append("SELECT ");
+
+ if (columns.length == 0) {
+ qb.query.append("* ");
+ } else {
+ StringJoiner joiner = new StringJoiner(", ");
+ for (String column : columns) {
+ if (column != null) {
+ joiner.add(column);
+ }
+ }
+ qb.query.append(joiner).append(" ");
+ }
+
+ qb.query.append("FROM ").append(tableName).append(" ");
+ return qb;
+ }
+
+ /**
+ * Añade una cláusula WHERE a la consulta actual, filtrando por los campos del mapa.
+ * Los valores pueden ser números o cadenas, y se manejan adecuadamente.
+ *
+ * @param filters un mapa de filtros donde la clave es el nombre del campo y el valor es el valor a filtrar
+ * @return el QueryBuilder actual para encadenar más métodos
+ */
+ public QueryBuilder where(Map filters) {
+ if (filters == null || filters.isEmpty()) {
+ return this;
+ }
+
+ Set validFields = entityClass != null
+ ? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
+ : Collections.emptySet();
+
+ List conditions = new ArrayList<>();
+
+ for (Map.Entry entry : filters.entrySet()) {
+ String key = entry.getKey();
+ String value = entry.getValue();
+
+ if (!validFields.contains(key)) {
+ Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key);
+ continue;
+ }
+
+ if (value.startsWith("(") && value.endsWith(")")) {
+ conditions.add(key + " IN " + value);
+ } else if (value.matches("-?\\d+(\\.\\d+)?")) {
+ conditions.add(key + " = " + value);
+ } else {
+ conditions.add(key + " = '" + value + "'");
+ }
+ }
+
+ if (!conditions.isEmpty()) {
+ query.append("WHERE ").append(String.join(" AND ", conditions)).append(" ");
+ }
+
+ return this;
+ }
+
+ /**
+ * Añade una cláusula WHERE a la consulta actual, filtrando por los campos del objeto.
+ * Los valores se extraen mediante reflexión y se manejan adecuadamente.
+ *
+ * @param object el objeto del cual se extraerán los campos para filtrar
+ * @return el QueryBuilder actual para encadenar más métodos
+ */
+ public QueryBuilder where(T object) {
+ if (object == null) {
+ throw new IllegalArgumentException("Object cannot be null");
+ }
+
+ Set validFields = entityClass != null
+ ? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
+ : Collections.emptySet();
+
+ this.query.append("WHERE ");
+ StringJoiner joiner = new StringJoiner(" AND ");
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ Object fieldValue = field.get(object);
+ if (fieldValue != null) {
+ String key = field.getName();
+ if (!validFields.contains(key)) {
+ Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key);
+ continue;
+ }
+ Object value = extractValue(fieldValue);
+ if (value instanceof String || value instanceof LocalDateTime) {
+ joiner.add(key + " = '" + value + "'");
+ } else {
+ joiner.add(key + " = " + value.toString());
+ }
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+ this.query.append(joiner).append(" ");
+ return this;
+ }
+
+ /**
+ * Construye una consulta INSERT para el objeto dado, insertando todos sus campos.
+ * Los valores se extraen mediante reflexión y se manejan adecuadamente.
+ *
+ * @param object el objeto a insertar
+ * @return el QueryBuilder actual para encadenar más métodos
+ * @param el tipo del objeto a insertar
+ */
+ public static QueryBuilder insert(T object) {
+ if (object == null) {
+ throw new IllegalArgumentException("Object cannot be null");
+ }
+
+ QueryBuilder qb = new QueryBuilder();
+ String table = getTableName(object.getClass());
+ qb.query.append("INSERT INTO ").append(table).append(" ");
+ qb.query.append("(");
+ StringJoiner columns = new StringJoiner(", ");
+ StringJoiner values = new StringJoiner(", ");
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ columns.add(field.getName());
+ Object fieldValue = field.get(object);
+ if (fieldValue != null) {
+ Object value = extractValue(fieldValue);
+ if (value instanceof String || value instanceof LocalDateTime) {
+ values.add("'" + escapeSql((String) value) + "'");
+ } else {
+ values.add(value.toString());
+ }
+ } else {
+ values.add("NULL");
+ }
+ } catch (IllegalArgumentException | IllegalAccessException e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+ qb.query.append(columns).append(") ");
+ qb.query.append("VALUES (").append(values).append(") RETURNING * ");
+ return qb;
+ }
+
+ /**
+ * Construye una consulta UPDATE para el objeto dado, actualizando todos sus campos.
+ * Los valores se extraen mediante reflexión y se manejan adecuadamente.
+ * Requiere que el objeto tenga un campo ID (terminado en _id) para la cláusula WHERE.
+ *
+ * @param object el objeto a actualizar
+ * @return el QueryBuilder actual para encadenar más métodos
+ * @param el tipo del objeto a actualizar
+ */
+ public static QueryBuilder update(T object) {
+ if (object == null) {
+ throw new IllegalArgumentException("Object cannot be null");
+ }
+
+ QueryBuilder qb = new QueryBuilder();
+ String table = getTableName(object.getClass());
+ qb.query.append("UPDATE ").append(table).append(" SET ");
+
+ StringJoiner setJoiner = new StringJoiner(", ");
+ StringJoiner whereJoiner = new StringJoiner(" AND ");
+
+ Field idField = null;
+
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ Object fieldValue = field.get(object);
+ if (fieldValue == null) continue;
+
+ String fieldName = field.getName();
+ Object value = extractValue(fieldValue);
+
+ if (fieldName.endsWith("_id")) {
+ idField = field;
+ whereJoiner.add(fieldName + " = " + (value instanceof String
+ || value instanceof LocalDateTime ? "'" + value + "'" : value));
+ continue;
+ }
+
+ setJoiner.add(fieldName + " = " + (value instanceof String
+ || value instanceof LocalDateTime ? "'" + value + "'" : value));
+ } catch (Exception e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+
+ if (idField == null) {
+ throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
+ }
+
+ qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
+ return qb;
+ }
+
+ /**
+ * Construye una consulta UPDATE que establece los campos a NULL si son nulos.
+ * Requiere que el objeto tenga un campo ID (terminado en _id) para la cláusula WHERE.
+ *
+ * @param object el objeto a actualizar
+ * @return el QueryBuilder actual para encadenar más métodos
+ * @param el tipo del objeto a actualizar
+ */
+ public static QueryBuilder updateWithNulls(T object) {
+ if (object == null) {
+ throw new IllegalArgumentException("Object cannot be null");
+ }
+
+ QueryBuilder qb = new QueryBuilder();
+ String table = getTableName(object.getClass());
+ qb.query.append("UPDATE ").append(table).append(" SET ");
+
+ StringJoiner setJoiner = new StringJoiner(", ");
+ StringJoiner whereJoiner = new StringJoiner(" AND ");
+
+ Field idField = null;
+
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ String fieldName = field.getName();
+ Object fieldValue = field.get(object);
+
+ if (fieldName.endsWith("_id")) {
+ idField = field;
+ Object value = extractValue(fieldValue);
+ whereJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
+ continue;
+ }
+
+ if (fieldValue == null) {
+ setJoiner.add(fieldName + " = NULL");
+ } else {
+ Object value = extractValue(fieldValue);
+ setJoiner.add(fieldName + " = " + (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value));
+ }
+ } catch (Exception e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+
+ if (idField == null) {
+ throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
+ }
+
+ qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner);
+ return qb;
+ }
+
+ /**
+ * Construye una consulta UPSERT (INSERT o UPDATE) para el objeto dado.
+ * Si hay claves de conflicto, se actualizan los campos excepto las claves duplicadas.
+ *
+ * @param object el objeto a insertar o actualizar
+ * @param conflictKeys las claves que causan conflictos y no deben actualizarse
+ * @return el QueryBuilder actual para encadenar más métodos
+ * @param el tipo del objeto a insertar o actualizar
+ */
+ public static QueryBuilder upsert(T object, String... conflictKeys) {
+ if (object == null) throw new IllegalArgumentException("Object cannot be null");
+
+ QueryBuilder qb = new QueryBuilder();
+ String table = getTableName(object.getClass());
+ qb.query.append("INSERT INTO ").append(table).append(" ");
+
+ StringJoiner columns = new StringJoiner(", ");
+ StringJoiner values = new StringJoiner(", ");
+ Map updates = new HashMap<>();
+
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ Object fieldValue = field.get(object);
+ String columnName = field.getName();
+ columns.add(columnName);
+
+ Object value = extractValue(fieldValue);
+ String valueStr = value == null ? "NULL"
+ : (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value.toString());
+ values.add(valueStr);
+
+ // no actualizamos la clave duplicada
+ boolean isConflictKey = Arrays.asList(conflictKeys).contains(columnName);
+ if (!isConflictKey) {
+ updates.put(columnName, valueStr);
+ }
+
+ } catch (Exception e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+
+ qb.query.append("(").append(columns).append(") VALUES (").append(values).append(")");
+
+ if (conflictKeys.length > 0 && !updates.isEmpty()) {
+ qb.query.append(" ON DUPLICATE KEY UPDATE ");
+ StringJoiner updateSet = new StringJoiner(", ");
+ updates.forEach((k, v) -> updateSet.add(k + " = " + v));
+ qb.query.append(updateSet);
+ }
+
+ return qb;
+ }
+
+ /**
+ * Construye una consulta DELETE para el objeto dado, eliminando registros que coincidan con sus campos.
+ * Los valores se extraen mediante reflexión y se manejan adecuadamente.
+ *
+ * @param object el objeto a eliminar
+ * @return el QueryBuilder actual para encadenar más métodos
+ * @param el tipo del objeto a eliminar
+ */
+ public static QueryBuilder delete(T object) {
+ if (object == null) throw new IllegalArgumentException("Object cannot be null");
+
+ QueryBuilder qb = new QueryBuilder();
+ String table = getTableName(object.getClass());
+ qb.query.append("DELETE FROM ").append(table).append(" WHERE ");
+
+ StringJoiner joiner = new StringJoiner(" AND ");
+ for (Field field : object.getClass().getDeclaredFields()) {
+ field.setAccessible(true);
+ try {
+ Object fieldValue = field.get(object);
+ if (fieldValue != null) {
+ Object value = extractValue(fieldValue);
+ joiner.add(field.getName() + " = " + (value instanceof String
+ || value instanceof LocalDateTime ? "'" + value + "'" : value.toString()));
+ }
+ } catch (Exception e) {
+ Constants.LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
+ }
+ }
+
+ qb.query.append(joiner).append(" ");
+ return qb;
+ }
+
+ /**
+ * Añade una cláusula ORDER BY a la consulta actual, ordenando por la columna y el orden especificados.
+ * Si la columna no es válida, se ignora.
+ *
+ * @param column la columna por la que ordenar
+ * @param order el orden (ASC o DESC); si no se especifica, se asume ASC
+ * @return el QueryBuilder actual para encadenar más métodos
+ */
+ public QueryBuilder orderBy(Optional column, Optional order) {
+ column.ifPresent(c -> {
+ if (entityClass != null) {
+ boolean isValid = Arrays.stream(entityClass.getDeclaredFields())
+ .map(Field::getName)
+ .anyMatch(f -> f.equals(c));
+
+ if (!isValid) {
+ Constants.LOGGER.warn("[QueryBuilder] Ignorando campo invalido en ORDER BY: {}", c);
+ return;
+ }
+ }
+
+ sort = "ORDER BY " + c + " ";
+ order.ifPresent(o -> sort += o.equalsIgnoreCase("asc") ? "ASC" : "DESC" + " ");
+ });
+ return this;
+ }
+
+ /**
+ * Añade una cláusula LIMIT a la consulta actual, limitando el número de resultados.
+ * Si se especifica un offset, se añade también.
+ *
+ * @param limitParam el número máximo de resultados a devolver; si no se especifica, no se aplica límite
+ * @return el QueryBuilder actual para encadenar más métodos
+ */
+ public QueryBuilder limit(Optional limitParam) {
+ limitParam.ifPresent(param -> limit = "LIMIT " + param + " ");
+ return this;
+ }
+
+ /**
+ * Añade una cláusula OFFSET a la consulta actual, desplazando el inicio de los resultados.
+ * Si se especifica un offset, se añade también.
+ *
+ * @param offsetParam el número de resultados a omitir antes de empezar a devolver resultados; si no se especifica, no se aplica offset
+ * @return el QueryBuilder actual para encadenar más métodos
+ */
+ public QueryBuilder offset(Optional offsetParam) {
+ offsetParam.ifPresent(param -> limit += "OFFSET " + param + " ");
+ return this;
+ }
+
+ /**
+ * Construye y devuelve la consulta SQL completa.
+ * Si no se han añadido cláusulas ORDER BY, LIMIT o OFFSET, las omite.
+ *
+ * @return la consulta SQL construida
+ */
+ public String build() {
+ if (order != null && !order.isEmpty()) {
+ query.append(order);
+ }
+ if (sort != null && !sort.isEmpty()) {
+ query.append(sort);
+ }
+ if (limit != null && !limit.isEmpty()) {
+ query.append(limit);
+ }
+ return query.toString().trim() + ";";
+ }
+}
\ No newline at end of file
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/AlreadyExistsException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/AlreadyExistsException.java
new file mode 100644
index 0000000..0d0a864
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/AlreadyExistsException.java
@@ -0,0 +1,35 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando se intenta crear o registrar un recurso
+ * que ya existe en el sistema. Por ejemplo, un usuario con un email duplicado
+ * o un identificador ya registrado.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class AlreadyExistsException extends RuntimeException {
+
+ private static final long serialVersionUID = -6479166578011003074L;
+
+ /**
+ * Crea una nueva instancia de {@code AlreadyExistsException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public AlreadyExistsException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code AlreadyExistsException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public AlreadyExistsException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadGatewayException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadGatewayException.java
new file mode 100644
index 0000000..b76cdb5
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadGatewayException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando un servidor actúa como puerta de enlace o proxy
+ * y recibe una respuesta inválida o no válida de un servidor ascendente.
+ * Esto puede ocurrir, por ejemplo, cuando el servidor ascendente está inactivo
+ * o devuelve un error inesperado.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+@SuppressWarnings("serial")
+public class BadGatewayException extends RuntimeException {
+
+ /**
+ * Crea una nueva instancia de {@code BadGatewayException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public BadGatewayException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code BadGatewayException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public BadGatewayException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code BadGatewayException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public BadGatewayException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadRequestException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadRequestException.java
new file mode 100644
index 0000000..6ce30ef
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/BadRequestException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando se recibe una solicitud con datos inválidos o mal formados.
+ * Por ejemplo, cuando un campo requerido está vacío o un valor no cumple con las restricciones
+ * del sistema.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class BadRequestException extends RuntimeException {
+
+ private static final long serialVersionUID = -6954469492272938899L;
+
+ /**
+ * Crea una nueva instancia de {@code BadRequestException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public BadRequestException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code BadRequestException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public BadRequestException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code BadRequestException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public BadRequestException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/ConflictException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ConflictException.java
new file mode 100644
index 0000000..449a791
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ConflictException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando se produce un conflicto en el estado actual del recurso.
+ * Por ejemplo, cuando se intenta actualizar un recurso que ha sido modificado por otro usuario
+ * o cuando se intenta realizar una operación que no es válida debido al estado actual del sistema.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ConflictException extends RuntimeException {
+
+ private static final long serialVersionUID = -2065645862249312298L;
+
+ /**
+ * Crea una nueva instancia de {@code ConflictException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public ConflictException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ConflictException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public ConflictException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ConflictException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public ConflictException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/ForbiddenException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ForbiddenException.java
new file mode 100644
index 0000000..263b752
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ForbiddenException.java
@@ -0,0 +1,46 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando se intenta acceder a un recurso o realizar una operación
+ * que no está permitida para el usuario actual. Por ejemplo, cuando un usuario intenta
+ * acceder a un recurso que requiere permisos especiales o cuando intenta realizar una
+ * acción que no está autorizada.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ForbiddenException extends RuntimeException {
+
+ private static final long serialVersionUID = -1825202221085820141L;
+
+ /**
+ * Crea una nueva instancia de {@code ForbiddenException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public ForbiddenException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ForbiddenException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public ForbiddenException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ForbiddenException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public ForbiddenException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/InternalServerErrorException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/InternalServerErrorException.java
new file mode 100644
index 0000000..03e9fed
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/InternalServerErrorException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando ocurre un error interno en el servidor que impide
+ * completar la solicitud. Esto puede deberse a problemas de configuración, errores
+ * en el código del servidor o fallos inesperados.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class InternalServerErrorException extends RuntimeException {
+
+ private static final long serialVersionUID = 1081785471638808116L;
+
+ /**
+ * Crea una nueva instancia de {@code InternalServerErrorException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public InternalServerErrorException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code InternalServerErrorException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public InternalServerErrorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code InternalServerErrorException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public InternalServerErrorException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/NotFoundException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/NotFoundException.java
new file mode 100644
index 0000000..eda4ce1
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/NotFoundException.java
@@ -0,0 +1,44 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando un recurso solicitado no se encuentra en el sistema.
+ * Esto puede ocurrir, por ejemplo, cuando se intenta acceder a un recurso
+ * que no existe o ha sido eliminado.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class NotFoundException extends RuntimeException {
+
+ private static final long serialVersionUID = -8503378655195825178L;
+
+ /**
+ * Crea una nueva instancia de {@code NotFoundException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public NotFoundException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code NotFoundException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public NotFoundException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code NotFoundException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public NotFoundException(Throwable cause) {
+ super(cause);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/ServiceUnavailableException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ServiceUnavailableException.java
new file mode 100644
index 0000000..21b68e4
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ServiceUnavailableException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando un servicio no está disponible temporalmente.
+ * Esto puede ocurrir, por ejemplo, cuando el servidor está en mantenimiento
+ * o cuando hay problemas de conectividad con un servicio externo.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ServiceUnavailableException extends RuntimeException {
+
+ private static final long serialVersionUID = 2007517776804187799L;
+
+ /**
+ * Crea una nueva instancia de {@code ServiceUnavailableException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public ServiceUnavailableException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ServiceUnavailableException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public ServiceUnavailableException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code ServiceUnavailableException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public ServiceUnavailableException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/TeapotException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/TeapotException.java
new file mode 100644
index 0000000..60a5b2a
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/TeapotException.java
@@ -0,0 +1,46 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando se recibe un código de estado HTTP 418 (I'm a teapot).
+ * Esta excepción indica que el servidor se niega a preparar café porque es una tetera.
+ * Es una broma del protocolo HTTP y no debe ser utilizada en aplicaciones reales, sin embargo,
+ * la uso como excepción cuando alguien accede a un recurso que no debería.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class TeapotException extends RuntimeException {
+
+ private static final long serialVersionUID = 6105284989060090791L;
+
+ /**
+ * Crea una nueva instancia de {@code TeapotException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public TeapotException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code TeapotException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public TeapotException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code TeapotException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public TeapotException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnauthorizedException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnauthorizedException.java
new file mode 100644
index 0000000..ece5564
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnauthorizedException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando un usuario no está autorizado para realizar una acción
+ * o acceder a un recurso específico. Esto puede ocurrir, por ejemplo, cuando
+ * se intenta acceder a un recurso sin las credenciales adecuadas o sin los permisos necesarios.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class UnauthorizedException extends RuntimeException {
+
+ private static final long serialVersionUID = -3536275114764799718L;
+
+ /**
+ * Crea una nueva instancia de {@code UnauthorizedException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public UnauthorizedException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnauthorizedException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public UnauthorizedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnauthorizedException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public UnauthorizedException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnprocessableEntityException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnprocessableEntityException.java
new file mode 100644
index 0000000..a9e73f5
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnprocessableEntityException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando la solicitud no puede ser procesada debido a
+ * errores de validación o problemas con los datos proporcionados.
+ * Esto puede incluir datos faltantes, formatos incorrectos o valores no válidos.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class UnprocessableEntityException extends RuntimeException {
+
+ private static final long serialVersionUID = 5492048796111026459L;
+
+ /**
+ * Crea una nueva instancia de {@code UnprocessableEntityException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public UnprocessableEntityException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnprocessableEntityException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public UnprocessableEntityException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnprocessableEntityException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public UnprocessableEntityException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnsupportedMediaTypeException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnsupportedMediaTypeException.java
new file mode 100644
index 0000000..9135d87
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/UnsupportedMediaTypeException.java
@@ -0,0 +1,45 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando el tipo de medio (MIME type) de una solicitud no es compatible
+ * con lo que el servidor puede procesar. Esto puede ocurrir, por ejemplo, cuando se envía
+ * un tipo de contenido no soportado en una solicitud HTTP.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class UnsupportedMediaTypeException extends RuntimeException {
+
+ private static final long serialVersionUID = 1829890832415237556L;
+
+ /**
+ * Crea una nueva instancia de {@code UnsupportedMediaTypeException} con un mensaje descriptivo.
+ *
+ * @param message El mensaje que describe el error.
+ */
+ public UnsupportedMediaTypeException(String message) {
+ super(message);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnsupportedMediaTypeException} con un mensaje y una causa.
+ *
+ * @param message El mensaje que describe el error.
+ * @param cause La causa original de esta excepción.
+ */
+ public UnsupportedMediaTypeException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ /**
+ * Crea una nueva instancia de {@code UnsupportedMediaTypeException} con una causa.
+ *
+ * @param cause La causa original de esta excepción.
+ */
+ public UnsupportedMediaTypeException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/exceptions/ValidationException.java b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ValidationException.java
new file mode 100644
index 0000000..c5e289c
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/exceptions/ValidationException.java
@@ -0,0 +1,28 @@
+package net.miarma.api.backlib.exceptions;
+
+/**
+ * Excepción lanzada cuando los datos proporcionados no cumplen con las reglas de validación
+ * establecidas en el sistema. Esto puede ocurrir, por ejemplo, cuando se envían datos
+ * incompletos o incorrectos en una solicitud.
+ *
+ * Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
+ * declararla explícitamente en los métodos que la lanzan.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ValidationException extends RuntimeException {
+
+ /**
+ *
+ */
+ private static final long serialVersionUID = 7857157229263093210L;
+
+ /**
+ * Crea una nueva instancia de {@code ValidationException} con un mensaje descriptivo.
+ *
+ * @param json El JSON que describe el error de validación.
+ */
+ public ValidationException(String json) {
+ super(json);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/gson/APIDontReturnExclusionStrategy.java b/backlib/src/main/java/net/miarma/api/backlib/gson/APIDontReturnExclusionStrategy.java
new file mode 100644
index 0000000..5ae6b86
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/gson/APIDontReturnExclusionStrategy.java
@@ -0,0 +1,25 @@
+package net.miarma.api.backlib.gson;
+
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import net.miarma.api.backlib.annotations.APIDontReturn;
+
+/**
+ * Estrategia de exclusión para Gson que omite campos anotados con @APIDontReturn.
+ * Esta estrategia se utiliza para evitar que ciertos campos sean serializados
+ * y enviados en las respuestas de la API.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class APIDontReturnExclusionStrategy implements ExclusionStrategy {
+
+ @Override
+ public boolean shouldSkipField(FieldAttributes f) {
+ return f.getAnnotation(APIDontReturn.class) != null;
+ }
+
+ @Override
+ public boolean shouldSkipClass(Class> clazz) {
+ return false;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/gson/JsonObjectTypeAdapter.java b/backlib/src/main/java/net/miarma/api/backlib/gson/JsonObjectTypeAdapter.java
new file mode 100644
index 0000000..792f101
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/gson/JsonObjectTypeAdapter.java
@@ -0,0 +1,37 @@
+package net.miarma.api.backlib.gson;
+
+import com.google.gson.*;
+import io.vertx.core.json.JsonObject;
+
+import java.lang.reflect.Type;
+import java.util.Map;
+
+/**
+ * Adaptador de tipo para Gson que maneja la serialización y deserialización de objetos JsonObject.
+ * Este adaptador asegura que los objetos JsonObject se serialicen correctamente sin incluir el mapa interno.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class JsonObjectTypeAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override
+ public JsonElement serialize(JsonObject src, Type typeOfSrc, JsonSerializationContext context) {
+ JsonObject safe = src == null ? new JsonObject() : src;
+ JsonObject wrapped = new JsonObject(safe.getMap()); // evita el map dentro
+ return context.serialize(wrapped.getMap());
+ }
+
+ @Override
+ public JsonObject deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ if (!json.isJsonObject()) {
+ throw new JsonParseException("Expected JsonObject");
+ }
+
+ JsonObject obj = new JsonObject();
+ for (Map.Entry entry : json.getAsJsonObject().entrySet()) {
+ obj.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
+ }
+
+ return obj;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/gson/LocalDateTimeAdapter.java b/backlib/src/main/java/net/miarma/api/backlib/gson/LocalDateTimeAdapter.java
new file mode 100644
index 0000000..bc17d8b
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/gson/LocalDateTimeAdapter.java
@@ -0,0 +1,29 @@
+package net.miarma.api.backlib.gson;
+
+import com.google.gson.*;
+
+import java.lang.reflect.Type;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+
+/**
+ * Adaptador de tipo para Gson que maneja la serialización y deserialización de LocalDateTime.
+ * Este adaptador utiliza el formato ISO_LOCAL_DATE_TIME para convertir LocalDateTime a String
+ * y viceversa.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class LocalDateTimeAdapter implements JsonSerializer, JsonDeserializer {
+ private static final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
+
+ @Override
+ public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(src.format(formatter));
+ }
+
+ @Override
+ public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ return LocalDateTime.parse(json.getAsString(), formatter);
+ }
+}
+
diff --git a/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumDeserializer.java b/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumDeserializer.java
new file mode 100644
index 0000000..fde87b7
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumDeserializer.java
@@ -0,0 +1,30 @@
+package net.miarma.api.backlib.gson;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import net.miarma.api.backlib.ValuableEnum;
+
+import java.lang.reflect.Type;
+import java.util.Arrays;
+
+/**
+ * Deserializador de Gson para enumeraciones que implementan ValuableEnum.
+ * Este deserializador convierte un valor entero en una instancia de la enumeración correspondiente.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ValuableEnumDeserializer implements JsonDeserializer {
+
+ @Override
+ public ValuableEnum deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+ Class> enumClass = (Class>) typeOfT;
+ int value = json.getAsInt();
+
+ return (ValuableEnum) Arrays.stream(enumClass.getEnumConstants())
+ .filter(e -> ((ValuableEnum) e).getValue() == value)
+ .findFirst()
+ .orElseThrow(() -> new JsonParseException("Invalid enum value: " + value));
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumTypeAdapter.java b/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumTypeAdapter.java
new file mode 100644
index 0000000..89e92b0
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/gson/ValuableEnumTypeAdapter.java
@@ -0,0 +1,23 @@
+package net.miarma.api.backlib.gson;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import net.miarma.api.backlib.ValuableEnum;
+
+import java.lang.reflect.Type;
+
+/**
+ * Adaptador de tipo para Gson que maneja la serialización de enumeraciones que implementan ValuableEnum.
+ * Este adaptador convierte el valor de la enumeración en un elemento JSON primitivo.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ValuableEnumTypeAdapter implements JsonSerializer {
+
+ @Override
+ public JsonElement serialize(ValuableEnum src, Type typeOfSrc, JsonSerializationContext context) {
+ return new JsonPrimitive(src.getValue());
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/http/ApiResponse.java b/backlib/src/main/java/net/miarma/api/backlib/http/ApiResponse.java
new file mode 100644
index 0000000..d5d9150
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/http/ApiResponse.java
@@ -0,0 +1,60 @@
+package net.miarma.api.backlib.http;
+
+/**
+ * Clase genérica para representar una respuesta de la API.
+ *
+ * Esta clase encapsula el estado de la respuesta, un mensaje descriptivo y los datos devueltos.
+ * Se utiliza para estandarizar las respuestas de la API y facilitar el manejo de errores y datos.
+ *
+ * Ejemplo de uso:
+ *
+ * ApiResponse response = new ApiResponse<>(ApiStatus.SUCCESS, "Data retrieved successfully", myData);
+ *
+ * @see ApiStatus
+ *
+ * @param Tipo de dato que contendrá la respuesta.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ApiResponse {
+ private final int status;
+ private final String message;
+ private final T data;
+
+ /**
+ * Constructor para crear una respuesta de la API con un estado, mensaje y datos.
+ *
+ * @param status El estado de la respuesta, representado por un código.
+ * @param message Un mensaje descriptivo de la respuesta.
+ * @param data Los datos devueltos en la respuesta, puede ser null si no hay datos.
+ */
+ public ApiResponse(ApiStatus status, String message, T data) {
+ this.status = status.getCode();
+ this.message = message;
+ this.data = data;
+ }
+
+ /**
+ * Constructor para crear una respuesta de la API con un estado y mensaje, sin datos.
+ *
+ * @param status El estado de la respuesta, representado por un código.
+ * @param message Un mensaje descriptivo de la respuesta.
+ */
+ public ApiResponse(ApiStatus status, String message) {
+ this(status, message, null);
+ }
+
+ public int getStatus() {
+ return status;
+ }
+
+ public String getMessage() {
+ return message;
+ }
+
+ public T getData() {
+ return data;
+ }
+
+}
+
diff --git a/backlib/src/main/java/net/miarma/api/backlib/http/ApiStatus.java b/backlib/src/main/java/net/miarma/api/backlib/http/ApiStatus.java
new file mode 100644
index 0000000..e6aea04
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/http/ApiStatus.java
@@ -0,0 +1,108 @@
+package net.miarma.api.backlib.http;
+
+import net.miarma.api.backlib.exceptions.*;
+
+/**
+ * Enum que representa los códigos de estado HTTP utilizados en la API.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public enum ApiStatus {
+ OK(200),
+ CREATED(201),
+ ACCEPTED(202),
+ NO_CONTENT(204),
+ BAD_REQUEST(400),
+ UNAUTHORIZED(401),
+ FORBIDDEN(403),
+ NOT_FOUND(404),
+ CONFLICT(409),
+ IM_A_TEAPOT(418),
+ UNPROCESSABLE_ENTITY(422),
+ UNSUPPORTED_MEDIA_TYPE(415),
+ TOO_MANY_REQUESTS(429),
+ INTERNAL_SERVER_ERROR(500),
+ SERVICE_UNAVAILABLE(503);
+
+ private final int code;
+
+ ApiStatus(int code) {
+ this.code = code;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ /**
+ * Obtiene el mensaje por defecto asociado al código de estado.
+ *
+ * @return El mensaje por defecto.
+ */
+ public String getDefaultMessage() {
+ return switch (this) {
+ case OK -> "OK";
+ case CREATED -> "Created";
+ case ACCEPTED -> "Accepted";
+ case NO_CONTENT -> "No Content";
+ case BAD_REQUEST -> "Bad Request";
+ case UNAUTHORIZED -> "Unauthorized";
+ case FORBIDDEN -> "Forbidden";
+ case NOT_FOUND -> "Not Found";
+ case CONFLICT -> "Conflict";
+ case IM_A_TEAPOT -> "The server refuses the attempt to brew coffee with a teapot";
+ case UNPROCESSABLE_ENTITY -> "Unprocessable Entity";
+ case UNSUPPORTED_MEDIA_TYPE -> "Unsupported Media Type";
+ case TOO_MANY_REQUESTS -> "Too many requests";
+ case INTERNAL_SERVER_ERROR -> "Internal Server Error";
+ case SERVICE_UNAVAILABLE -> "Service Unavailable";
+ };
+ }
+
+ /**
+ * Crea un ApiStatus a partir de una excepción.
+ * @param t la excepción que se desea convertir a ApiStatus
+ * @return ApiStatus correspondiente a la excepción, o INTERNAL_SERVER_ERROR si no se reconoce la excepción
+ */
+ public static ApiStatus fromException(Throwable t) {
+ if (t instanceof NotFoundException) return ApiStatus.NOT_FOUND;
+ if (t instanceof BadRequestException) return ApiStatus.BAD_REQUEST;
+ if (t instanceof UnauthorizedException) return ApiStatus.UNAUTHORIZED;
+ if (t instanceof ForbiddenException) return ApiStatus.FORBIDDEN;
+ if (t instanceof ConflictException) return ApiStatus.CONFLICT;
+ if (t instanceof TeapotException) return ApiStatus.IM_A_TEAPOT;
+ if (t instanceof ServiceUnavailableException) return ApiStatus.SERVICE_UNAVAILABLE;
+ if (t instanceof UnprocessableEntityException) return ApiStatus.UNPROCESSABLE_ENTITY;
+ if (t instanceof UnsupportedMediaTypeException) return ApiStatus.UNSUPPORTED_MEDIA_TYPE;
+ if (t instanceof ValidationException) return ApiStatus.BAD_REQUEST;
+ return ApiStatus.INTERNAL_SERVER_ERROR;
+ }
+
+ /**
+ * Obtiene el ApiStatus correspondiente al código de estado HTTP.
+ *
+ * @param code El código de estado HTTP.
+ * @return El ApiStatus correspondiente, o null si no se encuentra.
+ */
+ public static ApiStatus fromCode(int code) {
+ return switch (code) {
+ case 200 -> OK;
+ case 201 -> CREATED;
+ case 202 -> ACCEPTED;
+ case 204 -> NO_CONTENT;
+ case 400 -> BAD_REQUEST;
+ case 401 -> UNAUTHORIZED;
+ case 403 -> FORBIDDEN;
+ case 404 -> NOT_FOUND;
+ case 409 -> CONFLICT;
+ case 418 -> IM_A_TEAPOT;
+ case 422 -> UNPROCESSABLE_ENTITY;
+ case 415 -> UNSUPPORTED_MEDIA_TYPE;
+ case 429 -> TOO_MANY_REQUESTS;
+ case 500 -> INTERNAL_SERVER_ERROR;
+ case 503 -> SERVICE_UNAVAILABLE;
+ default -> null;
+ };
+ }
+}
+
diff --git a/backlib/src/main/java/net/miarma/api/backlib/http/QueryFilters.java b/backlib/src/main/java/net/miarma/api/backlib/http/QueryFilters.java
new file mode 100644
index 0000000..b2bb013
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/http/QueryFilters.java
@@ -0,0 +1,80 @@
+package net.miarma.api.backlib.http;
+
+import io.vertx.ext.web.RoutingContext;
+
+import java.util.Optional;
+
+/**
+ * Representa los filtros de consulta para una solicitud HTTP.
+ * Esta clase encapsula los parámetros de ordenamiento, límite y desplazamiento
+ * que se pueden aplicar a una consulta.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class QueryFilters {
+
+ private Optional sort = Optional.empty();
+ private Optional order = Optional.of("ASC");
+ private Optional limit = Optional.empty();
+ private Optional offset = Optional.empty();
+
+ public QueryFilters() {}
+
+ public QueryFilters(Optional sort, Optional order, Optional limit, Optional offset) {
+ this.sort = sort;
+ this.order = order;
+ this.limit = limit;
+ this.offset = offset;
+ }
+
+ public Optional getSort() {
+ return sort;
+ }
+
+ public void setSort(String sort) {
+ this.sort = Optional.ofNullable(sort);
+ }
+
+ public Optional getOrder() {
+ return order;
+ }
+
+ public void setOrder(String order) {
+ this.order = Optional.ofNullable(order);
+ }
+
+ public Optional getLimit() {
+ return limit;
+ }
+
+ public void setLimit(Integer limit) {
+ this.limit = Optional.ofNullable(limit);
+ }
+
+ public Optional getOffset() {
+ return offset;
+ }
+
+ public void setOffset(Integer offset) {
+ this.offset = Optional.ofNullable(offset);
+ }
+
+ @Override
+ public String toString() {
+ return "QueryFilters{" +
+ "sort=" + sort +
+ ", order=" + order +
+ ", limit=" + limit +
+ ", offset=" + offset +
+ '}';
+ }
+
+ public static QueryFilters from(RoutingContext ctx) {
+ QueryFilters filters = new QueryFilters();
+ filters.setSort(ctx.request().getParam("_sort"));
+ filters.setOrder(ctx.request().getParam("_order"));
+ filters.setLimit(ctx.request().getParam("_limit") != null ? Integer.parseInt(ctx.request().getParam("_limit")) : null);
+ filters.setOffset(ctx.request().getParam("_offset") != null ? Integer.parseInt(ctx.request().getParam("_offset")) : null);
+ return filters;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/http/QueryParams.java b/backlib/src/main/java/net/miarma/api/backlib/http/QueryParams.java
new file mode 100644
index 0000000..ee7ea58
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/http/QueryParams.java
@@ -0,0 +1,85 @@
+package net.miarma.api.backlib.http;
+
+import io.vertx.ext.web.RoutingContext;
+
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * Representa los parámetros de consulta para una solicitud HTTP.
+ * Esta clase encapsula los filtros de consulta y los filtros adicionales
+ * que se pueden aplicar a una consulta.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class QueryParams {
+
+ private final Map filters;
+ private final QueryFilters queryFilters;
+
+ public QueryParams(Map filters, QueryFilters queryFilters) {
+ this.filters = filters;
+ this.queryFilters = queryFilters;
+ }
+
+ public Map getFilters() {
+ return filters;
+ }
+
+ public QueryFilters getQueryFilters() {
+ return queryFilters;
+ }
+
+ public static QueryParams from(RoutingContext ctx) {
+ Map filters = new HashMap<>();
+
+ QueryFilters queryFilters = QueryFilters.from(ctx);
+
+ ctx.queryParams().forEach(entry -> {
+ String key = entry.getKey();
+ String value = entry.getValue();
+
+ if (!key.startsWith("_")) { // esto es un filtro válido
+ filters.put(key, value);
+ }
+ });
+
+ return new QueryParams(filters, queryFilters);
+ }
+
+ public static QueryParams filterForEntity(QueryParams original, Class> entityClass, String prefix) {
+ Set validKeys = getFieldNames(entityClass);
+
+ Map filtered = original.getFilters().entrySet().stream()
+ .filter(e -> {
+ String key = e.getKey();
+ return key.startsWith(prefix + ".") && validKeys.contains(key.substring(prefix.length() + 1));
+ })
+ .collect(Collectors.toMap(
+ e -> e.getKey().substring(prefix.length() + 1), // quitar el prefijo
+ Map.Entry::getValue
+ ));
+
+ return new QueryParams(filtered, original.getQueryFilters());
+ }
+
+
+
+ private static Set getFieldNames(Class> clazz) {
+ return Arrays.stream(clazz.getDeclaredFields())
+ .map(Field::getName)
+ .collect(Collectors.toSet());
+ }
+
+ @Override
+ public String toString() {
+ return "QueryParams{" +
+ "filters=" + filters +
+ ", queryFilters=" + queryFilters +
+ '}';
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/http/SingleJsonResponse.java b/backlib/src/main/java/net/miarma/api/backlib/http/SingleJsonResponse.java
new file mode 100644
index 0000000..6d3107b
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/http/SingleJsonResponse.java
@@ -0,0 +1,14 @@
+package net.miarma.api.backlib.http;
+
+/**
+ * Representa una respuesta JSON que contiene un único mensaje.
+ * Esta clase se utiliza para encapsular una respuesta simple en formato JSON.
+ *
+ * @param el tipo del mensaje que se envía en la respuesta
+ * @author José Manuel Amador Gallardo
+ */
+public record SingleJsonResponse(T message) {
+ public static SingleJsonResponse of(T message) {
+ return new SingleJsonResponse<>(message);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUser.java b/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUser.java
new file mode 100644
index 0000000..d21fcfd
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUser.java
@@ -0,0 +1,21 @@
+package net.miarma.api.backlib.interfaces;
+
+import java.time.LocalDateTime;
+
+import net.miarma.api.backlib.Constants.CoreUserGlobalStatus;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+
+public interface IUser {
+ Integer getUser_id();
+ String getUser_name();
+ String getEmail();
+ String getDisplay_name();
+ String getPassword();
+ String getAvatar();
+ CoreUserGlobalStatus getGlobal_status();
+ CoreUserRole getGlobal_role();
+ LocalDateTime getCreated_at();
+ default LocalDateTime getUpdated_at() {
+ return null;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUserRole.java b/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUserRole.java
new file mode 100644
index 0000000..d46ad4f
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/interfaces/IUserRole.java
@@ -0,0 +1,7 @@
+package net.miarma.api.backlib.interfaces;
+
+import net.miarma.api.backlib.ValuableEnum;
+
+public interface IUserRole extends ValuableEnum {
+ String name();
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/middlewares/AbstractAuthGuard.java b/backlib/src/main/java/net/miarma/api/backlib/middlewares/AbstractAuthGuard.java
new file mode 100644
index 0000000..3caa063
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/middlewares/AbstractAuthGuard.java
@@ -0,0 +1,76 @@
+package net.miarma.api.backlib.middlewares;
+
+import java.util.function.Consumer;
+
+import io.vertx.core.Handler;
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.http.ApiStatus;
+import net.miarma.api.backlib.interfaces.IUserRole;
+import net.miarma.api.backlib.security.JWTManager;
+import net.miarma.api.backlib.util.JsonUtil;
+
+/**
+ * Base para AuthGuards de microservicios.
+ * Maneja extracción de JWT y verificación básica.
+ * Los microservicios solo implementan getUserEntity y hasPermission.
+ */
+@SuppressWarnings("unchecked") // arreglar el warning de heap pollution de los arrays de genéricos
+public abstract class AbstractAuthGuard & IUserRole> {
+
+ protected abstract R parseRole(String roleStr);
+ protected abstract void getUserEntity(int userId, RoutingContext ctx, Consumer callback);
+ protected abstract boolean hasPermission(U user, R role);
+
+ public Handler check(R... allowedRoles) {
+ return ctx -> {
+ String token = extractToken(ctx);
+ if (token == null || !JWTManager.getInstance().isValid(token)) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, "Invalid or missing token");
+ return;
+ }
+
+ int userId = JWTManager.getInstance().extractUserId(token);
+ String roleStr = JWTManager.getInstance().extractRole(token);
+
+ R role;
+ try {
+ role = parseRole(roleStr);
+ } catch (Exception e) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, "Invalid role");
+ return;
+ }
+
+ ctx.put("userId", userId);
+ ctx.put("role", role);
+
+ getUserEntity(userId, ctx, entity -> {
+ if (entity == null) {
+ JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, "User not found");
+ return;
+ }
+
+ if (allowedRoles.length == 0 || isRoleAllowed(role, allowedRoles)) {
+ ctx.put("userEntity", entity);
+ ctx.next();
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.FORBIDDEN, "Forbidden");
+ }
+ });
+ };
+ }
+
+ private boolean isRoleAllowed(R role, R... allowedRoles) {
+ for (R allowed : allowedRoles) {
+ if (role == allowed) return true;
+ }
+ return false;
+ }
+
+ private String extractToken(RoutingContext ctx) {
+ String authHeader = ctx.request().getHeader("Authorization");
+ if (authHeader != null && authHeader.startsWith("Bearer ")) {
+ return authHeader.substring(7);
+ }
+ return null;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/security/DNIValidator.java b/backlib/src/main/java/net/miarma/api/backlib/security/DNIValidator.java
new file mode 100644
index 0000000..337ce8b
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/security/DNIValidator.java
@@ -0,0 +1,66 @@
+package net.miarma.api.backlib.security;
+
+/**
+ * Validador de DNI/NIE español.
+ *
+ * Este validador comprueba si un DNI o NIE es válido según las reglas establecidas por la legislación española.
+ * Un DNI debe tener 8 dígitos seguidos de una letra, y un NIE debe comenzar con X, Y o Z seguido de 7 dígitos y una letra.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class DNIValidator {
+
+ /**
+ * Valida un DNI o NIE español.
+ *
+ * @param id El DNI o NIE a validar.
+ * @return true si el DNI/NIE es válido, false en caso contrario.
+ */
+ public static boolean isValid(String id) {
+ if (id == null || id.length() != 9) {
+ return false;
+ }
+
+ id = id.toUpperCase(); // Pa evitar problemas con minúsculas
+ String numberPart;
+ char letterPart = id.charAt(8);
+
+ if (id.startsWith("X") || id.startsWith("Y") || id.startsWith("Z")) {
+ // NIE
+ char prefix = id.charAt(0);
+ String numericPrefix = switch (prefix) {
+ case 'X' -> "0";
+ case 'Y' -> "1";
+ case 'Z' -> "2";
+ default -> null;
+ };
+
+ if (numericPrefix == null) return false;
+
+ numberPart = numericPrefix + id.substring(1, 8);
+ } else {
+ // DNI
+ numberPart = id.substring(0, 8);
+ }
+
+ if (!numberPart.matches("\\d{8}")) {
+ return false;
+ }
+
+ int number = Integer.parseInt(numberPart);
+ char expectedLetter = calculateLetter(number);
+
+ return letterPart == expectedLetter;
+ }
+
+ /**
+ * Calcula la letra correspondiente a un número de DNI.
+ *
+ * @param number El número del DNI (8 dígitos).
+ * @return La letra correspondiente.
+ */
+ private static char calculateLetter(int number) {
+ String letters = "TRWAGMYFPDXBNJZSQVHLCKE";
+ return letters.charAt(number % 23);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/security/JWTManager.java b/backlib/src/main/java/net/miarma/api/backlib/security/JWTManager.java
new file mode 100644
index 0000000..bc2b5a5
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/security/JWTManager.java
@@ -0,0 +1,161 @@
+package net.miarma.api.backlib.security;
+
+import com.auth0.jwt.JWT;
+import com.auth0.jwt.algorithms.Algorithm;
+import com.auth0.jwt.interfaces.DecodedJWT;
+import com.auth0.jwt.interfaces.JWTVerifier;
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+
+import java.util.Date;
+
+/**
+ * Clase de gestión de JSON Web Tokens (JWT).
+ * Proporciona métodos para generar, verificar y decodificar tokens JWT.
+ *
+ * Esta clase sigue el patron Singleton para asegurar una sola instancia.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class JWTManager {
+
+ private final ConfigManager config = ConfigManager.getInstance();
+ private final Algorithm algorithm;
+ private final JWTVerifier verifier;
+ private static JWTManager instance;
+
+ private JWTManager() {
+ this.algorithm = Algorithm.HMAC256(config.getStringProperty("jwt.secret"));
+ this.verifier = JWT.require(algorithm).build();
+ }
+
+ /**
+ * Obtiene la instancia única de JWTManager.
+ *
+ * @return La instancia única de JWTManager.
+ */
+ public static synchronized JWTManager getInstance() {
+ if (instance == null) {
+ instance = new JWTManager();
+ }
+ return instance;
+ }
+
+ /**
+ * Genera un token JWT para un usuario.
+ *
+ * @param user El usuario para el cual se generará el token.
+ * @param keepLoggedIn Indica si el token debe tener una duración prolongada.
+ * @return El token JWT generado.
+ */
+ public String generateToken(String user_name, Integer user_id, CoreUserRole role, boolean keepLoggedIn) {
+ final long EXPIRATION_TIME_MS = 1000L * (keepLoggedIn ? config.getIntProperty("jwt.expiration") : config.getIntProperty("jwt.expiration.short"));
+ return JWT.create()
+ .withSubject(user_name)
+ .withClaim("userId", user_id)
+ .withClaim("role", role.name())
+ .withClaim("isAdmin", role == Constants.CoreUserRole.ADMIN)
+ .withIssuedAt(new Date())
+ .withExpiresAt(new Date(System.currentTimeMillis() + EXPIRATION_TIME_MS))
+ .sign(algorithm);
+ }
+
+ /**
+ * Decodifica un token JWT sin verificar su firma.
+ *
+ * @param token El token JWT a decodificar.
+ * @return Un objeto DecodedJWT que contiene la información del token.
+ */
+ public DecodedJWT decodeWithoutVerification(String token) {
+ return JWT.decode(token);
+ }
+
+ /**
+ * Verifica la validez de un token JWT.
+ *
+ * @param token El token JWT a verificar.
+ * @return true si el token es válido, false en caso contrario.
+ */
+ public boolean isValid(String token) {
+ try {
+ verifier.verify(token);
+ return true;
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Verifica si un token JWT pertenece a un usuario administrador.
+ *
+ * @param token El token JWT a verificar.
+ * @return true si el token pertenece a un administrador, false en caso contrario.
+ */
+ public boolean isAdmin(String token) {
+ try {
+ DecodedJWT jwt = verifier.verify(token);
+ return jwt.getClaim("isAdmin").asBoolean();
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Obtiene el ID de usuario a partir de un token JWT.
+ *
+ * @param token El token JWT del cual se extraerá el ID de usuario.
+ * @return El ID de usuario si el token es válido, -1 en caso contrario.
+ */
+ public int getUserId(String token) {
+ try {
+ DecodedJWT jwt = verifier.verify(token);
+ return jwt.getClaim("userId").asInt();
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Obtiene el sub especificado en un token JWT, que generalmente es el nombre de usuario.
+ *
+ * @param token El token JWT del cual se extraerá el nombre de usuario.
+ * @return El nombre de usuario si el token es válido, null en caso contrario.
+ */
+ public String getSubject(String token) {
+ try {
+ DecodedJWT jwt = verifier.verify(token);
+ return jwt.getSubject();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+
+ /**
+ * Extrae el ID de usuario de un token JWT.
+ * @param token El token JWT del cual se extraerá el ID de usuario.
+ * @return El ID de usuario si el token es válido, -1 en caso contrario.
+ */
+ public int extractUserId(String token) {
+ try {
+ DecodedJWT jwt = verifier.verify(token);
+ return jwt.getClaim("userId").asInt();
+ } catch (Exception e) {
+ return -1;
+ }
+ }
+
+ /**
+ * Extrae el rol de usuario de un token JWT.
+ * @param token El token JWT del cual se extraerá el ID de usuario.
+ * @return El rol de usuario si el token es válido, null en caso contrario.
+ */
+ public String extractRole(String token) {
+ try {
+ DecodedJWT jwt = verifier.verify(token);
+ return jwt.getClaim("role").asString();
+ } catch (Exception e) {
+ return null;
+ }
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/security/PasswordHasher.java b/backlib/src/main/java/net/miarma/api/backlib/security/PasswordHasher.java
new file mode 100644
index 0000000..15a099f
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/security/PasswordHasher.java
@@ -0,0 +1,21 @@
+package net.miarma.api.backlib.security;
+import org.mindrot.jbcrypt.BCrypt;
+
+/**
+ * Clase de utilidad para el hash de contraseñas.
+ * Utiliza BCrypt para generar y verificar hashes de contraseñas.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class PasswordHasher {
+
+ private static final int SALT_ROUNDS = 12;
+
+ public static String hash(String plainPassword) {
+ return BCrypt.hashpw(plainPassword, BCrypt.gensalt(SALT_ROUNDS));
+ }
+
+ public static boolean verify(String plainPassword, String hashedPassword) {
+ return BCrypt.checkpw(plainPassword, hashedPassword);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/security/SecretManager.java b/backlib/src/main/java/net/miarma/api/backlib/security/SecretManager.java
new file mode 100644
index 0000000..9cd38af
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/security/SecretManager.java
@@ -0,0 +1,81 @@
+package net.miarma.api.backlib.security;
+
+import net.miarma.api.backlib.ConfigManager;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * Clase encargada de generar los secrets necesarios para la autenticación JWT.
+ * Si el secret ya existe en el archivo de configuración, lo devuelve.
+ * Si no, genera un nuevo secret de 64 bytes, lo guarda en el archivo de configuración
+ * y lo devuelve.
+ *
+ * Esta clase sigue el patron Singleton para asegurar una sola instancia.
+ * @author José Manuel Amador Gallardo
+ */
+public class SecretManager {
+
+ private static String cachedSecret = null;
+
+ public static String getOrCreateSecret() {
+ if (cachedSecret != null) return cachedSecret;
+
+ try {
+ File configFile = ConfigManager.getInstance().getConfigFile();
+ Properties config = new Properties();
+
+ if (configFile.exists()) {
+ try (FileInputStream fis = new FileInputStream(configFile)) {
+ config.load(fis);
+ }
+ }
+
+ String secret = config.getProperty("jwt.secret");
+ if (secret != null && !secret.trim().isEmpty()) {
+ cachedSecret = secret.trim();
+ } else {
+ cachedSecret = generateSecret(64);
+
+ List lines = Files.readAllLines(configFile.toPath());
+
+ boolean replaced = false;
+ for (int i = 0; i < lines.size(); i++) {
+ if (lines.get(i).trim().startsWith("jwt.secret=")) {
+ lines.set(i, "jwt.secret=" + cachedSecret);
+ replaced = true;
+ break;
+ }
+ }
+
+ if (!replaced) {
+ lines.add("# Security Configuration");
+ lines.add("jwt.secret=" + cachedSecret);
+ }
+
+ Files.write(configFile.toPath(), lines);
+ }
+
+ return cachedSecret;
+
+ } catch (IOException e) {
+ throw new RuntimeException("Could not create or get the secret", e);
+ }
+ }
+
+ private static String generateSecret(int byteLength) {
+ SecureRandom random = new SecureRandom();
+ byte[] bytes = new byte[byteLength];
+ random.nextBytes(bytes);
+ StringBuilder sb = new StringBuilder();
+ for (byte b : bytes) {
+ sb.append(String.format("%02x", b));
+ }
+ return sb.toString();
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/security/SusPather.java b/backlib/src/main/java/net/miarma/api/backlib/security/SusPather.java
new file mode 100644
index 0000000..e267eee
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/security/SusPather.java
@@ -0,0 +1,24 @@
+package net.miarma.api.backlib.security;
+
+/**
+ * Clase que verifica si una ruta es sospechosa o no.
+ * Utilizada para evitar el acceso a rutas potencialmente peligrosas.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class SusPather {
+ public static boolean isSusPath(String path) {
+ return path.endsWith(".env") ||
+ path.endsWith(".git") ||
+ path.endsWith(".DS_Store") ||
+ path.endsWith("wp-login.php") ||
+ path.endsWith("admin.php") ||
+ path.contains(".git/") ||
+ path.contains(".svn/") ||
+ path.contains(".idea/") ||
+ path.contains(".vscode/") ||
+ path.contains(".settings/") ||
+ path.contains(".classpath") ||
+ path.contains(".project");
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/DateParser.java b/backlib/src/main/java/net/miarma/api/backlib/util/DateParser.java
new file mode 100644
index 0000000..40fc95e
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/DateParser.java
@@ -0,0 +1,14 @@
+package net.miarma.api.backlib.util;
+
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+
+/**
+ * Clase de utilidad para convertir fechas a segundos desde la época Unix.
+ * @author José Manuel Amador Gallardo
+ */
+public class DateParser {
+ public static long parseDate(LocalDateTime date) {
+ return date.toEpochSecond(ZoneOffset.UTC);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/DeploymentUtil.java b/backlib/src/main/java/net/miarma/api/backlib/util/DeploymentUtil.java
new file mode 100644
index 0000000..f0b9946
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/DeploymentUtil.java
@@ -0,0 +1,20 @@
+package net.miarma.api.backlib.util;
+
+/**
+ * Clase de utilidad para mensajes de despliegue.
+ * @author José Manuel Amador Gallardo
+ */
+public class DeploymentUtil {
+
+ public static String successMessage(Class clazz) {
+ return String.join(" ", "🟢", clazz.getSimpleName(), "deployed successfully");
+ }
+
+ public static String failMessage(Class clazz, Throwable e) {
+ return String.join(" ", "🔴 Error deploying", clazz.getSimpleName()+":", e.getMessage());
+ }
+
+ public static String apiUrlMessage(String host, Integer port) {
+ return String.join(" ", "\t🔗 API URL:", host+":"+port);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/EventBusUtil.java b/backlib/src/main/java/net/miarma/api/backlib/util/EventBusUtil.java
new file mode 100644
index 0000000..ed4af2b
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/EventBusUtil.java
@@ -0,0 +1,60 @@
+package net.miarma.api.backlib.util;
+
+import io.vertx.core.Handler;
+import io.vertx.core.eventbus.Message;
+import io.vertx.core.eventbus.ReplyException;
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.http.ApiStatus;
+
+/**
+ * Clase de utilidad para manejar errores en el EventBus.
+ * @author José Manuel Amador Gallardo
+ */
+public class EventBusUtil {
+ public static Handler fail(Message msg) {
+ return err -> {
+ if(err instanceof ReplyException re) {
+ msg.fail(re.failureCode(), re.getMessage());
+ } else {
+ ApiStatus status = ApiStatus.fromException(err);
+ msg.fail(status.getCode(), err.getMessage());
+ }
+ };
+ }
+
+ public static Handler fail(Throwable err) {
+ return _ -> {
+ ApiStatus status = ApiStatus.fromException(err);
+ throw new RuntimeException(status.getDefaultMessage(), err);
+ };
+ }
+
+ public static void handleReplyError(RoutingContext ctx, Throwable err) {
+ if (err instanceof ReplyException replyEx) {
+ int code = replyEx.failureCode();
+ String message = replyEx.getMessage();
+
+ ApiStatus status = ApiStatus.fromCode(code);
+ if (status == null) status = ApiStatus.INTERNAL_SERVER_ERROR;
+
+ JsonUtil.sendJson(ctx, status, null, message);
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.INTERNAL_SERVER_ERROR, null, "Internal server error");
+ }
+ }
+
+ public static void handleReplyError(RoutingContext ctx, Throwable err, String fallbackMsg) {
+ if (err instanceof ReplyException replyEx) {
+ int code = replyEx.failureCode();
+ String message = replyEx.getMessage();
+
+ ApiStatus status = ApiStatus.fromCode(code);
+ if (status == null) status = ApiStatus.INTERNAL_SERVER_ERROR;
+
+ JsonUtil.sendJson(ctx, status, null, message != null ? message : fallbackMsg);
+ } else {
+ JsonUtil.sendJson(ctx, ApiStatus.INTERNAL_SERVER_ERROR, null, fallbackMsg);
+ }
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/JsonUtil.java b/backlib/src/main/java/net/miarma/api/backlib/util/JsonUtil.java
new file mode 100644
index 0000000..985259f
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/JsonUtil.java
@@ -0,0 +1,33 @@
+package net.miarma.api.backlib.util;
+
+import io.vertx.core.json.JsonArray;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.http.ApiResponse;
+import net.miarma.api.backlib.http.ApiStatus;
+
+/**
+ * Clase de utilidad para enviar respuestas JSON.
+ * @author José Manuel Amador Gallardo
+ */
+public class JsonUtil {
+ public static void sendJson(RoutingContext ctx, ApiStatus status, T data) {
+ sendJson(ctx, status, data, status.getDefaultMessage());
+ }
+
+ public static void sendJson(RoutingContext ctx, ApiStatus status, T data, String message) {
+ ctx.response().putHeader("Content-Type", "application/json").setStatusCode(status.getCode());
+
+ if (data instanceof JsonObject || data instanceof JsonArray) {
+ JsonObject response = new JsonObject()
+ .put("status", status.getCode())
+ .put("message", message)
+ .put("data", data);
+ ctx.response().end(response.encode());
+ } else {
+ ctx.response().end(Constants.GSON.toJson(new ApiResponse<>(status, message, data)));
+ }
+ }
+
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/MessageUtil.java b/backlib/src/main/java/net/miarma/api/backlib/util/MessageUtil.java
new file mode 100644
index 0000000..c6a22d7
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/MessageUtil.java
@@ -0,0 +1,15 @@
+package net.miarma.api.backlib.util;
+
+/**
+ * Clase de utilidad para mensajes comunes en la API.
+ * @author José Manuel Amador Gallardo
+ */
+public class MessageUtil {
+ public static String notFound(String what, String where) {
+ return String.join(" ", "❌", what, "not found in", where);
+ }
+
+ public static String failedTo(String action, String on, Throwable e) {
+ return String.join(" ", "❌ Failed to", action, on+":", e.getMessage());
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/NameCensorer.java b/backlib/src/main/java/net/miarma/api/backlib/util/NameCensorer.java
new file mode 100644
index 0000000..ab16f39
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/NameCensorer.java
@@ -0,0 +1,35 @@
+package net.miarma.api.backlib.util;
+
+/**
+ * Clase de utilidad para censurar nombres.
+ * Censura los nombres dejando las primeras 3 letras visibles y el resto con asteriscos.
+ * Si el nombre es muy largo, lo acorta a 16 caracteres y añade "..." al final.
+ * @author José Manuel Amador Gallardo
+ */
+public class NameCensorer {
+
+ public static String censor(String nombre) {
+ if (nombre == null || nombre.isBlank()) return "";
+
+ String[] palabras = nombre.trim().split("\\s+");
+
+ for (int i = 0; i < palabras.length; i++) {
+ String palabra = palabras[i];
+ int len = palabra.length();
+
+ if (len > 3) {
+ palabras[i] = palabra.substring(0, 3) + "*".repeat(len - 3);
+ } else if (len > 0) {
+ palabras[i] = palabra.charAt(0) + "*".repeat(len - 1);
+ }
+ }
+
+ String censurado = String.join(" ", palabras);
+
+ if (censurado.length() > 16) {
+ censurado = censurado.substring(0, 16) + "...";
+ }
+
+ return censurado;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/PasteKeyGenerator.java b/backlib/src/main/java/net/miarma/api/backlib/util/PasteKeyGenerator.java
new file mode 100644
index 0000000..23d4fec
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/PasteKeyGenerator.java
@@ -0,0 +1,17 @@
+package net.miarma.api.backlib.util;
+
+import java.security.SecureRandom;
+
+public class PasteKeyGenerator {
+ private static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ public static String generate(int length) {
+ StringBuilder sb = new StringBuilder(length);
+ for (int i = 0; i < length; i++) {
+ int index = RANDOM.nextInt(ALPHABET.length());
+ sb.append(ALPHABET.charAt(index));
+ }
+ return sb.toString();
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/RateLimiter.java b/backlib/src/main/java/net/miarma/api/backlib/util/RateLimiter.java
new file mode 100644
index 0000000..682c826
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/RateLimiter.java
@@ -0,0 +1,29 @@
+package net.miarma.api.backlib.util;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.time.Instant;
+
+public class RateLimiter {
+ private static final int MAX_REQUESTS = 5; // max 5 requests
+ private static final long WINDOW_MS = 60_000; // 1 minuto
+ private Map requests = new ConcurrentHashMap<>();
+
+ static class UserRequests {
+ int count;
+ long windowStart;
+ }
+
+ public boolean allow(String ip) {
+ long now = Instant.now().toEpochMilli();
+ UserRequests ur = requests.getOrDefault(ip, new UserRequests());
+ if (now - ur.windowStart > WINDOW_MS) {
+ ur.count = 1;
+ ur.windowStart = now;
+ } else {
+ ur.count++;
+ }
+ requests.put(ip, ur);
+ return ur.count <= MAX_REQUESTS;
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/RouterUtil.java b/backlib/src/main/java/net/miarma/api/backlib/util/RouterUtil.java
new file mode 100644
index 0000000..3867e4e
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/RouterUtil.java
@@ -0,0 +1,88 @@
+package net.miarma.api.backlib.util;
+
+import io.vertx.ext.web.Router;
+import net.miarma.api.backlib.Constants;
+
+/**
+ * Clase de utilidad para adjuntar un logger a un router de Vert.x.
+ * @author José Manuel Amador Gallardo
+ */
+public class RouterUtil {
+
+ public static void attachLogger(Router router) {
+ router.route().handler(ctx -> {
+ long startTime = System.currentTimeMillis();
+
+ ctx.addBodyEndHandler(_ -> {
+ long duration = System.currentTimeMillis() - startTime;
+
+ String method = ctx.request().method().name();
+ String path = ctx.normalizedPath();
+ String query = ctx.request().query();
+ int status = ctx.response().getStatusCode();
+
+ String statusMessage = getStatusMessage(status);
+ String emoji = getEmoji(status);
+
+ String formattedQuery = (query != null && !query.isEmpty()) ? "?" + query : "";
+
+ String clientIP = ctx.request().getHeader("X-Forwarded-For");
+ if (clientIP != null && !clientIP.isBlank()) {
+ clientIP = clientIP.split(",")[0].trim(); // IP real del cliente
+ } else {
+ clientIP = ctx.request().remoteAddress().host(); // fallback
+ }
+
+
+ String log = String.format(
+ "%s [%d %s] %s %s%s (IP: %s) (⏱ %dms)",
+ emoji,
+ status,
+ statusMessage,
+ method,
+ path,
+ formattedQuery,
+ clientIP,
+ duration
+ );
+
+ Constants.LOGGER.info(log);
+ });
+
+ ctx.next();
+ });
+ }
+
+ private static String getStatusMessage(int code) {
+ return switch (code) {
+ case 100 -> "Continue";
+ case 101 -> "Switching Protocols";
+ case 200 -> "OK";
+ case 201 -> "Created";
+ case 202 -> "Accepted";
+ case 204 -> "No Content";
+ case 301 -> "Moved Permanently";
+ case 302 -> "Found";
+ case 304 -> "Not Modified";
+ case 400 -> "Bad Request";
+ case 401 -> "Unauthorized";
+ case 403 -> "Forbidden";
+ case 404 -> "Not Found";
+ case 409 -> "Conflict";
+ case 415 -> "Unsupported Media Type";
+ case 422 -> "Unprocessable Entity";
+ case 500 -> "Internal Server Error";
+ case 502 -> "Bad Gateway";
+ case 503 -> "Service Unavailable";
+ default -> "Unknown";
+ };
+ }
+
+ private static String getEmoji(int statusCode) {
+ if (statusCode >= 200 && statusCode < 300) return "✅";
+ if (statusCode >= 300 && statusCode < 400) return "🔁";
+ if (statusCode >= 400 && statusCode < 500) return "❌";
+ if (statusCode >= 500) return "💥";
+ return "📥";
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/util/UserNameGenerator.java b/backlib/src/main/java/net/miarma/api/backlib/util/UserNameGenerator.java
new file mode 100644
index 0000000..77ed5b6
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/util/UserNameGenerator.java
@@ -0,0 +1,26 @@
+package net.miarma.api.backlib.util;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * Clase de utilidad para generar nombres de usuario únicos basados en un hash.
+ * Utiliza SHA-256 para crear un hash del nombre de usuario y lo convierte a hexadecimal.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class UserNameGenerator {
+ public static String generateUserName(String baseName, String input, int hashBytesCount) throws NoSuchAlgorithmException {
+ MessageDigest digest = MessageDigest.getInstance("SHA-256");
+ byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+
+ StringBuilder hexHash = new StringBuilder();
+ for (int i = 0; i < hashBytesCount && i < hash.length; i++) {
+ hexHash.append(String.format("%02x", hash[i]));
+ }
+
+ return baseName + "-" + hexHash;
+ }
+}
+
diff --git a/backlib/src/main/java/net/miarma/api/backlib/validation/ValidationResult.java b/backlib/src/main/java/net/miarma/api/backlib/validation/ValidationResult.java
new file mode 100644
index 0000000..b1b3564
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/validation/ValidationResult.java
@@ -0,0 +1,31 @@
+package net.miarma.api.backlib.validation;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Representa el resultado de una validación, conteniendo errores asociados a campos específicos.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class ValidationResult {
+
+ private final Map errors = new HashMap<>();
+
+ public ValidationResult addError(String field, String message) {
+ errors.put(field, message);
+ return this;
+ }
+
+ public boolean isValid() {
+ return errors.isEmpty();
+ }
+
+ public Map getErrors() {
+ return errors;
+ }
+
+ public String getFirstError() {
+ return errors.values().stream().findFirst().orElse(null);
+ }
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/validation/Validator.java b/backlib/src/main/java/net/miarma/api/backlib/validation/Validator.java
new file mode 100644
index 0000000..dbb2782
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/validation/Validator.java
@@ -0,0 +1,12 @@
+package net.miarma.api.backlib.validation;
+
+import io.vertx.core.Future;
+
+/**
+ * Interfaz para la validación de entidades.
+ * @param Tipo de entidad a validar.
+ * @author José Manuel Amador Gallardo
+ */
+public interface Validator {
+ Future validate(T entity);
+}
diff --git a/backlib/src/main/java/net/miarma/api/backlib/vertx/VertxJacksonConfig.java b/backlib/src/main/java/net/miarma/api/backlib/vertx/VertxJacksonConfig.java
new file mode 100644
index 0000000..4479423
--- /dev/null
+++ b/backlib/src/main/java/net/miarma/api/backlib/vertx/VertxJacksonConfig.java
@@ -0,0 +1,26 @@
+package net.miarma.api.backlib.vertx;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import io.vertx.core.json.jackson.DatabindCodec;
+
+/**
+ * Configura el ObjectMapper de Vert.x para manejar correctamente
+ * fechas y tiempos. Esto es necesario para que las fechas se serialicen
+ * y deserialicen correctamente.
+ *
+ * @author José Manuel Amador Gallardo
+ */
+public class VertxJacksonConfig {
+ @SuppressWarnings("deprecation")
+ public static void configure() {
+ ObjectMapper mapper = DatabindCodec.mapper();
+ mapper.registerModule(new JavaTimeModule());
+ mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+
+ ObjectMapper prettyBase = DatabindCodec.prettyMapper();
+ prettyBase.registerModule(new JavaTimeModule());
+ prettyBase.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+ }
+}
diff --git a/bootstrap/.gitignore b/bootstrap/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/bootstrap/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/bootstrap/pom.xml b/bootstrap/pom.xml
new file mode 100644
index 0000000..db880ce
--- /dev/null
+++ b/bootstrap/pom.xml
@@ -0,0 +1,90 @@
+
+ 4.0.0
+
+
+ net.miarma.api
+ miarma-ecosystem
+ 1.2.0
+
+
+ bootstrap
+ jar
+
+
+ 23
+ 23
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+ MiarmaGit
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+
+
+
+ net.miarma.api
+ backlib
+ ${project.version}
+
+
+ net.miarma.api
+ core
+ ${project.version}
+
+
+ net.miarma.api
+ huertos
+ ${project.version}
+
+
+ net.miarma.api
+ huertosdecine
+ ${project.version}
+
+
+ net.miarma.api
+ miarmacraft
+ ${project.version}
+
+
+ net.miarma.api
+ mpaste
+ ${project.version}
+
+
+
+
+ MiarmaEcosystem
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ net.miarma.api.AppInitializer
+
+
+
+
+
+
+
+
+
diff --git a/bootstrap/src/main/java/net/miarma/api/AppInitializer.java b/bootstrap/src/main/java/net/miarma/api/AppInitializer.java
new file mode 100644
index 0000000..befd14e
--- /dev/null
+++ b/bootstrap/src/main/java/net/miarma/api/AppInitializer.java
@@ -0,0 +1,75 @@
+package net.miarma.api;
+
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.security.SecretManager;
+import net.miarma.api.backlib.vertx.VertxJacksonConfig;
+import net.miarma.api.backlib.util.MessageUtil;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+
+import io.vertx.core.Launcher;
+
+/**
+ * Punto de entrada para inicializar la aplicación.
+ * Se encarga de:
+ * - Crear directorios base si no existen
+ * - Copiar el fichero default.properties
+ * - Inicializar ConfigManager y secretos
+ * - Configurar Jackson para Vert.x
+ * - Desplegar el Verticle Master
+ */
+public class AppInitializer {
+
+ public static void main(String[] args) {
+ AppInitializer initializer = new AppInitializer();
+ initializer.init();
+ initializer.deployMaster();
+ Constants.LOGGER.info("✅ App initialized successfully!");
+ }
+
+ private final ConfigManager configManager;
+
+ public AppInitializer() {
+ this.configManager = ConfigManager.getInstance();
+ }
+
+ public void init() {
+ initializeDirectories();
+ copyDefaultConfig();
+ configManager.loadConfig();
+ SecretManager.getOrCreateSecret();
+ VertxJacksonConfig.configure();
+ }
+
+ private void initializeDirectories() {
+ File baseDir = new File(configManager.getBaseDir());
+ if (!baseDir.exists() && baseDir.mkdirs()) {
+ Constants.LOGGER.info("Created base directory: " + baseDir.getAbsolutePath());
+ }
+ }
+
+ private void copyDefaultConfig() {
+ File configFile = configManager.getConfigFile();
+ if (!configFile.exists()) {
+ try (InputStream in = getClass().getClassLoader().getResourceAsStream("default.properties")) {
+ if (in != null) {
+ Files.copy(in, configFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
+ Constants.LOGGER.info("Copied default.properties to: " + configFile.getAbsolutePath());
+ } else {
+ Constants.LOGGER.error(MessageUtil.notFound("Default config", "resources"));
+ }
+ } catch (IOException e) {
+ Constants.LOGGER.error(MessageUtil.failedTo("copy", "default config", e));
+ }
+ }
+ }
+
+ private void deployMaster() {
+ Launcher.executeCommand("run", MasterVerticle.class.getName());
+ }
+}
diff --git a/bootstrap/src/main/java/net/miarma/api/MasterVerticle.java b/bootstrap/src/main/java/net/miarma/api/MasterVerticle.java
new file mode 100644
index 0000000..8a02d5d
--- /dev/null
+++ b/bootstrap/src/main/java/net/miarma/api/MasterVerticle.java
@@ -0,0 +1,66 @@
+package net.miarma.api;
+
+import java.util.Set;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.LogAccumulator;
+import net.miarma.api.backlib.util.DeploymentUtil;
+import net.miarma.api.microservices.core.verticles.CoreMainVerticle;
+import net.miarma.api.microservices.huertos.verticles.HuertosMainVerticle;
+import net.miarma.api.microservices.huertosdecine.verticles.CineMainVerticle;
+import net.miarma.api.microservices.miarmacraft.verticles.MMCMainVerticle;
+import net.miarma.api.microservices.mpaste.verticles.MPasteMainVerticle;
+
+public class MasterVerticle extends AbstractVerticle {
+ @Override
+ public void start(Promise startPromise) {
+ deploy()
+ .onSuccess(v -> {
+ vertx.setTimer(300, id -> {
+ LogAccumulator.flushToLogger(Constants.LOGGER);
+ startPromise.complete();
+ });
+ })
+ .onFailure(startPromise::fail);
+ }
+
+ private Future deploy() {
+ Promise promise = Promise.promise();
+
+ Future core = vertx.deployVerticle(new CoreMainVerticle())
+ .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CoreMainVerticle.class)))
+ .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CoreMainVerticle.class, err)));
+
+ Future huertos = vertx.deployVerticle(new HuertosMainVerticle())
+ .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(HuertosMainVerticle.class)))
+ .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(HuertosMainVerticle.class, err)));
+
+ Future mmc = vertx.deployVerticle(new MMCMainVerticle())
+ .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(MMCMainVerticle.class)))
+ .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(MMCMainVerticle.class, err)));
+
+ Future cine = vertx.deployVerticle(new CineMainVerticle())
+ .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CineMainVerticle.class)))
+ .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CineMainVerticle.class, err)));
+
+ Future mpaste = vertx.deployVerticle(new MPasteMainVerticle())
+ .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(MPasteMainVerticle.class)))
+ .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(MPasteMainVerticle.class, err)));
+
+ Future.all(core, huertos, mmc, cine, mpaste)
+ .onSuccess(_ -> promise.complete())
+ .onFailure(promise::fail);
+
+ return promise.future();
+ }
+
+
+ @Override
+ public void stop(Promise stopPromise) {
+ vertx.deploymentIDs().forEach(id -> vertx.undeploy(id));
+ stopPromise.complete();
+ }
+}
diff --git a/bootstrap/src/main/resources/default.properties b/bootstrap/src/main/resources/default.properties
new file mode 100644
index 0000000..e75ab2e
--- /dev/null
+++ b/bootstrap/src/main/resources/default.properties
@@ -0,0 +1,41 @@
+# DB Configuration
+db.protocol=jdbc:mariadb
+db.host=localhost
+db.port=3306
+db.name=miarma
+db.user=root
+db.password=root
+dp.poolSize=5
+
+# HTTP Server Configuration
+inet.host=localhost
+sso.logic.port=8080
+sso.data.port=8081
+mmc.logic.port=8100
+mmc.data.port=8101
+huertos.logic.port=8120
+huertos.data.port=8121
+cine.data.port = 8140
+cine.logic.port = 8141
+mpaste.data.port = 8160
+mpaste.logic.port = 8161
+
+# Security Configuration
+jwt.secret=
+jwt.expiration=604800
+jwt.expiration.short=3600
+
+# Mail Configuration
+smtp.server=
+smtp.port=
+imap.server=
+imap.port=
+
+smtp.password.presidente=
+smtp.password.secretaria=
+smtp.password.tesoreria=
+smtp.password.admin=
+smtp.password.noreply=
+
+# Discord Configuration
+discord.webhook=
\ No newline at end of file
diff --git a/bootstrap/src/main/resources/logback.xml b/bootstrap/src/main/resources/logback.xml
new file mode 100644
index 0000000..a7b88d9
--- /dev/null
+++ b/bootstrap/src/main/resources/logback.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+ %cyan([%d{HH:mm:ss}]) %highlight(%-5level) %green(%logger{20}) - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/microservices/core/.gitignore b/microservices/core/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/microservices/core/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/microservices/core/pom.xml b/microservices/core/pom.xml
new file mode 100644
index 0000000..a4d4c2f
--- /dev/null
+++ b/microservices/core/pom.xml
@@ -0,0 +1,54 @@
+
+ 4.0.0
+ net.miarma.api
+ core
+ 1.2.0
+
+
+ 23
+ 23
+
+
+
+
+ MiarmaGit
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+
+
+
+ net.miarma.api
+ backlib
+ 1.2.0
+
+
+
+
+ ME-Core
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ net.miarma.api.microservices.core.verticles.CoreMainVerticle
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreDataRouter.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreDataRouter.java
new file mode 100644
index 0000000..114ca94
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreDataRouter.java
@@ -0,0 +1,35 @@
+package net.miarma.api.microservices.core.routing;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.core.handlers.FileDataHandler;
+import net.miarma.api.backlib.core.handlers.UserDataHandler;
+import net.miarma.api.backlib.core.services.UserService;
+import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard;
+
+public class CoreDataRouter {
+ public static void mount(Router router, Vertx vertx, Pool pool) {
+ UserDataHandler hUserData = new UserDataHandler(pool);
+ FileDataHandler hFileData = new FileDataHandler(pool);
+ UserService userService = new UserService(pool);
+ CoreAuthGuard authGuard = new CoreAuthGuard(userService);
+
+ router.route().handler(BodyHandler.create());
+
+ router.get(CoreEndpoints.USERS).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::getAll);
+ router.get(CoreEndpoints.USER).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::getById);
+ router.post(CoreEndpoints.USERS).handler(hUserData::create);
+ router.put(CoreEndpoints.USER).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::update);
+ router.delete(CoreEndpoints.USER).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::delete);
+
+ router.get(CoreEndpoints.FILES).handler(authGuard.check()).handler(hFileData::getAll);
+ router.get(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::getById);
+ router.post(CoreEndpoints.FILE_UPLOAD).handler(authGuard.check()).handler(hFileData::create);
+ router.put(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::update);
+ router.delete(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::delete);
+
+ }
+}
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreEndpoints.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreEndpoints.java
new file mode 100644
index 0000000..d8e5ba2
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreEndpoints.java
@@ -0,0 +1,39 @@
+package net.miarma.api.microservices.core.routing;
+
+import net.miarma.api.backlib.Constants;
+
+public class CoreEndpoints {
+
+ /*
+ * RUTAS DE LA API DE DATOS
+ * DE NEGOCIO DEL SSO
+ */
+
+ // Usuarios
+ public static final String USERS = Constants.CORE_PREFIX + "/users"; // GET, POST, PUT, DELETE
+ public static final String USER = Constants.CORE_PREFIX + "/users/:user_id"; // GET, PUT, DELETE
+ public static final String USER_STATUS = Constants.CORE_PREFIX + "/users/:user_id/status"; // GET, PUT
+ public static final String USER_ROLE = Constants.CORE_PREFIX + "/users/:user_id/role"; // GET, PUT
+ public static final String USER_EXISTS = Constants.CORE_PREFIX + "/users/:user_id/exists"; // GET
+ public static final String USER_AVATAR = Constants.CORE_PREFIX + "/users/:user_id/avatar"; // GET, PUT
+ public static final String USER_INFO = Constants.CORE_PREFIX + "/users/me"; // GET
+
+ // Archivos
+ public static final String FILES = Constants.CORE_PREFIX + "/files"; // GET, POST
+ public static final String FILE = Constants.CORE_PREFIX + "/files/:file_id"; // GET, PUT, DELETE
+ public static final String FILE_UPLOAD = Constants.CORE_PREFIX + "/files/upload"; // POST
+ public static final String FILE_DOWNLOAD = Constants.CORE_PREFIX + "/files/:file_id/download"; // GET
+ public static final String USER_FILES = Constants.CORE_PREFIX + "/files/myfiles"; // GET
+
+ /*
+ * RUTAS DE LA API DE LOGICA
+ * DE NEGOCIO DEL SSO
+ */
+ public static final String LOGIN = Constants.AUTH_PREFIX + "/login"; // POST
+ public static final String LOGIN_VALID = Constants.AUTH_PREFIX + "/login/validate"; // POST
+ public static final String REGISTER = Constants.AUTH_PREFIX + "/register"; // POST
+ public static final String CHANGE_PASSWORD = Constants.AUTH_PREFIX + "/change-password"; // POST
+ public static final String VALIDATE_TOKEN = Constants.AUTH_PREFIX + "/validate-token"; // POST
+ public static final String REFRESH_TOKEN = Constants.AUTH_PREFIX + "/refresh-token"; // POST
+ public static final String SCREENSHOT = Constants.CORE_PREFIX + "/screenshot"; // GET
+}
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreLogicRouter.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreLogicRouter.java
new file mode 100644
index 0000000..c62a53a
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/CoreLogicRouter.java
@@ -0,0 +1,44 @@
+package net.miarma.api.microservices.core.routing;
+
+import io.vertx.core.Vertx;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.core.handlers.FileLogicHandler;
+import net.miarma.api.backlib.core.handlers.ScreenshotHandler;
+import net.miarma.api.backlib.core.handlers.UserLogicHandler;
+import net.miarma.api.backlib.core.services.UserService;
+import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard;
+
+public class CoreLogicRouter {
+ public static void mount(Router router, Vertx vertx, Pool pool) {
+ UserLogicHandler hUserLogic = new UserLogicHandler(vertx);
+ FileLogicHandler hFileLogic = new FileLogicHandler(vertx);
+ ScreenshotHandler hScreenshot = new ScreenshotHandler(vertx);
+ UserService userService = new UserService(pool);
+ CoreAuthGuard authGuard = new CoreAuthGuard(userService);
+
+ router.route().handler(BodyHandler.create());
+
+ router.post(CoreEndpoints.LOGIN).handler(hUserLogic::login);
+ router.get(CoreEndpoints.USER_INFO).handler(authGuard.check()).handler(hUserLogic::getInfo);
+ router.post(CoreEndpoints.REGISTER).handler(hUserLogic::register);
+ router.post(CoreEndpoints.CHANGE_PASSWORD).handler(authGuard.check()).handler(hUserLogic::changePassword);
+ router.post(CoreEndpoints.LOGIN_VALID).handler(hUserLogic::loginValidate);
+ router.get(CoreEndpoints.VALIDATE_TOKEN).handler(hUserLogic::validateToken);
+ router.get(CoreEndpoints.REFRESH_TOKEN).handler(hUserLogic::refreshToken);
+
+ router.get(CoreEndpoints.USER_EXISTS).handler(authGuard.check()).handler(hUserLogic::exists);
+ router.get(CoreEndpoints.USER_STATUS).handler(authGuard.check()).handler(hUserLogic::getStatus);
+ router.put(CoreEndpoints.USER_STATUS).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserLogic::updateStatus);
+ router.get(CoreEndpoints.USER_ROLE).handler(authGuard.check()).handler(hUserLogic::getRole);
+ router.put(CoreEndpoints.USER_ROLE).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserLogic::updateRole);
+ router.get(CoreEndpoints.USER_AVATAR).handler(authGuard.check()).handler(hUserLogic::getAvatar);
+
+ router.get(CoreEndpoints.FILE_DOWNLOAD).handler(authGuard.check()).handler(hFileLogic::downloadFile);
+ router.get(CoreEndpoints.USER_FILES).handler(authGuard.check()).handler(hFileLogic::getUserFiles);
+
+ router.get(CoreEndpoints.SCREENSHOT).handler(hScreenshot::getScreenshot);
+ }
+}
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/middlewares/CoreAuthGuard.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/middlewares/CoreAuthGuard.java
new file mode 100644
index 0000000..0d8ee21
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/routing/middlewares/CoreAuthGuard.java
@@ -0,0 +1,35 @@
+package net.miarma.api.microservices.core.routing.middlewares;
+
+import java.util.function.Consumer;
+
+import io.vertx.ext.web.RoutingContext;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.middlewares.AbstractAuthGuard;
+import net.miarma.api.backlib.core.entities.UserEntity;
+import net.miarma.api.backlib.core.services.UserService;
+
+public class CoreAuthGuard extends AbstractAuthGuard {
+ private final UserService userService;
+
+ public CoreAuthGuard(UserService userService) {
+ this.userService = userService;
+ }
+
+ @Override
+ protected CoreUserRole parseRole(String roleStr) {
+ return CoreUserRole.valueOf(roleStr.toUpperCase());
+ }
+
+ @Override
+ protected void getUserEntity(int userId, RoutingContext ctx, Consumer callback) {
+ userService.getById(userId).onComplete(ar -> {
+ if (ar.succeeded()) callback.accept(ar.result());
+ else callback.accept(null);
+ });
+ }
+
+ @Override
+ protected boolean hasPermission(UserEntity user, CoreUserRole role) {
+ return user.getGlobal_role() == CoreUserRole.ADMIN;
+ }
+}
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreDataVerticle.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreDataVerticle.java
new file mode 100644
index 0000000..f5041e0
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreDataVerticle.java
@@ -0,0 +1,197 @@
+package net.miarma.api.microservices.core.verticles;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.Constants.CoreUserGlobalStatus;
+import net.miarma.api.backlib.Constants.CoreUserRole;
+import net.miarma.api.backlib.core.entities.UserEntity;
+import net.miarma.api.backlib.core.services.FileService;
+import net.miarma.api.backlib.core.services.UserService;
+import net.miarma.api.backlib.db.DatabaseProvider;
+import net.miarma.api.backlib.util.EventBusUtil;
+import net.miarma.api.backlib.util.RouterUtil;
+import net.miarma.api.microservices.core.routing.CoreDataRouter;
+
+@SuppressWarnings("unused")
+public class CoreDataVerticle extends AbstractVerticle {
+ private ConfigManager configManager;
+ private UserService userService;
+ private FileService fileService;
+
+ @Override
+ public void start(Promise startPromise) {
+ configManager = ConfigManager.getInstance();
+ Pool pool = DatabaseProvider.createPool(vertx, configManager);
+ userService = new UserService(pool);
+ fileService = new FileService(pool);
+ Router router = Router.router(vertx);
+ RouterUtil.attachLogger(router);
+ CoreDataRouter.mount(router, vertx, pool);
+ registerLogicVerticleConsumer();
+
+ vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(configManager.getIntProperty("sso.data.port"), res -> {
+ if (res.succeeded()) startPromise.complete();
+ else startPromise.fail(res.cause());
+ });
+ }
+
+ private void registerLogicVerticleConsumer() {
+ vertx.eventBus().consumer(Constants.AUTH_EVENT_BUS, message -> {
+ JsonObject body = (JsonObject) message.body();
+ String action = body.getString("action");
+
+ switch (action) {
+ case "login" -> {
+ String email = body.getString("email");
+ String userName = body.getString("userName");
+ String password = body.getString("password");
+ boolean keepLoggedIn = body.getBoolean("keepLoggedIn", false);
+
+ userService.login(email != null ? email : userName, password, keepLoggedIn)
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "register" -> {
+ UserEntity user = new UserEntity();
+ user.setUser_name(body.getString("userName"));
+ user.setEmail(body.getString("email"));
+ user.setDisplay_name(body.getString("displayName"));
+ user.setPassword(body.getString("password"));
+
+ userService.register(user)
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "changePassword" -> {
+ Integer userId = body.getInteger("userId");
+ String newPassword = body.getString("newPassword");
+
+ userService.changePassword(userId, newPassword)
+ .onSuccess(user -> {
+ String userJson = Constants.GSON.toJson(user);
+ message.reply(new JsonObject(userJson));
+ })
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "validateToken" -> {
+ String token = body.getString("token");
+
+ userService.validateToken(token)
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "getInfo", "getById" -> {
+ Integer userId = body.getInteger("userId");
+
+ userService.getById(userId)
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "userExists" -> {
+ Integer userId = body.getInteger("userId");
+
+ userService.getById(userId)
+ .onSuccess(user -> {
+ Map result = new HashMap<>();
+ result.put("user_id", userId);
+ result.put("exists", user != null);
+ message.reply(result);
+ })
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ case "getByEmail" -> userService.getByEmail(body.getString("email"))
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getByUserName" -> userService.getByUserName(body.getString("userName"))
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getStatus" -> userService.getById(body.getInteger("userId"))
+ .onSuccess(user -> {
+ Map result = new HashMap<>();
+ result.put("user_id", user.getUser_id());
+ result.put("status", user.getGlobal_status());
+ message.reply(result);
+ })
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getRole" -> userService.getById(body.getInteger("userId"))
+ .onSuccess(user -> {
+ Map result = new HashMap<>();
+ result.put("user_id", user.getUser_id());
+ result.put("role", user.getGlobal_role());
+ message.reply(result);
+ })
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getAvatar" -> userService.getById(body.getInteger("userId"))
+ .onSuccess(user -> {
+ Map result = new HashMap<>();
+ result.put("user_id", user.getUser_id());
+ result.put("avatar", user.getAvatar());
+ message.reply(result);
+ })
+ .onFailure(EventBusUtil.fail(message));
+
+ case "updateStatus" -> userService.updateStatus(
+ body.getInteger("userId"),
+ CoreUserGlobalStatus.fromInt(body.getInteger("status")))
+ .onSuccess(res -> message.reply("Status updated successfully"))
+ .onFailure(EventBusUtil.fail(message));
+
+ case "updateRole" -> userService.updateRole(
+ body.getInteger("userId"),
+ CoreUserRole.fromInt(body.getInteger("role")))
+ .onSuccess(res -> message.reply("Role updated successfully"))
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getUserFiles" -> fileService.getUserFiles(body.getInteger("userId"))
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+
+ case "downloadFile" -> fileService.downloadFile(body.getInteger("fileId"))
+ .onSuccess(message::reply)
+ .onFailure(EventBusUtil.fail(message));
+
+ case "getUserById" -> userService.getById(body.getInteger("userId"))
+ .onSuccess(user -> {
+ String userJson = Constants.GSON.toJson(user);
+ message.reply(new JsonObject(userJson));
+ })
+ .onFailure(EventBusUtil.fail(message));
+
+ case "loginValidate" -> {
+ Integer userId = body.getInteger("userId");
+ String password = body.getString("password");
+
+ userService.loginValidate(userId, password)
+ .onSuccess(user -> {
+ String userJson = Constants.GSON.toJson(user);
+ message.reply(new JsonObject(userJson));
+ })
+ .onFailure(EventBusUtil.fail(message));
+ }
+
+ default -> EventBusUtil.fail(message);
+ }
+ });
+ }
+}
\ No newline at end of file
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreLogicVerticle.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreLogicVerticle.java
new file mode 100644
index 0000000..e6a1603
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreLogicVerticle.java
@@ -0,0 +1,32 @@
+package net.miarma.api.microservices.core.verticles;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.Promise;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.handler.BodyHandler;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.db.DatabaseProvider;
+import net.miarma.api.backlib.util.RouterUtil;
+import net.miarma.api.microservices.core.routing.CoreLogicRouter;
+
+public class CoreLogicVerticle extends AbstractVerticle {
+ ConfigManager configManager;
+
+ @Override
+ public void start(Promise startPromise) {
+ configManager = ConfigManager.getInstance();
+ Pool pool = DatabaseProvider.createPool(vertx, configManager);
+ Router router = Router.router(vertx);
+ RouterUtil.attachLogger(router);
+ CoreLogicRouter.mount(router, vertx, pool);
+
+
+ vertx.createHttpServer()
+ .requestHandler(router)
+ .listen(configManager.getIntProperty("sso.logic.port"), res -> {
+ if (res.succeeded()) startPromise.complete();
+ else startPromise.fail(res.cause());
+ });
+ }
+}
diff --git a/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreMainVerticle.java b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreMainVerticle.java
new file mode 100644
index 0000000..a4a13f8
--- /dev/null
+++ b/microservices/core/src/main/java/net/miarma/api/microservices/core/verticles/CoreMainVerticle.java
@@ -0,0 +1,62 @@
+package net.miarma.api.microservices.core.verticles;
+
+import io.vertx.core.AbstractVerticle;
+import io.vertx.core.DeploymentOptions;
+import io.vertx.core.Promise;
+import io.vertx.core.ThreadingModel;
+import net.miarma.api.backlib.ConfigManager;
+import net.miarma.api.backlib.Constants;
+import net.miarma.api.backlib.LogAccumulator;
+import net.miarma.api.backlib.util.DeploymentUtil;
+
+public class CoreMainVerticle extends AbstractVerticle {
+
+ private ConfigManager configManager;
+
+ @Override
+ public void start(Promise startPromise) {
+ try {
+ this.configManager = ConfigManager.getInstance();
+ deployVerticles();
+ startPromise.complete();
+ } catch (Exception e) {
+ Constants.LOGGER.error(DeploymentUtil.failMessage(CoreMainVerticle.class, e));
+ startPromise.fail(e);
+ }
+ }
+
+ private void deployVerticles() {
+ final DeploymentOptions options = new DeploymentOptions()
+ .setThreadingModel(ThreadingModel.WORKER);
+
+ vertx.deployVerticle(new CoreDataVerticle(), options, result -> {
+ if (result.succeeded()) {
+ String message = String.join("\n\r ",
+ DeploymentUtil.successMessage(CoreDataVerticle.class),
+ DeploymentUtil.apiUrlMessage(
+ configManager.getHost(),
+ configManager.getIntProperty("sso.data.port")
+ )
+ );
+ LogAccumulator.add(message);
+ } else {
+ LogAccumulator.add(DeploymentUtil.failMessage(CoreDataVerticle.class, result.cause()));
+ }
+ });
+
+ vertx.deployVerticle(new CoreLogicVerticle(), options, result -> {
+ if (result.succeeded()) {
+ String message = String.join("\n\r ",
+ DeploymentUtil.successMessage(CoreLogicVerticle.class),
+ DeploymentUtil.apiUrlMessage(
+ configManager.getHost(),
+ configManager.getIntProperty("sso.logic.port")
+ )
+ );
+ LogAccumulator.add(message);
+ } else {
+ LogAccumulator.add(DeploymentUtil.failMessage(CoreLogicVerticle.class, result.cause()));
+ }
+ });
+ }
+}
diff --git a/microservices/huertos/.gitignore b/microservices/huertos/.gitignore
new file mode 100644
index 0000000..b83d222
--- /dev/null
+++ b/microservices/huertos/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/microservices/huertos/pom.xml b/microservices/huertos/pom.xml
new file mode 100644
index 0000000..de60f98
--- /dev/null
+++ b/microservices/huertos/pom.xml
@@ -0,0 +1,55 @@
+
+ 4.0.0
+ net.miarma.api
+ huertos
+ 1.2.0
+
+
+ 23
+ 23
+
+
+
+
+ MiarmaGit
+ https://git.miarma.net/api/packages/Gallardo7761/maven
+
+
+
+
+
+ net.miarma.api
+ backlib
+ 1.2.0
+
+
+
+
+ ME-Huertos
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.3
+
+
+ package
+
+ shade
+
+
+ false
+
+
+ net.miarma.api.microservices.huertos.HuertosMainVerticle
+
+
+
+
+
+
+
+
+
+
diff --git a/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/AnnouncementDAO.java b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/AnnouncementDAO.java
new file mode 100644
index 0000000..92da3db
--- /dev/null
+++ b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/AnnouncementDAO.java
@@ -0,0 +1,133 @@
+package net.miarma.api.microservices.huertos.dao;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.backlib.http.QueryFilters;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.microservices.huertos.entities.AnnouncementEntity;
+
+import java.util.List;
+import java.util.Map;
+
+public class AnnouncementDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public AnnouncementDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ return getAll(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(AnnouncementEntity.class)
+ .where(Map.of("announce_id", id.toString()))
+ .build();
+
+ db.executeOne(query, AnnouncementEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAll(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(AnnouncementEntity.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, AnnouncementEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future insert(AnnouncementEntity announce) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.insert(announce).build();
+
+ db.execute(query, AnnouncementEntity.class,
+ list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future upsert(AnnouncementEntity announcementEntity, String... conflictKeys) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.upsert(announcementEntity, conflictKeys).build();
+
+ db.executeOne(query, AnnouncementEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future update(AnnouncementEntity announce) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.update(announce).build();
+
+ db.executeOne(query, AnnouncementEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future delete(Integer id) {
+ Promise promise = Promise.promise();
+ AnnouncementEntity announce = new AnnouncementEntity();
+ announce.setAnnounce_id(id);
+
+ String query = QueryBuilder.delete(announce).build();
+
+ db.executeOne(query, AnnouncementEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future exists(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(AnnouncementEntity.class)
+ .where(Map.of("announce_id", id.toString()))
+ .build();
+
+ db.executeOne(query, AnnouncementEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+}
diff --git a/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/BalanceDAO.java b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/BalanceDAO.java
new file mode 100644
index 0000000..f8bb592
--- /dev/null
+++ b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/BalanceDAO.java
@@ -0,0 +1,134 @@
+package net.miarma.api.microservices.huertos.dao;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.microservices.huertos.entities.BalanceEntity;
+import net.miarma.api.microservices.huertos.entities.ViewBalanceWithTotals;
+
+import java.util.List;
+import java.util.Map;
+
+public class BalanceDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public BalanceDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder.select(BalanceEntity.class).build();
+
+ db.execute(query, BalanceEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(BalanceEntity.class)
+ .where(Map.of("id", id.toString()))
+ .build();
+
+ db.executeOne(query, BalanceEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAllWithTotals() {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder.select(ViewBalanceWithTotals.class).build();
+
+ db.execute(query, ViewBalanceWithTotals.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future insert(BalanceEntity balance) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.insert(balance).build();
+
+ db.execute(query, BalanceEntity.class,
+ list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future upsert(BalanceEntity balanceEntity, String... conflictKeys) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.upsert(balanceEntity, conflictKeys).build();
+
+ db.executeOne(query, BalanceEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future update(BalanceEntity balance) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.update(balance).build();
+
+ db.executeOne(query, BalanceEntity.class,
+ _ -> promise.complete(balance),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future delete(Integer id) {
+ Promise promise = Promise.promise();
+ BalanceEntity balance = new BalanceEntity();
+ balance.setId(id);
+
+ String query = QueryBuilder.delete(balance).build();
+
+ db.executeOne(query, BalanceEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future exists(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(BalanceEntity.class)
+ .where(Map.of("id", id.toString()))
+ .build();
+
+ db.executeOne(query, BalanceEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+}
diff --git a/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/ExpenseDAO.java b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/ExpenseDAO.java
new file mode 100644
index 0000000..3d34a51
--- /dev/null
+++ b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/ExpenseDAO.java
@@ -0,0 +1,133 @@
+package net.miarma.api.microservices.huertos.dao;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.backlib.http.QueryFilters;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.microservices.huertos.entities.ExpenseEntity;
+
+import java.util.List;
+import java.util.Map;
+
+public class ExpenseDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public ExpenseDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ return getAll(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(ExpenseEntity.class)
+ .where(Map.of("expense_id", id.toString()))
+ .build();
+
+ db.executeOne(query, ExpenseEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAll(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(ExpenseEntity.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, ExpenseEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future insert(ExpenseEntity expense) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.insert(expense).build();
+
+ db.execute(query, ExpenseEntity.class,
+ list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future upsert(ExpenseEntity expenseEntity, String... conflictKeys) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.upsert(expenseEntity, conflictKeys).build();
+
+ db.executeOne(query, ExpenseEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future update(ExpenseEntity expense) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder.update(expense).build();
+
+ db.executeOne(query, ExpenseEntity.class,
+ _ -> promise.complete(expense),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future delete(Integer id) {
+ Promise promise = Promise.promise();
+ ExpenseEntity expense = new ExpenseEntity();
+ expense.setExpense_id(id);
+
+ String query = QueryBuilder.delete(expense).build();
+
+ db.executeOne(query, ExpenseEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ @Override
+ public Future exists(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(ExpenseEntity.class)
+ .where(Map.of("expense_id", id.toString()))
+ .build();
+
+ db.executeOne(query, ExpenseEntity.class,
+ result -> promise.complete(result != null),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+}
diff --git a/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/IncomeDAO.java b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/IncomeDAO.java
new file mode 100644
index 0000000..8346544
--- /dev/null
+++ b/microservices/huertos/src/main/java/net/miarma/api/microservices/huertos/dao/IncomeDAO.java
@@ -0,0 +1,171 @@
+package net.miarma.api.microservices.huertos.dao;
+
+import io.vertx.core.Future;
+import io.vertx.core.Promise;
+import io.vertx.sqlclient.Pool;
+import net.miarma.api.backlib.db.DataAccessObject;
+import net.miarma.api.backlib.db.DatabaseManager;
+import net.miarma.api.backlib.db.QueryBuilder;
+import net.miarma.api.backlib.http.QueryFilters;
+import net.miarma.api.backlib.http.QueryParams;
+import net.miarma.api.microservices.huertos.entities.IncomeEntity;
+import net.miarma.api.microservices.huertos.entities.ViewIncomesWithFullNames;
+
+import java.util.List;
+import java.util.Map;
+
+public class IncomeDAO implements DataAccessObject {
+
+ private final DatabaseManager db;
+
+ public IncomeDAO(Pool pool) {
+ this.db = DatabaseManager.getInstance(pool);
+ }
+
+ @Override
+ public Future> getAll() {
+ return getAll(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ @Override
+ public Future getById(Integer id) {
+ Promise promise = Promise.promise();
+ String query = QueryBuilder
+ .select(IncomeEntity.class)
+ .where(Map.of("income_id", id.toString()))
+ .build();
+
+ db.executeOne(query, IncomeEntity.class,
+ promise::complete,
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAll(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(IncomeEntity.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, IncomeEntity.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getAllWithNames() {
+ return getAllWithNames(new QueryParams(Map.of(), new QueryFilters()));
+ }
+
+ public Future> getAllWithNames(QueryParams params) {
+ Promise> promise = Promise.promise();
+ String query = QueryBuilder
+ .select(ViewIncomesWithFullNames.class)
+ .where(params.getFilters())
+ .orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
+ .limit(params.getQueryFilters().getLimit())
+ .offset(params.getQueryFilters().getOffset())
+ .build();
+
+ db.execute(query, ViewIncomesWithFullNames.class,
+ list -> promise.complete(list.isEmpty() ? List.of() : list),
+ promise::fail
+ );
+
+ return promise.future();
+ }
+
+ public Future> getUserIncomes(Integer memberNumber) {
+ Promise