Restored dev

This commit is contained in:
Jose
2026-02-15 06:26:40 +01:00
parent de98ed5cc5
commit 3796e6f190
191 changed files with 7581 additions and 883 deletions

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

1
.gitignore vendored
View File

@@ -24,3 +24,4 @@
hs_err_pid* hs_err_pid*
replay_pid* replay_pid*
.idea

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>backend</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -1,8 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8

View File

@@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

View File

@@ -1,3 +1,3 @@
# miarma-backend # miarma-backend
Backend that powers all my services. Backend that provides auth and management for my self-hosted microservices

18
TODO Normal file
View File

@@ -0,0 +1,18 @@
POR HACER --------------------------------
- implementar urlParams para filtros
- documentación
- mail wrapper
RESUELTO ---------------------------------
- añadir colaborador desde perfil
- apuntarse lista espera
- aceptar solicitudes LE/Colab (sobre todo por crear preusers)
- mejorar queries para no filtrar en memoria -> IMPOSIBLE CON ENDPOINTS INTERNOS DE CORE: RESUELTO CON CACHING
- normalizar el uso de services y repositories desde otros services y repositories
- sistema comun de errores en back & front
- nombre del requester
- cambiar contraseña (?)
- todos los socios en dropdown ingresos
- validacion LE/COlab
- createdAt custom en ing,gastos
- editar createdAt

9
backend.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

9
backlib/backlib.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

91
backlib/pom.xml Normal file
View File

@@ -0,0 +1,91 @@
<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>
<artifactId>backlib</artifactId>
<groupId>net.miarma</groupId>
<version>1.1.1</version>
<properties>
<java.version>25</java.version>
<spring.boot.version>4.0.1</spring.boot.version>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
<snapshotRepository>
<id>MiarmaGit</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</snapshotRepository>
</distributionManagement>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,29 @@
package net.miarma.backlib.config;
import net.miarma.backlib.security.JwtService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityCommonConfig {
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
@Bean
public JwtService jwtService() {
return new JwtService();
}
}

View File

@@ -0,0 +1,65 @@
package net.miarma.backlib.dto;
import tools.jackson.databind.ObjectMapper;
import java.time.Instant;
public class ApiErrorDto {
private int status;
private String error;
private String message;
private String path;
private Instant timestamp;
public ApiErrorDto(int status, String error, String message, String path) {
this.status = status;
this.error = error;
this.message = message;
this.path = path;
this.timestamp = Instant.now();
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public Instant getTimestamp() {
return timestamp;
}
public void setTimestamp(Instant timestamp) {
this.timestamp = timestamp;
}
public String toJson() {
return new ObjectMapper().writeValueAsString(this);
}
}

View File

@@ -0,0 +1,57 @@
package net.miarma.backlib.dto;
import tools.jackson.databind.ObjectMapper;
import java.time.Instant;
import java.util.List;
import java.util.Map;
public class ApiValidationErrorDto {
private int status;
private Map<String,String> errors;
private String path;
private Instant timestamp;
public ApiValidationErrorDto(Map<String,String> errors, String path) {
this.status = 422;
this.errors = errors;
this.path = path;
this.timestamp = Instant.now();
}
public Map<String,String> getErrors() {
return errors;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public void setErrors(Map<String,String> errors) {
this.errors = errors;
}
public Instant getTimestamp() {
return timestamp;
}
public void setTimestamp(Instant timestamp) {
this.timestamp = timestamp;
}
public String toJson() {
return new ObjectMapper().writeValueAsString(this);
}
}

View File

@@ -0,0 +1,3 @@
package net.miarma.backlib.dto;
public record ChangeAvatarRequest(String avatar) {}

View File

@@ -0,0 +1,10 @@
package net.miarma.backlib.dto;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
public record ChangePasswordRequest(@NotBlank String oldPassword,
@NotBlank String newPassword,
@NotBlank Byte serviceId) {}

View File

@@ -0,0 +1,4 @@
package net.miarma.backlib.dto;
public record ChangeRoleRequest(Byte role) {
}

View File

@@ -0,0 +1,3 @@
package net.miarma.backlib.dto;
public record ChangeStatusRequest(Byte status) {}

View File

@@ -0,0 +1,56 @@
package net.miarma.backlib.dto;
import java.time.Instant;
import java.util.UUID;
public class CreateCredentialDto {
private UUID userId;
private Byte serviceId;
private String username;
private String email;
private String password;
private Byte status;
public CreateCredentialDto() {}
public CreateCredentialDto(UUID userId, Byte serviceId, String username, String email,
String password, Byte status) {
this.userId = userId;
this.serviceId = serviceId;
this.username = username;
this.email = email;
this.password = password;
this.status = status;
}
// Getters y setters
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public Byte getServiceId() {
return serviceId;
}
public void setServiceId(Byte serviceId) {
this.serviceId = serviceId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
}

View File

@@ -0,0 +1,6 @@
package net.miarma.backlib.dto;
public record CreateUserDto(String displayName,
String avatar) {
}

View File

@@ -0,0 +1,79 @@
package net.miarma.backlib.dto;
import java.time.Instant;
import java.util.UUID;
public class CredentialDto {
private UUID credentialId;
private UUID userId;
private Byte serviceId;
private String username;
private String email;
private Byte status;
private Instant createdAt;
private Instant updatedAt;
public CredentialDto() {}
public CredentialDto(UUID credentialId, UUID userId, Byte serviceId, String username, String email,
Byte status, Instant createdAt, Instant updatedAt) {
this.credentialId = credentialId;
this.userId = userId;
this.serviceId = serviceId;
this.username = username;
this.email = email;
this.status = status;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Getters y setters
public UUID getCredentialId() {
return credentialId;
}
public void setCredentialId(UUID credentialId) {
this.credentialId = credentialId;
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public Byte getServiceId() {
return serviceId;
}
public void setServiceId(Byte serviceId) {
this.serviceId = serviceId;
}
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 Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,123 @@
package net.miarma.backlib.dto;
import java.time.Instant;
import java.util.UUID;
public class FileDto {
private FileDto() {}
public static class Request {
private String fileName;
private String filePath;
private String mimeType;
private Byte context;
private UUID uploadedBy;
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public Byte getContext() {
return context;
}
public void setContext(Byte context) {
this.context = context;
}
public UUID getUploadedBy() {
return uploadedBy;
}
public void setUploadedBy(UUID uploadedBy) {
this.uploadedBy = uploadedBy;
}
}
public static class Response {
private UUID fileId;
private String fileName;
private String filePath;
private String mimeType;
private UUID uploadedBy;
private Instant uploadedAt;
private Byte context;
public UUID getFileId() {
return fileId;
}
public void setFileId(UUID fileId) {
this.fileId = fileId;
}
public String getFileName() {
return fileName;
}
public void setFileName(String fileName) {
this.fileName = fileName;
}
public String getFilePath() {
return filePath;
}
public void setFilePath(String filePath) {
this.filePath = filePath;
}
public String getMimeType() {
return mimeType;
}
public void setMimeType(String mimeType) {
this.mimeType = mimeType;
}
public UUID getUploadedBy() {
return uploadedBy;
}
public void setUploadedBy(UUID uploadedBy) {
this.uploadedBy = uploadedBy;
}
public Instant getUploadedAt() {
return uploadedAt;
}
public void setUploadedAt(Instant uploadedAt) {
this.uploadedAt = uploadedAt;
}
public Byte getContext() {
return context;
}
public void setContext(Byte context) {
this.context = context;
}
}
}

View File

@@ -0,0 +1,8 @@
package net.miarma.backlib.dto;
import jakarta.validation.constraints.NotBlank;
import tools.jackson.databind.JsonNode;
public record LoginRequest(@NotBlank String username,
@NotBlank String password,
Byte serviceId) {}

View File

@@ -0,0 +1,7 @@
package net.miarma.backlib.dto;
import com.fasterxml.jackson.annotation.JsonProperty;
public record LoginResponse(@JsonProperty("token") String token,
UserDto user,
CredentialDto account) {}

View File

@@ -0,0 +1,9 @@
package net.miarma.backlib.dto;
import jakarta.validation.constraints.NotBlank;
public record RegisterRequest(String displayName,
@NotBlank String username,
@NotBlank String email,
@NotBlank String password,
Byte serviceId) {}

View File

@@ -0,0 +1,71 @@
package net.miarma.backlib.dto;
import java.time.Instant;
import java.util.UUID;
public class UserDto {
private UUID userId;
private String displayName;
private String avatar;
private Byte globalStatus;
private Byte globalRole;
private Instant createdAt;
private Instant updatedAt;
public UserDto() {}
public UserDto(UUID userId, String displayName, String avatar, Byte globalStatus, Byte globalRole,
Instant createdAt, Instant updatedAt) {
this.userId = userId;
this.displayName = displayName;
this.avatar = avatar;
this.globalStatus = globalStatus;
this.globalRole = globalRole;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
// Getters y setters
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Byte getGlobalStatus() {
return globalStatus;
}
public void setGlobalStatus(Byte globalStatus) {
this.globalStatus = globalStatus;
}
public Byte getGlobalRole() {
return globalRole;
}
public void setGlobalRole(Byte globalRole) {
this.globalRole = globalRole;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,4 @@
package net.miarma.backlib.dto;
public record UserExistsResponse(boolean exists) {
}

View File

@@ -0,0 +1,3 @@
package net.miarma.backlib.dto;
public record UserWithCredentialDto(UserDto user, CredentialDto account) {}

View File

@@ -0,0 +1,7 @@
package net.miarma.backlib.exception;
public class BadRequestException extends RuntimeException {
public BadRequestException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package net.miarma.backlib.exception;
public class ConflictException extends RuntimeException {
public ConflictException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,7 @@
package net.miarma.backlib.exception;
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,5 @@
package net.miarma.backlib.exception;
public class NotFoundException extends RuntimeException {
public NotFoundException(String message) { super(message); }
}

View File

@@ -0,0 +1,7 @@
package net.miarma.backlib.exception;
public class UnauthorizedException extends RuntimeException {
public UnauthorizedException(String message) {
super(message);
}
}

View File

@@ -0,0 +1,21 @@
package net.miarma.backlib.exception;
public class ValidationException extends RuntimeException {
private final String field;
private final String message;
public ValidationException(String field, String message) {
super(message);
this.field = field;
this.message = message;
}
public String getField() {
return field;
}
@Override
public String getMessage() {
return message;
}
}

View File

@@ -0,0 +1,41 @@
package net.miarma.backlib.filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class RequestLoggingFilter extends OncePerRequestFilter {
private static final Logger log = LoggerFactory.getLogger(RequestLoggingFilter.class);
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
long start = System.currentTimeMillis();
filterChain.doFilter(request, response);
long duration = System.currentTimeMillis() - start;
log.info("({}) {} {} -> {} ({} ms)",
request.getRemoteAddr(),
request.getMethod(),
request.getRequestURI(),
response.getStatus(),
duration
);
}
}

View File

@@ -0,0 +1,27 @@
package net.miarma.backlib.http;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
@Profile("dev") // esto asegura que solo se cargue en dev
public class DevCorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("http://localhost:3000") // tu frontend React
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.allowedHeaders("*");
}
};
}
}

View File

@@ -0,0 +1,106 @@
package net.miarma.backlib.http;
import jakarta.servlet.http.HttpServletRequest;
import net.miarma.backlib.dto.ApiErrorDto;
import net.miarma.backlib.dto.ApiValidationErrorDto;
import net.miarma.backlib.exception.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<ApiErrorDto> handleNotFound(
NotFoundException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.NOT_FOUND.value(),
HttpStatus.NOT_FOUND.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(BadRequestException.class)
public ResponseEntity<ApiErrorDto> handleBadRequest(
BadRequestException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.BAD_REQUEST.value(),
HttpStatus.BAD_REQUEST.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error);
}
@ExceptionHandler(UnauthorizedException.class)
public ResponseEntity<ApiErrorDto> handleUnauthorized(
UnauthorizedException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error);
}
@ExceptionHandler(ForbiddenException.class)
public ResponseEntity<ApiErrorDto> handleForbidden(
ForbiddenException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error);
}
@ExceptionHandler(ConflictException.class)
public ResponseEntity<ApiErrorDto> handleConflict(
ConflictException ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.CONFLICT.value(),
HttpStatus.CONFLICT.getReasonPhrase(),
ex.getMessage(),
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiValidationErrorDto> handleValidation(
ValidationException ex, HttpServletRequest req) {
Map<String, String> errors = Map.of(ex.getField(), ex.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors, req.getRequestURI()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiErrorDto> handleAll(
Exception ex, HttpServletRequest req) {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(),
"Internal server error",
req.getRequestURI()
);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}

View File

@@ -0,0 +1,31 @@
package net.miarma.backlib.http;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.miarma.backlib.dto.ApiErrorDto;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RestAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.FORBIDDEN.value(),
HttpStatus.FORBIDDEN.getReasonPhrase(),
"Forbidden",
request.getRequestURI()
);
response.setStatus(HttpStatus.FORBIDDEN.value());
response.setContentType("application/json");
response.getWriter().write(error.toJson());
}
}

View File

@@ -0,0 +1,34 @@
package net.miarma.backlib.http;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.miarma.backlib.dto.ApiErrorDto;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
public class RestAuthEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
ApiErrorDto error = new ApiErrorDto(
HttpStatus.UNAUTHORIZED.value(),
HttpStatus.UNAUTHORIZED.getReasonPhrase(),
"Unauthorized",
request.getRequestURI()
);
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write(error.toJson());
}
}

View File

@@ -0,0 +1,24 @@
package net.miarma.backlib.security;
import org.springframework.stereotype.Component;
import java.time.Instant;
@Component
public class CoreAuthTokenHolder {
private volatile String token;
private volatile Instant expiresAt;
public String getToken() {
return token;
}
public boolean isExpired() {
return expiresAt == null || Instant.now().isAfter(expiresAt.minusSeconds(30));
}
public void setToken(String token, Instant expiresAt) {
this.token = token;
this.expiresAt = expiresAt;
}
}

View File

@@ -0,0 +1,102 @@
package net.miarma.backlib.security;
import io.jsonwebtoken.*;
import jakarta.annotation.PostConstruct;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.List;
import java.util.UUID;
@Service
public class JwtService {
@Value("${jwt.private-key-path}")
private String privateKeyPath;
@Value("${jwt.public-key-path}")
private String publicKeyPath;
@Value("${jwt.expiration-ms}")
private long expiration;
private PrivateKey privateKey;
private PublicKey publicKey;
@PostConstruct
public void init() throws Exception {
this.privateKey = loadPrivateKey(privateKeyPath);
this.publicKey = loadPublicKey(publicKeyPath);
}
private PrivateKey loadPrivateKey(String path) throws Exception {
String pem = Files.readString(Path.of(path));
pem = pem.replace("-----BEGIN PRIVATE KEY-----", "")
.replace("-----END PRIVATE KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(pem);
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(spec);
}
private PublicKey loadPublicKey(String path) throws Exception {
String pem = Files.readString(Path.of(path));
pem = pem.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] decoded = Base64.getDecoder().decode(pem);
X509EncodedKeySpec spec = new X509EncodedKeySpec(decoded);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePublic(spec);
}
public String generateToken(UUID userId, Byte serviceId) {
Date now = new Date();
Date exp = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId.toString())
.claim("service", serviceId)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(privateKey, SignatureAlgorithm.RS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public UUID getUserId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody();
return UUID.fromString(claims.getSubject());
}
public Byte getServiceId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody();
return ((Number) claims.get("service")).byteValue();
}
public Date getExpiration(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(publicKey).build().parseClaimsJws(token).getBody();
return claims.getExpiration();
}
}

