[REPO REFACTOR]: changed to a better git repository structure with branches

This commit is contained in:
2025-10-31 03:32:24 +01:00
parent ad689049d5
commit 8360c7e8e0
212 changed files with 15955 additions and 0 deletions

1
backlib/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

164
backlib/pom.xml Normal file
View File

@@ -0,0 +1,164 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.miarma.api</groupId>
<artifactId>backlib</artifactId>
<version>1.2.0</version>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
<snapshotRepository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</snapshotRepository>
</distributionManagement>
<dependencies>
<!-- Vert.X Core -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-core</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Web -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Web Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-web-client</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X MariaDB/MySQL Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mysql-client</artifactId>
<version>4.5.13</version>
</dependency>
<!-- Vert.X Mail Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-mail-client</artifactId>
<version>4.5.16</version>
</dependency>
<!-- Vert.X Redis Client -->
<dependency>
<groupId>io.vertx</groupId>
<artifactId>vertx-redis-client</artifactId>
<version>4.5.16</version>
</dependency>
<!-- Gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.12.1</version>
</dependency>
<!-- BCrypt -->
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<!-- JWT -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.5.0</version>
</dependency>
<!-- SLF4J + Logback -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.12</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.13</version>
</dependency>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.3</version>
</dependency>
<!-- Jakarta Mail -->
<dependency>
<groupId>com.sun.mail</groupId>
<artifactId>jakarta.mail</artifactId>
<version>2.0.1</version>
</dependency>
<!-- Discord Webhook -->
<dependency>
<groupId>com.github.eduardomcb</groupId>
<artifactId>discord-webhook</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<build>
<finalName>BackLib</finalName>
<plugins>
<!-- Maven Shade Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.backlib.MainVerticle</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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.
* <p>
* 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);
}
}
}

View File

@@ -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<String> 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.");
}
}

View File

@@ -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<LogEntry> 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();
}
}

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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();
}

View File

@@ -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 {}

View File

@@ -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();
}

View File

@@ -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<FileEntity, Integer> {
private final DatabaseManager db;
public FileDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<FileEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<FileEntity> getById(Integer id) {
Promise<FileEntity> 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<List<FileEntity>> getAll(QueryParams params) {
Promise<List<FileEntity>> 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<List<FileEntity>> getUserFiles(Integer userId) {
Promise<List<FileEntity>> 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<FileEntity> insert(FileEntity file) {
Promise<FileEntity> promise = Promise.promise();
String query = QueryBuilder.insert(file).build();
db.executeOne(query, FileEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<FileEntity> upsert(FileEntity file, String... conflictKeys) {
Promise<FileEntity> 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<FileEntity> update(FileEntity file) {
Promise<FileEntity> promise = Promise.promise();
String query = QueryBuilder.update(file).build();
db.executeOne(query, FileEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> 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<Boolean> delete(Integer id) {
Promise<Boolean> 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();
}
}

View File

@@ -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<UserEntity, Integer> {
private final DatabaseManager db;
public UserDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<UserEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<UserEntity> getById(Integer id) {
Promise<UserEntity> 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<List<UserEntity>> getAll(QueryParams params) {
Promise<List<UserEntity>> 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<UserEntity> getByEmail(String email) {
Promise<UserEntity> 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<UserEntity> getByUserName(String userName) {
Promise<UserEntity> 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<UserEntity> insert(UserEntity user) {
Promise<UserEntity> promise = Promise.promise();
String query = QueryBuilder.insert(user).build();
db.executeOne(query, UserEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<UserEntity> upsert(UserEntity userEntity, String... conflictKeys) {
Promise<UserEntity> 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<UserEntity> update(UserEntity user) {
Promise<UserEntity> 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<Boolean> delete(Integer id) {
Promise<Boolean> 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<Boolean> exists(Integer id) {
Promise<Boolean> 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();
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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()));
}
}

View File

@@ -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");
}
});
}
}

View File

@@ -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");
}
});
}
}

View File

@@ -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());
});
}
}

View File

@@ -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());
}
});
}
}

View File

@@ -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<List<FileEntity>> getAll(QueryParams params) {
return fileDAO.getAll(params);
}
public Future<FileEntity> 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<List<FileEntity>> getUserFiles(Integer userId) {
return fileDAO.getUserFiles(userId);
}
public Future<FileEntity> 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<FileEntity> downloadFile(Integer fileId) {
return getById(fileId);
}
public Future<FileEntity> 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<FileEntity> 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<Boolean> 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<Boolean> exists(Integer fileId) {
return fileDAO.exists(fileId);
}
}

View File

