Compare commits

...

7 Commits

50 changed files with 908 additions and 398 deletions

20
TODO
View File

@@ -1,14 +1,18 @@
POR HACER -------------------------------- POR HACER --------------------------------
- implementar urlParams para filtros
- documentación
- mail wrapper
RESUELTO ---------------------------------
- añadir colaborador desde perfil - añadir colaborador desde perfil
- apuntarse lista espera - apuntarse lista espera
- aceptar solicitudes LE/Colab (sobre todo por crear preusers) - aceptar solicitudes LE/Colab (sobre todo por crear preusers)
- mail wrapper
- documentación
- cambiar contraseña (?)
- implementar urlParams para filtros
- sistema comun de errores en back & front
RESUELTO ---------------------------------
- mejorar queries para no filtrar en memoria -> IMPOSIBLE CON ENDPOINTS INTERNOS DE CORE: RESUELTO CON CACHING - 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 - 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

View File

@@ -4,7 +4,7 @@
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>backlib</artifactId> <artifactId>backlib</artifactId>
<groupId>net.miarma</groupId> <groupId>net.miarma</groupId>
<version>1.0.1</version> <version>1.1.0</version>
<properties> <properties>
<java.version>25</java.version> <java.version>25</java.version>

View File

@@ -7,11 +7,15 @@ import java.util.List;
import java.util.Map; import java.util.Map;
public class ApiValidationErrorDto { public class ApiValidationErrorDto {
private int status;
private Map<String,String> errors; private Map<String,String> errors;
private String path;
private Instant timestamp; private Instant timestamp;
public ApiValidationErrorDto(Map<String,String> errors) { public ApiValidationErrorDto(Map<String,String> errors, String path) {
this.status = 422;
this.errors = errors; this.errors = errors;
this.path = path;
this.timestamp = Instant.now(); this.timestamp = Instant.now();
} }
@@ -19,6 +23,22 @@ public class ApiValidationErrorDto {
return errors; 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) { public void setErrors(Map<String,String> errors) {
this.errors = errors; this.errors = errors;
} }

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

@@ -1,12 +1,14 @@
package net.miarma.backend.huertos.config; package net.miarma.backlib.http;
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.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration @Configuration
public class CorsConfig { @Profile("dev") // esto asegura que solo se cargue en dev
public class DevCorsConfig {
@Bean @Bean
public WebMvcConfigurer corsConfigurer() { public WebMvcConfigurer corsConfigurer() {
@@ -14,12 +16,10 @@ public class CorsConfig {
@Override @Override
public void addCorsMappings(CorsRegistry registry) { public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") registry.addMapping("/**")
.allowedOrigins( .allowedOrigins("http://localhost:3000") // tu frontend React
"http://localhost:3000"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*") .allowCredentials(true)
.allowCredentials(true); .allowedHeaders("*");
} }
}; };
} }

View File

@@ -84,9 +84,10 @@ public class GlobalExceptionHandler {
} }
@ExceptionHandler(ValidationException.class) @ExceptionHandler(ValidationException.class)
public ResponseEntity<ApiValidationErrorDto> handleValidation(ValidationException ex) { public ResponseEntity<ApiValidationErrorDto> handleValidation(
ValidationException ex, HttpServletRequest req) {
Map<String, String> errors = Map.of(ex.getField(), ex.getMessage()); Map<String, String> errors = Map.of(ex.getField(), ex.getMessage());
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors)); return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors, req.getRequestURI()));
} }
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)

View File

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

View File

@@ -1,10 +1,12 @@
/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/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/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/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/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/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/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/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/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/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/LoginRequest.java
@@ -19,10 +21,13 @@
/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/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/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/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/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/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/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/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/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/security/ServiceAuthFilter.java
/home/jomaa/git/miarma-backend/backlib/src/main/java/net/miarma/backlib/util/UuidUtil.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

@@ -83,7 +83,7 @@
<dependency> <dependency>
<groupId>net.miarma</groupId> <groupId>net.miarma</groupId>
<artifactId>backlib</artifactId> <artifactId>backlib</artifactId>
<version>1.0.1</version> <version>1.1.0</version>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -1,28 +0,0 @@
package net.miarma.backend.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig {
@Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:3000",
"http://localhost:8081",
"http://huertos:8081"
)
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}

View File

@@ -5,22 +5,16 @@ import net.miarma.backlib.http.RestAccessDeniedHandler;
import net.miarma.backlib.http.RestAuthEntryPoint; 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.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer; import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; 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.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List; import java.util.Optional;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -29,34 +23,27 @@ public class SecurityConfig {
private final JwtFilter jwtFilter; private final JwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint; private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler; private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig( public SecurityConfig(
JwtFilter jwtFilter, JwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint, RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) { ) {
this.jwtFilter = jwtFilter; this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint; this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler; this.accessDeniedHandler = accessDeniedHandler;
} this.corsConfigurationSource = corsConfigurationSource.orElse(null);
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
} }
@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
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex .exceptionHandling(ex -> ex
@@ -64,7 +51,10 @@ public class SecurityConfig {
.accessDeniedHandler(accessDeniedHandler) .accessDeniedHandler(accessDeniedHandler)
) )
.authorizeHttpRequests(auth -> auth .authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**", "/screenshot").permitAll() .requestMatchers("/auth/login").permitAll()
.requestMatchers("/auth/refresh").permitAll()
.requestMatchers("/auth/change-password").permitAll()
.requestMatchers("/screenshot").permitAll()
.anyRequest().authenticated() .anyRequest().authenticated()
); );

View File

@@ -4,6 +4,7 @@ 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.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
@@ -14,25 +15,16 @@ import net.miarma.backend.core.model.Credential;
import net.miarma.backend.core.service.AuthService; import net.miarma.backend.core.service.AuthService;
import net.miarma.backend.core.service.CredentialService; import net.miarma.backend.core.service.CredentialService;
import net.miarma.backlib.security.JwtService; import net.miarma.backlib.security.JwtService;
import net.miarma.backlib.dto.ChangePasswordRequest;
import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse;
import net.miarma.backlib.dto.RegisterRequest;
@RestController @RestController
@RequestMapping("/auth") @RequestMapping("/auth")
public class AuthController { public class AuthController {
private final CredentialService credentialService;
private final JwtService jwtService; private final JwtService jwtService;
private final PasswordEncoder passwordEncoder;
private final AuthService authService; private final AuthService authService;
public AuthController(CredentialService credentialService, JwtService jwtService, public AuthController(JwtService jwtService, AuthService authService) {
PasswordEncoder passwordEncoder, AuthService authService) {
this.credentialService = credentialService;
this.jwtService = jwtService; this.jwtService = jwtService;
this.passwordEncoder = passwordEncoder;
this.authService = authService; this.authService = authService;
} }
@@ -49,15 +41,29 @@ public class AuthController {
return ResponseEntity.ok(authService.register(request)); return ResponseEntity.ok(authService.register(request));
} }
@PostMapping("/refresh") @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);
@@ -66,45 +72,47 @@ public class AuthController {
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") @PostMapping("/change-password")
public ResponseEntity<?> changePassword( public ResponseEntity<?> changePassword(
@RequestHeader("Authorization") String authHeader, @RequestHeader("Authorization") String authHeader,
@Valid @RequestBody ChangePasswordRequest request @RequestBody ChangePasswordRequest request
) { ) {
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);
Credential cred = credentialService.getByUserId(userId) authService.changePassword(userId, request);
.stream() return ResponseEntity.ok(Map.of("message", "Contraseña cambiada correctamente"));
.filter(c -> c.getServiceId().equals(request.serviceId()))
.findFirst().get();
if (cred == null) {
return ResponseEntity.status(404).body("Credential not found");
}
if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) {
return ResponseEntity.status(400).body("Old password is incorrect");
}
credentialService.updatePassword(cred.getCredentialId(), request);
return ResponseEntity.ok(Map.of("message", "Password changed successfully"));
} }
@GetMapping("/validate") @GetMapping("/validate")
public ResponseEntity<Boolean> validate(@RequestHeader("Authorization") String authHeader) { public ResponseEntity<Boolean> validate(@RequestHeader("Authorization") String authHeader) {
String token = authHeader.substring(7); String token = authHeader.substring(7);

View File

@@ -70,17 +70,18 @@ public class CredentialController {
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
@GetMapping("/{credential_id}/status") @GetMapping("/{service_id}/{user_id}/status")
public ResponseEntity<Byte> getStatus(@PathVariable("credential_id") UUID credentialId) { public ResponseEntity<Byte> getStatus(@PathVariable("user_id") UUID userId, @PathVariable("service_id") Byte serviceId) {
return ResponseEntity.ok(credentialService.getStatus(credentialId)); return ResponseEntity.ok(credentialService.getStatus(userId, serviceId));
} }
@PutMapping("/{credential_id}/status") @PutMapping("/{service_id}/{user_id}/status")
public ResponseEntity<Void> updateStatus( public ResponseEntity<Void> updateStatus(
@PathVariable("credential_id") UUID credentialId, @PathVariable("user_id") UUID userId,
@PathVariable("service_id") Byte serviceId,
@RequestBody ChangeStatusRequest req @RequestBody ChangeStatusRequest req
) { ) {
credentialService.updateStatus(credentialId, req.status()); credentialService.updateStatus(userId, serviceId, req.status());
return ResponseEntity.noContent().build(); return ResponseEntity.noContent().build();
} }
} }

View File

@@ -44,33 +44,44 @@ public class FileController {
return ResponseEntity.ok(files); return ResponseEntity.ok(files);
} }
@GetMapping("/{fileId}") @GetMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)") @PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> getById(@PathVariable("file_id") UUID fileId) { public ResponseEntity<File> getById(@PathVariable("file_id") UUID fileId) {
File file = fileService.getById(fileId); File file = fileService.getById(fileId);
return ResponseEntity.ok(file); return ResponseEntity.ok(file);
} }
@PostMapping @PostMapping(consumes = "multipart/form-data")
@PreAuthorize("hasRole('ADMIN') or #uploadedBy == authentication.principal.userId") @PreAuthorize("hasRole('ADMIN') or #uploadedBy == authentication.principal.userId")
public ResponseEntity<FileDto.Response> create( public ResponseEntity<FileDto.Response> create(
@RequestBody FileDto.Request dto, @RequestPart("file") MultipartFile file,
@RequestPart("file") MultipartFile file @RequestPart("fileName") String fileName,
@RequestPart("mimeType") String mimeType,
@RequestPart("uploadedBy") UUID uploadedBy,
@RequestPart("context") Integer context
) throws IOException { ) throws IOException {
File created = fileService.create(FileMapper.toEntity(dto), file.getBytes());
return ResponseEntity.status(HttpStatus.CREATED).body(FileMapper.toResponse(created)); 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("/{fileId}") @PutMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)") @PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<File> update(@PathVariable("fileId") UUID fileId, @RequestBody FileDto.Request request) { public ResponseEntity<File> update(@PathVariable("file_id") UUID fileId, @RequestBody FileDto.Request request) {
File updated = fileService.update(fileId, FileMapper.toEntity(request)); File updated = fileService.update(fileId, FileMapper.toEntity(request));
return ResponseEntity.ok(updated); return ResponseEntity.ok(updated);
} }
@DeleteMapping("/{fileId}") @DeleteMapping("/{file_id}")
@PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)") @PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#file_id, authentication.principal.userId)")
public ResponseEntity<Void> delete(@PathVariable("fileId") UUID fileId, @RequestBody Map<String,String> body) throws IOException { public ResponseEntity<Void> delete(@PathVariable("file_id") UUID fileId, @RequestBody Map<String,String> body) throws IOException {
String filePath = body.get("file_path"); String filePath = body.get("file_path");
Files.deleteIfExists(Paths.get(filePath)); Files.deleteIfExists(Paths.get(filePath));
fileService.delete(fileId); fileService.delete(fileId);

View File

@@ -3,9 +3,7 @@ package net.miarma.backend.core.service;
import java.util.UUID; import java.util.UUID;
import net.miarma.backlib.dto.*; import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.ConflictException; import net.miarma.backlib.exception.*;
import net.miarma.backlib.exception.ForbiddenException;
import net.miarma.backlib.exception.UnauthorizedException;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -75,4 +73,22 @@ public class AuthService {
return new LoginResponse(token, UserMapper.toDto(user), CredentialMapper.toDto(cred)); 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

@@ -23,10 +23,12 @@ import net.miarma.backlib.util.UuidUtil;
public class CredentialService { public class CredentialService {
private final CredentialRepository credentialRepository; private final CredentialRepository credentialRepository;
private final UserService userService;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
public CredentialService(CredentialRepository credentialRepository, PasswordEncoder passwordEncoder) { public CredentialService(CredentialRepository credentialRepository, UserService userService, PasswordEncoder passwordEncoder) {
this.credentialRepository = credentialRepository; this.credentialRepository = credentialRepository;
this.userService = userService;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
@@ -155,15 +157,13 @@ public class CredentialService {
credentialRepository.deleteById(idBytes); credentialRepository.deleteById(idBytes);
} }
public Byte getStatus(UUID credentialId) { public Byte getStatus(UUID userId, Byte serviceId) {
Credential credential = credentialRepository.findById(UuidUtil.uuidToBin(credentialId)) Credential credential = getByUserIdAndService(userId, serviceId);
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
return credential.getStatus(); return credential.getStatus();
} }
public void updateStatus(UUID credentialId, Byte status) { public void updateStatus(UUID userId, Byte serviceId, Byte status) {
Credential credential = credentialRepository.findById(UuidUtil.uuidToBin(credentialId)) Credential credential = getByUserIdAndService(userId, serviceId);
.orElseThrow(() -> new NotFoundException("Usuario no encontrado"));;
credential.setStatus(status); credential.setStatus(status);
credentialRepository.save(credential); credentialRepository.save(credential);
} }

View File

@@ -76,7 +76,7 @@
<dependency> <dependency>
<groupId>net.miarma</groupId> <groupId>net.miarma</groupId>
<artifactId>backlib</artifactId> <artifactId>backlib</artifactId>
<version>1.0.1</version> <version>1.1.0</version>
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -1,9 +1,12 @@
package net.miarma.backend.huertos.client; package net.miarma.backend.huertos.client;
import net.miarma.backlib.dto.ApiErrorDto;
import net.miarma.backlib.dto.LoginRequest; import net.miarma.backlib.dto.LoginRequest;
import net.miarma.backlib.dto.LoginResponse; import net.miarma.backlib.dto.LoginResponse;
import net.miarma.backlib.exception.*;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.HttpServerErrorException; import org.springframework.web.client.HttpServerErrorException;
@@ -25,10 +28,46 @@ public class CoreAuthClient {
public LoginResponse login(LoginRequest req) { public LoginResponse login(LoginRequest req) {
return restTemplate.postForObject( HttpHeaders headers = new HttpHeaders();
coreUrl + "/auth/login", headers.setContentType(MediaType.APPLICATION_JSON);
req,
LoginResponse.class 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

@@ -4,11 +4,14 @@ import net.miarma.backend.huertos.dto.RequestMetadataDto;
import net.miarma.backend.huertos.model.RequestMetadata; import net.miarma.backend.huertos.model.RequestMetadata;
import net.miarma.backend.huertos.util.UsernameGenerator; import net.miarma.backend.huertos.util.UsernameGenerator;
import net.miarma.backlib.dto.*; import net.miarma.backlib.dto.*;
import net.miarma.backlib.exception.*;
import net.miarma.backlib.security.PasswordGenerator; import net.miarma.backlib.security.PasswordGenerator;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import tools.jackson.databind.ObjectMapper;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -19,68 +22,170 @@ public class HuertosWebClient {
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final String coreUrl; private final String coreUrl;
private final ObjectMapper objectMapper;
public HuertosWebClient(@Qualifier("secureRestTemplate") RestTemplate restTemplate, public HuertosWebClient(@Qualifier("secureRestTemplate") RestTemplate restTemplate,
@Value("${core.url}") String coreUrl) { @Value("${core.url}") String coreUrl,
ObjectMapper objectMapper) {
this.restTemplate = restTemplate; this.restTemplate = restTemplate;
this.coreUrl = coreUrl; this.coreUrl = coreUrl;
this.objectMapper = objectMapper;
} }
public UserWithCredentialDto getUserWithCredential(UUID userId, Byte serviceId) { public UserWithCredentialDto getUserWithCredential(UUID userId, Byte serviceId) {
return restTemplate.getForObject( ResponseEntity<UserWithCredentialDto> response = restTemplate.exchange(
coreUrl + "/users/{user_id}/service/{service_id}", coreUrl + "/users/{user_id}/service/{service_id}",
HttpMethod.GET,
null,
UserWithCredentialDto.class, UserWithCredentialDto.class,
userId, serviceId userId, serviceId
); );
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
return response.getBody();
} }
public List<UserWithCredentialDto> getAllUsersWithCredentials(Byte serviceId) { public List<UserWithCredentialDto> getAllUsersWithCredentials(Byte serviceId) {
UserWithCredentialDto[] arr = restTemplate.getForObject( ResponseEntity<UserWithCredentialDto[]> response = restTemplate.exchange(
coreUrl + "/users/service/{service_id}", coreUrl + "/users/service/{service_id}",
UserWithCredentialDto[].class, HttpMethod.GET,
serviceId null,
UserWithCredentialDto[].class,
serviceId
); );
if (!response.getStatusCode().is2xxSuccessful()) {
handleError(response);
}
UserWithCredentialDto[] arr = response.getBody();
return arr == null ? List.of() : Arrays.asList(arr); return arr == null ? List.of() : Arrays.asList(arr);
} }
public UserWithCredentialDto createUser( public UserWithCredentialDto createUser(RequestMetadataDto metadataDto) {
RequestMetadataDto metadataDto // 1. Crear el usuario
) {
CreateUserDto userDto = new CreateUserDto(metadataDto.displayName(), null); CreateUserDto userDto = new CreateUserDto(metadataDto.displayName(), null);
UserDto createdUser = restTemplate.postForObject( HttpEntity<CreateUserDto> userRequestEntity = new HttpEntity<>(userDto);
coreUrl + "/users",
userDto, ResponseEntity<UserDto> userResponse = restTemplate.exchange(
UserDto.class coreUrl + "/users",
HttpMethod.POST,
userRequestEntity,
UserDto.class
); );
if (createdUser == null) if (!userResponse.getStatusCode().is2xxSuccessful()) {
handleError(userResponse);
}
UserDto createdUser = userResponse.getBody();
if (createdUser == null) {
throw new RuntimeException("No se pudo crear al usuario"); throw new RuntimeException("No se pudo crear al usuario");
}
CreateCredentialDto credDto = new CreateCredentialDto( CreateCredentialDto credDto = new CreateCredentialDto(
createdUser.getUserId(), createdUser.getUserId(),
(byte)1, (byte) 1,
UsernameGenerator.generate(metadataDto.displayName(), metadataDto.memberNumber()), UsernameGenerator.generate(metadataDto.displayName(), metadataDto.memberNumber()),
metadataDto.email(), metadataDto.email(),
PasswordGenerator.generate(8), PasswordGenerator.generate(8),
(byte)1 (byte) 1
);
CredentialDto createdCred = restTemplate.postForObject(
coreUrl + "/credentials",
credDto,
CredentialDto.class
); );
if (createdCred == null) 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"); throw new RuntimeException("No se pudo crear la cuenta del usuario");
}
return new UserWithCredentialDto(createdUser, createdCred); return new UserWithCredentialDto(createdUser, createdCred);
} }
public void deleteUser(UUID userId) { public void deleteUser(UUID userId) {
try { ResponseEntity<Void> response = restTemplate.exchange(
restTemplate.delete(coreUrl + "/users/{user_id}", userId); coreUrl + "/users/{user_id}",
} catch (Exception e) { } 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");
}
}
} }
} }

View File

@@ -1,10 +1,13 @@
package net.miarma.backend.huertos.config; package net.miarma.backend.huertos.config;
import io.jsonwebtoken.io.IOException;
import net.miarma.backend.huertos.service.CoreAuthService; import net.miarma.backend.huertos.service.CoreAuthService;
import net.miarma.backlib.security.CoreAuthTokenHolder; import net.miarma.backlib.security.CoreAuthTokenHolder;
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.http.client.ClientHttpRequestInterceptor; import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.util.ArrayList; import java.util.ArrayList;
@@ -15,20 +18,31 @@ public class RestTemplateConfig {
@Bean @Bean
public RestTemplate authRestTemplate() { public RestTemplate authRestTemplate() {
return new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
return restTemplate;
} }
@Bean @Bean
public RestTemplate secureRestTemplate(CoreAuthService coreAuthService) { public RestTemplate secureRestTemplate(CoreAuthService coreAuthService) {
RestTemplate rt = new RestTemplate(); RestTemplate restTemplate = new RestTemplate();
rt.getInterceptors().add((request, body, execution) -> { restTemplate.getInterceptors().add((request, body, execution) -> {
String token = coreAuthService.getToken(); String token = coreAuthService.getToken();
request.getHeaders().setBearerAuth(token); request.getHeaders().setBearerAuth(token);
return execution.execute(request, body); return execution.execute(request, body);
}); });
return rt; restTemplate.setErrorHandler(new NoOpResponseErrorHandler());
return restTemplate;
}
public static class NoOpResponseErrorHandler implements ResponseErrorHandler {
@Override
public boolean hasError(ClientHttpResponse response) throws IOException {
return false;
}
} }
} }

View File

@@ -12,11 +12,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
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.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List; import java.util.Optional;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -26,34 +24,27 @@ public class SecurityConfig {
private final HuertosJwtFilter jwtFilter; private final HuertosJwtFilter jwtFilter;
private final RestAuthEntryPoint authEntryPoint; private final RestAuthEntryPoint authEntryPoint;
private final RestAccessDeniedHandler accessDeniedHandler; private final RestAccessDeniedHandler accessDeniedHandler;
private final CorsConfigurationSource corsConfigurationSource;
public SecurityConfig( public SecurityConfig(
HuertosJwtFilter jwtFilter, HuertosJwtFilter jwtFilter,
RestAuthEntryPoint authEntryPoint, RestAuthEntryPoint authEntryPoint,
RestAccessDeniedHandler accessDeniedHandler RestAccessDeniedHandler accessDeniedHandler,
Optional<CorsConfigurationSource> corsConfigurationSource
) { ) {
this.jwtFilter = jwtFilter; this.jwtFilter = jwtFilter;
this.authEntryPoint = authEntryPoint; this.authEntryPoint = authEntryPoint;
this.accessDeniedHandler = accessDeniedHandler; this.accessDeniedHandler = accessDeniedHandler;
} this.corsConfigurationSource = corsConfigurationSource.orElse(null);
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("http://localhost:3000"));
config.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
} }
@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
.cors(Customizer.withDefaults())
.csrf(csrf -> csrf.disable()) .csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(ex -> ex .exceptionHandling(ex -> ex
@@ -65,9 +56,7 @@ public class SecurityConfig {
.requestMatchers("/auth/login").permitAll() .requestMatchers("/auth/login").permitAll()
.requestMatchers("/users/waitlist/limited").permitAll() .requestMatchers("/users/waitlist/limited").permitAll()
.requestMatchers("/requests").permitAll() .requestMatchers("/requests").permitAll()
.requestMatchers("/pre-users").permitAll()
.requestMatchers("/users/latest-number").permitAll() .requestMatchers("/users/latest-number").permitAll()
.requestMatchers("/pre-users/validate").permitAll()
// PRIVADAS // PRIVADAS
.anyRequest().authenticated() .anyRequest().authenticated()
); );

View File

@@ -98,6 +98,7 @@ public class IncomeController {
@PathVariable("income_id") UUID incomeId, @PathVariable("income_id") UUID incomeId,
@RequestBody IncomeDto.Request dto @RequestBody IncomeDto.Request dto
) { ) {
IO.println(dto.getCreatedAt());
return ResponseEntity.ok( return ResponseEntity.ok(
IncomeMapper.toResponse( IncomeMapper.toResponse(
incomeService.update( incomeService.update(

View File

@@ -1,6 +1,7 @@
package net.miarma.backend.huertos.controller; package net.miarma.backend.huertos.controller;
import net.miarma.backend.huertos.dto.*; import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.dto.view.VIncomesWithInfoDto;
import net.miarma.backend.huertos.mapper.IncomeMapper; import net.miarma.backend.huertos.mapper.IncomeMapper;
import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.security.HuertosPrincipal; import net.miarma.backend.huertos.security.HuertosPrincipal;
@@ -31,7 +32,7 @@ public class MemberController {
@GetMapping @GetMapping
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<List<MemberDto>> getAll() { public ResponseEntity<List<MemberDto>> getAll() {
return ResponseEntity.ok(memberService.getAll((byte)1)); return ResponseEntity.ok(memberService.getAll());
} }
@GetMapping("/me") @GetMapping("/me")
@@ -41,10 +42,16 @@ public class MemberController {
} }
return ResponseEntity.ok( return ResponseEntity.ok(
memberService.getMyProfile(principal.getUserId()) memberService.getMyProfile(principal.getUserId())
); );
} }
@GetMapping("/dropdown")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<List<DropdownDto>> getDropdown() {
return ResponseEntity.ok(memberService.getDropdown());
}
@GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}") @GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<MemberDto> getById(@PathVariable("user_id") UUID userId) { public ResponseEntity<MemberDto> getById(@PathVariable("user_id") UUID userId) {
@@ -75,7 +82,7 @@ public class MemberController {
@GetMapping("/number/{member_number}/incomes") @GetMapping("/number/{member_number}/incomes")
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<List<IncomeDto.Response>> getMemberIncomes(@PathVariable("member_number") Integer memberNumber) { public ResponseEntity<List<VIncomesWithInfoDto>> getMemberIncomes(@PathVariable("member_number") Integer memberNumber) {
return ResponseEntity.ok(memberService.getIncomes(memberNumber)); return ResponseEntity.ok(memberService.getIncomes(memberNumber));
} }

View File

@@ -2,6 +2,7 @@ package net.miarma.backend.huertos.controller;
import net.miarma.backend.huertos.dto.*; import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.RequestMetadataMapper;
import net.miarma.backend.huertos.mapper.RequestWithMetadataMapper; import net.miarma.backend.huertos.mapper.RequestWithMetadataMapper;
import net.miarma.backend.huertos.model.Request; import net.miarma.backend.huertos.model.Request;
import net.miarma.backend.huertos.service.RequestAcceptanceService; import net.miarma.backend.huertos.service.RequestAcceptanceService;
@@ -131,6 +132,7 @@ public class RequestController {
@PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')")
public ResponseEntity<Map<String, String>> acceptRequest(@PathVariable("request_id") UUID requestId) { public ResponseEntity<Map<String, String>> acceptRequest(@PathVariable("request_id") UUID requestId) {
Request r = requestAcceptanceService.acceptRequest(requestId); Request r = requestAcceptanceService.acceptRequest(requestId);
requestAcceptanceService.handleSideEffects(r);
return ResponseEntity.ok(Map.of("message", "Accepted request: " + r.getRequestId())); return ResponseEntity.ok(Map.of("message", "Accepted request: " + r.getRequestId()));
} }

View File

@@ -0,0 +1,6 @@
package net.miarma.backend.huertos.dto;
import java.util.UUID;
public record DropdownDto(UUID userId, Integer memberNumber, String displayName) {
}

View File

@@ -11,6 +11,7 @@ public class ExpenseDto {
private String supplier; private String supplier;
private String invoice; private String invoice;
private Byte type; private Byte type;
private Instant createdAt;
public String getConcept() { public String getConcept() {
return concept; return concept;
@@ -51,6 +52,14 @@ public class ExpenseDto {
public void setType(Byte type) { public void setType(Byte type) {
this.type = type; this.type = type;
} }
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
} }
public static class Response { public static class Response {
@@ -59,6 +68,8 @@ public class ExpenseDto {
private BigDecimal amount; private BigDecimal amount;
private String supplier; private String supplier;
private String invoice; private String invoice;
private Byte type;
private Instant createdAt;
public UUID getExpenseId() { public UUID getExpenseId() {
return expenseId; return expenseId;
@@ -115,8 +126,5 @@ public class ExpenseDto {
public void setCreatedAt(Instant createdAt) { public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
private Byte type;
private Instant createdAt;
} }
} }

View File

@@ -12,6 +12,7 @@ public class IncomeDto {
private BigDecimal amount; private BigDecimal amount;
private Byte type; private Byte type;
private Byte frequency; private Byte frequency;
private Instant createdAt;
public Integer getMemberNumber() { public Integer getMemberNumber() {
return memberNumber; return memberNumber;
@@ -60,6 +61,14 @@ public class IncomeDto {
public void setFrequency(Byte frequency) { public void setFrequency(Byte frequency) {
this.frequency = frequency; this.frequency = frequency;
} }
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
} }
public static class Response { public static class Response {

View File

@@ -1,5 +1,6 @@
package net.miarma.backend.huertos.dto; package net.miarma.backend.huertos.dto;
import net.miarma.backend.huertos.dto.view.VIncomesWithInfoDto;
import net.miarma.backlib.dto.CredentialDto; import net.miarma.backlib.dto.CredentialDto;
import net.miarma.backlib.dto.UserDto; import net.miarma.backlib.dto.UserDto;
@@ -10,7 +11,7 @@ public record MemberProfileDto(
CredentialDto account, CredentialDto account,
UserMetadataDto metadata, UserMetadataDto metadata,
List<RequestDto.Response> requests, List<RequestDto.Response> requests,
List<IncomeDto.Response> payments, List<VIncomesWithInfoDto> payments,
boolean hasCollaborator, boolean hasCollaborator,
boolean hasGreenhouse, boolean hasGreenhouse,
boolean hasCollaboratorRequest, boolean hasCollaboratorRequest,

View File

@@ -9,6 +9,7 @@ public class RequestDto {
public static class Request { public static class Request {
private Byte type; private Byte type;
private UUID userId; private UUID userId;
private String name;
private RequestMetadataDto metadata; private RequestMetadataDto metadata;
public Byte getType() { public Byte getType() {
@@ -27,6 +28,14 @@ public class RequestDto {
this.userId = userId; this.userId = userId;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public RequestMetadataDto getMetadata() { public RequestMetadataDto getMetadata() {
return metadata; return metadata;
} }
@@ -41,6 +50,7 @@ public class RequestDto {
private Byte type; private Byte type;
private Byte status; private Byte status;
private UUID userId; private UUID userId;
private String name;
private RequestMetadataDto metadata; private RequestMetadataDto metadata;
private Instant createdAt; private Instant createdAt;
@@ -76,6 +86,14 @@ public class RequestDto {
this.userId = userId; this.userId = userId;
} }
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public RequestMetadataDto getMetadata() { public RequestMetadataDto getMetadata() {
return metadata; return metadata;
} }

View File

@@ -6,6 +6,7 @@ import java.util.UUID;
public record RequestWithMetadataDto( public record RequestWithMetadataDto(
UUID requestId, UUID requestId,
UUID userId, UUID userId,
String name,
Byte type, Byte type,
Byte status, Byte status,
Instant createdAt, Instant createdAt,

View File

@@ -0,0 +1,10 @@
package net.miarma.backend.huertos.mapper;
import net.miarma.backend.huertos.dto.DropdownDto;
import net.miarma.backend.huertos.dto.MemberDto;
public class DropdownDtoMapper {
public static DropdownDto toDto(MemberDto dto) {
return new DropdownDto(dto.user().getUserId(), dto.metadata().getMemberNumber(), dto.user().getDisplayName());
}
}

View File

@@ -28,6 +28,7 @@ public class ExpenseMapper {
entity.setSupplier(dto.getSupplier()); entity.setSupplier(dto.getSupplier());
entity.setInvoice(dto.getInvoice()); entity.setInvoice(dto.getInvoice());
entity.setType(dto.getType()); entity.setType(dto.getType());
entity.setCreatedAt(dto.getCreatedAt());
return entity; return entity;
} }
} }

View File

@@ -28,6 +28,7 @@ public class IncomeMapper {
entity.setAmount(dto.getAmount()); entity.setAmount(dto.getAmount());
entity.setType(dto.getType()); entity.setType(dto.getType());
entity.setFrequency(dto.getFrequency()); entity.setFrequency(dto.getFrequency());
entity.setCreatedAt(dto.getCreatedAt());
return entity; return entity;
} }
} }

View File

@@ -30,11 +30,12 @@ public class RequestMapper {
Request entity = new Request(); Request entity = new Request();
entity.setType(dto.getType()); entity.setType(dto.getType());
entity.setUserId(dto.getUserId()); entity.setUserId(dto.getUserId());
entity.setName(dto.getName());
entity.setStatus((byte) 0); entity.setStatus((byte) 0);
if (dto.getMetadata() != null) { if (dto.getMetadata() != null) {
entity.setMetadata( entity.setMetadata(
RequestMetadataMapper.fromDto(dto.getMetadata()) RequestMetadataMapper.fromDto(dto.getMetadata())
); );
} }

View File

@@ -9,6 +9,7 @@ public class RequestWithMetadataMapper {
return new RequestWithMetadataDto( return new RequestWithMetadataDto(
r.getRequestId(), r.getRequestId(),
r.getUserId(), r.getUserId(),
r.getName(),
r.getType(), r.getType(),
r.getStatus(), r.getStatus(),
r.getCreatedAt(), r.getCreatedAt(),

View File

@@ -38,7 +38,7 @@ public class Income {
@Column(name = "frequency") @Column(name = "frequency")
private Byte frequency; private Byte frequency;
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false, updatable = true)
private Instant createdAt; private Instant createdAt;
@PrePersist @PrePersist

View File

@@ -2,7 +2,6 @@ package net.miarma.backend.huertos.model;
import java.time.Instant; import java.time.Instant;
import java.util.UUID; import java.util.UUID;
import jakarta.persistence.*; import jakarta.persistence.*;
import net.miarma.backlib.util.UuidUtil; import net.miarma.backlib.util.UuidUtil;
@@ -18,103 +17,60 @@ public class Request {
private UUID requestId; private UUID requestId;
@Column(name = "user_id", columnDefinition = "BINARY(16)") @Column(name = "user_id", columnDefinition = "BINARY(16)")
private byte[] userIdBin; // usuario que hace la solicitud (puede ser null si anon) private byte[] userIdBin;
@Transient @Transient
private UUID userId; private UUID userId;
@Column(name = "type", nullable = false) private String name;
@Column(nullable = false)
private Byte type; private Byte type;
@Column(name = "status", nullable = false) @Column(nullable = false)
private Byte status; private Byte status;
@Column(name = "created_at", nullable = false) @Column(name = "created_at", nullable = false)
private Instant createdAt; private Instant createdAt;
@OneToOne(cascade = CascadeType.ALL) @OneToOne(mappedBy = "request", cascade = CascadeType.ALL, fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "metadata_id")
private RequestMetadata metadata; private RequestMetadata metadata;
@PrePersist @PrePersist
private void prePersist() { private void prePersist() {
if (requestId != null) { if (requestId != null) requestIdBin = UuidUtil.uuidToBin(requestId);
requestIdBin = net.miarma.backlib.util.UuidUtil.uuidToBin(requestId); if (userId != null) userIdBin = UuidUtil.uuidToBin(userId);
}
createdAt = Instant.now();
} }
@PostLoad @PostLoad
private void postLoad() { private void postLoad() {
if (requestIdBin != null) { if (requestIdBin != null) requestId = UuidUtil.binToUUID(requestIdBin);
requestId = net.miarma.backlib.util.UuidUtil.binToUUID(requestIdBin); if (userIdBin != null) userId = UuidUtil.binToUUID(userIdBin);
}
if (userIdBin != null) {
userId = net.miarma.backlib.util.UuidUtil.binToUUID(userIdBin);
}
} }
public byte[] getRequestIdBin() { public UUID getRequestId() { return requestId; }
return requestIdBin; public void setRequestId(UUID requestId) { this.requestId = requestId; }
public UUID getUserId() { return userId; }
public void setUserId(UUID userId) { this.userId = userId; }
public String getName() {
return name;
} }
public void setRequestIdBin(byte[] requestIdBin) { public void setName(String name) {
this.requestIdBin = requestIdBin; this.name = name;
} }
public UUID getRequestId() { public Byte getType() { return type; }
return requestId; public void setType(Byte type) { this.type = type; }
}
public void setRequestId(UUID requestId) { public Byte getStatus() { return status; }
this.requestId = requestId; public void setStatus(Byte status) { this.status = status; }
}
public byte[] getUserIdBin() { public Instant getCreatedAt() { return createdAt; }
return userIdBin; public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; }
}
public void setUserIdBin(byte[] userIdBin) { public RequestMetadata getMetadata() { return metadata; }
this.userIdBin = userIdBin; public void setMetadata(RequestMetadata metadata) { this.metadata = metadata; }
}
public UUID getUserId() {
return userId;
}
public void setUserId(UUID userId) {
this.userId = userId;
}
public Byte getType() {
return type;
}
public void setType(Byte type) {
this.type = type;
}
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 RequestMetadata getMetadata() {
return metadata;
}
public void setMetadata(RequestMetadata metadata) {
this.metadata = metadata;
}
} }

View File

@@ -1,8 +1,10 @@
package net.miarma.backend.huertos.model; package net.miarma.backend.huertos.model;
import jakarta.persistence.*; import jakarta.persistence.*;
import net.miarma.backlib.util.UuidUtil;
import java.time.Instant; import java.time.Instant;
import java.util.UUID;
@Entity @Entity
@Table(name = "huertos_request_metadata") @Table(name = "huertos_request_metadata")
@@ -12,6 +14,16 @@ public class RequestMetadata {
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(name = "request_id", columnDefinition = "BINARY(16)", nullable = false, unique = true)
private byte[] requestIdBin;
@Transient
private UUID requestId;
@OneToOne
@JoinColumn(name = "request_id", referencedColumnName = "request_id", insertable = false, updatable = false)
private Request request;
@Column(name = "display_name", nullable = false, length = 150) @Column(name = "display_name", nullable = false, length = 150)
private String displayName; private String displayName;
@@ -39,17 +51,22 @@ public class RequestMetadata {
@Column(name = "plot_number") @Column(name = "plot_number")
private Integer plotNumber; private Integer plotNumber;
private Byte type;
@Column(nullable = false, length = 100) @Column(nullable = false, length = 100)
private String username; private String username;
private Byte type;
@Column(name = "created_at", nullable = false, updatable = false) @Column(name = "created_at", nullable = false, updatable = false)
private Instant createdAt; private Instant createdAt;
@PrePersist @PrePersist
private void prePersist() { private void prePersist() {
createdAt = Instant.now(); if (requestId != null) requestIdBin = UuidUtil.uuidToBin(requestId);
}
@PostLoad
private void postLoad() {
if (requestIdBin != null) requestId = UuidUtil.binToUUID(requestIdBin);
} }
public Long getId() { public Long getId() {
@@ -60,6 +77,30 @@ public class RequestMetadata {
this.id = id; this.id = id;
} }
public byte[] getRequestIdBin() {
return requestIdBin;
}
public void setRequestIdBin(byte[] requestIdBin) {
this.requestIdBin = requestIdBin;
}
public UUID getRequestId() {
return requestId;
}
public void setRequestId(UUID requestId) {
this.requestId = requestId;
}
public Request getRequest() {
return request;
}
public void setRequest(Request request) {
this.request = request;
}
public String getDisplayName() { public String getDisplayName() {
return displayName; return displayName;
} }
@@ -140,14 +181,6 @@ public class RequestMetadata {
this.type = type; this.type = type;
} }
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Instant getCreatedAt() { public Instant getCreatedAt() {
return createdAt; return createdAt;
} }
@@ -155,5 +188,12 @@ public class RequestMetadata {
public void setCreatedAt(Instant createdAt) { public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt; this.createdAt = createdAt;
} }
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}

View File

@@ -2,6 +2,17 @@ package net.miarma.backend.huertos.repository;
import net.miarma.backend.huertos.model.Request; import net.miarma.backend.huertos.model.Request;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.Optional;
public interface RequestRepository extends JpaRepository<Request, byte[]> { public interface RequestRepository extends JpaRepository<Request, byte[]> {
@Query("""
SELECT r FROM Request r
LEFT JOIN FETCH r.metadata
WHERE r.requestIdBin = :id
""")
Optional<Request> findByIdWithMetadata(@Param("id") byte[] id);
} }

View File

@@ -3,12 +3,14 @@ package net.miarma.backend.huertos.service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import net.miarma.backend.huertos.dto.AnnouncementDto; import net.miarma.backend.huertos.dto.AnnouncementDto;
import net.miarma.backend.huertos.model.Announcement; import net.miarma.backend.huertos.model.Announcement;
import net.miarma.backend.huertos.model.Income;
import net.miarma.backend.huertos.repository.AnnouncementRepository; import net.miarma.backend.huertos.repository.AnnouncementRepository;
import net.miarma.backlib.exception.NotFoundException; import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.util.UuidUtil; import net.miarma.backlib.util.UuidUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -25,7 +27,9 @@ public class AnnouncementService {
} }
public List<Announcement> getAll() { public List<Announcement> getAll() {
return announcementRepository.findAll(); return announcementRepository.findAll().stream()
.sorted(Comparator.comparing(Announcement::getCreatedAt).reversed())
.toList();
} }
public Announcement getById(UUID announceId) { public Announcement getById(UUID announceId) {

View File

@@ -3,6 +3,7 @@ package net.miarma.backend.huertos.service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import net.miarma.backend.huertos.dto.ExpenseDto; import net.miarma.backend.huertos.dto.ExpenseDto;
import net.miarma.backend.huertos.model.Expense; import net.miarma.backend.huertos.model.Expense;
import net.miarma.backend.huertos.model.Income;
import net.miarma.backend.huertos.repository.ExpenseRepository; import net.miarma.backend.huertos.repository.ExpenseRepository;
import net.miarma.backlib.exception.NotFoundException; import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException; import net.miarma.backlib.exception.ValidationException;
@@ -10,6 +11,7 @@ import net.miarma.backlib.util.UuidUtil;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant; import java.time.Instant;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@@ -24,7 +26,9 @@ public class ExpenseService {
} }
public List<Expense> getAll() { public List<Expense> getAll() {
return expenseRepository.findAll(); return expenseRepository.findAll().stream()
.sorted(Comparator.comparing(Expense::getCreatedAt).reversed())
.toList();
} }
public Expense getById(UUID expenseId) { public Expense getById(UUID expenseId) {
@@ -46,9 +50,11 @@ public class ExpenseService {
if (expense.getInvoice() == null || expense.getInvoice().isBlank()) { if (expense.getInvoice() == null || expense.getInvoice().isBlank()) {
throw new ValidationException("invoice", "La factura es obligatoria"); throw new ValidationException("invoice", "La factura es obligatoria");
} }
if (expense.getCreatedAt() == null) {
expense.setCreatedAt(Instant.now());
}
expense.setExpenseId(UUID.randomUUID()); expense.setExpenseId(UUID.randomUUID());
expense.setCreatedAt(Instant.now());
return expenseRepository.save(expense); return expenseRepository.save(expense);
} }
@@ -74,6 +80,9 @@ public class ExpenseService {
if (changes.getType() != null) if (changes.getType() != null)
expense.setType(changes.getType()); expense.setType(changes.getType());
if (changes.getCreatedAt() != null)
expense.setCreatedAt(changes.getCreatedAt());
return expenseRepository.save(expense); return expenseRepository.save(expense);
} }

View File

@@ -1,9 +1,13 @@
package net.miarma.backend.huertos.service; package net.miarma.backend.huertos.service;
import java.time.Instant; import java.time.Instant;
import java.time.temporal.TemporalAmount;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import net.miarma.backend.huertos.model.view.VIncomesWithInfo;
import net.miarma.backend.huertos.service.view.VIncomesWithInfoService;
import net.miarma.backlib.exception.BadRequestException; import net.miarma.backlib.exception.BadRequestException;
import net.miarma.backlib.exception.NotFoundException; import net.miarma.backlib.exception.NotFoundException;
import net.miarma.backlib.exception.ValidationException; import net.miarma.backlib.exception.ValidationException;
@@ -19,16 +23,21 @@ import net.miarma.backlib.util.UuidUtil;
public class IncomeService { public class IncomeService {
private final IncomeRepository incomeRepository; private final IncomeRepository incomeRepository;
private final VIncomesWithInfoService incomesWithInfoService;
private final UserMetadataService metadataService; private final UserMetadataService metadataService;
public IncomeService(IncomeRepository incomeRepository, public IncomeService(IncomeRepository incomeRepository,
VIncomesWithInfoService incomesWithInfoService,
UserMetadataService metadataService) { UserMetadataService metadataService) {
this.incomeRepository = incomeRepository; this.incomeRepository = incomeRepository;
this.incomesWithInfoService = incomesWithInfoService;
this.metadataService = metadataService; this.metadataService = metadataService;
} }
public List<Income> getAll() { public List<Income> getAll() {
return incomeRepository.findAll(); return incomeRepository.findAll().stream()
.sorted(Comparator.comparing(Income::getCreatedAt).reversed())
.toList();
} }
public Income getById(UUID incomeId) { public Income getById(UUID incomeId) {
@@ -47,15 +56,20 @@ public class IncomeService {
if (income.getUserId() == null) { if (income.getUserId() == null) {
throw new BadRequestException("El identificador de usuario es obligatorio"); throw new BadRequestException("El identificador de usuario es obligatorio");
} }
if (income.getConcept() == null || income.getConcept().isBlank()) { if (income.getConcept() == null) {
throw new BadRequestException("El concepto es obligatorio"); throw new BadRequestException("El concepto es obligatorio");
} }
if (income.getConcept().isBlank() || income.getConcept().isEmpty()) {
throw new ValidationException("concept", "El concepto no puede ir vacío");
}
if (income.getAmount() == null || income.getAmount().signum() <= 0) { if (income.getAmount() == null || income.getAmount().signum() <= 0) {
throw new ValidationException("amount", "La cantidad debe ser positiva"); throw new ValidationException("amount", "La cantidad debe ser positiva");
} }
if (income.getCreatedAt() == null) {
income.setCreatedAt(Instant.now());
}
income.setIncomeId(UUID.randomUUID()); income.setIncomeId(UUID.randomUUID());
income.setCreatedAt(Instant.now());
return incomeRepository.save(income); return incomeRepository.save(income);
} }
@@ -75,6 +89,9 @@ public class IncomeService {
} }
if (changes.getType() != null) income.setType(changes.getType()); if (changes.getType() != null) income.setType(changes.getType());
if (changes.getFrequency() != null) income.setFrequency(changes.getFrequency()); if (changes.getFrequency() != null) income.setFrequency(changes.getFrequency());
if (changes.getCreatedAt() != null && !changes.getCreatedAt().equals(income.getCreatedAt())) {
income.setCreatedAt(changes.getCreatedAt());
}
return incomeRepository.save(income); return incomeRepository.save(income);
} }
@@ -104,8 +121,8 @@ public class IncomeService {
return !incomes.isEmpty() && incomes.stream().allMatch(Income::isPaid); return !incomes.isEmpty() && incomes.stream().allMatch(Income::isPaid);
} }
public List<Income> getByMemberNumber(Integer memberNumber) { public List<VIncomesWithInfo> getByMemberNumber(Integer memberNumber) {
UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId(); UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId();
return getByUserId(userId); return incomesWithInfoService.getByUserId(userId);
} }
} }

View File

@@ -2,9 +2,12 @@
import net.miarma.backend.huertos.client.HuertosWebClient; import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backend.huertos.dto.*; import net.miarma.backend.huertos.dto.*;
import net.miarma.backend.huertos.dto.view.VIncomesWithInfoDto;
import net.miarma.backend.huertos.mapper.DropdownDtoMapper;
import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.UserMetadataMapper; import net.miarma.backend.huertos.mapper.UserMetadataMapper;
import net.miarma.backend.huertos.mapper.IncomeMapper; import net.miarma.backend.huertos.mapper.IncomeMapper;
import net.miarma.backend.huertos.mapper.view.VIncomesWithInfoMapper;
import net.miarma.backend.huertos.security.NameCensorer; import net.miarma.backend.huertos.security.NameCensorer;
import net.miarma.backlib.dto.UserWithCredentialDto; import net.miarma.backlib.dto.UserWithCredentialDto;
import net.miarma.backlib.exception.NotFoundException; import net.miarma.backlib.exception.NotFoundException;
@@ -52,8 +55,8 @@
} }
@Cacheable("members") @Cacheable("members")
public List<MemberDto> getAll(Byte serviceId) { public List<MemberDto> getAll() {
List<UserWithCredentialDto> all = huertosWebClient.getAllUsersWithCredentials(serviceId); List<UserWithCredentialDto> all = huertosWebClient.getAllUsersWithCredentials((byte)1);
return all.stream() return all.stream()
.filter(uwc -> metadataService.existsById(uwc.user().getUserId())) .filter(uwc -> metadataService.existsById(uwc.user().getUserId()))
@@ -65,6 +68,7 @@
UserMetadataMapper.toDto(meta) UserMetadataMapper.toDto(meta)
); );
}) })
.sorted(Comparator.comparing(dto -> dto.metadata().getMemberNumber()))
.toList(); .toList();
} }
@@ -76,8 +80,8 @@
.map(RequestMapper::toResponse) .map(RequestMapper::toResponse)
.toList(); .toList();
List<IncomeDto.Response> payments = incomeService.getByMemberNumber(memberNumber).stream() List<VIncomesWithInfoDto> payments = incomeService.getByMemberNumber(memberNumber).stream()
.map(IncomeMapper::toResponse) .map(VIncomesWithInfoMapper::toResponse)
.toList(); .toList();
return new MemberProfileDto( return new MemberProfileDto(
@@ -125,29 +129,29 @@
} }
public MemberDto getByMemberNumber(Integer memberNumber) { public MemberDto getByMemberNumber(Integer memberNumber) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber)) .filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese número")); .orElseThrow(() -> new NotFoundException("No hay socio con ese número"));
} }
public MemberDto getByPlotNumber(Integer plotNumber) { public MemberDto getByPlotNumber(Integer plotNumber) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dto.metadata().getPlotNumber().equals(plotNumber)) .filter(dto -> dto.metadata().getPlotNumber().equals(plotNumber))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese huerto")); .orElseThrow(() -> new NotFoundException("No hay socio con ese huerto"));
} }
public MemberDto getByDni(String dni) { public MemberDto getByDni(String dni) {
return getAll((byte)1).stream() return getAll().stream()
.filter(dto -> dni.equals(dto.metadata().getDni())) .filter(dto -> dni.equals(dto.metadata().getDni()))
.findFirst() .findFirst()
.orElseThrow(() -> new NotFoundException("No hay socio con ese DNI")); .orElseThrow(() -> new NotFoundException("No hay socio con ese DNI"));
} }
public List<IncomeDto.Response> getIncomes(Integer memberNumber) { public List<VIncomesWithInfoDto> getIncomes(Integer memberNumber) {
return incomeService.getByMemberNumber(memberNumber).stream() return incomeService.getByMemberNumber(memberNumber).stream()
.map(IncomeMapper::toResponse) .map(VIncomesWithInfoMapper::toResponse)
.toList(); .toList();
} }
@@ -156,7 +160,7 @@
} }
public Boolean hasCollaborator(Integer memberNumber) { public Boolean hasCollaborator(Integer memberNumber) {
List<MemberDto> all = getAll((byte)1); List<MemberDto> all = getAll();
var member = all.stream() var member = all.stream()
.filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber)) .filter(dto -> dto.metadata().getMemberNumber().equals(memberNumber))
@@ -182,11 +186,17 @@
public Boolean hasCollaboratorRequest(Integer memberNumber) { public Boolean hasCollaboratorRequest(Integer memberNumber) {
UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId(); UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId();
return requestService.hasPendingCollaboratorRequest(userId); return requestService.hasCollaboratorRequest(userId);
} }
public Boolean hasGreenhouseRequest(Integer memberNumber) { public Boolean hasGreenhouseRequest(Integer memberNumber) {
UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId(); UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId();
return requestService.hasPendingGreenhouseRequest(userId); return requestService.hasGreenhouseRequest(userId);
}
public List<DropdownDto> getDropdown() {
return getAll().stream()
.map(DropdownDtoMapper::toDto)
.toList();
} }
} }

View File

@@ -2,7 +2,6 @@ package net.miarma.backend.huertos.service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
import net.miarma.backend.huertos.client.HuertosWebClient; import net.miarma.backend.huertos.client.HuertosWebClient;
import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.RequestMetadataMapper; import net.miarma.backend.huertos.mapper.RequestMetadataMapper;
import net.miarma.backend.huertos.model.Request; import net.miarma.backend.huertos.model.Request;
import net.miarma.backend.huertos.model.RequestMetadata; import net.miarma.backend.huertos.model.RequestMetadata;
@@ -11,88 +10,131 @@ import net.miarma.backlib.dto.UserWithCredentialDto;
import net.miarma.backlib.exception.BadRequestException; import net.miarma.backlib.exception.BadRequestException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.time.Instant;
import java.util.UUID; import java.util.UUID;
@Service @Service
@Transactional
public class RequestAcceptanceService { public class RequestAcceptanceService {
private final RequestService requestService; private final RequestService requestService;
private final UserMetadataService metadataService; private final UserMetadataService metadataService;
private final HuertosWebClient huertosWebClient; private final HuertosWebClient huertosWebClient;
private final MemberService memberService;
public RequestAcceptanceService(RequestService requestService, UserMetadataService metadataService, HuertosWebClient huertosWebClient) { public RequestAcceptanceService(
RequestService requestService,
UserMetadataService metadataService,
HuertosWebClient huertosWebClient,
MemberService memberService
) {
this.requestService = requestService; this.requestService = requestService;
this.metadataService = metadataService; this.metadataService = metadataService;
this.huertosWebClient = huertosWebClient; this.huertosWebClient = huertosWebClient;
this.memberService = memberService;
} }
@Transactional
public Request acceptRequest(UUID requestId) { public Request acceptRequest(UUID requestId) {
Request request = requestService.accept(requestId); Request request = requestService.accept(requestId);
RequestMetadata metadata = request.getMetadata();
if (metadata == null) throw new BadRequestException("No hay metadata asociada"); if (request.getMetadata() == null) {
throw new BadRequestException("No hay metadata asociada");
switch (request.getType()) {
case 0: // REGISTER
UserWithCredentialDto createdUser = huertosWebClient.createUser(
RequestMetadataMapper.toDto(metadata)
);
UserMetadata userMetadata = new UserMetadata();
userMetadata.setUserId(createdUser.user().getUserId());
userMetadata.setMemberNumber(metadata.getMemberNumber());
userMetadata.setPlotNumber(metadata.getPlotNumber());
userMetadata.setDni(metadata.getDni());
userMetadata.setPhone(metadata.getPhone());
userMetadata.setType((byte)0);
userMetadata.setRole((byte)0);
metadataService.create(userMetadata);
break;
case 1: // UNREGISTER
UserMetadata toRemove = metadataService.getByMemberNumber(metadata.getMemberNumber());
huertosWebClient.deleteUser(toRemove.getUserId()); // borramos User + Credential + Metadata
break;
case 2: // ADD_COLLABORATOR
UserWithCredentialDto newCollab = huertosWebClient.createUser(
RequestMetadataMapper.toDto(metadata)
);
UserMetadata collabMeta = new UserMetadata();
collabMeta.setUserId(newCollab.user().getUserId());
collabMeta.setMemberNumber(metadata.getMemberNumber());
collabMeta.setPlotNumber(metadata.getPlotNumber());
collabMeta.setDni(metadata.getDni());
collabMeta.setPhone(metadata.getPhone());
collabMeta.setType((byte)3); // colaborador
collabMeta.setRole((byte)0);
metadataService.create(collabMeta);
break;
case 3: // REMOVE_COLLABORATOR
UserMetadata collabToRemove = metadataService.getByMemberNumber(metadata.getMemberNumber());
huertosWebClient.deleteUser(collabToRemove.getUserId());
break;
case 4: // ADD_GREENHOUSE
UserMetadata greenhouseMeta = metadataService.getByMemberNumber(metadata.getMemberNumber());
greenhouseMeta.setType((byte)2); // invernadero
metadataService.update(greenhouseMeta.getUserId(), greenhouseMeta);
break;
case 5: // REMOVE_GREENHOUSE
UserMetadata ghToRemove = metadataService.getByMemberNumber(metadata.getMemberNumber());
ghToRemove.setType((byte)1); // socio normal
metadataService.update(ghToRemove.getUserId(), ghToRemove);
break;
default:
throw new BadRequestException("Tipo de solicitud no soportado para aceptar");
} }
return request; return request;
} }
public void handleSideEffects(Request request) {
RequestMetadata metadata = request.getMetadata();
switch (request.getType()) {
case 0: // REGISTER
handleRegister(metadata);
break;
case 1: // UNREGISTER
handleUnregister(metadata);
break;
case 2: // ADD_COLLABORATOR
handleAddCollaborator(metadata);
break;
case 3: // REMOVE_COLLABORATOR
handleRemoveCollaborator(metadata);
break;
case 4: // ADD_GREENHOUSE
handleAddGreenhouse(metadata);
break;
case 5: // REMOVE_GREENHOUSE
handleRemoveGreenhouse(metadata);
break;
default:
throw new BadRequestException("Tipo de solicitud no soportado");
}
}
private void handleRegister(RequestMetadata metadata) {
UserWithCredentialDto createdUser =
huertosWebClient.createUser(RequestMetadataMapper.toDto(metadata));
UserMetadata userMetadata = buildBaseUserMetadata(metadata, createdUser.user().getUserId());
userMetadata.setType((byte) 0); // socio
userMetadata.setRole((byte) 0);
metadataService.create(userMetadata);
}
private void handleUnregister(RequestMetadata metadata) {
UserMetadata toRemove = metadataService.getByMemberNumber(metadata.getMemberNumber());
huertosWebClient.updateCredentialStatus(toRemove.getUserId(), (byte)1, (byte)0);
}
private void handleAddCollaborator(RequestMetadata metadata) {
UserWithCredentialDto newCollab =
huertosWebClient.createUser(RequestMetadataMapper.toDto(metadata));
UserMetadata collabMeta = buildBaseUserMetadata(
metadata,
newCollab.user().getUserId()
);
collabMeta.setType((byte) 3); // colaborador
collabMeta.setRole((byte) 0);
metadataService.create(collabMeta);
}
private void handleRemoveCollaborator(RequestMetadata metadata) {
UserMetadata collab = metadataService.getByMemberNumber(metadata.getMemberNumber());
huertosWebClient.updateCredentialStatus(collab.getUserId(), (byte)1, (byte)0);
}
private void handleAddGreenhouse(RequestMetadata metadata) {
UserMetadata user =
metadataService.getByMemberNumber(metadata.getMemberNumber());
user.setType((byte) 2); // invernadero
metadataService.update(user.getUserId(), user);
}
private void handleRemoveGreenhouse(RequestMetadata metadata) {
UserMetadata user =
metadataService.getByMemberNumber(metadata.getMemberNumber());
user.setType((byte) 1); // socio normal
metadataService.update(user.getUserId(), user);
}
private UserMetadata buildBaseUserMetadata(RequestMetadata metadata, UUID userId) {
UserMetadata um = new UserMetadata();
um.setUserId(userId);
um.setMemberNumber(metadata.getMemberNumber());
um.setPlotNumber(metadata.getPlotNumber());
um.setDni(metadata.getDni());
um.setPhone(metadata.getPhone());
return um;
}
} }

View File

@@ -8,6 +8,8 @@ import net.miarma.backend.huertos.dto.RequestDto;
import net.miarma.backend.huertos.dto.RequestMetadataDto; import net.miarma.backend.huertos.dto.RequestMetadataDto;
import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.mapper.RequestMapper;
import net.miarma.backend.huertos.mapper.RequestMetadataMapper; import net.miarma.backend.huertos.mapper.RequestMetadataMapper;
import net.miarma.backend.huertos.validation.RequestValidator;
import net.miarma.backlib.exception.ConflictException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import jakarta.transaction.Transactional; import jakarta.transaction.Transactional;
@@ -47,25 +49,42 @@ public class RequestService {
.toList(); .toList();
} }
public Request create(Request request) {
if (request.getType() == null) throw new BadRequestException("Tipo de solicitud obligatorio");
request.setRequestId(UUID.randomUUID());
request.setCreatedAt(Instant.now());
return requestRepository.save(request);
}
@Transactional @Transactional
public Request createWithMetadata(RequestDto.Request requestDto, public Request create(Request request) {
RequestMetadataDto metadataDto) {
Request request = RequestMapper.toEntity(requestDto); if (request == null) {
RequestMetadata metadata = RequestMetadataMapper.fromDto(metadataDto); throw new BadRequestException("La solicitud es obligatoria");
request.setMetadata(metadata); }
if (request.getType() == null) {
throw new BadRequestException("El tipo de solicitud es obligatorio");
}
if (request.getType() == 1 && hasUnregisterRequest(request.getUserId())) {
throw new ConflictException("Ya tienes una solicitud, espera que se acepte o se elimine al ser rechazada");
}
if ((request.getType() == 2 || request.getType() == 3) &&
hasCollaboratorRequest(request.getUserId())) { // tiene soli de collab
throw new ConflictException("Ya tienes una solicitud, espera que se acepte o se elimine al ser rechazada");
}
if ((request.getType() == 4 || request.getType() == 5) &&
hasGreenhouseRequest(request.getUserId())) { // tiene soli de invernadero
throw new ConflictException("Ya tienes una solicitud, espera que se acepte o se elimine al ser rechazada");
}
request.setRequestId(UUID.randomUUID()); request.setRequestId(UUID.randomUUID());
request.setCreatedAt(Instant.now()); request.setCreatedAt(Instant.now());
request.getMetadata().setRequestId(request.getRequestId());
if (request.getMetadata() != null) {
RequestValidator.validate(request.getMetadata(), request.getType());
}
return requestRepository.save(request); return requestRepository.save(request);
} }
public Request update(UUID requestId, Request changes) { public Request update(UUID requestId, Request changes) {
Request request = getById(requestId); Request request = getById(requestId);
@@ -77,13 +96,21 @@ public class RequestService {
} }
public Request accept(UUID requestId) { public Request accept(UUID requestId) {
Request request = getById(requestId); byte[] bin = UuidUtil.uuidToBin(requestId);
Request request = requestRepository.findByIdWithMetadata(bin)
.orElseThrow(() -> new NotFoundException("Request no encontrada"));
if (request.getStatus() != 0) {
throw new BadRequestException("La solicitud ya ha sido procesada");
}
request.setStatus((byte)1); request.setStatus((byte)1);
return requestRepository.save(request); return requestRepository.save(request);
} }
public Request reject(UUID requestId) { public Request reject(UUID requestId) {
Request request = getById(requestId); Request request = getById(requestId);
if (request.getStatus() != 0) {
throw new BadRequestException("La solicitud ya ha sido procesada");
}
request.setStatus((byte)2); request.setStatus((byte)2);
return requestRepository.save(request); return requestRepository.save(request);
} }
@@ -96,13 +123,18 @@ public class RequestService {
requestRepository.deleteById(UuidUtil.uuidToBin(id)); requestRepository.deleteById(UuidUtil.uuidToBin(id));
} }
public boolean hasPendingGreenhouseRequest(UUID userId) { public boolean hasGreenhouseRequest(UUID userId) {
return getByUserId(userId).stream()
.anyMatch(r -> r.getType() == 4 || r.getType() == 5);
}
public boolean hasCollaboratorRequest(UUID userId) {
return getByUserId(userId).stream()
.anyMatch(r -> r.getType() == 2 || r.getType() == 3);
}
public boolean hasUnregisterRequest(UUID userId) {
return getByUserId(userId).stream() return getByUserId(userId).stream()
.anyMatch(r -> r.getType() == 1); .anyMatch(r -> r.getType() == 1);
} }
public boolean hasPendingCollaboratorRequest(UUID userId) {
return getByUserId(userId).stream()
.anyMatch(r -> r.getType() == 2);
}
} }

View File

@@ -77,7 +77,7 @@ public class UserMetadataService {
"metadataByMemberNumber", "metadataByMemberNumber",
"metadataExists" "metadataExists"
}, },
key = "#userId" key = "#p0"
) )
public UserMetadata update(UUID userId, UserMetadata changes) { public UserMetadata update(UUID userId, UserMetadata changes) {
byte[] idBytes = UuidUtil.uuidToBin(userId); byte[] idBytes = UuidUtil.uuidToBin(userId);

View File

@@ -1,8 +1,10 @@
package net.miarma.backend.huertos.service.view; package net.miarma.backend.huertos.service.view;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import net.miarma.backend.huertos.model.Income;
import net.miarma.backlib.exception.NotFoundException; import net.miarma.backlib.exception.NotFoundException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -22,7 +24,9 @@ public class VIncomesWithInfoService {
} }
public List<VIncomesWithInfo> getAll() { public List<VIncomesWithInfo> getAll() {
return repository.findAll(); return repository.findAll().stream()
.sorted(Comparator.comparing(VIncomesWithInfo::getCreatedAt).reversed())
.toList();
} }
public VIncomesWithInfo getById(UUID incomeId) { public VIncomesWithInfo getById(UUID incomeId) {

View File

@@ -1,7 +1,9 @@
package net.miarma.backend.huertos.util; package net.miarma.backend.huertos.util;
import java.util.Locale;
public class UsernameGenerator { public class UsernameGenerator {
public static String generate(String name, Integer number) { public static String generate(String name, Integer number) {
return name.split(" ")[0] + number; return name.split(" ")[0].toLowerCase() + number;
} }
} }

View File

@@ -0,0 +1,90 @@
package net.miarma.backend.huertos.validation;
import net.miarma.backend.huertos.model.RequestMetadata;
import net.miarma.backlib.exception.BadRequestException;
import net.miarma.backlib.exception.ValidationException;
import java.util.regex.Pattern;
public class RequestValidator {
private static final Pattern DNI_PATTERN = Pattern.compile("\\d{8}[A-Za-z]");
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[\\w.%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$");
private static final Pattern PHONE_PATTERN = Pattern.compile("^\\+?\\d{9,15}$");
public static void validate(RequestMetadata metadata, Byte requestType) {
if (metadata.getRequestId() == null) {
throw new BadRequestException("Estos metadatos deben pertenecer a una solicitud (falta ID)");
}
if (isBlank(metadata.getDisplayName())) {
throw new BadRequestException("El nombre a mostrar es obligatorio");
} else if (metadata.getDisplayName().length() < 3) {
throw new ValidationException("displayName", "El nombre a mostrar debe tener al menos 3 caracteres");
}
if (isBlank(metadata.getDni())) {
throw new BadRequestException("El DNI es obligatorio");
} else if (!DNI_PATTERN.matcher(metadata.getDni()).matches()) {
throw new ValidationException("dni", "Formato de DNI inválido (ej: 12345678A)");
} else if (!DniValidator.isValid(metadata.getDni())) {
throw new ValidationException("dni", "Este DNI no es un DNI real");
}
if (isBlank(metadata.getEmail())) {
throw new BadRequestException("El email es obligatorio");
} else if (!EMAIL_PATTERN.matcher(metadata.getEmail()).matches()) {
throw new ValidationException("email", "Email inválido");
}
if (isBlank(metadata.getUsername())) {
throw new BadRequestException("El usuario es obligatorio");
} else if (metadata.getUsername().length() < 3) {
throw new ValidationException("username", "El usuario debe tener al menos 3 caracteres");
}
if (metadata.getType() == null) {
throw new BadRequestException("El tipo de usuario es obligatorio");
}
if (requestType == 2) {
if (metadata.getPlotNumber() == null) {
throw new BadRequestException("El colaborador debe tener huerto");
}
}
if (requestType == 0 || requestType == 1) {
if (metadata.getMemberNumber() == null) {
throw new BadRequestException("El número de socio es obligatorio");
}
}
if (requestType == 0) {
if (metadata.getAddress() == null || metadata.getZipCode() == null || metadata.getCity() == null) {
throw new BadRequestException("La dirección, código postal y ciudad son obligatorios");
}
}
if (requestType == 0) {
if (isBlank(metadata.getAddress())) {
throw new ValidationException("address", "La dirección es obligatoria");
}
if (isBlank(metadata.getZipCode())) {
throw new ValidationException("zipCode", "El código postal es obligatorio");
} else if(metadata.getZipCode().length() < 5) {
throw new ValidationException("zipCode", "El código postal debe tener 5 dígitos");
}
if (isBlank(metadata.getCity())) {
throw new ValidationException("city", "La ciudad es obligatoria");
}
}
if (metadata.getPhone() != null && !PHONE_PATTERN.matcher(metadata.getPhone()).matches()) {
throw new ValidationException("phone", "Teléfono inválido (debe tener 9 dígitos)");
}
}
private static boolean isBlank(String s) {
return s == null || s.trim().isEmpty();
}
}