View File

@@ -0,0 +1,45 @@
package net.miarma.backlib.security;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class PasswordGenerator {
private static final String UPPER = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String LOWER = "abcdefghijklmnopqrstuvwxyz";
private static final String DIGITS = "0123456789";
private static final String SYMBOLS = "!@#$%^&*"; // compatibles con bcrypt
private static final String ALL = UPPER + LOWER + DIGITS + SYMBOLS;
private static final SecureRandom random = new SecureRandom();
public static String generate(int length) {
if (length < 8) length = 8;
List<Character> password = new ArrayList<>();
password.add(getRandChar(UPPER));
password.add(getRandChar(LOWER));
password.add(getRandChar(DIGITS));
password.add(getRandChar(SYMBOLS));
while (password.size() < length) {
password.add(getRandChar(ALL));
}
Collections.shuffle(password, random);
StringBuilder sb = new StringBuilder();
for (char c : password) {
sb.append(c);
}
return sb.toString();
}
private static char getRandChar(String chars) {
return chars.charAt(random.nextInt(chars.length()));
}
}

View File

@@ -0,0 +1,45 @@
package net.miarma.backlib.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class ServiceAuthFilter extends OncePerRequestFilter {
private final JwtService jwtService;
public ServiceAuthFilter(JwtService jwtService) {
this.jwtService = jwtService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String path = request.getRequestURI();
if (path.startsWith("/users/service")) {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
String token = authHeader.substring(7);
if (!jwtService.validateToken(token)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return;
}
}
filterChain.doFilter(request, response);
}
}

View File

@@ -0,0 +1,21 @@
package net.miarma.backlib.util;
import java.nio.ByteBuffer;
import java.util.UUID;
public class UuidUtil {
public static byte[] uuidToBin(UUID uuid) {
ByteBuffer bb = ByteBuffer.allocate(16);
bb.putLong(uuid.getMostSignificantBits());
bb.putLong(uuid.getLeastSignificantBits());
return bb.array();
}
public static UUID binToUUID(byte[] bin) {
ByteBuffer bb = ByteBuffer.wrap(bin);
long high = bb.getLong();
long low = bb.getLong();
return new UUID(high, low);
}
}

View File

@@ -0,0 +1,3 @@
artifactId=backlib
groupId=net.miarma
version=1.1.0

View File

@@ -0,0 +1,36 @@
net/miarma/backlib/dto/LoginResponse.class
net/miarma/backlib/dto/CreateUserDto.class
net/miarma/backlib/http/DevCorsConfig$1.class
net/miarma/backlib/dto/ApiErrorDto.class
net/miarma/backlib/dto/ChangeStatusRequest.class
net/miarma/backlib/exception/ConflictException.class
net/miarma/backlib/http/RestAuthEntryPoint.class
net/miarma/backlib/exception/NotFoundException.class
net/miarma/backlib/http/RestAccessDeniedHandler.class
net/miarma/backlib/dto/ChangeAvatarRequest.class
net/miarma/backlib/dto/FileDto$Response.class
net/miarma/backlib/dto/CredentialDto.class
net/miarma/backlib/util/UuidUtil.class
net/miarma/backlib/dto/FileDto$Request.class
net/miarma/backlib/dto/ApiValidationErrorDto.class
net/miarma/backlib/security/JwtService.class
net/miarma/backlib/dto/UserDto.class
net/miarma/backlib/dto/LoginRequest.class
net/miarma/backlib/exception/BadRequestException.class
net/miarma/backlib/security/CoreAuthTokenHolder.class
net/miarma/backlib/dto/CreateCredentialDto.class
net/miarma/backlib/dto/UserWithCredentialDto.class
net/miarma/backlib/dto/RegisterRequest.class
net/miarma/backlib/dto/ChangePasswordRequest.class
net/miarma/backlib/http/GlobalExceptionHandler.class
net/miarma/backlib/dto/UserExistsResponse.class
net/miarma/backlib/dto/ChangeRoleRequest.class
net/miarma/backlib/exception/ForbiddenException.class
net/miarma/backlib/exception/UnauthorizedException.class
net/miarma/backlib/dto/FileDto.class
net/miarma/backlib/security/PasswordGenerator.class
net/miarma/backlib/config/SecurityCommonConfig.class
net/miarma/backlib/filter/RequestLoggingFilter.class
net/miarma/backlib/exception/ValidationException.class
net/miarma/backlib/security/ServiceAuthFilter.class
net/miarma/backlib/http/DevCorsConfig.class

View File

@@ -0,0 +1,33 @@
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/config/SecurityCommonConfig.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ApiErrorDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ChangeAvatarRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ChangePasswordRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ChangeRoleRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/ChangeStatusRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/CreateCredentialDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/CreateUserDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/CredentialDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/FileDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/LoginRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/LoginResponse.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/RegisterRequest.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/UserDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/UserExistsResponse.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/dto/UserWithCredentialDto.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/BadRequestException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/ConflictException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/ForbiddenException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/NotFoundException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/UnauthorizedException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/filter/RequestLoggingFilter.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/DevCorsConfig.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAccessDeniedHandler.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/http/RestAuthEntryPoint.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/security/CoreAuthTokenHolder.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/security/JwtService.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/security/PasswordGenerator.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/security/ServiceAuthFilter.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/util/UuidUtil.java

10
build-upload.sh Executable file
View File