@@ -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<JsonObject> 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<JsonObject> 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<UserEntity> 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<UserEntity> 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<Boolean> validateToken(String token) {
JWTManager jwtManager = JWTManager.getInstance();
return jwtManager.isValid(token) ?
Future.succeededFuture(true) :
Future.failedFuture(new UnauthorizedException("Invalid token"));
}
/* USERS OPERATIONS */
public Future<List<UserEntity>> getAll(QueryParams params) {
return userDAO.getAll(params);
}
public Future<UserEntity> 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<UserEntity> getByEmail(String email) {
return userDAO.getByEmail(email);
}
public Future<UserEntity> getByUserName(String userName) {
return userDAO.getByUserName(userName);
}
public Future<UserEntity> 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<UserEntity> 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<UserEntity> 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<UserEntity> 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<UserEntity> 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<Boolean> 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);
});
}
}

View File

@@ -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<ValidationResult> 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<ValidationResult> 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);
}
}

View File

@@ -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<ValidationResult> 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);
}
}

View File

@@ -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.
* <p>
* Proporciona utilidades para:
* <ul>
* <li>Construir una entidad a partir de una fila de base de datos ({@link Row})</li>
* <li>Serializar una entidad a {@link JsonObject}</li>
* <li>Generar una representación en texto</li>
* </ul>
*
* 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).
* <p>
* 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}.
*
* <p>Si un campo implementa {@link ValuableEnum}, se usará su valor en lugar del nombre del enum.</p>
*
* @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.
*
* <p>Útil para logs y debugging.</p>
*
* @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();
}
}

View File

@@ -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 <T> Tipo de la entidad gestionada.
* @param <ID> Tipo del identificador único de la entidad.
*
* @author José Manuel Amador Gallardo
*/
public interface DataAccessObject<T, ID> {
/**
* Recupera todos los registros de la entidad.
*
* @return Un {@link Future} que contiene una lista con todas las entidades encontradas.
*/
Future<List<T>> 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<T> 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<T> 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<T> 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<T> 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<Boolean> 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<Boolean> exists(ID id);
}

View File

@@ -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.
*
* <p>
* 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<RowSet<Row>> 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 <T> tipo del objeto a devolver
* @return un {@link Future} con la lista de resultados convertidos
*/
public <T> Future<List<T>> execute(String query, Class<T> clazz, Handler<List<T>> onSuccess,
Handler<Throwable> onFailure) {
return pool.query(query).execute().map(rows -> {
List<T> results = new ArrayList<>();
for (Row row : rows) {
try {
Constructor<T> 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 <T> tipo del objeto a devolver
* @return un {@link Future} con el objeto instanciado, o null si no hay resultados
*/
public <T> Future<T> executeOne(String query, Class<T> clazz, Handler<T> onSuccess, Handler<Throwable> onFailure) {
return pool.query(query).execute().map(rows -> {
for (Row row : rows) {
try {
Constructor<T> 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());
}
});
}
}

View File

@@ -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.
*
* <p>
* 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.
* </p>
*
* @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:
* <ul>
* <li><b>db.port</b> puerto del servidor MySQL</li>
* <li><b>db.host</b> host o IP</li>
* <li><b>db.name</b> nombre de la base de datos</li>
* <li><b>db.user</b> usuario de la base</li>
* <li><b>db.password</b> contraseña del usuario</li>
* </ul>
* @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);
}
}

View File

@@ -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}.
* <p>
* 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.
* <p>
* ¡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 <T> String getTableName(Class<T> 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 <T> the type of the entity class
*/
public static <T> QueryBuilder select(Class<T> 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<String, String> filters) {
if (filters == null || filters.isEmpty()) {
return this;
}
Set<String> validFields = entityClass != null
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
: Collections.emptySet();
List<String> conditions = new ArrayList<>();
for (Map.Entry<String, String> 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 <T> QueryBuilder where(T object) {
if (object == null) {
throw new IllegalArgumentException("Object cannot be null");
}
Set<String> 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 <T> el tipo del objeto a insertar
*/
public static <T> 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 <T> el tipo del objeto a actualizar
*/
public static <T> 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 <T> el tipo del objeto a actualizar
*/
public static <T> 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 <T> el tipo del objeto a insertar o actualizar
*/
public static <T> 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<String, String> 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 <T> el tipo del objeto a eliminar
*/
public static <T> 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<String> column, Optional<String> 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<Integer> 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<Integer> 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() + ";";
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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.
*
* <p>Esta excepción es de tipo {@link RuntimeException}, por lo que no es necesario
* declararla explícitamente en los métodos que la lanzan.</p>
*
* @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);
}
}

View File

@@ -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;
}
}

View File

@@ -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<JsonObject>, JsonDeserializer<JsonObject> {
@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<String, JsonElement> entry : json.getAsJsonObject().entrySet()) {
obj.put(entry.getKey(), context.deserialize(entry.getValue(), Object.class));
}
return obj;
}
}

