add: login working

This commit is contained in:
Jose
2025-12-21 08:33:58 +01:00
parent 5136a67fba
commit af675a32b8
24 changed files with 628 additions and 929 deletions

View File

@@ -124,6 +124,11 @@ public class ConfigManager {
return Boolean.parseBoolean(System.getenv("RUNNING_IN_DOCKER")); return Boolean.parseBoolean(System.getenv("RUNNING_IN_DOCKER"));
} }
public String getApiPrefix(String domain) {
return getStringProperty("api." + domain + ".prefix").replace("${app.version}",
String.valueOf(getStringProperty("app.version")));
}
public String getStringProperty(String key) { public String getStringProperty(String key) {
return config.getProperty(key); return config.getProperty(key);
} }

View File

@@ -1,67 +1,45 @@
package net.miarma.api.backlib.db; package net.miarma.api.backlib.db;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.UUID;
import com.google.gson.annotations.SerializedName;
import org.slf4j.Logger; import org.slf4j.Logger;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.sqlclient.Row; import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.annotations.APIDontReturn; import net.miarma.api.backlib.annotations.APIDontReturn;
import net.miarma.api.backlib.interfaces.IValuableEnum; import net.miarma.api.backlib.interfaces.IValuableEnum;
import net.miarma.api.backlib.log.LoggerProvider; import net.miarma.api.backlib.log.LoggerProvider;
import net.miarma.api.backlib.util.BufferUtil;
/**
* 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 { public abstract class AbstractEntity {
private final Logger LOGGER = LoggerProvider.getLogger(); private final Logger LOGGER = LoggerProvider.getLogger();
/**
* Constructor por defecto. Requerido para instanciación sin datos.
*/
public AbstractEntity() {} 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) { public AbstractEntity(Row row) {
populateFromRow(row); populateFromRow(row);
} }
/** private String getColumnName(Field field) {
* Rellena los campos del objeto usando reflexión a partir de una {@link Row} de Vert.x. if (field.isAnnotationPresent(SerializedName.class)) {
* Se soportan tipos básicos (String, int, boolean, etc.), enums con método estático {@code fromInt(int)}, return field.getAnnotation(SerializedName.class).value();
* y {@link java.math.BigDecimal} (a través del tipo {@code Numeric} de Vert.x). }
* <p> return field.getName();
* 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) { private void populateFromRow(Row row) {
Field[] fields = this.getClass().getDeclaredFields(); Field[] fields = this.getClass().getDeclaredFields();
for (Field field : fields) { for (Field field : fields) {
try { try {
field.setAccessible(true); field.setAccessible(true);
Class<?> type = field.getType(); Class<?> type = field.getType();
String name = field.getName(); String columnName = getColumnName(field);
Object value; Object value;
if (type.isEnum()) { if (type.isEnum()) {
Integer intValue = row.getInteger(name); Integer intValue = row.getInteger(columnName);
if (intValue != null) { if (intValue != null) {
try { try {
var method = type.getMethod("fromInt", int.class); var method = type.getMethod("fromInt", int.class);
@@ -74,42 +52,43 @@ public abstract class AbstractEntity {
} }
} else { } else {
value = switch (type.getSimpleName()) { value = switch (type.getSimpleName()) {
case "Integer", "int" -> row.getInteger(name); case "Integer", "int" -> row.getInteger(columnName);
case "String" -> row.getString(name); case "String" -> row.getString(columnName);
case "double", "Double" -> row.getDouble(name); case "double", "Double" -> row.getDouble(columnName);
case "long", "Long" -> row.getLong(name); case "long", "Long" -> row.getLong(columnName);
case "boolean", "Boolean" -> row.getBoolean(name); case "boolean", "Boolean" -> row.getBoolean(columnName);
case "LocalDateTime" -> row.getLocalDateTime(name); case "LocalDateTime" -> row.getLocalDateTime(columnName);
case "BigDecimal" -> { case "UUID" -> {
try { Object raw = row.getValue(columnName);
var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(name)); if (raw instanceof UUID) yield raw;
yield numeric != null ? numeric.bigDecimalValue() : null; if (raw instanceof String s) yield UUID.fromString(s);
} catch (Exception e) { if (raw instanceof Buffer b) {
yield BufferUtil.uuidFromBuffer(b);
}
yield null; yield null;
} }
case "BigDecimal" -> {
try {
var numeric = row.get(io.vertx.sqlclient.data.Numeric.class, row.getColumnIndex(columnName));
yield numeric != null ? numeric.bigDecimalValue() : null;
} catch (Exception e) { yield null; }
} }
default -> { default -> {
LOGGER.error("Type not supported yet: {} for field {}", type.getName(), name); LOGGER.error("Type not supported yet: {} for field {}", type.getName(), field.getName());
yield null; yield null;
} }
}; };
} }
if (value != null) {
field.set(this, value); field.set(this, value);
}
} catch (Exception e) { } catch (Exception e) {
LOGGER.error("Error populating field {}: {}", field.getName(), e.getMessage()); 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 IValuableEnum}, se usará su valor en lugar del nombre del enum.</p>
*
* @return Representación JSON de esta entidad.
*/
public String encode() { public String encode() {
JsonObject json = new JsonObject(); JsonObject json = new JsonObject();
Class<?> clazz = this.getClass(); Class<?> clazz = this.getClass();
@@ -117,13 +96,13 @@ public abstract class AbstractEntity {
while (clazz != null) { while (clazz != null) {
for (Field field : clazz.getDeclaredFields()) { for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(APIDontReturn.class)) continue; if (field.isAnnotationPresent(APIDontReturn.class)) continue;
field.setAccessible(true); field.setAccessible(true);
try { try {
Object value = field.get(this); Object value = field.get(this);
if (value instanceof IValuableEnum ve) { if (value instanceof IValuableEnum ve) {
json.put(field.getName(), ve.getValue()); json.put(field.getName(), ve.getValue());
} else if (value instanceof UUID u) {
json.put(field.getName(), u.toString());
} else { } else {
json.put(field.getName(), value); json.put(field.getName(), value);
} }
@@ -133,17 +112,9 @@ public abstract class AbstractEntity {
} }
clazz = clazz.getSuperclass(); clazz = clazz.getSuperclass();
} }
return json.encode(); 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() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(this.getClass().getSimpleName()).append(" [ "); sb.append(this.getClass().getSimpleName()).append(" [ ");
@@ -159,5 +130,4 @@ public abstract class AbstractEntity {
sb.append("]"); sb.append("]");
return sb.toString(); return sb.toString();
} }
} }

View File

@@ -3,73 +3,40 @@ package net.miarma.api.backlib.db;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.*;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.StringJoiner;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.slf4j.Logger; import org.slf4j.Logger;
import com.google.gson.annotations.SerializedName;
import net.miarma.api.backlib.annotations.Table; import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.log.LoggerProvider; import net.miarma.api.backlib.log.LoggerProvider;
/**
* 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 { public class QueryBuilder {
private final static Logger LOGGER = LoggerProvider.getLogger();
private static final Logger LOGGER = LoggerProvider.getLogger();
private final StringBuilder query; private final StringBuilder query;
private String sort; private String orderByClause;
private String order; private String limitClause;
private String limit; private String offsetClause;
private Class<?> entityClass; private Class<?> entityClass;
public QueryBuilder() { public QueryBuilder() {
this.query = new StringBuilder(); 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) { private static <T> String getTableName(Class<T> clazz) {
if (clazz == null) { if (clazz == null) throw new IllegalArgumentException("Class cannot be null");
throw new IllegalArgumentException("Class cannot be null"); if (!clazz.isAnnotationPresent(Table.class)) throw new IllegalArgumentException("Class does not have @Table annotation");
return clazz.getAnnotation(Table.class).value();
} }
if (clazz.isAnnotationPresent(Table.class)) { private static String getColumnName(Field field) {
Table annotation = clazz.getAnnotation(Table.class); SerializedName annotation = field.getAnnotation(SerializedName.class);
return annotation.value(); return annotation != null ? annotation.value() : field.getName();
}
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) { private static Object extractValue(Object fieldValue) {
if (fieldValue == null) return null;
if (fieldValue instanceof Enum<?>) { if (fieldValue instanceof Enum<?>) {
try { try {
var method = fieldValue.getClass().getMethod("getValue"); var method = fieldValue.getClass().getMethod("getValue");
@@ -78,199 +45,137 @@ public class QueryBuilder {
return ((Enum<?>) fieldValue).name(); return ((Enum<?>) fieldValue).name();
} }
} }
if (fieldValue instanceof LocalDateTime ldt) { if (fieldValue instanceof LocalDateTime ldt) {
return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); return ldt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
} }
return fieldValue; return fieldValue;
} }
/** private String formatValue(Object value) {
* Escapa los caracteres especiales en una cadena para evitar inyecciones SQL. if (value == null) return "NULL";
* @param value the string value to escape if (value instanceof UUID uuid) {
* @return the escaped string return "UNHEX(REPLACE('" + uuid.toString() + "','-',''))";
*/ }
if (value instanceof String || value instanceof LocalDateTime) {
return "'" + escapeSql(value.toString()) + "'";
}
return value.toString();
}
private static String escapeSql(String value) { private static String escapeSql(String value) {
return value.replace("'", "''"); return value.replace("'", "''");
} }
/** /**
* Construye una consulta SELECT para la clase dada, con columnas opcionales. * @param clazz La clase de la entidad a seleccionar
* @param clazz the entity class to query * @param columns Columnas a seleccionar, si no se pasan selecciona "*"
* @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) { public static <T> QueryBuilder select(Class<T> clazz, String... columns) {
if (clazz == null) {
throw new IllegalArgumentException("Class cannot be null");
}
QueryBuilder qb = new QueryBuilder(); QueryBuilder qb = new QueryBuilder();
qb.entityClass = clazz; qb.entityClass = clazz;
String tableName = getTableName(clazz);
qb.query.append("SELECT "); qb.query.append("SELECT ");
if (columns.length == 0) qb.query.append("* ");
if (columns.length == 0) { else qb.query.append(String.join(", ", columns)).append(" ");
qb.query.append("* "); qb.query.append("FROM ").append(getTableName(clazz)).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; return qb;
} }
/** /**
* Añade una cláusula WHERE a la consulta actual, filtrando por los campos del mapa. * @param filters Mapa clave = valor para el WHERE. Detecta UUID y Strings tipo IN
* 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) { public QueryBuilder where(Map<String, Object> filters) {
if (filters == null || filters.isEmpty()) { if (filters == null || filters.isEmpty() || entityClass == null) return this;
return this;
Map<String, String> javaToSql = new HashMap<>();
Map<String, String> sqlToSql = new HashMap<>();
for (Field f : entityClass.getDeclaredFields()) {
String sqlCol = getColumnName(f);
javaToSql.put(f.getName(), sqlCol);
sqlToSql.put(sqlCol, sqlCol);
} }
Set<String> validFields = entityClass != null
? Arrays.stream(entityClass.getDeclaredFields()).map(Field::getName).collect(Collectors.toSet())
: Collections.emptySet();
List<String> conditions = new ArrayList<>(); List<String> conditions = new ArrayList<>();
for (Map.Entry<String, Object> entry : filters.entrySet()) {
for (Map.Entry<String, String> entry : filters.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
String value = entry.getValue(); String sqlCol = javaToSql.getOrDefault(key, sqlToSql.get(key));
if (sqlCol == null) {
if (!validFields.contains(key)) {
LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key); LOGGER.warn("[QueryBuilder] Ignorando campo invalido en WHERE: {}", key);
continue; continue;
} }
Object val = entry.getValue();
if (val == null) continue;
if (val instanceof String s && s.startsWith("(") && s.endsWith(")")) conditions.add(sqlCol + " IN " + s);
else conditions.add(sqlCol + " = " + formatValue(val));
}
if (value.startsWith("(") && value.endsWith(")")) { if (!conditions.isEmpty()) {
conditions.add(key + " IN " + value); String prefix = query.toString().contains("WHERE") ? "AND " : "WHERE ";
} else if (value.matches("-?\\d+(\\.\\d+)?")) { query.append(prefix).append(String.join(" AND ", conditions)).append(" ");
conditions.add(key + " = " + value); }
} else { return this;
conditions.add(key + " = '" + value + "'"); }
/**
* @param object Objeto con campos para WHERE. Soporta UUID y LocalDateTime
*/
public <T> QueryBuilder where(T object) {
if (object == null) throw new IllegalArgumentException("Object cannot be null");
if (entityClass == null) return this;
Set<String> validColumns = Arrays.stream(entityClass.getDeclaredFields())
.map(QueryBuilder::getColumnName)
.collect(Collectors.toSet());
List<String> conditions = new ArrayList<>();
for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true);
try {
Object value = field.get(object);
if (value != null) {
String col = getColumnName(field);
if (!validColumns.contains(col)) continue;
Object extracted = extractValue(value);
conditions.add(col + " = " + formatValue(extracted));
}
} catch (IllegalAccessException e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
} }
} }
if (!conditions.isEmpty()) { if (!conditions.isEmpty()) {
query.append("WHERE ").append(String.join(" AND ", conditions)).append(" "); String prefix = query.toString().contains("WHERE") ? "AND " : "WHERE ";
query.append(prefix).append(String.join(" AND ", conditions)).append(" ");
} }
return this; 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)) {
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) {
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) { public static <T> QueryBuilder insert(T object) {
if (object == null) { if (object == null) throw new IllegalArgumentException("Object cannot be null");
throw new IllegalArgumentException("Object cannot be null");
}
QueryBuilder qb = new QueryBuilder(); QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass()); String table = getTableName(object.getClass());
qb.query.append("INSERT INTO ").append(table).append(" "); qb.query.append("INSERT INTO ").append(table).append(" ");
qb.query.append("(");
StringJoiner columns = new StringJoiner(", "); StringJoiner cols = new StringJoiner(", ");
StringJoiner values = new StringJoiner(", "); StringJoiner vals = new StringJoiner(", ");
for (Field field : object.getClass().getDeclaredFields()) { for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true); field.setAccessible(true);
try { try {
columns.add(field.getName()); cols.add(getColumnName(field));
Object fieldValue = field.get(object); Object value = extractValue(field.get(object));
if (fieldValue != null) { vals.add(qb.formatValue(value));
Object value = extractValue(fieldValue); } catch (IllegalAccessException e) {
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) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage()); LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
} }
} }
qb.query.append(columns).append(") ");
qb.query.append("VALUES (").append(values).append(") RETURNING * "); qb.query.append("(").append(cols).append(") VALUES (").append(vals).append(") RETURNING * ");
return qb; return qb;
} }
/** private static <T> QueryBuilder buildUpdate(T object, boolean includeNulls) {
* Construye una consulta UPDATE para el objeto dado, actualizando todos sus campos. if (object == null) throw new IllegalArgumentException("Object cannot be null");
* 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(); QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass()); String table = getTableName(object.getClass());
@@ -278,103 +183,36 @@ public class QueryBuilder {
StringJoiner setJoiner = new StringJoiner(", "); StringJoiner setJoiner = new StringJoiner(", ");
StringJoiner whereJoiner = new StringJoiner(" AND "); StringJoiner whereJoiner = new StringJoiner(" AND ");
Field idField = null; Field idField = null;
for (Field field : object.getClass().getDeclaredFields()) { for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true); field.setAccessible(true);
try { try {
Object fieldValue = field.get(object); String col = getColumnName(field);
if (fieldValue == null) continue; Object value = extractValue(field.get(object));
if (col.endsWith("_id")) {
String fieldName = field.getName();
Object value = extractValue(fieldValue);
if (fieldName.endsWith("_id")) {
idField = field; idField = field;
whereJoiner.add(fieldName + " = " + (value instanceof String if (value != null) whereJoiner.add(col + " = " + qb.formatValue(value));
|| value instanceof LocalDateTime ? "'" + value + "'" : value)); else throw new IllegalArgumentException("ID field cannot be null");
continue; continue;
} }
if (value != null) setJoiner.add(col + " = " + qb.formatValue(value));
setJoiner.add(fieldName + " = " + (value instanceof String else if (includeNulls) setJoiner.add(col + " = NULL");
|| value instanceof LocalDateTime ? "'" + value + "'" : value)); } catch (IllegalAccessException e) {
} catch (Exception e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage()); LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
} }
} }
if (idField == null) { if (idField == null) throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
throw new IllegalArgumentException("No ID field (ending with _id) found for WHERE clause");
}
qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner); qb.query.append(setJoiner).append(" WHERE ").append(whereJoiner).append(" ");
return qb; return qb;
} }
/** public static <T> QueryBuilder update(T object) { return buildUpdate(object, false); }
* 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(); public static <T> QueryBuilder updateWithNulls(T object) { return buildUpdate(object, true); }
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) {
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) { public static <T> QueryBuilder upsert(T object, String... conflictKeys) {
if (object == null) throw new IllegalArgumentException("Object cannot be null"); if (object == null) throw new IllegalArgumentException("Object cannot be null");
@@ -382,146 +220,75 @@ public class QueryBuilder {
String table = getTableName(object.getClass()); String table = getTableName(object.getClass());
qb.query.append("INSERT INTO ").append(table).append(" "); qb.query.append("INSERT INTO ").append(table).append(" ");
StringJoiner columns = new StringJoiner(", "); StringJoiner cols = new StringJoiner(", ");
StringJoiner values = new StringJoiner(", "); StringJoiner vals = new StringJoiner(", ");
Map<String, String> updates = new HashMap<>(); Map<String, String> updates = new HashMap<>();
for (Field field : object.getClass().getDeclaredFields()) { for (Field field : object.getClass().getDeclaredFields()) {
field.setAccessible(true); field.setAccessible(true);
try { try {
Object fieldValue = field.get(object); String col = getColumnName(field);
String columnName = field.getName(); Object value = extractValue(field.get(object));
columns.add(columnName); String formatted = qb.formatValue(value);
cols.add(col);
Object value = extractValue(fieldValue); vals.add(formatted);
String valueStr = value == null ? "NULL" if (!Arrays.asList(conflictKeys).contains(col)) updates.put(col, formatted);
: (value instanceof String || value instanceof LocalDateTime ? "'" + value + "'" : value.toString()); } catch (IllegalAccessException e) {
values.add(valueStr);
// no actualizamos la clave duplicada
boolean isConflictKey = Arrays.asList(conflictKeys).contains(columnName);
if (!isConflictKey) {
updates.put(columnName, valueStr);
}
} catch (Exception e) {
LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage()); LOGGER.error("(REFLECTION) Error reading field: {}", e.getMessage());
} }
} }
qb.query.append("(").append(columns).append(") VALUES (").append(values).append(")"); qb.query.append("(").append(cols).append(") VALUES (").append(vals).append(")");
if (!updates.isEmpty() && conflictKeys.length > 0) {
if (conflictKeys.length > 0 && !updates.isEmpty()) {
qb.query.append(" ON DUPLICATE KEY UPDATE "); qb.query.append(" ON DUPLICATE KEY UPDATE ");
StringJoiner updateSet = new StringJoiner(", "); qb.query.append(updates.entrySet().stream()
updates.forEach((k, v) -> updateSet.add(k + " = " + v)); .map(e -> e.getKey() + " = " + e.getValue())
qb.query.append(updateSet); .collect(Collectors.joining(", ")));
} }
return qb; 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) { public static <T> QueryBuilder delete(T object) {
if (object == null) throw new IllegalArgumentException("Object cannot be null"); if (object == null) throw new IllegalArgumentException("Object cannot be null");
QueryBuilder qb = new QueryBuilder(); QueryBuilder qb = new QueryBuilder();
String table = getTableName(object.getClass()); qb.entityClass = object.getClass();
qb.query.append("DELETE FROM ").append(table).append(" WHERE "); String table = getTableName(qb.entityClass);
qb.query.append("DELETE FROM ").append(table).append(" ");
StringJoiner joiner = new StringJoiner(" AND "); return qb.where(object);
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) {
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) { public QueryBuilder orderBy(Optional<String> column, Optional<String> order) {
column.ifPresent(c -> { column.ifPresent(c -> {
if (entityClass != null) { if (entityClass != null) {
boolean isValid = Arrays.stream(entityClass.getDeclaredFields()) boolean valid = Arrays.stream(entityClass.getDeclaredFields())
.map(Field::getName) .map(QueryBuilder::getColumnName)
.anyMatch(f -> f.equals(c)); .anyMatch(f -> f.equals(c));
if (!valid) {
if (!isValid) {
LOGGER.warn("[QueryBuilder] Ignorando campo invalido en ORDER BY: {}", c); LOGGER.warn("[QueryBuilder] Ignorando campo invalido en ORDER BY: {}", c);
return; return;
} }
} }
orderByClause = "ORDER BY " + c + " " + order.orElse("ASC") + " ";
sort = "ORDER BY " + c + " ";
order.ifPresent(o -> sort += o.equalsIgnoreCase("asc") ? "ASC" : "DESC" + " ");
}); });
return this; 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) { public QueryBuilder limit(Optional<Integer> limitParam) {
limitParam.ifPresent(param -> limit = "LIMIT " + param + " "); limitParam.ifPresent(l -> limitClause = "LIMIT " + l + " ");
return this; 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) { public QueryBuilder offset(Optional<Integer> offsetParam) {
offsetParam.ifPresent(param -> limit += "OFFSET " + param + " "); offsetParam.ifPresent(o -> offsetClause = "OFFSET " + o + " ");
return this; 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() { public String build() {
if (order != null && !order.isEmpty()) { if (orderByClause != null) query.append(orderByClause);
query.append(order); if (limitClause != null) query.append(limitClause);
} if (offsetClause != null) query.append(offsetClause);
if (sort != null && !sort.isEmpty()) {
query.append(sort);
}
if (limit != null && !limit.isEmpty()) {
query.append(limit);
}
return query.toString().trim() + ";"; return query.toString().trim() + ";";
} }
} }

View File

@@ -32,7 +32,7 @@ public class JWTManager {
* Genera un token JWT usando UUID para el usuario. * Genera un token JWT usando UUID para el usuario.
*/ */
public String generateToken( public String generateToken(
String userName, String displayName,
UUID userId, UUID userId,
IUserRole role, IUserRole role,
Integer serviceId, Integer serviceId,
@@ -45,7 +45,7 @@ public class JWTManager {
); );
return JWT.create() return JWT.create()
.withSubject(userName) .withSubject(displayName)
.withClaim("userId", userId.toString()) .withClaim("userId", userId.toString())
.withClaim("serviceId", serviceId) .withClaim("serviceId", serviceId)
.withClaim("role", role.name()) .withClaim("role", role.name())
@@ -54,6 +54,10 @@ public class JWTManager {
.sign(algorithm); .sign(algorithm);
} }
public DecodedJWT decode(String token) {
return JWT.decode(token);
}
public UUID extractUserId(String token) { public UUID extractUserId(String token) {
try { try {
DecodedJWT jwt = verifier.verify(token); DecodedJWT jwt = verifier.verify(token);

View File

@@ -0,0 +1,16 @@
package net.miarma.api.backlib.util;
import java.util.UUID;
import io.vertx.core.buffer.Buffer;
public class BufferUtil {
public static UUID uuidFromBuffer(Buffer b) {
if (b == null || b.length() != 16) {
return null;
}
long mostSigBits = b.getLong(0);
long leastSigBits = b.getLong(8);
return new UUID(mostSigBits, leastSigBits);
}
}

View File

@@ -39,6 +39,7 @@
<artifactId>core</artifactId> <artifactId>core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>net.miarma.api</groupId> <groupId>net.miarma.api</groupId>
<artifactId>huertos</artifactId> <artifactId>huertos</artifactId>
@@ -59,6 +60,7 @@
<artifactId>mpaste</artifactId> <artifactId>mpaste</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
-->
</dependencies> </dependencies>
<build> <build>

View File

@@ -1,5 +1,7 @@
package net.miarma.api; package net.miarma.api;
import java.util.List;
import org.slf4j.Logger; import org.slf4j.Logger;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
@@ -9,10 +11,10 @@ import net.miarma.api.backlib.log.LogAccumulator;
import net.miarma.api.backlib.log.LoggerProvider; import net.miarma.api.backlib.log.LoggerProvider;
import net.miarma.api.backlib.util.DeploymentUtil; import net.miarma.api.backlib.util.DeploymentUtil;
import net.miarma.api.microservices.core.verticles.CoreMainVerticle; import net.miarma.api.microservices.core.verticles.CoreMainVerticle;
import net.miarma.api.microservices.huertos.verticles.HuertosMainVerticle; //import net.miarma.api.microservices.huertos.verticles.HuertosMainVerticle;
import net.miarma.api.microservices.huertosdecine.verticles.CineMainVerticle; //import net.miarma.api.microservices.huertosdecine.verticles.CineMainVerticle;
import net.miarma.api.microservices.minecraft.verticles.MMCMainVerticle; //import net.miarma.api.microservices.minecraft.verticles.MMCMainVerticle;
import net.miarma.api.microservices.mpaste.verticles.MPasteMainVerticle; //import net.miarma.api.microservices.mpaste.verticles.MPasteMainVerticle;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class MasterVerticle extends AbstractVerticle { public class MasterVerticle extends AbstractVerticle {
@@ -38,7 +40,7 @@ public class MasterVerticle extends AbstractVerticle {
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CoreMainVerticle.class))) .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(CoreMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CoreMainVerticle.class, err))); .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(CoreMainVerticle.class, err)));
Future<String> huertos = vertx.deployVerticle(new HuertosMainVerticle()) /*Future<String> huertos = vertx.deployVerticle(new HuertosMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(HuertosMainVerticle.class))) .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(HuertosMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(HuertosMainVerticle.class, err))); .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(HuertosMainVerticle.class, err)));
@@ -52,9 +54,10 @@ public class MasterVerticle extends AbstractVerticle {
Future<String> mpaste = vertx.deployVerticle(new MPasteMainVerticle()) Future<String> mpaste = vertx.deployVerticle(new MPasteMainVerticle())
.onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(MPasteMainVerticle.class))) .onSuccess(id -> LogAccumulator.add(DeploymentUtil.successMessage(MPasteMainVerticle.class)))
.onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(MPasteMainVerticle.class, err))); .onFailure(err -> LogAccumulator.add(DeploymentUtil.failMessage(MPasteMainVerticle.class, err)));*/
Future.all(core, huertos, mmc, cine, mpaste) //Future.all(core, huertos, mmc, cine, mpaste)
Future.all(List.of(core))
.onSuccess(_ -> promise.complete()) .onSuccess(_ -> promise.complete())
.onFailure(promise::fail); .onFailure(promise::fail);