@@ -0,0 +1,10 @@
#!/bin/bash
cd core/
mvn clean package
cd ..
cd huertos/
mvn clean package
cd ..
scp core/target/core-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer
scp huertos/target/huertos-1.0.0.jar jomaa@10.0.0.254:/home/jomaa/transfer

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>cine</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -1,8 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8

View File

@@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

9
cine/cine.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

View File

@@ -9,6 +9,13 @@
</parent> </parent>
<artifactId>cine</artifactId> <artifactId>cine</artifactId>
<repositories>
<repository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies> <dependencies>
<!-- Spring Boot --> <!-- Spring Boot -->
<dependency> <dependency>

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>core</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -1,8 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8

View File

@@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

9
core/core.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

View File

@@ -9,6 +9,18 @@
</parent> </parent>
<artifactId>core</artifactId> <artifactId>core</artifactId>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies> <dependencies>
<!-- Spring Boot --> <!-- Spring Boot -->
<dependency> <dependency>
@@ -44,6 +56,10 @@
<version>1.18.42</version> <version>1.18.42</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<!-- JWT --> <!-- JWT -->
<dependency> <dependency>
@@ -63,6 +79,29 @@
<version>0.11.5</version> <version>0.11.5</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>net.miarma</groupId>
<artifactId>backlib</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies> </dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -1,9 +1,14 @@
package net.miarma.backend.core; package net.miarma.backend.core;
import net.miarma.backlib.config.SecurityCommonConfig;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication @SpringBootApplication(scanBasePackages = {
"net.miarma.backend.core",
"net.miarma.backlib"
})
public class CoreApplication { public class CoreApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(CoreApplication.class, args); SpringApplication.run(CoreApplication.class, args);

View File

@@ -1,31 +1,62 @@
package net.miarma.backend.core.config; package net.miarma.backend.core.config;
import net.miarma.backend.core.security.JwtFilter;
import net.miarma.backlib.http.RestAccessDeniedHandler;
import net.miarma.backlib.http.RestAuthEntryPoint;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfigurationSource;
import java.util.Optional;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig { public class SecurityConfig {
private final JwtFilter jwtFilter; private final JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig(JwtFilter jwtFilter) { public SecurityConfig(
JwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) {
this.jwtFilter = jwtFilter; this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler;
this.corsConfigurationSource = corsConfigurationSource.orElse(null);
} }
@Bean @Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
if (corsConfigurationSource != null) {
http.cors(Customizer.withDefaults());
}
http http
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authEntryPoint)
.accessDeniedHandler(accessDeniedHandler)
)
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll() .requestMatchers("/auth/login").permitAll()
.requestMatchers("/auth/refresh").permitAll()
.requestMatchers("/auth/change-password").permitAll()
.requestMatchers("/auth/register").permitAll()
.requestMatchers("/test").permitAll()
.requestMatchers("/screenshot").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
); );
@@ -33,9 +64,4 @@ public class SecurityConfig {
return http.build(); return http.build();
} }
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(12);
}
} }

View File

@@ -0,0 +1,13 @@
package net.miarma.backend.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class WebClientConfig {
@Bean
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}

View File

@@ -1,60 +1,122 @@
package net.miarma.backend.core.controller; package net.miarma.backend.core.controller;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.miarma.backlib.dto.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import net.miarma.backend.core.dto.LoginRequest; import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.dto.LoginResponse;
import net.miarma.backend.core.service.AuthService; import net.miarma.backend.core.service.AuthService;
import net.miarma.backend.core.service.JwtService; import net.miarma.backend.core.service.CredentialService;
import net.miarma.backlib.security.JwtService;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthController { public class AuthController {
private final AuthService authService;
private final JwtService jwtService; private final JwtService jwtService;
private final AuthService authService;
public AuthController(AuthService authService, JwtService jwtService) { public AuthController(JwtService jwtService, AuthService authService) {
this.authService = authService;
this.jwtService = jwtService; this.jwtService = jwtService;
this.authService = authService;
} }
@PostMapping("/login") @PostMapping("/login")
public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) { public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
LoginResponse response = authService.login(request); LoginResponse response = authService.login(request);
return ResponseEntity.ok(response); return ResponseEntity.ok(
new LoginResponse(response.token(), response.user(), response.account())
);
} }
@PostMapping("/refresh") @PostMapping("/register")
public ResponseEntity<LoginResponse> register(@RequestBody RegisterRequest request) {
return ResponseEntity.ok(authService.register(request));
}
@GetMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String authHeader) { public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) { if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).body("Token missing"); return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"No hay token",
"/v2/core/auth/change-password"
)
);
} }
String token = authHeader.substring(7); String token = authHeader.substring(7);
if (!jwtService.validateToken(token)) { if (!jwtService.validateToken(token)) {
return ResponseEntity.status(401).body("Invalid token"); return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"Invalid token",
"/v2/core/auth/change-password"
)
);
} }
UUID userId = jwtService.getUserId(token); UUID userId = jwtService.getUserId(token);
short serviceId = jwtService.getServiceId(token); Byte serviceId = jwtService.getServiceId(token);
String newToken = jwtService.generateToken(userId, serviceId); String newToken = jwtService.generateToken(userId, serviceId);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"token", newToken, "token", newToken,
"userId", userId, "userId", userId,
"serviceId", serviceId "serviceId", serviceId
)); ));
} }
@PostMapping("/change-password")
public ResponseEntity<?> changePassword(
@RequestHeader("Authorization") String authHeader,
@RequestBody ChangePasswordRequest request
) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"No hay token",
"/v2/core/auth/change-password"
)
);
}
String token = authHeader.substring(7);
if (!jwtService.validateToken(token)) {
return ResponseEntity.status(401).body(
new ApiErrorDto(
401,
"Unauthorized",
"Invalid token",
"/v2/core/auth/change-password"
)
);
}
UUID userId = jwtService.getUserId(token);
authService.changePassword(userId, request);
return ResponseEntity.ok(Map.of("message", "Contraseña cambiada correctamente"));
}
@GetMapping("/validate")
public ResponseEntity<Boolean> validate(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7);
return ResponseEntity.ok(jwtService.validateToken(token));
}
} }

View File