View File

@@ -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<LocalDateTime>, JsonDeserializer<LocalDateTime> {
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);
}
}

View File

@@ -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<ValuableEnum> {
@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));
}
}

View File

@@ -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<ValuableEnum> {
@Override
public JsonElement serialize(ValuableEnum src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.getValue());
}
}

View File

@@ -0,0 +1,60 @@
package net.miarma.api.backlib.http;
/**
* Clase genérica para representar una respuesta de la API.
* <p>
* 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.
* <p>
* Ejemplo de uso:
* <pre>
* ApiResponse<MyDataType> response = new ApiResponse<>(ApiStatus.SUCCESS, "Data retrieved successfully", myData);
* </pre>
* @see ApiStatus
*
* @param <T> Tipo de dato que contendrá la respuesta.
*
* @author José Manuel Amador Gallardo
*/
public class ApiResponse<T> {
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;
}
}

View File

@@ -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;
};
}
}

View File

@@ -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<String> sort = Optional.empty();
private Optional<String> order = Optional.of("ASC");
private Optional<Integer> limit = Optional.empty();
private Optional<Integer> offset = Optional.empty();
public QueryFilters() {}
public QueryFilters(Optional<String> sort, Optional<String> order, Optional<Integer> limit, Optional<Integer> offset) {
this.sort = sort;
this.order = order;
this.limit = limit;
this.offset = offset;
}
public Optional<String> getSort() {
return sort;
}
public void setSort(String sort) {
this.sort = Optional.ofNullable(sort);
}
public Optional<String> getOrder() {
return order;
}
public void setOrder(String order) {
this.order = Optional.ofNullable(order);
}
public Optional<Integer> getLimit() {
return limit;
}
public void setLimit(Integer limit) {
this.limit = Optional.ofNullable(limit);
}
public Optional<Integer> 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;
}
}

View File

@@ -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<String, String> filters;
private final QueryFilters queryFilters;
public QueryParams(Map<String, String> filters, QueryFilters queryFilters) {
this.filters = filters;
this.queryFilters = queryFilters;
}
public Map<String, String> getFilters() {
return filters;
}
public QueryFilters getQueryFilters() {
return queryFilters;
}
public static QueryParams from(RoutingContext ctx) {
Map<String, String> 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<String> validKeys = getFieldNames(entityClass);
Map<String, String> 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<String> getFieldNames(Class<?> clazz) {
return Arrays.stream(clazz.getDeclaredFields())
.map(Field::getName)
.collect(Collectors.toSet());
}
@Override
public String toString() {
return "QueryParams{" +
"filters=" + filters +
", queryFilters=" + queryFilters +
'}';
}
}

View File

@@ -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 <T> el tipo del mensaje que se envía en la respuesta
* @author José Manuel Amador Gallardo
*/
public record SingleJsonResponse<T>(T message) {
public static <T> SingleJsonResponse<T> of(T message) {
return new SingleJsonResponse<>(message);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,7 @@
package net.miarma.api.backlib.interfaces;
import net.miarma.api.backlib.ValuableEnum;
public interface IUserRole extends ValuableEnum {
String name();
}

View File

@@ -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<U, R extends Enum<R> & IUserRole> {
protected abstract R parseRole(String roleStr);
protected abstract void getUserEntity(int userId, RoutingContext ctx, Consumer<U> callback);
protected abstract boolean hasPermission(U user, R role);
public Handler<RoutingContext> 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;
}
}

View File

@@ -0,0 +1,66 @@
package net.miarma.api.backlib.security;
/**
* Validador de DNI/NIE español.
* <p>
* 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);
}
}

View File

@@ -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.
* <p>
* 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;
}
}
}

View File

@@ -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);
}
}

View File

@@ -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.
* <p>
* 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<String> 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();
}
}

View File

@@ -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");
}
}

View File

@@ -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);
}
}

View File

@@ -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 <T> String successMessage(Class<T> clazz) {
return String.join(" ", "🟢", clazz.getSimpleName(), "deployed successfully");
}
public static <T> String failMessage(Class<T> 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);
}
}

View File

@@ -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 <T> Handler<Throwable> fail(Message<T> 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 <T> Handler<Throwable> 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);
}
}
}

View File

@@ -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 <T> void sendJson(RoutingContext ctx, ApiStatus status, T data) {
sendJson(ctx, status, data, status.getDefaultMessage());
}
public static <T> 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)));
}
}
}

View File

@@ -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());
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, UserRequests> 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;
}
}

View File

@@ -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 "📥";
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, String> errors = new HashMap<>();
public ValidationResult addError(String field, String message) {
errors.put(field, message);
return this;
}
public boolean isValid() {
return errors.isEmpty();
}
public Map<String, String> getErrors() {
return errors;
}
public String getFirstError() {
return errors.values().stream().findFirst().orElse(null);
}
}

View File

@@ -0,0 +1,12 @@
package net.miarma.api.backlib.validation;
import io.vertx.core.Future;
/**
* Interfaz para la validación de entidades.
* @param <T> Tipo de entidad a validar.
* @author José Manuel Amador Gallardo
*/
public interface Validator<T> {
Future<ValidationResult> validate(T entity);
}

View File

@@ -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);
}
}