View File

@@ -31,8 +31,8 @@ dp.poolSize=5
# HTTP Server Configuration # HTTP Server Configuration
inet.host=localhost inet.host=localhost
sso.logic.port=8080 core.logic.port=8080
sso.data.port=8081 core.data.port=8081
minecraft.logic.port=8100 minecraft.logic.port=8100
minecraft.data.port=8101 minecraft.data.port=8101
huertos.logic.port=8120 huertos.logic.port=8120

View File

@@ -32,9 +32,11 @@ public class UserDAO implements DataAccessObject<UserEntity, UUID> {
Promise<UserEntity> promise = Promise.promise(); Promise<UserEntity> promise = Promise.promise();
String query = QueryBuilder String query = QueryBuilder
.select(UserEntity.class) .select(UserEntity.class)
.where(Map.of("user_id", id.toString())) .where(Map.of("user_id", id))
.build(); .build();
System.out.println(query);
db.executeOne(query, UserEntity.class, db.executeOne(query, UserEntity.class,
promise::complete, promise::complete,
promise::fail promise::fail

View File

@@ -1,14 +1,17 @@
package net.miarma.api.microservices.core.entities; package net.miarma.api.microservices.core.entities;
import java.time.LocalDateTime;
import java.util.UUID;
import com.google.gson.annotations.SerializedName; import com.google.gson.annotations.SerializedName;
import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.annotations.APIDontReturn; import net.miarma.api.backlib.annotations.APIDontReturn;
import net.miarma.api.backlib.annotations.Table; import net.miarma.api.backlib.annotations.Table;
import java.util.UUID; import net.miarma.api.backlib.db.AbstractEntity;
import java.time.LocalDateTime;
@Table("credentials") @Table("credentials")
public class CredentialEntity { public class CredentialEntity extends AbstractEntity {
@SerializedName("credential_id") @SerializedName("credential_id")
private UUID credentialId; private UUID credentialId;
@@ -35,6 +38,7 @@ public class CredentialEntity {
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
public CredentialEntity() { } public CredentialEntity() { }
public CredentialEntity(Row row) { super(row); }
public UUID getCredentialId() { public UUID getCredentialId() {
return credentialId; return credentialId;

View File

@@ -3,6 +3,8 @@ package net.miarma.api.microservices.core.entities;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.UUID; import java.util.UUID;
import com.google.gson.annotations.SerializedName;
import io.vertx.sqlclient.Row; import io.vertx.sqlclient.Row;
import net.miarma.api.backlib.annotations.Table; import net.miarma.api.backlib.annotations.Table;
import net.miarma.api.backlib.db.AbstractEntity; import net.miarma.api.backlib.db.AbstractEntity;
@@ -11,14 +13,24 @@ import net.miarma.api.microservices.core.enums.CoreUserGlobalStatus;
@Table("users") @Table("users")
public class UserEntity extends AbstractEntity { public class UserEntity extends AbstractEntity {
@SerializedName("user_id")
private UUID userId; private UUID userId;
private String userName;
private String email; @SerializedName("display_name")
private String displayName; private String displayName;
private String avatar; private String avatar;
@SerializedName("global_status")
private CoreUserGlobalStatus globalStatus; private CoreUserGlobalStatus globalStatus;
@SerializedName("global_role")
private CoreUserGlobalRole globalRole; private CoreUserGlobalRole globalRole;
@SerializedName("created_at")
private LocalDateTime createdAt; private LocalDateTime createdAt;
@SerializedName("updated_at")
private LocalDateTime updatedAt; private LocalDateTime updatedAt;
public UserEntity() { } public UserEntity() { }
@@ -27,12 +39,6 @@ public class UserEntity extends AbstractEntity {
public UUID getUserId() { return userId; } public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; } public void setUserId(UUID userId) { this.userId = userId; }
public String getUserName() { return userName; }
public void setUserName(String userName) { this.userName = userName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getDisplayName() { return displayName; } public String getDisplayName() { return displayName; }
public void setDisplayName(String displayName) { this.displayName = displayName; } public void setDisplayName(String displayName) { this.displayName = displayName; }

View File

@@ -0,0 +1,146 @@
package net.miarma.api.microservices.core.handlers;
import java.util.UUID;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.gson.Gson;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext;
import net.miarma.api.backlib.config.ConfigManager;
import net.miarma.api.backlib.gson.GsonProvider;
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.microservices.core.entities.UserEntity;
@SuppressWarnings("unused")
public class AuthHandler {
private final Gson GSON = GsonProvider.get();
private final Vertx vertx;
private final String AUTH_EVENT_BUS = ConfigManager.getInstance()
.getStringProperty("eventbus.auth.address");
public AuthHandler(Vertx vertx) {
this.vertx = vertx;
}
public void login(RoutingContext ctx) {
vertx.eventBus().request(AUTH_EVENT_BUS, ctx.body().asJsonObject().put("action", "login"))
.onSuccess(reply -> JsonUtil.sendJson(ctx, ApiStatus.OK, reply.body()))
.onFailure(err -> EventBusUtil.handleReplyError(ctx, err));
}
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(AUTH_EVENT_BUS, request, ar -> {
if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} 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(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.decode(token);
String userIdStr = decoded.getClaim("userId").asString();
if (userIdStr == null) {
JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token claims");
return;
}
UUID userId = UUID.fromString(userIdStr);
vertx.eventBus().request(AUTH_EVENT_BUS, new JsonObject()
.put("action", "getUserById")
.put("userId", userId.toString()), ar -> {
if (ar.succeeded()) {
JsonObject userJson = (JsonObject) ar.result().body();
UserEntity user = GSON.fromJson(userJson.encode(), UserEntity.class);
String newToken = jwt.generateToken(
user.getDisplayName(),
user.getUserId(),
user.getGlobalRole(),
0,
false
);
JsonUtil.sendJson(ctx, ApiStatus.OK, new JsonObject().put("token", newToken));
} else {
JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "User not found or service unavailable");
}
});
} catch (Exception e) {
JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token format");
}
}
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(AUTH_EVENT_BUS, request, ar -> {
if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, true, "Updated");
} else {
EventBusUtil.handleReplyError(ctx, ar.cause());
}
});
}
public void register(RoutingContext ctx) {
vertx.eventBus().request(AUTH_EVENT_BUS, ctx.body().asJsonObject().put("action", "register"))
.onSuccess(reply -> JsonUtil.sendJson(ctx, ApiStatus.CREATED, null))
.onFailure(err -> EventBusUtil.handleReplyError(ctx, err));
}
}

View File

@@ -1,10 +1,10 @@
package net.miarma.api.microservices.core.handlers; package net.miarma.api.microservices.core.handlers;
import java.util.UUID;
import com.google.gson.Gson; import com.google.gson.Gson;
import io.vertx.core.Future;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.Pool;
import net.miarma.api.backlib.Constants;
import net.miarma.api.backlib.gson.GsonProvider; import net.miarma.api.backlib.gson.GsonProvider;
import net.miarma.api.backlib.http.ApiStatus; import net.miarma.api.backlib.http.ApiStatus;
import net.miarma.api.backlib.http.QueryParams; import net.miarma.api.backlib.http.QueryParams;
@@ -23,52 +23,32 @@ public class UserDataHandler {
} }
public void getAll(RoutingContext ctx) { public void getAll(RoutingContext ctx) {
QueryParams params = QueryParams.from(ctx); userService.getAll(QueryParams.from(ctx))
.onSuccess(users -> JsonUtil.sendJson(ctx, ApiStatus.OK, users))
userService.getAll(params) .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.fromException(err), null, err.getMessage()));
.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) { public void getById(RoutingContext ctx) {
Integer userId = Integer.parseInt(ctx.pathParam("user_id")); Future.succeededFuture(ctx.pathParam("user_id"))
.map(UUID::fromString)
userService.getById(userId) .compose(userService::getById)
.onSuccess(user -> JsonUtil.sendJson(ctx, ApiStatus.OK, user)).onFailure(err -> { .onSuccess(user -> JsonUtil.sendJson(ctx, ApiStatus.OK, user))
ApiStatus status = ApiStatus.fromException(err); .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.BAD_REQUEST, null, "Invalid UUID format"));
JsonUtil.sendJson(ctx, status, null, err.getMessage());
});
}
public void create(RoutingContext ctx) {
UserEntity user = 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) { public void update(RoutingContext ctx) {
UserEntity user = GSON.fromJson(ctx.body().asString(), UserEntity.class); Future.succeededFuture(ctx.body().asString())
.map(body -> GSON.fromJson(body, UserEntity.class))
userService.update(user) .compose(userService::update)
.onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, result)).onFailure(err -> { .onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.OK, result))
ApiStatus status = ApiStatus.fromException(err); .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.BAD_REQUEST, null, "Invalid user data"));
JsonUtil.sendJson(ctx, status, null, err.getMessage());
});
} }
public void delete(RoutingContext ctx) { public void delete(RoutingContext ctx) {
Integer userId = Integer.parseInt(ctx.pathParam("user_id")); Future.succeededFuture(ctx.pathParam("user_id"))
.map(UUID::fromString)
userService.delete(userId) .compose(userService::delete)
.onSuccess(result -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, result)).onFailure(err -> { .onSuccess(v -> JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null))
ApiStatus status = ApiStatus.fromException(err); .onFailure(err -> JsonUtil.sendJson(ctx, ApiStatus.BAD_REQUEST, null, "Invalid UUID format"));
JsonUtil.sendJson(ctx, status, null, err.getMessage());
});
} }
} }