@@ -1,5 +1,97 @@
package net.miarma.backend.core.controller; package net.miarma.backend.core.controller;
import java.util.List;
import java.util.UUID;
import net.miarma.backend.core.dto.UpdateCredentialDto;
import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.security.CorePrincipal;
import net.miarma.backend.core.service.UserService;
import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.ForbiddenException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.service.CredentialService;
@RestController
@RequestMapping("/credentials")
public class CredentialController { public class CredentialController {
private final CredentialService credentialService;
public CredentialController(CredentialService credentialService) {
this.credentialService = credentialService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<Credential>> getAll() {
return ResponseEntity.ok(credentialService.getAll());
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<CredentialDto> create(@RequestBody CreateCredentialDto dto) {
return ResponseEntity.ok(
CredentialMapper.toDto(
credentialService.create(
CredentialMapper.toEntity(dto)))
);
}
@GetMapping("/test/{userId}")
public ResponseEntity<String> test(@PathVariable("userId") UUID userId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
return ResponseEntity.ok(
userId.equals(principal.getUserId()) ? "OK" : "NO"
);
}
@GetMapping("/user/{userId}")
public ResponseEntity<List<CredentialDto>> getByUserId(@PathVariable("userId") UUID userId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (!principal.getRole().equals((byte)1) && !principal.getUserId().equals(userId)) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(credentialService.getByUserId(userId).stream()
.map(CredentialMapper::toDto)
.toList());
}
@GetMapping("/{credential_id}")
public ResponseEntity<CredentialDto> getById(@PathVariable("credential_id") UUID credentialId) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Credential cred = credentialService.getById(credentialId);
if (!principal.getRole().equals((byte)1) && !cred.getUserId().equals(principal.getUserId())) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(CredentialMapper.toDto(cred));
}
@PutMapping("/{credential_id}")
public ResponseEntity<CredentialDto> update(
@PathVariable("credential_id") UUID credentialId,
@RequestBody UpdateCredentialDto dto
) {
CorePrincipal principal = (CorePrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
Credential cred = credentialService.getById(credentialId);
if (!principal.getRole().equals((byte)1) && !cred.getUserId().equals(principal.getUserId())) {
throw new ForbiddenException("No tienes permiso");
}
return ResponseEntity.ok(
CredentialMapper.toDto(
credentialService.update(credentialId, CredentialMapper.toEntity(dto))
)
);
}
@GetMapping("/{service_id}/{user_id}/status")
public ResponseEntity<Byte> getStatus(@PathVariable("user_id") UUID userId, @PathVariable("service_id") Byte serviceId) {
return ResponseEntity.ok(credentialService.getStatus(userId, serviceId));
}
} }

View File

@@ -1,5 +1,90 @@
package net.miarma.backend.core.controller; package net.miarma.backend.core.controller;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import net.miarma.backend.core.mapper.FileMapper;
import net.miarma.backlib.dto.FileDto;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import net.miarma.backend.core.model.File;
import net.miarma.backend.core.service.FileService;
@RestController
@RequestMapping("/files")
public class FileController { public class FileController {
private final FileService fileService;
public FileController(FileService fileService) {
this.fileService = fileService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<File>> getAll() {
List<File> files = fileService.getAll();
return ResponseEntity.ok(files);
}
@GetMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> getById(@PathVariable("file_id") UUID fileId) {
File file = fileService.getById(fileId);
return ResponseEntity.ok(file);
}
@PostMapping(consumes = "multipart/form-data")
@PreAuthorize("hasRole('ADMIN') or #uploadedBy == authentication.principal.userId")
public ResponseEntity<FileDto.Response> create(
@RequestPart("file") MultipartFile file,
@RequestPart("fileName") String fileName,
@RequestPart("mimeType") String mimeType,
@RequestPart("uploadedBy") UUID uploadedBy,
@RequestPart("context") Integer context
) throws IOException {
File entity = new File();
entity.setFileName(fileName);
entity.setMimeType(mimeType);
entity.setUploadedBy(uploadedBy);
entity.setContext(context.byteValue());
File created = fileService.create(entity, file.getBytes());
return ResponseEntity.status(HttpStatus.CREATED)
.body(FileMapper.toResponse(created));
}
@PutMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> update(@PathVariable("file_id") UUID fileId, @RequestBody FileDto.Request request) {
File updated = fileService.update(fileId, FileMapper.toEntity(request));
return ResponseEntity.ok(updated);
}
@DeleteMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<Void> delete(@PathVariable("file_id") UUID fileId, @RequestBody Map<String,String> body) throws IOException {
String filePath = body.get("file_path");
Files.deleteIfExists(Paths.get(filePath));
fileService.delete(fileId);
return ResponseEntity.ok().build();
}
} }

View File

@@ -1,10 +1,174 @@
package net.miarma.backend.core.controller; package net.miarma.backend.core.controller;
import org.springframework.web.bind.annotation.RequestMapping; import java.util.List;
import org.springframework.web.bind.annotation.RestController; import java.util.UUID;
import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.service.CredentialService;
import net.miarma.backlib.dto.*;
import org.apache.coyote.Response;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.model.User;
import net.miarma.backlib.security.JwtService;
import net.miarma.backend.core.service.UserService;
@RestController @RestController
@RequestMapping("/users") @RequestMapping("/users")
public class UserController { public class UserController {
private UserService userService;
private CredentialService credentialService;
private JwtService jwtService;
public UserController(UserService userService, CredentialService credentialService, JwtService jwtService) {
this.userService = userService;
this.credentialService = credentialService;
this.jwtService = jwtService;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<UserDto>> getAll() {
return ResponseEntity.ok(
userService.getAll()
.stream()
.map(UserMapper::toDto)
.toList()
);
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserDto> create(@RequestBody CreateUserDto dto) {
return ResponseEntity.ok(
UserMapper.toDto(
userService.create(
UserMapper.fromCreateDto(dto)))
);
}
@GetMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserDto> getById(@PathVariable("user_id") UUID userId) {
User user = userService.getById(userId);
return ResponseEntity.ok(UserMapper.toDto(user));
}
@GetMapping("/service/{service_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<List<UserWithCredentialDto>> getAllWithCredentials(
@PathVariable("service_id") Byte serviceId
) {
List<Credential> credentials = credentialService.getByServiceIdFetchUser(serviceId);
List<UserWithCredentialDto> result = credentials.stream()
.map(cred -> new UserWithCredentialDto(
UserMapper.toDto(cred.getUser()),
CredentialMapper.toDto(cred)
))
.toList();
return ResponseEntity.ok(result);
}
@GetMapping("/{user_id}/service/{service_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<UserWithCredentialDto> getByIdWithCredentials(
@PathVariable("user_id") UUID userId,
@PathVariable("service_id") Byte serviceId
) {
User user = userService.getById(userId);
Credential credential = credentialService.getByUserIdAndService(userId, serviceId);
return ResponseEntity.ok(
UserMapper.toDtoWithCredentials(
UserMapper.toDto(user),
CredentialMapper.toDto(credential)
)
);
}
@PutMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN') or #userId == principal.userId")
public ResponseEntity<UserDto> update(
@PathVariable("user_id") UUID userId,
@RequestBody UserDto dto
) {
User updated = userService.update(userId, UserMapper.fromDto(dto));
return ResponseEntity.ok(UserMapper.toDto(updated));
}
@GetMapping("/{user_id}/avatar")
public ResponseEntity<String> getAvatar(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getById(userId).getAvatar());
}
@PutMapping("/{user_id}/avatar")
public ResponseEntity<UserDto> updateAvatar(@PathVariable("user_id") UUID userId, @RequestBody ChangeAvatarRequest avatar) {
return ResponseEntity.ok(userService.updateAvatar(userId, avatar));
}
@GetMapping("/{user_id}/status")
public ResponseEntity<Byte> getStatus(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getStatus(userId));
}
@PutMapping("/{user_id}/status")
public ResponseEntity<Void> updateStatus(
@PathVariable("user_id") UUID userId,
@RequestBody ChangeStatusRequest req
) {
userService.updateStatus(userId, req.status());
return ResponseEntity.noContent().build();
}
@GetMapping("/{user_id}/role")
public ResponseEntity<Byte> getRole(@PathVariable("user_id") UUID userId) {
return ResponseEntity.ok(userService.getRole(userId));
}
@PutMapping("/{user_id}/role")
public ResponseEntity<Void> updateRole(
@PathVariable("user_id") UUID userid,
@RequestBody ChangeRoleRequest req
) {
userService.updateRole(userid, req.role());
return ResponseEntity.noContent().build();
}
@GetMapping("/{user_id}/exists")
public ResponseEntity<UserExistsResponse> exists(@PathVariable("user_id") UUID userId) {
boolean exists = userService.exists(userId);
return ResponseEntity.ok(new UserExistsResponse(exists));
}
@GetMapping("/me")
public ResponseEntity<UserDto> getMe(@RequestHeader("Authorization") String authHeader) {
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
String token = authHeader.substring(7);
UUID userId;
try {
userId = jwtService.getUserId(token);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
User user = userService.getById(userId);
return ResponseEntity.ok(UserMapper.toDto(user));
}
@DeleteMapping("/{user_id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<Void> delete(@PathVariable("user_id") UUID userId) {
userService.delete(userId);
return ResponseEntity.ok().build();
}
} }

View File

@@ -1,37 +0,0 @@
package net.miarma.backend.core.dto;
import jakarta.validation.constraints.NotBlank;
public class LoginRequest {
@NotBlank
private String username;
@NotBlank
private String password;
private short serviceId;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public short getServiceId() {
return serviceId;
}
public void setServiceId(short serviceId) {
this.serviceId = serviceId;
}
}

View File

@@ -1,37 +0,0 @@
package net.miarma.backend.core.dto;
public class LoginResponse {
private String token;
private short serviceId;
private UserDto user;
public LoginResponse(String token, short serviceId, UserDto user) {
this.token = token;
this.serviceId = serviceId;
this.user = user;
}
public String getToken() {
return token;
}
public void setToken(String token) {
this.token = token;
}
public short getServiceId() {
return serviceId;
}
public void setServiceId(short serviceId) {
this.serviceId = serviceId;
}
public UserDto getUser() {
return user;
}
public void setUser(UserDto user) {
this.user = user;
}
}

View File

@@ -0,0 +1,41 @@
package net.miarma.backend.core.dto;
import java.util.UUID;
public class UpdateCredentialDto {
private String email;
private String username;
private Byte status;
public UpdateCredentialDto() { }
public UpdateCredentialDto(String email, String username, Byte status) {
this.email = email;
this.username = username;
this.status = status;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Byte getStatus() {
return status;
}
public void setStatus(Byte status) {
this.status = status;
}
}

View File

@@ -1,81 +0,0 @@
package net.miarma.backend.core.dto;
import java.time.Instant;
import java.util.UUID;
public class UserDto {
private UUID userId;
private String displayName;
private String avatar;
private Byte globalStatus;
private Byte globalRole;
private Instant createdAt;
private Instant updatedAt;
public UserDto(UUID userId, String displayName, String avatar, Byte globalStatus, Byte globalRole,
Instant createdAt, Instant updatedAt) {
this.userId = userId;
this.displayName = displayName;
this.avatar = avatar;
this.globalStatus = globalStatus;
this.globalRole = globalRole;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public String getDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName;
}
public String getAvatar() {
return avatar;
}
public void setAvatar(String avatar) {
this.avatar = avatar;
}
public Byte getGlobalStatus() {
return globalStatus;
}
public void setGlobalStatus(Byte globalStatus) {
this.globalStatus = globalStatus;
}
public Byte getGlobalRole() {
return globalRole;
}
public void setGlobalRole(Byte globalRole) {
this.globalRole = globalRole;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(Instant updatedAt) {
this.updatedAt = updatedAt;
}
}

View File

@@ -0,0 +1,81 @@
package net.miarma.backend.core.mapper;
import net.miarma.backend.core.dto.UpdateCredentialDto;
import net.miarma.backend.core.model.Credential;
import net.miarma.backlib.dto.CreateCredentialDto;
import net.miarma.backlib.dto.CredentialDto;
import org.hibernate.sql.Update;
import java.util.UUID;
public class CredentialMapper {
public static CredentialDto toDto(Credential c) {
if (c == null) return null;
return new CredentialDto(
c.getCredentialId(),
c.getUserId(),
c.getServiceId(),
c.getUsername(),
c.getEmail(),
c.getStatus(),
c.getCreatedAt(),
c.getUpdatedAt()
);
}
public static CreateCredentialDto toCreateDto(Credential c) {
if (c == null) return null;
return new CreateCredentialDto(
c.getUserId(),
c.getServiceId(),
c.getUsername(),
c.getEmail(),
c.getPassword(),
c.getStatus()
);
}
public static Credential toEntity(CredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setCredentialId(dto.getCredentialId());
c.setUserId(dto.getUserId());
c.setServiceId(dto.getServiceId());
c.setUsername(dto.getUsername());
c.setEmail(dto.getEmail());
c.setStatus(dto.getStatus());
c.setCreatedAt(dto.getCreatedAt());
c.setUpdatedAt(dto.getUpdatedAt());
return c;
}
public static Credential toEntity(CreateCredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setUserId(dto.getUserId());
c.setServiceId(dto.getServiceId());
c.setUsername(dto.getUsername());
c.setEmail(dto.getEmail());
c.setPassword(dto.getPassword());
c.setStatus(dto.getStatus());
return c;
}
public static Credential toEntity(UpdateCredentialDto dto) {
if (dto == null) return null;
Credential c = new Credential();
c.setEmail(dto.getEmail());
c.setUsername(dto.getUsername());
c.setStatus(dto.getStatus());
return c;
}
}

View File

@@ -0,0 +1,38 @@
package net.miarma.backend.core.mapper;
import net.miarma.backend.core.model.File;
import net.miarma.backlib.dto.FileDto;
import java.time.Instant;
import java.util.UUID;
public class FileMapper {
private FileMapper() {}
public static FileDto.Response toResponse(File file) {
FileDto.Response res = new FileDto.Response();
res.setFileId(file.getFileId());
res.setFileName(file.getFileName());
res.setFilePath(file.getFilePath());
res.setMimeType(file.getMimeType());
res.setUploadedBy(file.getUploadedBy());
res.setUploadedAt(file.getUploadedAt());
res.setContext(file.getContext());
return res;
}
public static File toEntity(FileDto.Request req) {
File file = new File();
file.setFileId(UUID.randomUUID());
file.setFileName(req.getFileName());
file.setFilePath(req.getFilePath());
file.setMimeType(req.getMimeType());
file.setUploadedBy(req.getUploadedBy());
file.setContext(req.getContext());
return file;
}
}

View File

@@ -0,0 +1,52 @@
package net.miarma.backend.core.mapper;
import net.miarma.backend.core.model.User;
import net.miarma.backlib.dto.CreateUserDto;
import net.miarma.backlib.dto.CredentialDto;
import net.miarma.backlib.dto.UserDto;
import net.miarma.backlib.dto.UserWithCredentialDto;
import java.util.UUID;
public class UserMapper {
public static User fromDto(UserDto dto) {
if (dto == null) return null;
User user = new User();
user.setDisplayName(dto.getDisplayName());
user.setAvatar(dto.getAvatar());
user.setGlobalRole(dto.getGlobalRole());
user.setGlobalStatus(dto.getGlobalStatus());
return user;
}
public static User fromCreateDto(CreateUserDto dto) {
User user = new User();
user.setUserId(UUID.randomUUID());
user.setDisplayName(dto.displayName());
user.setAvatar(dto.avatar() != null ? dto.avatar() : null);
user.setGlobalRole((byte)0);
user.setGlobalStatus((byte)1);
return user;
}
public static UserDto toDto(User u) {
if (u == null) return null;
return new UserDto(
u.getUserId(),
u.getDisplayName(),
u.getAvatar(),
u.getGlobalStatus(),
u.getGlobalRole(),
u.getCreatedAt(),
u.getUpdatedAt()
);
}
public static UserWithCredentialDto toDtoWithCredentials(UserDto user, CredentialDto account){
if (user == null || account == null) return null;
return new UserWithCredentialDto(user, account);
}
}

View File

@@ -19,79 +19,66 @@ import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient; import jakarta.persistence.Transient;
import jakarta.persistence.UniqueConstraint; import jakarta.persistence.UniqueConstraint;
import net.miarma.backlib.util.UuidUtil;
@Entity @Entity
@Table( @Table(name = "credentials",
name = "credentials", uniqueConstraints = {
uniqueConstraints = { @UniqueConstraint(columnNames = { "service_id", "username" }),
@UniqueConstraint(columnNames = {"service_id", "username"}), @UniqueConstraint(columnNames = { "service_id", "email" })
@UniqueConstraint(columnNames = {"service_id", "email"}) })
}
)
public class Credential { public class Credential {
@Id @Id
@Column(columnDefinition = "BINARY(16)") @Column(name = "credential_id", columnDefinition = "BINARY(16)")
private byte[] credentialIdBin; private byte[] credentialIdBin;
@Column(name = "user_id", columnDefinition = "BINARY(16)")
private byte[] userIdBin;
@Transient
private UUID credentialId;
@Transient
private UUID userId;
@Column(name = "service_id")
private Byte serviceId;
private String username;
private String email;
private String password;
private Byte status;
@Column(name = "user_id", columnDefinition = "BINARY(16)") @CreationTimestamp
private byte[] userIdBin; private Instant createdAt;
@Transient @UpdateTimestamp
private UUID credentialId; private Instant updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@Transient @PrePersist
private UUID userId;
@Column(name = "service_id")
private Byte serviceId;
private String username;
private String email;
private String password;
private Byte status;
@CreationTimestamp
private Instant createdAt;
@UpdateTimestamp
private Instant updatedAt;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
private User user;
@PrePersist
@PreUpdate @PreUpdate
private void prePersist() { private void prePersist() {
if (credentialId != null) { if (credentialId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]); credentialIdBin = UuidUtil.uuidToBin(credentialId);
bb.putLong(credentialId.getMostSignificantBits());
bb.putLong(credentialId.getLeastSignificantBits());
credentialIdBin = bb.array();
} }
if (userId != null) { if (userId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]); userIdBin = UuidUtil.uuidToBin(userId);
bb.putLong(userId.getMostSignificantBits());
bb.putLong(userId.getLeastSignificantBits());
userIdBin = bb.array();
} }
} }
@PostLoad @PostLoad
private void postLoad() { private void postLoad() {
if (credentialIdBin != null) { if (credentialIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(credentialIdBin); credentialId = UuidUtil.binToUUID(credentialIdBin);
long high = bb.getLong();
long low = bb.getLong();
credentialId = new UUID(high, low);
} }
if (userIdBin != null) { if (userIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(userIdBin); userId = UuidUtil.binToUUID(userIdBin);
long high = bb.getLong();
long low = bb.getLong();
userId = new UUID(high, low);
} }
} }
@@ -174,4 +161,4 @@ public class Credential {
public void setUser(User user) { public void setUser(User user) {
this.user = user; this.user = user;
} }
} }

View File

@@ -6,9 +6,12 @@ import java.util.UUID;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.PostLoad;
import jakarta.persistence.PrePersist; import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient; import jakarta.persistence.Transient;
import net.miarma.backlib.util.UuidUtil;
@Entity @Entity
@Table(name = "files") @Table(name = "files")
@@ -40,15 +43,28 @@ public class File {
private Instant uploadedAt; private Instant uploadedAt;
@Column(name = "context", nullable = false) @Column(name = "context", nullable = false)
private Short context; private Byte context;
@PrePersist @PrePersist
public void prePersist() { @PreUpdate
if (fileId == null) { private void prePersist() {
fileId = UUID.randomUUID(); if (fileId != null) {
fileIdBin = UuidUtil.uuidToBin(fileId);
} }
if (uploadedAt == null) {
uploadedAt = Instant.now(); if (uploadedBy != null) {
uploadedByBin = UuidUtil.uuidToBin(uploadedBy);
}
}
@PostLoad
private void postLoad() {
if (fileIdBin != null) {
fileId = UuidUtil.binToUUID(fileIdBin);
}
if (uploadedByBin != null) {
uploadedBy = UuidUtil.binToUUID(uploadedByBin);
} }
} }
@@ -96,11 +112,11 @@ public class File {
return uploadedAt; return uploadedAt;
} }
public Short getContext() { public Byte getContext() {
return context; return context;
} }
public void setContext(Short context) { public void setContext(Byte context) {
this.context = context; this.context = context;
} }
} }

View File

@@ -1,6 +1,5 @@
package net.miarma.backend.core.model; package net.miarma.backend.core.model;
import java.nio.ByteBuffer;
import java.time.Instant; import java.time.Instant;
import java.util.UUID; import java.util.UUID;
@@ -15,6 +14,7 @@ import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate; import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import jakarta.persistence.Transient; import jakarta.persistence.Transient;
import net.miarma.backlib.util.UuidUtil;
@Entity @Entity
@Table(name = "users") @Table(name = "users")
@@ -48,20 +48,14 @@ public class User {
@PreUpdate @PreUpdate
private void prePersist() { private void prePersist() {
if (userId != null) { if (userId != null) {
ByteBuffer bb = ByteBuffer.wrap(new byte[16]); userIdBin = UuidUtil.uuidToBin(userId);
bb.putLong(userId.getMostSignificantBits());
bb.putLong(userId.getLeastSignificantBits());
userIdBin = bb.array();
} }
} }
@PostLoad @PostLoad
private void postLoad() { private void postLoad() {
if (userIdBin != null) { if (userIdBin != null) {
ByteBuffer bb = ByteBuffer.wrap(userIdBin); userId = UuidUtil.binToUUID(userIdBin);
long high = bb.getLong();
long low = bb.getLong();
userId = new UUID(high, low);
} }
} }

View File

@@ -4,19 +4,59 @@ import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import jakarta.validation.constraints.NotBlank;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import net.miarma.backend.core.model.Credential; import net.miarma.backend.core.model.Credential;
public interface CredentialRepository extends JpaRepository<Credential, UUID> { public interface CredentialRepository extends JpaRepository<Credential, byte[]> {
Optional<Credential> findByServiceIdAndUsername(short serviceId, String username); @Query("""
SELECT c FROM Credential c
JOIN FETCH c.user
WHERE c.serviceId = :serviceId
AND c.username = :username
""")
Optional<Credential> findByServiceIdAndUsername(@Param("serviceId") Byte serviceId,
@Param("username") String username);
Optional<Credential> findByServiceIdAndEmail(short serviceId, String email); List<Credential> findAllByServiceId(Byte serviceId);
Optional<Credential> findByUserIdAndServiceId(UUID userId, short serviceId); @Query("SELECT c FROM Credential c JOIN FETCH c.user WHERE c.serviceId = :serviceId")
List<Credential> getByServiceIdFetchUser(@Param("serviceId") Byte serviceId);
Optional<Credential> findByServiceIdAndEmail(Byte serviceId, String email);
@Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin AND c.serviceId = :serviceId")
Optional<Credential> findByUserIdAndServiceId(@Param("userIdBin") byte[] userIdBin, @Param("serviceId") Byte serviceId);
List<Credential> findByUserId(UUID userId); Optional<Credential> findByUsernameAndServiceId(String username, Byte serviceId);
@Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin")
List<Credential> findByUserId(@Param("userIdBin") byte[] userIdBin);
List<Credential> findByEmail(String email);
boolean existsByUsernameAndServiceId(String username, Byte serviceId);
boolean existsByEmailAndServiceId(String email, Byte serviceId);
@Modifying
@Query("""
UPDATE Credential c
SET c.username = :username,
c.email = :email,
c.status = :status
WHERE c.credentialIdBin = :credentialIdBin
""")
int update(@Param("credentialIdBin") byte[] credentialIdBin,
@Param("username") String username,
@Param("email") String email,
@Param("status") Byte status);
List<Credential> findByUsername(@NotBlank String username);
} }

View File

@@ -1,20 +1,16 @@
package net.miarma.backend.core.repository; package net.miarma.backend.core.repository;
import java.util.List; import java.util.List;
import java.util.Optional;
import java.util.UUID; import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import net.miarma.backend.core.model.File; import net.miarma.backend.core.model.File;
public interface FileRepository extends JpaRepository<File, UUID> { public interface FileRepository extends JpaRepository<File, byte[]> {
Optional<File> findById(UUID fileId);
List<File> findByUploadedBy(UUID uploadedBy); List<File> findByUploadedBy(UUID uploadedBy);
List<File> findByContext(short context); List<File> findByContext(Byte context);
List<File> findByUploadedByAndContext(UUID uploadedBy, short context); List<File> findByUploadedByAndContext(UUID uploadedBy, Byte context);
} }

View File

@@ -1,11 +1,9 @@
package net.miarma.backend.core.repository; package net.miarma.backend.core.repository;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import net.miarma.backend.core.model.User; import net.miarma.backend.core.model.User;
public interface UserRepository extends JpaRepository<User, UUID> { public interface UserRepository extends JpaRepository<User, byte[]> {
} }

View File

@@ -0,0 +1,46 @@
package net.miarma.backend.core.security;
import net.miarma.backlib.security.CoreAuthTokenHolder;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.UUID;
public class CorePrincipal implements UserDetails {
private final UUID userId;
private final Byte role;
public CorePrincipal(UUID userId, Byte role) {
this.userId = userId;
this.role = role;
}
public UUID getUserId() { return userId; }
public Byte getRole() { return role; }
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auth = new ArrayList<>();
String roleName = switch(role) {
case 0 -> "USER";
case 1 -> "ADMIN";
default -> "GUEST";
};
auth.add(new SimpleGrantedAuthority("ROLE_" + roleName));
return auth;
}
@Override public String getPassword() { return ""; }
@Override public String getUsername() { return userId.toString(); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; }
}

View File

@@ -1,4 +1,4 @@
package net.miarma.backend.core.config; package net.miarma.backend.core.security;
import java.io.IOException; import java.io.IOException;
import java.util.List; import java.util.List;
@@ -8,7 +8,6 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio
import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -17,7 +16,7 @@ import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import net.miarma.backend.core.model.User; import net.miarma.backend.core.model.User;
import net.miarma.backend.core.service.JwtService; import net.miarma.backlib.security.JwtService;
import net.miarma.backend.core.service.UserService; import net.miarma.backend.core.service.UserService;
@Component @Component
@@ -36,7 +35,6 @@ public class JwtFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request, protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException { FilterChain filterChain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization"); String authHeader = request.getHeader("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) { if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7); String token = authHeader.substring(7);
@@ -44,17 +42,11 @@ public class JwtFilter extends OncePerRequestFilter {
try { try {
if (jwtService.validateToken(token)) { if (jwtService.validateToken(token)) {
UUID userId = jwtService.getUserId(token); UUID userId = jwtService.getUserId(token);
short serviceId = jwtService.getServiceId(token); Byte serviceId = jwtService.getServiceId(token);
User user = userService.getById(userId); User user = userService.getById(userId);
CorePrincipal principal = new CorePrincipal(userId, user.getGlobalRole());
List<GrantedAuthority> authorities = List.of( UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(principal, null, principal.getAuthorities());
new SimpleGrantedAuthority("ROLE_" + user.getGlobalRole())
);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(user, null, authorities);
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth); SecurityContextHolder.getContext().setAuthentication(auth);
long timeLeft = jwtService.getExpiration(token).getTime() - System.currentTimeMillis(); long timeLeft = jwtService.getExpiration(token).getTime() - System.currentTimeMillis();
@@ -68,7 +60,7 @@ public class JwtFilter extends OncePerRequestFilter {
return; return;
} }
} }
filterChain.doFilter(request, response); filterChain.doFilter(request, response);
} }
} }