1
bootstrap/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

90
bootstrap/pom.xml Normal file
View File

@@ -0,0 +1,90 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.miarma.api</groupId>
<artifactId>miarma-ecosystem</artifactId>
<version>1.2.0</version>
</parent>
<artifactId>bootstrap</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>backlib</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>core</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>huertos</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>huertosdecine</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>miarmacraft</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>mpaste</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<finalName>MiarmaEcosystem</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.api.AppInitializer</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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());
}
}

View File

@@ -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<Void> startPromise) {
deploy()
.onSuccess(v -> {
vertx.setTimer(300, id -> {
LogAccumulator.flushToLogger(Constants.LOGGER);
startPromise.complete();
});
})
.onFailure(startPromise::fail);
}
private Future<Void> deploy() {
Promise<Void> promise = Promise.promise();
Future<String> core = vertx.deployVerticle(new CoreMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CoreMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CoreMainVerticle.class, err)));
Future<String> huertos = vertx.deployVerticle(new HuertosMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(HuertosMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(HuertosMainVerticle.class, err)));
Future<String> mmc = vertx.deployVerticle(new MMCMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(MMCMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(MMCMainVerticle.class, err)));
Future<String> cine = vertx.deployVerticle(new CineMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CineMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CineMainVerticle.class, err)));
Future<String> 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<Void> stopPromise) {
vertx.deploymentIDs().forEach(id -> vertx.undeploy(id));
stopPromise.complete();
}
}

View File

@@ -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=

View File

@@ -0,0 +1,20 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>
%cyan([%d{HH:mm:ss}]) %highlight(%-5level) %green(%logger{20}) - %msg%n
</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="STDOUT"/>
</root>
<logger name="io.netty" level="WARN"/>
<logger name="io.vertx" level="INFO"/>
<logger name="io.vertx.core.impl.launcher" level="INFO"/>
<logger name="io.vertx.core.logging" level="INFO"/>
</configuration>

1
microservices/core/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,54 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.miarma.api</groupId>
<artifactId>core</artifactId>
<version>1.2.0</version>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>backlib</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<finalName>ME-Core</finalName>
<plugins>
<!-- Maven Shade Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.api.microservices.core.verticles.CoreMainVerticle</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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);
}
}

View File

@@ -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
}

View File

@@ -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);
}
}

View File

@@ -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<UserEntity, CoreUserRole> {
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<UserEntity> 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;
}
}

View File

@@ -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<Void> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
}
});
}
}

View File

@@ -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<Void> 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());
});
}
}

View File

@@ -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<Void> 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()));
}
});
}
}

1
microservices/huertos/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target/

View File