View File

@@ -1,162 +1,26 @@
package net.miarma.api.microservices.core.handlers; package net.miarma.api.microservices.core.handlers;
import com.auth0.jwt.interfaces.DecodedJWT; import java.util.UUID;
import io.vertx.core.Vertx; import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.RoutingContext; import io.vertx.ext.web.RoutingContext;
import net.miarma.api.backlib.Constants; import net.miarma.api.backlib.config.ConfigManager;
import net.miarma.api.backlib.http.ApiStatus; import net.miarma.api.backlib.http.ApiStatus;
import net.miarma.api.backlib.security.JWTManager; import net.miarma.api.backlib.security.JWTManager;
import net.miarma.api.backlib.util.EventBusUtil; import net.miarma.api.backlib.util.EventBusUtil;
import net.miarma.api.backlib.util.JsonUtil; import net.miarma.api.backlib.util.JsonUtil;
import net.miarma.api.microservices.core.entities.UserEntity;
public class UserLogicHandler { public class UserLogicHandler {
private final String CORE_EVENT_BUS = ConfigManager.getInstance()
.getStringProperty("eventbus.core.address");
private final Vertx vertx; private final Vertx vertx;
public UserLogicHandler(Vertx vertx) { public UserLogicHandler(Vertx vertx) {
this.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) { public void getInfo(RoutingContext ctx) {
String authHeader = ctx.request().getHeader("Authorization"); String authHeader = ctx.request().getHeader("Authorization");
@@ -166,9 +30,9 @@ public class UserLogicHandler {
} }
String token = authHeader.substring(7); String token = authHeader.substring(7);
int userId = net.miarma.api.backlib.security.JWTManager.getInstance().getUserId(token); UUID userId = JWTManager.getInstance().extractUserId(token);
if (userId <= 0) { if (userId == null) {
JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token"); JsonUtil.sendJson(ctx, ApiStatus.UNAUTHORIZED, null, "Invalid token");
return; return;
} }
@@ -177,7 +41,7 @@ public class UserLogicHandler {
.put("action", "getInfo") .put("action", "getInfo")
.put("userId", userId); .put("userId", userId);
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body()); JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} else { } else {
@@ -193,7 +57,7 @@ public class UserLogicHandler {
.put("action", "userExists") .put("action", "userExists")
.put("userId", userId); .put("userId", userId);
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body()); JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} else { } else {
@@ -209,7 +73,7 @@ public class UserLogicHandler {
.put("action", "getStatus") .put("action", "getStatus")
.put("userId", userId); .put("userId", userId);
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body()); JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} else { } else {
@@ -225,7 +89,7 @@ public class UserLogicHandler {
.put("action", "getRole") .put("action", "getRole")
.put("userId", userId); .put("userId", userId);
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body()); JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} else { } else {
@@ -241,7 +105,7 @@ public class UserLogicHandler {
.put("action", "getAvatar") .put("action", "getAvatar")
.put("userId", userId); .put("userId", userId);
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body()); JsonUtil.sendJson(ctx, ApiStatus.OK, ar.result().body());
} else { } else {
@@ -258,7 +122,7 @@ public class UserLogicHandler {
.put("userId", body.getInteger("userId")) .put("userId", body.getInteger("userId"))
.put("status", body.getInteger("status")); .put("status", body.getInteger("status"));
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null); JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null);
} else { } else {
@@ -275,7 +139,7 @@ public class UserLogicHandler {
.put("userId", body.getInteger("userId")) .put("userId", body.getInteger("userId"))
.put("role", body.getInteger("role")); .put("role", body.getInteger("role"));
vertx.eventBus().request(Constants.AUTH_EVENT_BUS, request, ar -> { vertx.eventBus().request(CORE_EVENT_BUS, request, ar -> {
if (ar.succeeded()) { if (ar.succeeded()) {
JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null); JsonUtil.sendJson(ctx, ApiStatus.NO_CONTENT, null);
} else { } else {

View File

@@ -4,11 +4,11 @@ import io.vertx.core.Vertx;
import io.vertx.ext.web.Router; import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.Pool;
import net.miarma.api.backlib.Constants.CoreUserRole; import net.miarma.api.microservices.core.enums.CoreUserGlobalRole;
import net.miarma.api.microservices.core.handlers.FileDataHandler; import net.miarma.api.microservices.core.handlers.FileDataHandler;
import net.miarma.api.microservices.core.handlers.UserDataHandler; import net.miarma.api.microservices.core.handlers.UserDataHandler;
import net.miarma.api.microservices.core.services.UserService;
import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard; import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard;
import net.miarma.api.microservices.core.services.UserService;
public class CoreDataRouter { public class CoreDataRouter {
public static void mount(Router router, Vertx vertx, Pool pool) { public static void mount(Router router, Vertx vertx, Pool pool) {
@@ -19,17 +19,15 @@ public class CoreDataRouter {
router.route().handler(BodyHandler.create()); router.route().handler(BodyHandler.create());
router.get(CoreEndpoints.USERS).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::getAll); router.get(CoreEndpoints.USERS).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserData::getAll);
router.get(CoreEndpoints.USER).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::getById); router.get(CoreEndpoints.USER).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserData::getById);
router.post(CoreEndpoints.USERS).handler(hUserData::create); router.put(CoreEndpoints.USER).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserData::update);
router.put(CoreEndpoints.USER).handler(authGuard.check(CoreUserRole.ADMIN)).handler(hUserData::update); router.delete(CoreEndpoints.USER).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserData::delete);
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.FILES).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hFileData::getAll);
router.get(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::getById); router.get(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::getById);
router.post(CoreEndpoints.FILE_UPLOAD).handler(authGuard.check()).handler(hFileData::create); router.post(CoreEndpoints.FILE_UPLOAD).handler(authGuard.check()).handler(hFileData::create);
router.put(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::update); router.put(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::update);
router.delete(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::delete); router.delete(CoreEndpoints.FILE).handler(authGuard.check()).handler(hFileData::delete);
} }
} }

View File

@@ -1,39 +1,36 @@
package net.miarma.api.microservices.core.routing; package net.miarma.api.microservices.core.routing;
import net.miarma.api.backlib.Constants; import net.miarma.api.backlib.config.ConfigManager;
public class CoreEndpoints { public class CoreEndpoints {
/* private static final ConfigManager config = ConfigManager.getInstance();
* RUTAS DE LA API DE DATOS private static final String CORE = config.getApiPrefix("core");
* DE NEGOCIO DEL SSO private static final String AUTH = config.getApiPrefix("auth");
*/
// Usuarios /* API DE DATOS (UserDataHandler / FileDataHandler) */
public static final String USERS = Constants.CORE_PREFIX + "/users"; // GET, POST, PUT, DELETE public static final String USERS = CORE + "/users";
public static final String USER = Constants.CORE_PREFIX + "/users/:user_id"; // GET, PUT, DELETE public static final String USER = CORE + "/users/:user_id";
public static final String USER_STATUS = Constants.CORE_PREFIX + "/users/:user_id/status"; // GET, PUT public static final String FILES = CORE + "/files";
public static final String USER_ROLE = Constants.CORE_PREFIX + "/users/:user_id/role"; // GET, PUT public static final String FILE = CORE + "/files/:file_id";
public static final String USER_EXISTS = Constants.CORE_PREFIX + "/users/:user_id/exists"; // GET public static final String FILE_UPLOAD = CORE + "/files/upload";
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 /* API DE LÓGICA (AuthHandler / UserLogicHandler / FileLogicHandler) */
public static final String FILES = Constants.CORE_PREFIX + "/files"; // GET, POST public static final String LOGIN = AUTH + "/login";
public static final String FILE = Constants.CORE_PREFIX + "/files/:file_id"; // GET, PUT, DELETE public static final String LOGIN_VALID = AUTH + "/login/validate";
public static final String FILE_UPLOAD = Constants.CORE_PREFIX + "/files/upload"; // POST public static final String REGISTER = AUTH + "/register";
public static final String FILE_DOWNLOAD = Constants.CORE_PREFIX + "/files/:file_id/download"; // GET public static final String CHANGE_PASSWORD = AUTH + "/change-password";
public static final String USER_FILES = Constants.CORE_PREFIX + "/files/myfiles"; // GET public static final String VALIDATE_TOKEN = AUTH + "/validate-token";
public static final String REFRESH_TOKEN = AUTH + "/refresh-token";
/* public static final String USER_INFO = CORE + "/users/me";
* RUTAS DE LA API DE LOGICA public static final String USER_STATUS = CORE + "/users/:user_id/status";
* DE NEGOCIO DEL SSO public static final String USER_ROLE = CORE + "/users/:user_id/role";
*/ public static final String USER_EXISTS = CORE + "/users/:user_id/exists";
public static final String LOGIN = Constants.AUTH_PREFIX + "/login"; // POST public static final String USER_AVATAR = CORE + "/users/:user_id/avatar";
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 FILE_DOWNLOAD = CORE + "/files/:file_id/download";
public static final String CHANGE_PASSWORD = Constants.AUTH_PREFIX + "/change-password"; // POST public static final String USER_FILES = CORE + "/files/myfiles";
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 = CORE + "/screenshot";
public static final String SCREENSHOT = Constants.CORE_PREFIX + "/screenshot"; // GET
} }

View File

@@ -4,36 +4,40 @@ import io.vertx.core.Vertx;
import io.vertx.ext.web.Router; import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler; import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.sqlclient.Pool; import io.vertx.sqlclient.Pool;
import net.miarma.api.backlib.Constants.CoreUserRole; import net.miarma.api.microservices.core.enums.CoreUserGlobalRole;
import net.miarma.api.microservices.core.handlers.AuthHandler;
import net.miarma.api.microservices.core.handlers.FileLogicHandler; import net.miarma.api.microservices.core.handlers.FileLogicHandler;
import net.miarma.api.microservices.core.handlers.ScreenshotHandler; import net.miarma.api.microservices.core.handlers.ScreenshotHandler;
import net.miarma.api.microservices.core.handlers.UserLogicHandler; import net.miarma.api.microservices.core.handlers.UserLogicHandler;
import net.miarma.api.microservices.core.services.UserService;
import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard; import net.miarma.api.microservices.core.routing.middlewares.CoreAuthGuard;
import net.miarma.api.microservices.core.services.UserService;
public class CoreLogicRouter { public class CoreLogicRouter {
public static void mount(Router router, Vertx vertx, Pool pool) { public static void mount(Router router, Vertx vertx, Pool pool) {
UserLogicHandler hUserLogic = new UserLogicHandler(vertx); UserLogicHandler hUserLogic = new UserLogicHandler(vertx);
FileLogicHandler hFileLogic = new FileLogicHandler(vertx); FileLogicHandler hFileLogic = new FileLogicHandler(vertx);
AuthHandler hAuth = new AuthHandler(vertx);
ScreenshotHandler hScreenshot = new ScreenshotHandler(vertx); ScreenshotHandler hScreenshot = new ScreenshotHandler(vertx);
UserService userService = new UserService(pool); UserService userService = new UserService(pool);
CoreAuthGuard authGuard = new CoreAuthGuard(userService); CoreAuthGuard authGuard = new CoreAuthGuard(userService);
router.route().handler(BodyHandler.create()); router.route().handler(BodyHandler.create());
router.post(CoreEndpoints.LOGIN).handler(hUserLogic::login); router.post(CoreEndpoints.LOGIN).handler(hAuth::login);
router.get(CoreEndpoints.USER_INFO).handler(authGuard.check()).handler(hUserLogic::getInfo); router.post(CoreEndpoints.REGISTER).handler(hAuth::register);
router.post(CoreEndpoints.REGISTER).handler(hUserLogic::register); router.post(CoreEndpoints.LOGIN_VALID).handler(hAuth::loginValidate);
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.VALIDATE_TOKEN).handler(authGuard.check()).handler(hAuth::validateToken);
router.get(CoreEndpoints.REFRESH_TOKEN).handler(hAuth::refreshToken);
router.post(CoreEndpoints.CHANGE_PASSWORD).handler(authGuard.check()).handler(hAuth::changePassword);
router.get(CoreEndpoints.USER_INFO).handler(authGuard.check()).handler(hUserLogic::getInfo);
router.get(CoreEndpoints.USER_EXISTS).handler(authGuard.check()).handler(hUserLogic::exists); router.get(CoreEndpoints.USER_EXISTS).handler(authGuard.check()).handler(hUserLogic::exists);
router.get(CoreEndpoints.USER_STATUS).handler(authGuard.check()).handler(hUserLogic::getStatus); 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.put(CoreEndpoints.USER_STATUS).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserLogic::updateStatus);
router.get(CoreEndpoints.USER_ROLE).handler(authGuard.check()).handler(hUserLogic::getRole); 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.put(CoreEndpoints.USER_ROLE).handler(authGuard.check(CoreUserGlobalRole.ADMIN)).handler(hUserLogic::updateRole);
router.get(CoreEndpoints.USER_AVATAR).handler(authGuard.check()).handler(hUserLogic::getAvatar); 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.FILE_DOWNLOAD).handler(authGuard.check()).handler(hFileLogic::downloadFile);

View File

@@ -1,14 +1,15 @@
package net.miarma.api.microservices.core.routing.middlewares; package net.miarma.api.microservices.core.routing.middlewares;
import java.util.UUID;
import java.util.function.Consumer; import java.util.function.Consumer;
import io.vertx.ext.web.RoutingContext; 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.middlewares.AbstractAuthGuard;
import net.miarma.api.microservices.core.entities.UserEntity; import net.miarma.api.microservices.core.entities.UserEntity;
import net.miarma.api.microservices.core.enums.CoreUserGlobalRole;
import net.miarma.api.microservices.core.services.UserService; import net.miarma.api.microservices.core.services.UserService;
public class CoreAuthGuard extends AbstractAuthGuard<UserEntity, CoreUserRole> { public class CoreAuthGuard extends AbstractAuthGuard<UserEntity, CoreUserGlobalRole> {
private final UserService userService; private final UserService userService;
public CoreAuthGuard(UserService userService) { public CoreAuthGuard(UserService userService) {
@@ -16,12 +17,12 @@ public class CoreAuthGuard extends AbstractAuthGuard<UserEntity, CoreUserRole> {
} }
@Override @Override
protected CoreUserRole parseRole(String roleStr) { protected CoreUserGlobalRole parseRole(String roleStr) {
return CoreUserRole.valueOf(roleStr.toUpperCase()); return CoreUserGlobalRole.valueOf(roleStr.toUpperCase());
} }
@Override @Override
protected void getUserEntity(int userId, RoutingContext ctx, Consumer<UserEntity> callback) { protected void getUserEntity(UUID userId, RoutingContext ctx, Consumer<UserEntity> callback) {
userService.getById(userId).onComplete(ar -> { userService.getById(userId).onComplete(ar -> {
if (ar.succeeded()) callback.accept(ar.result()); if (ar.succeeded()) callback.accept(ar.result());
else callback.accept(null); else callback.accept(null);
@@ -29,8 +30,8 @@ public class CoreAuthGuard extends AbstractAuthGuard<UserEntity, CoreUserRole> {
} }
@Override @Override
protected boolean hasPermission(UserEntity user, CoreUserRole userRole) { protected boolean hasPermission(UserEntity user, CoreUserGlobalRole userRole) {
return user.getGlobal_role() == CoreUserRole.ADMIN; return user.getGlobalRole() == CoreUserGlobalRole.ADMIN;
} }
} }

View File

@@ -35,7 +35,7 @@ public class AuthService {
public Future<JsonObject> login(String login, String plainPassword, boolean keepLoggedIn) { public Future<JsonObject> login(String login, String plainPassword, boolean keepLoggedIn) {
return credentialDAO.getByServiceAndUsername(this.serviceId, login).compose(cred -> { return credentialDAO.getByServiceAndUsername(this.serviceId, login).compose(cred -> {
if (cred == null) { if (cred == null) {
return Future.failedFuture(new BadRequestException("Invalid credentials")); return Future.failedFuture(new NotFoundException("User not found in this domain"));
} }
if (!PasswordHasher.verify(plainPassword, cred.getPassword())) { if (!PasswordHasher.verify(plainPassword, cred.getPassword())) {

View File

@@ -31,7 +31,7 @@ public class UserService {
}); });
} }
public Future<UserEntity> updateProfile(UserEntity user) { public Future<UserEntity> update(UserEntity user) {
return userDAO.update(user); return userDAO.update(user);
} }
@@ -49,7 +49,7 @@ public class UserService {
}); });
} }
public Future<Boolean> deleteUser(UUID id) { public Future<Boolean> delete(UUID id) {
return userDAO.delete(id); return userDAO.delete(id);
} }
} }

View File

@@ -1,196 +1,123 @@
package net.miarma.api.microservices.core.verticles; package net.miarma.api.microservices.core.verticles;
import java.util.HashMap; import java.util.UUID;
import java.util.Map;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.json.JsonObject; import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router; import io.vertx.ext.web.Router;
import io.vertx.ext.web.handler.BodyHandler;
import io.vertx.sqlclient.Pool; 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.config.ConfigManager; import net.miarma.api.backlib.config.ConfigManager;
import net.miarma.api.microservices.core.entities.UserEntity;
import net.miarma.api.microservices.core.services.FileService;
import net.miarma.api.microservices.core.services.UserService;
import net.miarma.api.backlib.db.DatabaseProvider; import net.miarma.api.backlib.db.DatabaseProvider;
import net.miarma.api.backlib.util.EventBusUtil; import net.miarma.api.backlib.util.EventBusUtil;
import net.miarma.api.backlib.util.RouterUtil; import net.miarma.api.backlib.util.RouterUtil;
import net.miarma.api.microservices.core.entities.UserEntity;
import net.miarma.api.microservices.core.enums.CoreUserGlobalRole;
import net.miarma.api.microservices.core.enums.CoreUserGlobalStatus;
import net.miarma.api.microservices.core.routing.CoreDataRouter; import net.miarma.api.microservices.core.routing.CoreDataRouter;
import net.miarma.api.microservices.core.routing.CoreEndpoints;
import net.miarma.api.microservices.core.services.AuthService;
import net.miarma.api.microservices.core.services.FileService;
import net.miarma.api.microservices.core.services.UserService;
@SuppressWarnings("unused") @SuppressWarnings("unused")
public class CoreDataVerticle extends AbstractVerticle { public class CoreDataVerticle extends AbstractVerticle {
private ConfigManager configManager; private ConfigManager configManager;
private UserService userService; private UserService userService;
private FileService fileService; private FileService fileService;
private AuthService authService;
@Override @Override
public void start(Promise<Void> startPromise) { public void start(Promise<Void> startPromise) {
configManager = ConfigManager.getInstance(); configManager = ConfigManager.getInstance();
Pool pool = DatabaseProvider.createPool(vertx, configManager); Pool pool = DatabaseProvider.createPool(vertx, configManager);
userService = new UserService(pool); userService = new UserService(pool);
fileService = new FileService(pool); fileService = new FileService(pool);
authService = new AuthService(pool, 0);
Router router = Router.router(vertx); Router router = Router.router(vertx);
RouterUtil.attachLogger(router); RouterUtil.attachLogger(router);
CoreDataRouter.mount(router, vertx, pool); CoreDataRouter.mount(router, vertx, pool);
registerLogicVerticleConsumer(); registerLogicVerticleConsumer();
vertx.createHttpServer() vertx.createHttpServer()
.requestHandler(router) .requestHandler(router)
.listen(configManager.getIntProperty("sso.data.port"), res -> { .listen(configManager.getIntProperty("core.data.port"), res -> {
if (res.succeeded()) startPromise.complete(); if (res.succeeded()) startPromise.complete();
else startPromise.fail(res.cause()); else startPromise.fail(res.cause());
}); });
} }
private void registerLogicVerticleConsumer() { private void registerLogicVerticleConsumer() {
vertx.eventBus().consumer(Constants.AUTH_EVENT_BUS, message -> { String authAddress = configManager.getStringProperty("eventbus.auth.address");
vertx.eventBus().consumer(authAddress, message -> {
JsonObject body = (JsonObject) message.body(); JsonObject body = (JsonObject) message.body();
String action = body.getString("action"); String action = body.getString("action");
try {
switch (action) { switch (action) {
case "login" -> { case "login" -> {
String email = body.getString("email"); authService.login(
String userName = body.getString("userName"); body.getString("login"),
String password = body.getString("password"); body.getString("password"),
boolean keepLoggedIn = body.getBoolean("keepLoggedIn", false); body.getBoolean("keepLoggedIn", false)
).onSuccess(message::reply).onFailure(EventBusUtil.fail(message));
userService.login(email != null ? email : userName, password, keepLoggedIn)
.onSuccess(message::reply)
.onFailure(EventBusUtil.fail(message));
} }
case "register" -> { case "register" -> {
UserEntity user = new UserEntity(); UserEntity profile = new UserEntity();
user.setUser_name(body.getString("userName")); profile.setDisplayName(body.getString("displayName"));
user.setEmail(body.getString("email"));
user.setDisplay_name(body.getString("displayName"));
user.setPassword(body.getString("password"));
userService.register(user) authService.register(
profile,
body.getString("username"),
body.getString("email"),
body.getString("password")
).onSuccess(user -> message.reply(JsonObject.mapFrom(user)))
.onFailure(EventBusUtil.fail(message));
}
case "getInfo", "getById", "getUserById" -> {
userService.getById(UUID.fromString(body.getString("userId")))
.onSuccess(user -> message.reply(JsonObject.mapFrom(user)))
.onFailure(EventBusUtil.fail(message));
}
case "updateStatus" -> {
userService.changeStatus(
UUID.fromString(body.getString("userId")),
CoreUserGlobalStatus.fromInt(body.getInteger("status"))
).onSuccess(res -> message.reply(new JsonObject().put("message", "Status updated")))
.onFailure(EventBusUtil.fail(message));
}
case "updateRole" -> {
userService.changeRole(
UUID.fromString(body.getString("userId")),
CoreUserGlobalRole.fromInt(body.getInteger("role"))
).onSuccess(res -> message.reply(new JsonObject().put("message", "Role updated")))
.onFailure(EventBusUtil.fail(message));
}
case "getUserFiles" -> {
fileService.getUserFiles(UUID.fromString(body.getString("userId")))
.onSuccess(message::reply) .onSuccess(message::reply)
.onFailure(EventBusUtil.fail(message)); .onFailure(EventBusUtil.fail(message));
} }
case "changePassword" -> { case "downloadFile" -> {
Integer userId = body.getInteger("userId"); fileService.getById(UUID.fromString(body.getString("fileId")))
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) .onSuccess(message::reply)
.onFailure(EventBusUtil.fail(message)); .onFailure(EventBusUtil.fail(message));
} }
case "getInfo", "getById" -> { default -> message.fail(404, "Action not found: " + action);
Integer userId = body.getInteger("userId");
userService.getById(userId)
.onSuccess(message::reply)
.onFailure(EventBusUtil.fail(message));
} }
} catch (Exception e) {
case "userExists" -> { message.fail(400, "Invalid data format or UUID: " + e.getMessage());
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

@@ -24,7 +24,7 @@ public class CoreLogicVerticle extends AbstractVerticle {
vertx.createHttpServer() vertx.createHttpServer()
.requestHandler(router) .requestHandler(router)
.listen(configManager.getIntProperty("sso.logic.port"), res -> { .listen(configManager.getIntProperty("core.logic.port"), res -> {
if (res.succeeded()) startPromise.complete(); if (res.succeeded()) startPromise.complete();
else startPromise.fail(res.cause()); else startPromise.fail(res.cause());
}); });

View File

@@ -1,16 +1,19 @@
package net.miarma.api.microservices.core.verticles; package net.miarma.api.microservices.core.verticles;
import org.slf4j.Logger;
import io.vertx.core.AbstractVerticle; import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions; import io.vertx.core.DeploymentOptions;
import io.vertx.core.Promise; import io.vertx.core.Promise;
import io.vertx.core.ThreadingModel; import io.vertx.core.ThreadingModel;
import net.miarma.api.backlib.Constants;
import net.miarma.api.backlib.config.ConfigManager; import net.miarma.api.backlib.config.ConfigManager;
import net.miarma.api.backlib.log.LogAccumulator; import net.miarma.api.backlib.log.LogAccumulator;
import net.miarma.api.backlib.log.LoggerProvider;
import net.miarma.api.backlib.util.DeploymentUtil; import net.miarma.api.backlib.util.DeploymentUtil;
public class CoreMainVerticle extends AbstractVerticle { public class CoreMainVerticle extends AbstractVerticle {
private final Logger LOGGER = LoggerProvider.getLogger();
private ConfigManager configManager; private ConfigManager configManager;
@Override @Override
@@ -20,7 +23,7 @@ public class CoreMainVerticle extends AbstractVerticle {
deployVerticles(); deployVerticles();
startPromise.complete(); startPromise.complete();
} catch (Exception e) { } catch (Exception e) {
Constants.LOGGER.error(DeploymentUtil.failMessage(CoreMainVerticle.class, e)); LOGGER.error(DeploymentUtil.failMessage(CoreMainVerticle.class, e));
startPromise.fail(e); startPromise.fail(e);
} }
} }
@@ -35,7 +38,7 @@ public class CoreMainVerticle extends AbstractVerticle {
DeploymentUtil.successMessage(CoreDataVerticle.class), DeploymentUtil.successMessage(CoreDataVerticle.class),
DeploymentUtil.apiUrlMessage( DeploymentUtil.apiUrlMessage(
configManager.getHost(), configManager.getHost(),
configManager.getIntProperty("sso.data.port") configManager.getIntProperty("core.data.port")
) )
); );
LogAccumulator.add(message); LogAccumulator.add(message);
@@ -50,7 +53,7 @@ public class CoreMainVerticle extends AbstractVerticle {
DeploymentUtil.successMessage(CoreLogicVerticle.class), DeploymentUtil.successMessage(CoreLogicVerticle.class),
DeploymentUtil.apiUrlMessage( DeploymentUtil.apiUrlMessage(
configManager.getHost(), configManager.getHost(),
configManager.getIntProperty("sso.logic.port") configManager.getIntProperty("core.logic.port")
) )
); );
LogAccumulator.add(message); LogAccumulator.add(message);

View File

@@ -11,10 +11,10 @@
<modules> <modules>
<module>backlib</module> <module>backlib</module>
<module>bootstrap</module> <module>bootstrap</module>
<module>microservices/huertos</module> <!-- <module>microservices/huertos</module>
<module>microservices/huertosdecine</module> <module>microservices/huertosdecine</module>
<module>microservices/minecraft</module> <module>microservices/minecraft</module>
<module>microservices/mpaste</module> <module>microservices/mpaste</module> -->
<module>microservices/core</module> <module>microservices/core</module>
</modules> </modules>
@@ -29,7 +29,7 @@
<artifactId>core</artifactId> <artifactId>core</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency>
<dependency> <!-- <dependency>
<groupId>net.miarma.api</groupId> <groupId>net.miarma.api</groupId>
<artifactId>huertos</artifactId> <artifactId>huertos</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
@@ -48,7 +48,7 @@
<groupId>net.miarma.api</groupId> <groupId>net.miarma.api</groupId>
<artifactId>mpaste</artifactId> <artifactId>mpaste</artifactId>
<version>${project.version}</version> <version>${project.version}</version>
</dependency> </dependency> -->
</dependencies> </dependencies>
</project> </project>