View File

@@ -1,46 +1,112 @@
package net.miarma.backend.core.service; package net.miarma.backend.core.service;
import java.util.Optional;
import java.util.UUID;
import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.*;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import net.miarma.backend.core.dto.LoginRequest; import net.miarma.backend.core.mapper.CredentialMapper;
import net.miarma.backend.core.dto.LoginResponse; import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.dto.UserDto;
import net.miarma.backend.core.model.Credential; import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.model.User;
import tools.jackson.databind.JsonNode;
@Service @Service
public class AuthService { public class AuthService {
private final CredentialService credentialService; private final CredentialService credentialService;
private final JwtService jwtService; private final UserService userService;
private final net.miarma.backlib.security.JwtService jwtService;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
public AuthService(CredentialService credentialService, JwtService jwtService, public AuthService(CredentialService credentialService, UserService userService,
PasswordEncoder passwordEncoder) { net.miarma.backlib.security.JwtService jwtService, PasswordEncoder passwordEncoder) {
this.credentialService = credentialService; this.credentialService = credentialService;
this.userService = userService;
this.jwtService = jwtService; this.jwtService = jwtService;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
public LoginResponse login(LoginRequest request) { public LoginResponse login(LoginRequest request) {
Credential cred = credentialService.getByUserIdAndService(request.getServiceId(), request.getUsername()); Credential cred = credentialService.getForLogin(request.serviceId(), request.username());
if (!passwordEncoder.matches(request.getPassword(), cred.getPassword())) { if (!passwordEncoder.matches(request.password(), cred.getPassword())) {
throw new RuntimeException("Invalid credentials"); throw new UnauthorizedException("Credenciales no válidas");
} }
String token = jwtService.generateToken(cred.getUserId(), request.getServiceId()); if (cred.getStatus() == 0) {
throw new ForbiddenException("Esa cuenta está desactivada");
}
UserDto userDto = new UserDto( String token = jwtService.generateToken(cred.getUserId(), request.serviceId());
cred.getUser().getUserId(), UserDto userDto = UserMapper.toDto(cred.getUser());
cred.getUser().getDisplayName(), CredentialDto credentialDto = CredentialMapper.toDto(cred);
cred.getUser().getAvatar(),
cred.getUser().getGlobalStatus(),
cred.getUser().getGlobalRole(),
cred.getUser().getCreatedAt(),
cred.getUser().getUpdatedAt()
);
return new LoginResponse(token, request.getServiceId(), userDto); return new LoginResponse(token, userDto, credentialDto);
}
public LoginResponse register(RegisterRequest request) {
UUID userIdByUsername = null;
UUID userIdByEmail = null;
Optional<Credential> credByUsername = credentialService.getByUsername(request.username()).stream().findFirst();
if(credByUsername.isPresent()) {
userIdByUsername = credByUsername.get().getUserId();
}
try {
userIdByEmail = credentialService.getByEmail(request.email()).get(0).getUserId();
} catch (NotFoundException e) { }
User user;
if (userIdByUsername != null && userIdByEmail != null) {
if (!userIdByUsername.equals(userIdByEmail)) {
throw new ConflictException("Username y email ya existen pero pertenecen a usuarios distintos");
}
user = userService.getById(userIdByUsername);
} else if (userIdByUsername != null) {
user = userService.getById(userIdByUsername);
} else if (userIdByEmail != null) {
user = userService.getById(userIdByEmail);
} else {
CreateUserDto dto = new CreateUserDto(request.displayName(), null);
user = userService.create(UserMapper.fromCreateDto(dto));
}
Credential cred = new Credential();
cred.setCredentialId(UUID.randomUUID());
cred.setUserId(user.getUserId());
cred.setUser(user);
cred.setServiceId(request.serviceId());
cred.setUsername(request.username());
cred.setEmail(request.email());
cred.setPassword(request.password());
cred.setStatus((byte)1);
credentialService.create(cred);
String token = jwtService.generateToken(user.getUserId(), request.serviceId());
return new LoginResponse(token, UserMapper.toDto(user), CredentialMapper.toDto(cred));
}
public void changePassword(UUID userId, ChangePasswordRequest request) {
Credential cred = credentialService.getByUserId(userId)
.stream()
.filter(c -> c.getServiceId().equals(request.serviceId()))
.findFirst()
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
throw new ValidationException("oldPassword", "La contraseña actual es incorrecta");
}
if (request.newPassword().length() < 8) {
throw new ValidationException("newPassword", "La nueva contraseña debe tener al menos 8 caracteres");
}
credentialService.updatePassword(cred.getCredentialId(), request);
} }
} }

View File

@@ -3,47 +3,164 @@ package net.miarma.backend.core.service;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import jakarta.validation.constraints.NotBlank;
import net.miarma.backlib.exception.*;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import net.miarma.backend.core.model.Credential; import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.model.User;
import net.miarma.backend.core.repository.CredentialRepository; import net.miarma.backend.core.repository.CredentialRepository;
import net.miarma.backlib.dto.ChangePasswordRequest;
import net.miarma.backlib.dto.CredentialDto;
import net.miarma.backlib.util.UuidUtil;
@Service @Service
@Transactional @Transactional
public class CredentialService { public class CredentialService {
private final CredentialRepository credentialRepository; private final CredentialRepository credentialRepository;
private final UserService userService;
private final PasswordEncoder passwordEncoder;
public CredentialService(CredentialRepository credentialRepository) { public CredentialService(CredentialRepository credentialRepository, UserService userService, PasswordEncoder passwordEncoder) {
this.credentialRepository = credentialRepository; this.credentialRepository = credentialRepository;
this.userService = userService;
this.passwordEncoder = passwordEncoder;
} }
public Credential getById(UUID id) { public Credential getById(UUID credentialId) {
return credentialRepository.findById(id) byte[] idBytes = UuidUtil.uuidToBin(credentialId);
.orElseThrow(() -> new RuntimeException("Credential not found")); return credentialRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
} }
public Credential create(Credential credential) { public Credential create(Credential credential) {
// TODO: validate duplicates here if (credential.getUsername() == null || credential.getUsername().isBlank()) {
throw new ValidationException("userName", "El usuario no puede estar vacío");
}
if (credential.getEmail() == null || !credential.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) {
throw new ValidationException("email", "Formato de email no válido");
}
if (credential.getPassword() == null || credential.getPassword().length() < 6) {
throw new ValidationException("password", "La contraseña tiene que tener al menos 6 caracteres");
}
if (credential.getServiceId() == null || credential.getServiceId() < 0) {
throw new ValidationException("serviceId", "El identificador de servicio debe ser positivo");
}
boolean existsUsername = credentialRepository.existsByUsernameAndServiceId(
credential.getUsername(), credential.getServiceId());
if (existsUsername) throw new ConflictException("El usuario ya existe para este servicio");
boolean existsEmail = credentialRepository.existsByEmailAndServiceId(
credential.getEmail(), credential.getServiceId());
if (existsEmail) throw new ConflictException("El email ya existe para este servicio");
credential.setCredentialId(UUID.randomUUID());
credential.setPassword(passwordEncoder.encode(credential.getPassword()));
return credentialRepository.save(credential); return credentialRepository.save(credential);
} }
public List<Credential> getAll() {
return credentialRepository.findAll();
}
public List<Credential> getByServiceId(Byte serviceId) {
return credentialRepository.findAllByServiceId(serviceId);
}
public List<Credential> getByServiceIdFetchUser(Byte serviceId) {
return credentialRepository.getByServiceIdFetchUser(serviceId);
}
public List<Credential> getByUserId(UUID userId) { public List<Credential> getByUserId(UUID userId) {
List<Credential> creds = credentialRepository.findByUserId(userId); List<Credential> creds = credentialRepository.findByUserId(UuidUtil.uuidToBin(userId));
if (creds.isEmpty()) { if (creds.isEmpty()) {
throw new RuntimeException("User has no credentials"); throw new NotFoundException("El usuario no tiene cuenta");
} }
return creds; return creds;
} }
public List<Credential> getByUsername(@NotBlank String username) {
return credentialRepository.findByUsername(username);
}
public Credential getByUserIdAndService(short serviceId, String username) { public List<Credential> getByEmail(String email) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username) return credentialRepository.findByEmail(email).stream()
.orElseThrow(() -> new RuntimeException("Credential not found in this site")); .toList();
}
public Credential getByUserIdAndService(UUID userId, Byte serviceId) {
return credentialRepository.findByUserIdAndServiceId(UuidUtil.uuidToBin(userId), serviceId)
.orElseThrow(() -> new NotFoundException("El usuario no tiene cuenta en este sitio"));
} }
public Credential getForLogin(short serviceId, String username) { public Credential getForLogin(Byte serviceId, String username) {
return credentialRepository.findByServiceIdAndUsername(serviceId, username) return credentialRepository.findByServiceIdAndUsername(serviceId, username)
.orElseThrow(() -> new RuntimeException("Invalid credentials")); .orElseThrow(() -> new BadRequestException("Credenciales no válidas"));
}
public boolean existsByUsernameAndService(String username, Byte serviceId) {
return credentialRepository.findByUsernameAndServiceId(username, serviceId).isPresent();
}
public boolean isOwner(UUID credentialId, UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
return credentialRepository.findById(idBytes)
.map(c -> c.getUserId().equals(userId))
.orElse(false);
}
public Credential update(UUID credentialId, Credential changes) {
Credential cred = getById(credentialId);
if (cred.getStatus() == (byte)0) {
throw new ForbiddenException("La cuenta está inactiva, contacta con un administrador");
}
if (changes.getUsername() != null) cred.setUsername(changes.getUsername());
if (changes.getEmail() != null) cred.setEmail(changes.getEmail());
if (changes.getStatus() != null) cred.setStatus(changes.getStatus());
int updated = credentialRepository.update(
UuidUtil.uuidToBin(cred.getCredentialId()),
cred.getUsername(),
cred.getEmail(),
cred.getStatus()
);
if (updated == 0)
throw new RuntimeException("No se pudo actualizar la cuenta");
return getById(credentialId);
}
public Credential updatePassword(UUID credentialId, ChangePasswordRequest request) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
Credential cred = credentialRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Cuenta no encontrada"));
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
throw new ValidationException("oldPassword", "La contraseña actual es incorrecta");
}
cred.setPassword(passwordEncoder.encode(request.newPassword()));
return credentialRepository.save(cred);
}
public void delete(UUID credentialId) {
byte[] idBytes = UuidUtil.uuidToBin(credentialId);
if(!credentialRepository.existsById(idBytes))
throw new NotFoundException("Cuenta no encontrada");
credentialRepository.deleteById(idBytes);
}
public Byte getStatus(UUID userId, Byte serviceId) {
Credential credential = getByUserIdAndService(userId, serviceId);
return credential.getStatus();
} }
} }