@@ -0,0 +1,55 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>net.miarma.api</groupId>
<artifactId>huertos</artifactId>
<version>1.2.0</version>
<properties>
<maven.compiler.source>23</maven.compiler.source>
<maven.compiler.target>23</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.miarma.api</groupId>
<artifactId>backlib</artifactId>
<version>1.2.0</version>
</dependency>
</dependencies>
<build>
<finalName>ME-Huertos</finalName>
<plugins>
<!-- Maven Shade Plugin -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.3</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>net.miarma.api.microservices.huertos.HuertosMainVerticle</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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<AnnouncementEntity, Integer> {
private final DatabaseManager db;
public AnnouncementDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<AnnouncementEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<AnnouncementEntity> getById(Integer id) {
Promise<AnnouncementEntity> 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<List<AnnouncementEntity>> getAll(QueryParams params) {
Promise<List<AnnouncementEntity>> 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<AnnouncementEntity> insert(AnnouncementEntity announce) {
Promise<AnnouncementEntity> 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<AnnouncementEntity> upsert(AnnouncementEntity announcementEntity, String... conflictKeys) {
Promise<AnnouncementEntity> 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<AnnouncementEntity> update(AnnouncementEntity announce) {
Promise<AnnouncementEntity> promise = Promise.promise();
String query = QueryBuilder.update(announce).build();
db.executeOne(query, AnnouncementEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> delete(Integer id) {
Promise<Boolean> 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<Boolean> exists(Integer id) {
Promise<Boolean> 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();
}
}

View File

@@ -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<BalanceEntity, Integer> {
private final DatabaseManager db;
public BalanceDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<BalanceEntity>> getAll() {
Promise<List<BalanceEntity>> 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<BalanceEntity> getById(Integer id) {
Promise<BalanceEntity> 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<List<ViewBalanceWithTotals>> getAllWithTotals() {
Promise<List<ViewBalanceWithTotals>> 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<BalanceEntity> insert(BalanceEntity balance) {
Promise<BalanceEntity> 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<BalanceEntity> upsert(BalanceEntity balanceEntity, String... conflictKeys) {
Promise<BalanceEntity> 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<BalanceEntity> update(BalanceEntity balance) {
Promise<BalanceEntity> 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<Boolean> delete(Integer id) {
Promise<Boolean> 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<Boolean> exists(Integer id) {
Promise<Boolean> 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();
}
}

View File

@@ -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<ExpenseEntity, Integer> {
private final DatabaseManager db;
public ExpenseDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<ExpenseEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<ExpenseEntity> getById(Integer id) {
Promise<ExpenseEntity> 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<List<ExpenseEntity>> getAll(QueryParams params) {
Promise<List<ExpenseEntity>> 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<ExpenseEntity> insert(ExpenseEntity expense) {
Promise<ExpenseEntity> 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<ExpenseEntity> upsert(ExpenseEntity expenseEntity, String... conflictKeys) {
Promise<ExpenseEntity> 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<ExpenseEntity> update(ExpenseEntity expense) {
Promise<ExpenseEntity> 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<Boolean> delete(Integer id) {
Promise<Boolean> 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<Boolean> exists(Integer id) {
Promise<Boolean> 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();
}
}

View File

@@ -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<IncomeEntity, Integer> {
private final DatabaseManager db;
public IncomeDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<IncomeEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<IncomeEntity> getById(Integer id) {
Promise<IncomeEntity> 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<List<IncomeEntity>> getAll(QueryParams params) {
Promise<List<IncomeEntity>> 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<List<ViewIncomesWithFullNames>> getAllWithNames() {
return getAllWithNames(new QueryParams(Map.of(), new QueryFilters()));
}
public Future<List<ViewIncomesWithFullNames>> getAllWithNames(QueryParams params) {
Promise<List<ViewIncomesWithFullNames>> 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<List<IncomeEntity>> getUserIncomes(Integer memberNumber) {
Promise<List<IncomeEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(IncomeEntity.class)
.where(Map.of("member_number", memberNumber.toString()))
.build();
db.execute(query, IncomeEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
@Override
public Future<IncomeEntity> insert(IncomeEntity income) {
Promise<IncomeEntity> promise = Promise.promise();
String query = QueryBuilder.insert(income).build();
db.execute(query, IncomeEntity.class,
list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
promise::fail
);
return promise.future();
}
@Override
public Future<IncomeEntity> upsert(IncomeEntity incomeEntity, String... conflictKeys) {
Promise<IncomeEntity> promise = Promise.promise();
String query = QueryBuilder.upsert(incomeEntity, conflictKeys).build();
db.executeOne(query, IncomeEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<IncomeEntity> update(IncomeEntity income) {
Promise<IncomeEntity> promise = Promise.promise();
String query = QueryBuilder.update(income).build();
db.executeOne(query, IncomeEntity.class,
_ -> promise.complete(income),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> delete(Integer id) {
Promise<Boolean> promise = Promise.promise();
IncomeEntity income = new IncomeEntity();
income.setIncome_id(id);
String query = QueryBuilder.delete(income).build();
db.executeOne(query, IncomeEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(IncomeEntity.class)
.where(Map.of("income_id", id.toString()))
.build();
db.executeOne(query, IncomeEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
}

View File

@@ -0,0 +1,235 @@
package net.miarma.api.microservices.huertos.dao;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import io.vertx.core.Future;
import io.vertx.core.Promise;
import io.vertx.sqlclient.Pool;
import net.miarma.api.backlib.Constants;
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.MemberEntity;
public class MemberDAO implements DataAccessObject<MemberEntity, Integer> {
private final DatabaseManager db;
public MemberDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<MemberEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<MemberEntity> getById(Integer id) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("user_id", id.toString()))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<MemberEntity>> getAll(QueryParams params) {
Promise<List<MemberEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(params.getFilters())
.orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
.limit(params.getQueryFilters().getLimit())
.offset(params.getQueryFilters().getOffset())
.build();
db.execute(query, MemberEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getByMemberNumber(Integer memberNumber) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("member_number", memberNumber.toString()))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getByPlotNumber(Integer plotNumber) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("plot_number", plotNumber.toString()))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getByEmail(String email) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("email", email))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getByDni(String dni) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("dni", dni))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getByPhone(Integer phone) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("phone", phone.toString()))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<MemberEntity>> getWaitlist() {
Promise<List<MemberEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("type", "0", "status", String.valueOf(Constants.HuertosUserStatus.ACTIVE.getValue())))
.build();
db.execute(query, MemberEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
public Future<Integer> getLastMemberNumber() {
Promise<Integer> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class, "member_number")
.orderBy(Optional.of("member_number"), Optional.of("DESC"))
.limit(Optional.of(1))
.build();
db.executeOne(query, MemberEntity.class,
result -> promise.complete(result != null ? result.getMember_number() : 0),
promise::fail
);
return promise.future();
}
public Future<Boolean> hasCollaborator(Integer plotNumber) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("plot_number", plotNumber.toString(), "type", String.valueOf(Constants.HuertosUserType.COLLABORATOR.getValue())))
.build();
db.executeOne(query, MemberEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
public Future<MemberEntity> getCollaborator(Integer plotNumber) {
Promise<MemberEntity> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("plot_number", plotNumber.toString(), "type", String.valueOf(Constants.HuertosUserType.COLLABORATOR.getValue())))
.build();
db.executeOne(query, MemberEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<MemberEntity> insert(MemberEntity user) {
throw new UnsupportedOperationException("Insert not supported on view-based DAO");
}
@Override
public Future<MemberEntity> upsert(MemberEntity memberEntity, String... conflictKeys) {
throw new UnsupportedOperationException("Upsert not supported on view-based DAO");
}
@Override
public Future<MemberEntity> update(MemberEntity user) {
throw new UnsupportedOperationException("Update not supported on view-based DAO");
}
@Override
public Future<Boolean> delete(Integer id) {
throw new UnsupportedOperationException("Delete not supported on view-based DAO");
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(MemberEntity.class)
.where(Map.of("user_id", id.toString()))
.build();
db.executeOne(query, MemberEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
}

View File

@@ -0,0 +1,148 @@
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.PreUserEntity;
import java.util.List;
import java.util.Map;
public class PreUserDAO implements DataAccessObject<PreUserEntity, Integer> {
private final DatabaseManager db;
public PreUserDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<PreUserEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<PreUserEntity> getById(Integer id) {
Promise<PreUserEntity> promise = Promise.promise();
String query = QueryBuilder
.select(PreUserEntity.class)
.where(Map.of("pre_user_id", id.toString()))
.build();
db.executeOne(query, PreUserEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<PreUserEntity> getByRequestId(Integer requestId) {
Promise<PreUserEntity> promise = Promise.promise();
String query = QueryBuilder
.select(PreUserEntity.class)
.where(Map.of("request_id", requestId.toString()))
.build();
db.executeOne(query, PreUserEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<PreUserEntity>> getAll(QueryParams params) {
Promise<List<PreUserEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(PreUserEntity.class)
.where(params.getFilters())
.orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
.limit(params.getQueryFilters().getLimit())
.offset(params.getQueryFilters().getOffset())
.build();
db.execute(query, PreUserEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
@Override
public Future<PreUserEntity> insert(PreUserEntity preUser) {
Promise<PreUserEntity> promise = Promise.promise();
String query = QueryBuilder.insert(preUser).build();
db.execute(query, PreUserEntity.class,
list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
promise::fail
);
return promise.future();
}
@Override
public Future<PreUserEntity> upsert(PreUserEntity preUserEntity, String... conflictKeys) {
Promise<PreUserEntity> promise = Promise.promise();
String query = QueryBuilder.upsert(preUserEntity, conflictKeys).build();
db.execute(query, PreUserEntity.class,
list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
promise::fail
);
return promise.future();
}
@Override
public Future<PreUserEntity> update(PreUserEntity preUser) {
Promise<PreUserEntity> promise = Promise.promise();
String query = QueryBuilder.update(preUser).build();
db.executeOne(query, PreUserEntity.class,
_ -> promise.complete(preUser),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> delete(Integer id) {
Promise<Boolean> promise = Promise.promise();
PreUserEntity preUser = new PreUserEntity();
preUser.setPre_user_id(id);
String query = QueryBuilder.delete(preUser).build();
db.executeOne(query, PreUserEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(PreUserEntity.class)
.where(Map.of("pre_user_id", id.toString()))
.build();
db.execute(query, PreUserEntity.class,
list -> promise.complete(!list.isEmpty()),
promise::fail
);
return promise.future();
}
}

View File

@@ -0,0 +1,176 @@
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.RequestEntity;
import net.miarma.api.microservices.huertos.entities.ViewRequestsWithPreUsers;
import java.util.List;
import java.util.Map;
public class RequestDAO implements DataAccessObject<RequestEntity, Integer> {
private final DatabaseManager db;
public RequestDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<RequestEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<RequestEntity> getById(Integer id) {
Promise<RequestEntity> promise = Promise.promise();
String query = QueryBuilder
.select(RequestEntity.class)
.where(Map.of("request_id", id.toString()))
.build();
db.executeOne(query, RequestEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<RequestEntity>> getAll(QueryParams params) {
Promise<List<RequestEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(RequestEntity.class)
.where(params.getFilters())
.orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
.limit(params.getQueryFilters().getLimit())
.offset(params.getQueryFilters().getOffset())
.build();
db.execute(query, RequestEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
public Future<List<ViewRequestsWithPreUsers>> getRequestsWithPreUsers() {
Promise<List<ViewRequestsWithPreUsers>> promise = Promise.promise();
String query = QueryBuilder
.select(ViewRequestsWithPreUsers.class)
.build();
db.execute(query, ViewRequestsWithPreUsers.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
public Future<ViewRequestsWithPreUsers> getRequestWithPreUserById(Integer id) {
Promise<ViewRequestsWithPreUsers> promise = Promise.promise();
String query = QueryBuilder
.select(ViewRequestsWithPreUsers.class)
.where(Map.of("request_id", id.toString()))
.build();
db.executeOne(query, ViewRequestsWithPreUsers.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<RequestEntity>> getByUserId(Integer userId) {
Promise<List<RequestEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(RequestEntity.class)
.where(Map.of("requested_by", userId.toString()))
.build();
db.execute(query, RequestEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
@Override
public Future<RequestEntity> insert(RequestEntity request) {
Promise<RequestEntity> promise = Promise.promise();
String query = QueryBuilder.insert(request).build();
db.execute(query, RequestEntity.class,
list -> promise.complete(list.isEmpty() ? null : list.getFirst()),
promise::fail
);
return promise.future();
}
@Override
public Future<RequestEntity> upsert(RequestEntity requestEntity, String... conflictKeys) {
Promise<RequestEntity> promise = Promise.promise();
String query = QueryBuilder.upsert(requestEntity, conflictKeys).build();
db.executeOne(query, RequestEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<RequestEntity> update(RequestEntity request) {
Promise<RequestEntity> promise = Promise.promise();
String query = QueryBuilder.update(request).build();
db.executeOne(query, RequestEntity.class,
_ -> promise.complete(request),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> delete(Integer id) {
Promise<Boolean> promise = Promise.promise();
RequestEntity request = new RequestEntity();
request.setRequest_id(id);
String query = QueryBuilder.delete(request).build();
db.executeOne(query, RequestEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(RequestEntity.class)
.where(Map.of("request_id", id.toString()))
.build();
db.executeOne(query, RequestEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
}

View File

@@ -0,0 +1,146 @@
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.UserMetadataEntity;
import java.util.List;
import java.util.Map;
public class UserMetadataDAO implements DataAccessObject<UserMetadataEntity, Integer> {
private final DatabaseManager db;
public UserMetadataDAO(Pool pool) {
this.db = DatabaseManager.getInstance(pool);
}
@Override
public Future<List<UserMetadataEntity>> getAll() {
return getAll(new QueryParams(Map.of(), new QueryFilters()));
}
@Override
public Future<UserMetadataEntity> getById(Integer id) {
Promise<UserMetadataEntity> promise = Promise.promise();
String query = QueryBuilder
.select(UserMetadataEntity.class)
.where(Map.of("user_id", id.toString()))
.build();
db.executeOne(query, UserMetadataEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
public Future<List<UserMetadataEntity>> getAll(QueryParams params) {
Promise<List<UserMetadataEntity>> promise = Promise.promise();
String query = QueryBuilder
.select(UserMetadataEntity.class)
.where(params.getFilters())
.orderBy(params.getQueryFilters().getSort(), params.getQueryFilters().getOrder())
.limit(params.getQueryFilters().getLimit())
.offset(params.getQueryFilters().getOffset())
.build();
db.execute(query, UserMetadataEntity.class,
list -> promise.complete(list.isEmpty() ? List.of() : list),
promise::fail
);
return promise.future();
}
@Override
public Future<UserMetadataEntity> insert(UserMetadataEntity user) {
Promise<UserMetadataEntity> promise = Promise.promise();
String query = QueryBuilder.insert(user).build();
db.executeOne(query, UserMetadataEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<UserMetadataEntity> upsert(UserMetadataEntity userMetadataEntity, String... conflictKeys) {
Promise<UserMetadataEntity> promise = Promise.promise();
String query = QueryBuilder.upsert(userMetadataEntity, conflictKeys).build();
db.executeOne(query, UserMetadataEntity.class,
promise::complete,
promise::fail
);
return promise.future();
}
@Override
public Future<UserMetadataEntity> update(UserMetadataEntity user) {
Promise<UserMetadataEntity> promise = Promise.promise();
String query = QueryBuilder.update(user).build();
db.executeOne(query, UserMetadataEntity.class,
_ -> promise.complete(user),
promise::fail
);
return promise.future();
}
public Future<UserMetadataEntity> updateWithNulls(UserMetadataEntity user) {
Promise<UserMetadataEntity> promise = Promise.promise();
String query = QueryBuilder.updateWithNulls(user).build();
db.executeOne(query, UserMetadataEntity.class,
_ -> promise.complete(user),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> delete(Integer id) {
Promise<Boolean> promise = Promise.promise();
UserMetadataEntity user = new UserMetadataEntity();
user.setUser_id(id);
String query = QueryBuilder.delete(user).build();
db.executeOne(query, UserMetadataEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
@Override
public Future<Boolean> exists(Integer id) {
Promise<Boolean> promise = Promise.promise();
String query = QueryBuilder
.select(UserMetadataEntity.class)
.where(Map.of("user_id", id.toString()))
.build();
db.executeOne(query, UserMetadataEntity.class,
result -> promise.complete(result != null),
promise::fail
);
return promise.future();
}
}

View File

@@ -0,0 +1,58 @@
package net.miarma.api.microservices.huertos.entities;
import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.Constants.HuertosAnnouncePriority;
import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.db.AbstractEntity;
import java.time.LocalDateTime;
@Table("huertos_announces")
public class AnnouncementEntity extends AbstractEntity {
private Integer announce_id;
private String body;
private HuertosAnnouncePriority priority;
private Integer published_by;
private LocalDateTime created_at;
public AnnouncementEntity() {
super();
}
public AnnouncementEntity(Row row) {
super(row);
}
public Integer getAnnounce_id() {
return announce_id;
}
public void setAnnounce_id(Integer announce_id) {
this.announce_id = announce_id;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
public HuertosAnnouncePriority getPriority() {
return priority;
}
public void setPriority(HuertosAnnouncePriority priority) {
this.priority = priority;
}
public Integer getPublished_by() {
return published_by;
}
public void setPublished_by(Integer published_by) {
this.published_by = published_by;
}
public LocalDateTime getCreated_at() {
return created_at;
}
public void setCreated_at(LocalDateTime created_at) {
this.created_at = created_at;
}
}

View File

@@ -0,0 +1,51 @@
package net.miarma.api.microservices.huertos.entities;
import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.db.AbstractEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Table("huertos_balance")
public class BalanceEntity extends AbstractEntity {
private Integer id;
private BigDecimal initial_bank;
private BigDecimal initial_cash;
private LocalDateTime created_at;
public BalanceEntity() {
super();
}
public BalanceEntity(Row row) {
super(row);
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public BigDecimal getInitial_bank() {
return initial_bank;
}
public void setInitial_bank(BigDecimal initial_bank) {
this.initial_bank = initial_bank;
}
public BigDecimal getInitial_cash() {
return initial_cash;
}
public void setInitial_cash(BigDecimal initial_cash) {
this.initial_cash = initial_cash;
}
public LocalDateTime getCreated_at() {
return created_at;
}
public void setCreated_at(LocalDateTime created_at) {
this.created_at = created_at;
}
}

View File

@@ -0,0 +1,73 @@
package net.miarma.api.microservices.huertos.entities;
import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.Constants.HuertosPaymentType;
import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.db.AbstractEntity;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Table("huertos_expenses")
public class ExpenseEntity extends AbstractEntity{
private Integer expense_id;
private String concept;
private BigDecimal amount;
private String supplier;
private String invoice;
private HuertosPaymentType type;
private LocalDateTime created_at;
public ExpenseEntity() {
super();
}
public ExpenseEntity(Row row) {
super(row);
}
public Integer getExpense_id() {
return expense_id;
}
public void setExpense_id(Integer expense_id) {
this.expense_id = expense_id;
}
public String getConcept() {
return concept;
}
public void setConcept(String concept) {
this.concept = concept;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public String getSupplier() {
return supplier;
}
public void setSupplier(String supplier) {
this.supplier = supplier;
}
public String getInvoice() {
return invoice;
}
public void setInvoice(String invoice) {
this.invoice = invoice;
}
public HuertosPaymentType getType() {
return type;
}
public void setType(HuertosPaymentType type) {
this.type = type;
}
public LocalDateTime getCreated_at() {
return created_at;
}
public void setCreated_at(LocalDateTime created_at) {
this.created_at = created_at;
}
}

Some files were not shown because too many files have changed in this diff Show More