View File

@@ -1,13 +1,24 @@
package net.miarma.backend.core.service; package net.miarma.backend.core.service;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.UUID; import java.util.UUID;
import net.miarma.backend.core.mapper.FileMapper;
import net.miarma.backlib.dto.FileDto;
import net.miarma.backlib.exception.NotFoundException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import net.miarma.backend.core.model.File; import net.miarma.backend.core.model.File;
import net.miarma.backend.core.repository.FileRepository; import net.miarma.backend.core.repository.FileRepository;
import net.miarma.backlib.util.UuidUtil;
@Service @Service
@Transactional @Transactional
@@ -15,32 +26,65 @@ public class FileService {
private final FileRepository fileRepository; private final FileRepository fileRepository;
@Value("${filesDir}")
private String filesDir;
public FileService(FileRepository fileRepository) { public FileService(FileRepository fileRepository) {
this.fileRepository = fileRepository; this.fileRepository = fileRepository;
} }
public File get(UUID fileId) { public File getById(UUID fileId) {
return fileRepository.findById(fileId) byte[] idBytes = UuidUtil.uuidToBin(fileId);
.orElseThrow(() -> new RuntimeException("File not found")); return fileRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Archivo no encontrado"));
} }
public List<File> listByUser(UUID userId) { public List<File> getAll() {
return fileRepository.findAll();
}
public List<File> getByUserId(UUID userId) {
return fileRepository.findByUploadedBy(userId); return fileRepository.findByUploadedBy(userId);
} }
public List<File> listByContext(short context) { public File create(File file, byte[] fileBinary) throws IOException {
return fileRepository.findByContext(context); Path dirPath = Paths.get(filesDir, String.valueOf(file.getContext()));
} if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath);
}
Path filePath = dirPath.resolve(file.getFileName());
try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) {
fos.write(fileBinary);
}
file.setFilePath(filePath.toString());
public File create(File file) {
return fileRepository.save(file); return fileRepository.save(file);
} }
public void delete(UUID fileId) { public File update(UUID fileId, File file) {
if (!fileRepository.existsById(fileId)) { byte[] idBytes = UuidUtil.uuidToBin(fileId);
throw new RuntimeException("File not found"); if (!fileRepository.existsById(idBytes)) {
throw new NotFoundException("Archivo no encontrado");
} }
fileRepository.deleteById(fileId); return fileRepository.save(file);
} }
public void delete(UUID fileId) {
byte[] idBytes = UuidUtil.uuidToBin(fileId);
if (!fileRepository.existsById(idBytes)) {
throw new NotFoundException("Archivo no encontrado");
}
fileRepository.deleteById(idBytes);
}
public boolean isOwner(UUID fileId, UUID userId) {
byte[] fileBytes = UuidUtil.uuidToBin(fileId);
return fileRepository.findById(fileBytes)
.map(f -> f.getUploadedBy().equals(userId))
.orElse(false);
}
} }

View File

@@ -1,56 +0,0 @@
package net.miarma.backend.core.service;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Service;
import java.security.Key;
import java.util.Date;
import java.util.UUID;
@Service
public class JwtService {
private final String secret = "miarma-esto-es-un-secreto-super-largo-para-jwt-1234567890";
private final Key key = Keys.hmacShaKeyFor(secret.getBytes());
private final long expiration = 3600_000;
public String generateToken(UUID userId, short serviceId) {
Date now = new Date();
Date exp = new Date(now.getTime() + expiration);
return Jwts.builder()
.setSubject(userId.toString())
.claim("service", serviceId)
.setIssuedAt(now)
.setExpiration(exp)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
public UUID getUserId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return UUID.fromString(claims.getSubject());
}
public short getServiceId(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return ((Number) claims.get("service")).shortValue();
}
public Date getExpiration(String token) {
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
return claims.getExpiration();
}
}

View File

@@ -1,12 +1,21 @@
package net.miarma.backend.core.service; package net.miarma.backend.core.service;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import net.miarma.backend.core.mapper.UserMapper;
import net.miarma.backend.core.security.CorePrincipal;
import net.miarma.backlib.dto.ChangeAvatarRequest;
import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import net.miarma.backend.core.model.User; import net.miarma.backend.core.model.User;
import net.miarma.backend.core.repository.UserRepository; import net.miarma.backend.core.repository.UserRepository;
import net.miarma.backlib.dto.UserDto;
import net.miarma.backlib.util.UuidUtil;
@Service @Service
@Transactional @Transactional
@@ -17,25 +26,88 @@ public class UserService {
this.userRepository = userRepository; this.userRepository = userRepository;
} }
public List<User> getAll() {
return userRepository.findAll();
}
public User getById(UUID userId) { public User getById(UUID userId) {
return userRepository.findById(userId) byte[] idBytes = UuidUtil.uuidToBin(userId);
.orElseThrow(() -> new RuntimeException("User not found")); return userRepository.findById(idBytes)
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
} }
public User create(User user) { public User create(User user) {
// TODO: basic validation if(user.getDisplayName() == null || user.getDisplayName().isBlank()) {
return userRepository.save(user); throw new ValidationException("displayName", "El nombre a mostrar es necesario");
} }
return userRepository.save(user);
public User update(User user) { }
if(!userRepository.existsById(user.getUserId()))
throw new RuntimeException("User not found"); public User update(UUID userId, User changes) {
return userRepository.save(user); User user = userRepository.findById(UuidUtil.uuidToBin(userId))
} .orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
public void delete(UUID userId) { if (changes.getDisplayName() != null) {
if(!userRepository.existsById(userId)) String dn = changes.getDisplayName().trim();
throw new RuntimeException("User not found"); if (dn.isEmpty()) throw new ValidationException("displayName", "No puede estar vacío");
userRepository.deleteById(userId); if (dn.length() > 50) throw new ValidationException("displayName", "Máx 50 caracteres");
user.setDisplayName(dn);
}
if (changes.getAvatar() != null)
user.setAvatar(changes.getAvatar());
if (changes.getGlobalRole() != null)
user.setGlobalRole(changes.getGlobalRole());
if (changes.getGlobalStatus() != null)
user.setGlobalStatus(changes.getGlobalStatus());
return userRepository.save(user);
}
public void delete(UUID userId) {
byte[] idBytes = UuidUtil.uuidToBin(userId);
if(!userRepository.existsById(idBytes))
throw new NotFoundException("Usuario no encontrado");
userRepository.deleteById(idBytes);
} }
public UserDto updateAvatar(UUID userId, ChangeAvatarRequest req) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));
user.setAvatar(req.avatar());
userRepository.save(user);
return UserMapper.toDto(user);
}
public Byte getStatus(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
return user.getGlobalStatus();
}
public void updateStatus(UUID userId, Byte status) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
user.setGlobalStatus(status);
userRepository.save(user);
}
public Byte getRole(UUID userId) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
return user.getGlobalRole();
}
public void updateRole(UUID userId, Byte role) {
User user = userRepository.findById(UuidUtil.uuidToBin(userId))
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
user.setGlobalRole(role);
userRepository.save(user);
}
public boolean exists(UUID userId) {
return userRepository.existsById(UuidUtil.uuidToBin(userId));
}
} }

View File

@@ -0,0 +1,23 @@
server:
port: 8080
servlet:
context-path: /v2/core
spring:
datasource:
url: jdbc:mariadb://localhost:3306/miarma_v2
username: admin
password: ${DB_PASS}
driver-class-name: org.mariadb.jdbc.Driver
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.orm.jdbc.bind: TRACE
org.springframework.security: DEBUG
filesDir: "/home/jomaa/.config/miarma-backend/files"
jwt:
private-key-path: /home/jomaa/.config/miarma-backend/private.pem
public-key-path: /home/jomaa/.config/miarma-backend/public.pem

View File

@@ -0,0 +1,22 @@
server:
port: 8080
servlet:
context-path: /v2/core
spring:
datasource:
url: jdbc:mariadb://mariadb:3306/miarma_v2
username: ${DB_USER}
password: ${DB_PASS}
driver-class-name: org.mariadb.jdbc.Driver
logging:
level:
org.springframework.security: INFO
org.hibernate.SQL: WARN
filesDir: "/files"
jwt:
private-key-path: ${JWT_PRIVATE_KEY}
public-key-path: ${JWT_PUBLIC_KEY}

View File

@@ -1,12 +1,25 @@
spring: spring:
datasource: application:
url: jdbc:mariadb://localhost:3306/miarma_v2 name: core-service
username: USER
password: PASS
jpa: jpa:
open-in-view: false
hibernate: hibernate:
ddl-auto: update ddl-auto: validate
show-sql: true
properties: properties:
hibernate: hibernate:
format_sql: true jdbc:
time_zone: UTC
jackson:
default-property-inclusion: non_null
time-zone: Europe/Madrid
jwt:
expiration-ms: 3600000
management:
endpoints:
web:
exposure:
include: health,info

View File

@@ -1,40 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="target/classes" path="src/main/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/classes" path="src/main/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="target/test-classes" path="src/test/java">
<attributes>
<attribute name="optional" value="true"/>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry excluding="**" kind="src" output="target/test-classes" path="src/test/resources">
<attributes>
<attribute name="maven.pomderived" value="true"/>
<attribute name="test" value="true"/>
<attribute name="optional" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER">
<attributes>
<attribute name="maven.pomderived" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="output" path="target/classes"/>
</classpath>

View File

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>huertos</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.m2e.core.maven2Builder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.m2e.core.maven2Nature</nature>
</natures>
</projectDescription>

View File

@@ -1,8 +0,0 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled
org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore
org.eclipse.jdt.core.compiler.release=disabled
org.eclipse.jdt.core.compiler.source=1.8

View File

@@ -1,4 +0,0 @@
activeProfiles=
eclipse.preferences.version=1
resolveWorkspaceProjects=true
version=1

9
huertos/huertos.iml Normal file
View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$" dumb="true">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
</content>
</component>
</module>

View File

@@ -9,6 +9,18 @@
</parent> </parent>
<artifactId>huertos</artifactId> <artifactId>huertos</artifactId>
<properties>
<maven.compiler.source>25</maven.compiler.source>
<maven.compiler.target>25</maven.compiler.target>
</properties>
<repositories>
<repository>
<id>gitea</id>
<url>https://git.miarma.net/api/packages/Gallardo7761/maven</url>
</repository>
</repositories>
<dependencies> <dependencies>
<!-- Spring Boot --> <!-- Spring Boot -->
<dependency> <dependency>
@@ -28,6 +40,21 @@
<artifactId>mariadb-java-client</artifactId> <artifactId>mariadb-java-client</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-http-client</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<!-- CACHE -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<!-- JWT --> <!-- JWT -->
<dependency> <dependency>
<groupId>io.jsonwebtoken</groupId> <groupId>io.jsonwebtoken</groupId>
@@ -46,6 +73,30 @@
<version>0.11.5</version> <version>0.11.5</version>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
</dependencies> <dependency>
<groupId>net.miarma</groupId>
<artifactId>backlib</artifactId>
<version>1.1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project> </project>

View File

@@ -0,0 +1,16 @@
package net.miarma.backend.huertos;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching
@SpringBootApplication(scanBasePackages = {
"net.miarma.backend.huertos",
"net.miarma.backlib"
})
public class HuertosApplication {
public static void main(String[] args) {
SpringApplication.run(HuertosApplication.class, args);
}
}

View File

@@ -0,0 +1,73 @@
package net.miarma.backend.huertos.client;
import net.miarma.backlib.dto.ApiErrorDto;
import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse;
import net.miarma.backlib.exception.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException;
import org.springframework.web.client.RestTemplate;
@Component
public class CoreAuthClient {
private final RestTemplate restTemplate;
private final String coreUrl;
public CoreAuthClient(
@Qualifier("authRestTemplate") RestTemplate restTemplate,
@Value("${core.url}") String coreUrl
) {
this.restTemplate = restTemplate;
this.coreUrl = coreUrl;
}
public LoginResponse login(LoginRequest req) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<LoginRequest> requestEntity = new HttpEntity<>(req, headers);
ResponseEntity<LoginResponse> response = restTemplate.exchange(
coreUrl + "/auth/login",
HttpMethod.POST,
requestEntity,
LoginResponse.class
);
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
return response.getBody();
}
private void handleError(ResponseEntity<?> response) {
HttpStatusCode statusCode = response.getStatusCode();
if (statusCode.equals(HttpStatus.UNAUTHORIZED)) {
throw new UnauthorizedException("Credenciales no válidas");
} else if (statusCode.equals(HttpStatus.FORBIDDEN)) {
throw new ForbiddenException("Esa cuenta está desactivada");
} else if (statusCode.equals(HttpStatus.NOT_FOUND)) {
throw new NotFoundException("No encontrado");
} else if (statusCode.equals(HttpStatus.BAD_REQUEST)) {
throw new BadRequestException("Datos de solicitud faltantes");
} else if (statusCode.equals(HttpStatus.CONFLICT)) {
throw new ConflictException("Ya existe");
} else if (statusCode.equals(HttpStatus.UNPROCESSABLE_CONTENT)) {
throw new ValidationException("general", "Los datos no tienen formato válido");
} else {
if (statusCode.is4xxClientError()) {
throw new BadRequestException(response.getBody().toString());
} else {
throw new RuntimeException("Error desconocido");
}
}
}
}

View File

@@ -0,0 +1,191 @@
package net.miarma.backend.huertos.client;
import net.miarma.backend.huertos.dto.RequestMetadataDto;
import net.miarma.backend.huertos.model.RequestMetadata;
import net.miarma.backend.huertos.util.UsernameGenerator;
import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.*;
import net.miarma.backlib.security.PasswordGenerator;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import tools.jackson.databind.ObjectMapper;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
@Component
public class HuertosWebClient {
private final RestTemplate restTemplate;
private final String coreUrl;
private final ObjectMapper objectMapper;
public HuertosWebClient(@Qualifier("secureRestTemplate") RestTemplate restTemplate,
@Value("${core.url}") String coreUrl,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate;
this.coreUrl = coreUrl;
this.objectMapper = objectMapper;
}
public UserWithCredentialDto getUserWithCredential(UUID userId, Byte serviceId) {
ResponseEntity<UserWithCredentialDto> response = restTemplate.exchange(
coreUrl + "/users/{user_id}/service/{service_id}",
HttpMethod.GET,
null,
UserWithCredentialDto.class,
userId, serviceId
);
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
return response.getBody();
}
public List<UserWithCredentialDto> getAllUsersWithCredentials(Byte serviceId) {
ResponseEntity<UserWithCredentialDto[]> response = restTemplate.exchange(
coreUrl + "/users/service/{service_id}",
HttpMethod.GET,
null,
UserWithCredentialDto[].class,
serviceId
);
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
UserWithCredentialDto[] arr = response.getBody();
return arr == null ? List.of() : Arrays.asList(arr);
}
public UserWithCredentialDto createUser(RequestMetadataDto metadataDto) {
// 1. Crear el usuario
CreateUserDto userDto = new CreateUserDto(metadataDto.displayName(), null);
HttpEntity<CreateUserDto> userRequestEntity = new HttpEntity<>(userDto);
ResponseEntity<UserDto> userResponse = restTemplate.exchange(
coreUrl + "/users",
HttpMethod.POST,
userRequestEntity,
UserDto.class
);
if (!userResponse.getStatusCode().is2xxSuccessful()) {
handleError(userResponse);
}
UserDto createdUser = userResponse.getBody();
if (createdUser == null) {
throw new RuntimeException("No se pudo crear al usuario");
}
CreateCredentialDto credDto = new CreateCredentialDto(
createdUser.getUserId(),
(byte) 1,
UsernameGenerator.generate(metadataDto.displayName(), metadataDto.memberNumber()),
metadataDto.email(),
PasswordGenerator.generate(8),
(byte) 1
);
HttpEntity<CreateCredentialDto> credRequestEntity = new HttpEntity<>(credDto);
ResponseEntity<CredentialDto> credResponse = restTemplate.exchange(
coreUrl + "/credentials",
HttpMethod.POST,
credRequestEntity,
CredentialDto.class
);
if (!credResponse.getStatusCode().is2xxSuccessful()) {
handleError(credResponse);
}
CredentialDto createdCred = credResponse.getBody();
if (createdCred == null) {
throw new RuntimeException("No se pudo crear la cuenta del usuario");
}
return new UserWithCredentialDto(createdUser, createdCred);
}
public void deleteUser(UUID userId) {
ResponseEntity<Void> response = restTemplate.exchange(
coreUrl + "/users/{user_id}",
HttpMethod.DELETE,
null,
Void.class,
userId
);
if (!response.getStatusCode().is2xxSuccessful()) {
if (response.getStatusCode() != HttpStatus.NOT_FOUND) {
handleError(response);
}
}
}
public Byte getCredentialStatus(UUID userId, Byte serviceId) {
ResponseEntity<Byte> response = restTemplate.exchange(
coreUrl + "/credentials/{service_id}/{user_id}/status",
HttpMethod.GET,
null,
Byte.class,
serviceId, userId
);
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
return response.getBody();
}
public void updateCredentialStatus(UUID userId, Byte serviceId, Byte newStatus) {
ChangeStatusRequest req = new ChangeStatusRequest(newStatus);
HttpEntity<ChangeStatusRequest> requestEntity = new HttpEntity<>(req);
ResponseEntity<Void> response = restTemplate.exchange(
coreUrl + "/credentials/{service_id}/{user_id}/status",
HttpMethod.PUT,
requestEntity,
Void.class,
serviceId, userId
);
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
}
private void handleError(ResponseEntity<?> response) {
HttpStatusCode statusCode = response.getStatusCode();
if (statusCode.equals(HttpStatus.UNAUTHORIZED)) {
throw new UnauthorizedException("Credenciales no válidas");
} else if (statusCode.equals(HttpStatus.FORBIDDEN)) {
throw new ForbiddenException("Esa cuenta está desactivada");
} else if (statusCode.equals(HttpStatus.NOT_FOUND)) {
throw new NotFoundException("No encontrado");
} else if (statusCode.equals(HttpStatus.BAD_REQUEST)) {
throw new BadRequestException("Datos de solicitud faltantes");
} else if (statusCode.equals(HttpStatus.CONFLICT)) {
throw new ConflictException("Ya existe");
} else if (statusCode.equals(HttpStatus.UNPROCESSABLE_CONTENT)) {
throw new ValidationException("general", "Los datos no tienen formato válido");
} else {
if (statusCode.is4xxClientError()) {
throw new BadRequestException(response.getBody().toString());
} else {
throw new RuntimeException("Error desconocido");
}
}
}
}

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