diff --git a/backlib/pom.xml b/backlib/pom.xml index 56b9a29..a709b92 100644 --- a/backlib/pom.xml +++ b/backlib/pom.xml @@ -4,7 +4,7 @@ 4.0.0 backlib net.miarma - 1.0.0 + 1.0.1 25 diff --git a/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java b/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java new file mode 100644 index 0000000..8437faa --- /dev/null +++ b/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java @@ -0,0 +1,37 @@ +package net.miarma.backlib.dto; + +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; +import java.util.List; +import java.util.Map; + +public class ApiValidationErrorDto { + private Map errors; + private Instant timestamp; + + public ApiValidationErrorDto(Map errors) { + this.errors = errors; + this.timestamp = Instant.now(); + } + + public Map getErrors() { + return errors; + } + + public void setErrors(Map errors) { + this.errors = errors; + } + + public Instant getTimestamp() { + return timestamp; + } + + public void setTimestamp(Instant timestamp) { + this.timestamp = timestamp; + } + + public String toJson() { + return new ObjectMapper().writeValueAsString(this); + } +} diff --git a/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java b/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java index 941d52d..0a0741d 100644 --- a/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java +++ b/backlib/src/main/java/net/miarma/backlib/exception/ValidationException.java @@ -1,7 +1,21 @@ package net.miarma.backlib.exception; public class ValidationException extends RuntimeException { - public ValidationException(String message) { + private final String field; + private final String message; + + public ValidationException(String field, String message) { super(message); + this.field = field; + this.message = message; + } + + public String getField() { + return field; + } + + @Override + public String getMessage() { + return message; } } diff --git a/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java b/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java index 3ac8dc5..3028b35 100644 --- a/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java +++ b/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java @@ -2,12 +2,15 @@ package net.miarma.backlib.http; import jakarta.servlet.http.HttpServletRequest; import net.miarma.backlib.dto.ApiErrorDto; +import net.miarma.backlib.dto.ApiValidationErrorDto; import net.miarma.backlib.exception.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import java.util.Map; + @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(NotFoundException.class) @@ -81,17 +84,9 @@ public class GlobalExceptionHandler { } @ExceptionHandler(ValidationException.class) - public ResponseEntity handleValidation( - ValidationException ex, HttpServletRequest req) { - - ApiErrorDto error = new ApiErrorDto( - HttpStatus.UNPROCESSABLE_CONTENT.value(), - HttpStatus.UNPROCESSABLE_CONTENT.getReasonPhrase(), - ex.getMessage(), - req.getRequestURI() - ); - - return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(error); + public ResponseEntity handleValidation(ValidationException ex) { + Map errors = Map.of(ex.getField(), ex.getMessage()); + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors)); } @ExceptionHandler(Exception.class) diff --git a/backlib/src/main/java/net/miarma/backlib/security/CoreAuthTokenHolder.java b/backlib/src/main/java/net/miarma/backlib/security/CoreAuthTokenHolder.java index 9585a68..60b760f 100644 --- a/backlib/src/main/java/net/miarma/backlib/security/CoreAuthTokenHolder.java +++ b/backlib/src/main/java/net/miarma/backlib/security/CoreAuthTokenHolder.java @@ -2,15 +2,23 @@ package net.miarma.backlib.security; import org.springframework.stereotype.Component; +import java.time.Instant; + @Component public class CoreAuthTokenHolder { private volatile String token; + private volatile Instant expiresAt; public String getToken() { return token; } - public void setToken(String token) { + public boolean isExpired() { + return expiresAt == null || Instant.now().isAfter(expiresAt.minusSeconds(30)); + } + + public void setToken(String token, Instant expiresAt) { this.token = token; + this.expiresAt = expiresAt; } } diff --git a/core/pom.xml b/core/pom.xml index 446f550..48b09f8 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -83,7 +83,7 @@ net.miarma backlib - 1.0.0 + 1.0.1 diff --git a/core/src/main/java/net/miarma/backend/core/config/CorsConfig.java b/core/src/main/java/net/miarma/backend/core/config/CorsConfig.java new file mode 100644 index 0000000..ced0782 --- /dev/null +++ b/core/src/main/java/net/miarma/backend/core/config/CorsConfig.java @@ -0,0 +1,28 @@ +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); + } + }; + } +} diff --git a/core/src/main/java/net/miarma/backend/core/config/SecurityConfig.java b/core/src/main/java/net/miarma/backend/core/config/SecurityConfig.java index 80c9e00..6921383 100644 --- a/core/src/main/java/net/miarma/backend/core/config/SecurityConfig.java +++ b/core/src/main/java/net/miarma/backend/core/config/SecurityConfig.java @@ -6,6 +6,7 @@ import net.miarma.backlib.http.RestAuthEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; +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.web.builders.HttpSecurity; @@ -15,6 +16,11 @@ 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.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -34,9 +40,23 @@ public class SecurityConfig { this.accessDeniedHandler = accessDeniedHandler; } + @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 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http + .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(ex -> ex diff --git a/core/src/main/java/net/miarma/backend/core/controller/AuthController.java b/core/src/main/java/net/miarma/backend/core/controller/AuthController.java index 87fee3f..af50a43 100644 --- a/core/src/main/java/net/miarma/backend/core/controller/AuthController.java +++ b/core/src/main/java/net/miarma/backend/core/controller/AuthController.java @@ -1,15 +1,13 @@ package net.miarma.backend.core.controller; +import java.util.List; import java.util.Map; import java.util.UUID; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestHeader; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import jakarta.validation.Valid; import net.miarma.backend.core.model.Credential; @@ -106,4 +104,10 @@ public class AuthController { return ResponseEntity.ok(Map.of("message", "Password changed successfully")); } + + @GetMapping("/validate") + public ResponseEntity validate(@RequestHeader("Authorization") String authHeader) { + String token = authHeader.substring(7); + return ResponseEntity.ok(jwtService.validateToken(token)); + } } diff --git a/core/src/main/java/net/miarma/backend/core/controller/FileController.java b/core/src/main/java/net/miarma/backend/core/controller/FileController.java index da06b8b..43e9fe6 100644 --- a/core/src/main/java/net/miarma/backend/core/controller/FileController.java +++ b/core/src/main/java/net/miarma/backend/core/controller/FileController.java @@ -72,7 +72,7 @@ public class FileController { @PutMapping("/{fileId}") @PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)") - public ResponseEntity update(@PathVariable("file_id") UUID fileId, @RequestBody File file) { + public ResponseEntity update(@PathVariable("fileId") UUID fileId, @RequestBody File file) { file.setFileId(fileId); File updated = fileService.update(file); return ResponseEntity.ok(updated); @@ -80,7 +80,7 @@ public class FileController { @DeleteMapping("/{fileId}") @PreAuthorize("hasRole('ADMIN') or @fileService.isOwner(#fileId, authentication.principal.userId)") - public ResponseEntity delete(@PathVariable("file_id") UUID fileId, @RequestBody Map body) throws IOException { + public ResponseEntity delete(@PathVariable("fileId") UUID fileId, @RequestBody Map body) throws IOException { String filePath = body.get("file_path"); Files.deleteIfExists(Paths.get(filePath)); fileService.delete(fileId); diff --git a/core/src/main/java/net/miarma/backend/core/repository/CredentialRepository.java b/core/src/main/java/net/miarma/backend/core/repository/CredentialRepository.java index 32ee78c..5317644 100644 --- a/core/src/main/java/net/miarma/backend/core/repository/CredentialRepository.java +++ b/core/src/main/java/net/miarma/backend/core/repository/CredentialRepository.java @@ -28,7 +28,8 @@ public interface CredentialRepository extends JpaRepository Optional findByServiceIdAndEmail(Byte serviceId, String email); - Optional findByUserIdAndServiceId(UUID userId, Byte serviceId); + @Query("SELECT c FROM Credential c WHERE c.userIdBin = :userIdBin AND c.serviceId = :serviceId") + Optional findByUserIdAndServiceId(@Param("userIdBin") byte[] userIdBin, @Param("serviceId") Byte serviceId); Optional findByUsernameAndServiceId(String username, int serviceId); diff --git a/core/src/main/java/net/miarma/backend/core/service/CredentialService.java b/core/src/main/java/net/miarma/backend/core/service/CredentialService.java index 9b36308..d690c4d 100644 --- a/core/src/main/java/net/miarma/backend/core/service/CredentialService.java +++ b/core/src/main/java/net/miarma/backend/core/service/CredentialService.java @@ -38,16 +38,16 @@ public class CredentialService { public Credential create(Credential credential) { if (credential.getUsername() == null || credential.getUsername().isBlank()) { - throw new ValidationException("Username cannot be blank"); + throw new ValidationException("userName", "Username cannot be blank"); } if (credential.getEmail() == null || !credential.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { - throw new ValidationException("Invalid email format"); + throw new ValidationException("email", "Invalid email format"); } if (credential.getPassword() == null || credential.getPassword().length() < 6) { - throw new ValidationException("Password must be at least 6 characters"); + throw new ValidationException("password", "Password must be at least 6 characters"); } if (credential.getServiceId() == null || credential.getServiceId() < 0) { - throw new ValidationException("ServiceId must be positive"); + throw new ValidationException("serviceId", "ServiceId must be positive"); } boolean existsUsername = credentialRepository.existsByUsernameAndServiceId( @@ -90,7 +90,7 @@ public class CredentialService { } public Credential getByUserIdAndService(UUID userId, Byte serviceId) { - return credentialRepository.findByUserIdAndServiceId(userId, serviceId) + return credentialRepository.findByUserIdAndServiceId(UuidUtil.uuidToBin(userId), serviceId) .orElseThrow(() -> new NotFoundException("Credential not found in this site")); } @@ -117,13 +117,13 @@ public class CredentialService { .orElseThrow(() -> new NotFoundException("Credential not found")); if (dto.getUsername() != null && dto.getUsername().isBlank()) { - throw new ValidationException("Username cannot be blank"); + throw new ValidationException("userName", "Username cannot be blank"); } if (dto.getEmail() != null && !dto.getEmail().matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")) { - throw new ValidationException("Invalid email format"); + throw new ValidationException("email", "Invalid email format"); } if (dto.getServiceId() != null && dto.getServiceId() < 0) { - throw new ValidationException("ServiceId must be positive"); + throw new ValidationException("serviceId", "ServiceId must be positive"); } if (dto.getUsername() != null) cred.setUsername(dto.getUsername()); @@ -141,7 +141,7 @@ public class CredentialService { .orElseThrow(() -> new NotFoundException("Credential not found")); if (!passwordEncoder.matches(request.oldPassword(), cred.getPassword())) { - throw new ValidationException("Old password is incorrect"); + throw new ValidationException("oldPassword", "Old password is incorrect"); } cred.setPassword(passwordEncoder.encode(request.newPassword())); diff --git a/core/src/main/java/net/miarma/backend/core/service/FileService.java b/core/src/main/java/net/miarma/backend/core/service/FileService.java index 442bc0e..f7568f3 100644 --- a/core/src/main/java/net/miarma/backend/core/service/FileService.java +++ b/core/src/main/java/net/miarma/backend/core/service/FileService.java @@ -12,6 +12,7 @@ import java.util.UUID; import net.miarma.backend.core.mapper.FileMapper; import net.miarma.backlib.dto.FileDto; import net.miarma.backlib.exception.NotFoundException; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -24,7 +25,9 @@ import net.miarma.backlib.util.UuidUtil; public class FileService { private final FileRepository fileRepository; - private final String basePath = "/var/www/files"; + + @Value("${filesDir}") + private String filesDir; public FileService(FileRepository fileRepository) { this.fileRepository = fileRepository; @@ -45,7 +48,7 @@ public class FileService { } public File create(FileDto.Request dto, byte[] fileBinary) throws IOException { - Path dirPath = Paths.get(basePath, String.valueOf(dto.getContext())); + Path dirPath = Paths.get(filesDir, String.valueOf(dto.getContext())); if (!Files.exists(dirPath)) { Files.createDirectories(dirPath); } diff --git a/core/src/main/java/net/miarma/backend/core/service/UserService.java b/core/src/main/java/net/miarma/backend/core/service/UserService.java index 5c56aa1..7bced9e 100644 --- a/core/src/main/java/net/miarma/backend/core/service/UserService.java +++ b/core/src/main/java/net/miarma/backend/core/service/UserService.java @@ -36,7 +36,7 @@ public class UserService { public User create(UserDto dto) { if(dto.getDisplayName() == null || dto.getDisplayName().isBlank()) { - throw new ValidationException("Display name is required"); + throw new ValidationException("displayName", "Display name is required"); } User user = new User(); diff --git a/core/src/main/resources/application-dev.yml b/core/src/main/resources/application-dev.yml index 6b02d69..e59dee3 100644 --- a/core/src/main/resources/application-dev.yml +++ b/core/src/main/resources/application-dev.yml @@ -16,6 +16,8 @@ logging: org.hibernate.orm.jdbc.bind: TRACE org.springframework.security: DEBUG +filesDir: "/home/jomaa/.config/miarma-backend/files" + jwt: private-key-path: /home/jomaa/.config/miarma-backend/private.pem public-key-path: /home/jomaa/.config/miarma-backend/public.pem diff --git a/core/src/main/resources/application-prod.yml b/core/src/main/resources/application-prod.yml index d1881bd..0dc5436 100644 --- a/core/src/main/resources/application-prod.yml +++ b/core/src/main/resources/application-prod.yml @@ -15,6 +15,8 @@ logging: org.springframework.security: INFO org.hibernate.SQL: WARN +filesDir: "/files" + jwt: private-key-path: ${JWT_PRIVATE_KEY} public-key-path: ${JWT_PUBLIC_KEY} diff --git a/huertos/pom.xml b/huertos/pom.xml index 2bdaf75..e32e811 100644 --- a/huertos/pom.xml +++ b/huertos/pom.xml @@ -67,7 +67,7 @@ net.miarma backlib - 1.0.0 + 1.0.1 compile diff --git a/huertos/src/main/java/net/miarma/backend/huertos/HuertosApplication.java b/huertos/src/main/java/net/miarma/backend/huertos/HuertosApplication.java index 9b317f6..fd36931 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/HuertosApplication.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/HuertosApplication.java @@ -2,6 +2,7 @@ package net.miarma.backend.huertos; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; @SpringBootApplication(scanBasePackages = { "net.miarma.backend.huertos", diff --git a/huertos/src/main/java/net/miarma/backend/huertos/client/CoreAuthClient.java b/huertos/src/main/java/net/miarma/backend/huertos/client/CoreAuthClient.java new file mode 100644 index 0000000..03a13ec --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/client/CoreAuthClient.java @@ -0,0 +1,34 @@ +package net.miarma.backend.huertos.client; + +import net.miarma.backlib.dto.LoginRequest; +import net.miarma.backlib.dto.LoginResponse; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +@Component +public class CoreAuthClient { + + private final RestTemplate restTemplate; + private final String coreUrl; + + public CoreAuthClient( + @Qualifier("authRestTemplate") RestTemplate restTemplate, + @Value("${core.url}") String coreUrl + ) { + this.restTemplate = restTemplate; + this.coreUrl = coreUrl; + } + + + public LoginResponse login(LoginRequest req) { + return restTemplate.postForObject( + coreUrl + "/auth/login", + req, + LoginResponse.class + ); + } +} diff --git a/huertos/src/main/java/net/miarma/backend/huertos/client/HuertosWebClient.java b/huertos/src/main/java/net/miarma/backend/huertos/client/HuertosWebClient.java index cf5c462..1d9bfcb 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/client/HuertosWebClient.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/client/HuertosWebClient.java @@ -3,6 +3,7 @@ package net.miarma.backend.huertos.client; import net.miarma.backlib.dto.LoginRequest; import net.miarma.backlib.dto.LoginResponse; import net.miarma.backlib.dto.UserWithCredentialDto; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @@ -17,22 +18,12 @@ public class HuertosWebClient { private final RestTemplate restTemplate; private final String coreUrl; - public HuertosWebClient(RestTemplate restTemplate, + public HuertosWebClient(@Qualifier("secureRestTemplate") RestTemplate restTemplate, @Value("${core.url}") String coreUrl) { this.restTemplate = restTemplate; this.coreUrl = coreUrl; } - public LoginResponse login(String username, String password) { - LoginRequest req = new LoginRequest(username, password, (byte) 1); - LoginResponse resp = restTemplate.postForObject( - coreUrl + "/auth/login", - req, - LoginResponse.class - ); - return resp; - } - public UserWithCredentialDto getUserWithCredential(UUID userId, Byte serviceId) { return restTemplate.getForObject( coreUrl + "/users/{user_id}/service/{service_id}", diff --git a/huertos/src/main/java/net/miarma/backend/huertos/config/CorsConfig.java b/huertos/src/main/java/net/miarma/backend/huertos/config/CorsConfig.java new file mode 100644 index 0000000..0b5d0f2 --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/config/CorsConfig.java @@ -0,0 +1,27 @@ +package net.miarma.backend.huertos.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" + ) + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } + }; + } +} + diff --git a/huertos/src/main/java/net/miarma/backend/huertos/config/HuertosStartup.java b/huertos/src/main/java/net/miarma/backend/huertos/config/HuertosStartup.java deleted file mode 100644 index 3e717cf..0000000 --- a/huertos/src/main/java/net/miarma/backend/huertos/config/HuertosStartup.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.miarma.backend.huertos.config; - -import net.miarma.backend.huertos.client.HuertosWebClient; -import net.miarma.backlib.security.CoreAuthTokenHolder; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.ApplicationArguments; -import org.springframework.boot.ApplicationRunner; -import org.springframework.stereotype.Component; - -@Component -public class HuertosStartup implements ApplicationRunner { - - private final HuertosWebClient client; - private final CoreAuthTokenHolder tokenHolder; - private final String user; - private final String pass; - - public HuertosStartup( - HuertosWebClient client, - CoreAuthTokenHolder tokenHolder, - @Value("${huertos.user}") String user, - @Value("${huertos.password}") String pass - ) { - this.client = client; - this.tokenHolder = tokenHolder; - this.user = user; - this.pass = pass; - } - - @Override - public void run(ApplicationArguments args) { - String token = client.login(user, pass).token(); - tokenHolder.setToken(token); - System.out.println("TOKEN recibido: " + token); - System.out.println("HUERTOS CLIENT has been authenticated in CORE"); - } -} diff --git a/huertos/src/main/java/net/miarma/backend/huertos/config/RestTemplateConfig.java b/huertos/src/main/java/net/miarma/backend/huertos/config/RestTemplateConfig.java index c3dedb1..ef07032 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/config/RestTemplateConfig.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/config/RestTemplateConfig.java @@ -1,5 +1,6 @@ package net.miarma.backend.huertos.config; +import net.miarma.backend.huertos.service.CoreAuthService; import net.miarma.backlib.security.CoreAuthTokenHolder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -13,23 +14,21 @@ import java.util.List; public class RestTemplateConfig { @Bean - public RestTemplate restTemplate(CoreAuthTokenHolder tokenHolder) { - RestTemplate restTemplate = new RestTemplate(); - - List interceptors = new ArrayList<>(); - interceptors.add(authInterceptor(tokenHolder)); - - restTemplate.setInterceptors(interceptors); - return restTemplate; + public RestTemplate authRestTemplate() { + return new RestTemplate(); } - private ClientHttpRequestInterceptor authInterceptor(CoreAuthTokenHolder tokenHolder) { - return (request, body, execution) -> { - String token = tokenHolder.getToken(); - if (token != null) { - request.getHeaders().add("Authorization", "Bearer " + token); - } + @Bean + public RestTemplate secureRestTemplate(CoreAuthService coreAuthService) { + RestTemplate rt = new RestTemplate(); + + rt.getInterceptors().add((request, body, execution) -> { + String token = coreAuthService.getToken(); + request.getHeaders().setBearerAuth(token); return execution.execute(request, body); - }; + }); + + return rt; } } + diff --git a/huertos/src/main/java/net/miarma/backend/huertos/config/SecurityConfig.java b/huertos/src/main/java/net/miarma/backend/huertos/config/SecurityConfig.java index 1425347..5bc0f3d 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/config/SecurityConfig.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/config/SecurityConfig.java @@ -5,12 +5,18 @@ import net.miarma.backlib.http.RestAccessDeniedHandler; import net.miarma.backlib.http.RestAuthEntryPoint; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.List; @Configuration @EnableWebSecurity @@ -31,9 +37,23 @@ public class SecurityConfig { this.accessDeniedHandler = accessDeniedHandler; } + @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 public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http + .cors(Customizer.withDefaults()) .csrf(csrf -> csrf.disable()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(ex -> ex @@ -43,10 +63,9 @@ public class SecurityConfig { .authorizeHttpRequests(auth -> auth // PUBLICAS .requestMatchers("/auth/login").permitAll() - .requestMatchers("/announcements").permitAll() - .requestMatchers("/requests/mine").permitAll() .requestMatchers("/users/waitlist/limited").permitAll() .requestMatchers("/users/latest-number").permitAll() + .requestMatchers("/pre-users/validate").permitAll() // PRIVADAS .anyRequest().authenticated() ); diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/AnnouncementController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/AnnouncementController.java index 6201cb9..d7543fb 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/AnnouncementController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/AnnouncementController.java @@ -50,6 +50,16 @@ public class AnnouncementController { ); } + @PostMapping + @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") + public ResponseEntity create(@RequestBody AnnouncementDto.Request dto) { + return ResponseEntity.ok( + AnnouncementMapper.toResponse( + announcementService.create( + AnnouncementMapper.toEntity(dto) + ))); + } + @DeleteMapping("/{announce_id}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity> delete(@PathVariable("announce_id") UUID announcementId) { diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/BalanceController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/BalanceController.java index 7fd9005..81c86b1 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/BalanceController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/BalanceController.java @@ -7,6 +7,7 @@ import net.miarma.backend.huertos.service.BalanceService; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -25,4 +26,14 @@ public class BalanceController { Balance balance = balanceService.get(); return ResponseEntity.ok(BalanceMapper.toDto(balance)); } + + @PostMapping + @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") + public ResponseEntity setBalance(BalanceDto dto) { + return ResponseEntity.ok( + BalanceMapper.toDto( + balanceService.create(BalanceMapper.toEntity(dto)) + ) + ); + } } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/ExpenseController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/ExpenseController.java index d987abc..4130539 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/ExpenseController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/ExpenseController.java @@ -32,6 +32,16 @@ public class ExpenseController { ); } + @PostMapping + @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") + public ResponseEntity create(ExpenseDto.Request dto) { + return ResponseEntity.ok( + ExpenseMapper.toResponse( + expenseService.create( + ExpenseMapper.toEntity(dto) + ))); + } + @GetMapping("/{expense_id}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity getById(@PathVariable("expense_id") UUID expenseId) { diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/HuertosAuthController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/HuertosAuthController.java index 67e2adc..0bca611 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/HuertosAuthController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/HuertosAuthController.java @@ -1,5 +1,6 @@ package net.miarma.backend.huertos.controller; +import net.miarma.backend.huertos.client.CoreAuthClient; import net.miarma.backend.huertos.client.HuertosWebClient; import net.miarma.backend.huertos.dto.HuertosLoginResponse; import net.miarma.backend.huertos.dto.HuertosUserMetadataDto; @@ -18,17 +19,17 @@ import org.springframework.web.bind.annotation.RestController; @RequestMapping("/auth") public class HuertosAuthController { private final HuertosUserMetadataService metadataService; - private final HuertosWebClient webClient; + private final CoreAuthClient authClient; public HuertosAuthController(HuertosUserMetadataService metadataService, - HuertosWebClient webClient) { + CoreAuthClient authClient) { this.metadataService = metadataService; - this.webClient = webClient; + this.authClient = authClient; } @PostMapping("/login") public ResponseEntity login(@RequestBody LoginRequest req) { - LoginResponse coreResponse = webClient.login(req.username(), req.password()); + LoginResponse coreResponse = authClient.login(req); HuertosUserMetadata metadata = metadataService.getById(coreResponse.user().getUserId()); return ResponseEntity.ok( new HuertosLoginResponse( diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/IncomeController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/IncomeController.java index 125fa4c..df17993 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/IncomeController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/IncomeController.java @@ -1,7 +1,9 @@ package net.miarma.backend.huertos.controller; +import net.miarma.backend.huertos.dto.ExpenseDto; import net.miarma.backend.huertos.dto.IncomeDto; import net.miarma.backend.huertos.dto.view.VIncomesWithFullNamesDto; +import net.miarma.backend.huertos.mapper.ExpenseMapper; import net.miarma.backend.huertos.mapper.IncomeMapper; import net.miarma.backend.huertos.mapper.view.VIncomesWithFullNamesMapper; import net.miarma.backend.huertos.model.Income; @@ -75,6 +77,16 @@ public class IncomeController { ); } + @PostMapping + @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") + public ResponseEntity create(IncomeDto.Request dto) { + return ResponseEntity.ok( + IncomeMapper.toResponse( + incomeService.create( + IncomeMapper.toEntity(dto) + ))); + } + @GetMapping("/{income_id}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity getById(@PathVariable("income_id") UUID incomeId) { diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/MemberController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/MemberController.java index 8cd7897..4be3992 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/MemberController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/MemberController.java @@ -1,11 +1,17 @@ package net.miarma.backend.huertos.controller; -import net.miarma.backend.huertos.dto.MemberDto; -import net.miarma.backend.huertos.dto.WaitlistCensoredDto; +import net.miarma.backend.huertos.dto.*; +import net.miarma.backend.huertos.mapper.IncomeMapper; +import net.miarma.backend.huertos.mapper.RequestMapper; import net.miarma.backend.huertos.security.HuertosPrincipal; +import net.miarma.backend.huertos.service.IncomeService; import net.miarma.backend.huertos.service.MemberService; +import net.miarma.backend.huertos.service.RequestService; import net.miarma.backlib.security.JwtService; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.*; @@ -17,106 +23,130 @@ import java.util.UUID; public class MemberController { private final MemberService memberService; + private final RequestService requestService; + private final IncomeService incomeService; + private final JwtService jwtService; - public MemberController(MemberService memberService) { + public MemberController(MemberService memberService, RequestService requestService, IncomeService incomeService, JwtService jwtService) { this.memberService = memberService; - } - - private Byte getServiceIdFromToken() { - var auth = SecurityContextHolder.getContext().getAuthentication(); - - if (auth == null || !(auth.getPrincipal() instanceof HuertosPrincipal principal)) { - return null; - } - - return principal.getServiceId(); + this.requestService = requestService; + this.incomeService = incomeService; + this.jwtService = jwtService; } @GetMapping @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public List getAll() { - return memberService.getAll(getServiceIdFromToken()); - } - - @GetMapping("/{user_id}") - @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public MemberDto getById(@PathVariable("user_id") UUID userId) { - return memberService.getById(userId, getServiceIdFromToken()); + public ResponseEntity> getAll() { + return ResponseEntity.ok(memberService.getAll((byte)1)); } @GetMapping("/me") - public MemberDto getMe() { - HuertosPrincipal principal = - (HuertosPrincipal) SecurityContextHolder - .getContext() - .getAuthentication() - .getPrincipal(); + public ResponseEntity getMe() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - return memberService.getById(principal.getUserId(), getServiceIdFromToken()); + if (!(authentication.getPrincipal() instanceof HuertosPrincipal principal)) { + throw new IllegalStateException("Invalid principal type"); + } + + UUID userId = principal.getUserId(); + Byte serviceId = principal.getServiceId(); + + if (serviceId == null) { + throw new IllegalStateException("ServiceId missing in token"); + } + + MemberDto member = memberService.getById(userId); + Integer memberNumber = member.metadata().getMemberNumber(); + List requests = requestService.getByUserId(userId).stream() + .map(RequestMapper::toResponse) + .toList(); + List payments = incomeService.getByMemberNumber(memberNumber).stream() + .map(IncomeMapper::toResponse) + .toList(); + + return ResponseEntity.ok( + new MemberProfileDto( + member.user(), + member.account(), + member.metadata(), + requests, + payments, + memberService.hasCollaborator(memberNumber), + memberService.hasGreenhouse(memberNumber), + memberService.hasCollaboratorRequest(memberNumber), + memberService.hasGreenhouseRequest(memberNumber) + ) + ); + } + + @GetMapping("/{user_id:[0-9a-fA-F\\-]{36}}") + @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") + public ResponseEntity getById(@PathVariable("user_id") UUID userId) { + return ResponseEntity.ok(memberService.getById(userId)); } @GetMapping("/latest-number") - public Integer getLatestNumber() { - return memberService.getLatestMemberNumber(); + public ResponseEntity getLatestNumber() { + return ResponseEntity.ok(memberService.getLatestMemberNumber()); } @GetMapping("/waitlist") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public List getWaitlist() { - return memberService.getWaitlist(); + public ResponseEntity> getWaitlist() { + return ResponseEntity.ok(memberService.getWaitlist()); } @GetMapping("/waitlist/limited") - public List getWaitlistLimited() { - return memberService.getWaitlistLimited(); + public ResponseEntity> getWaitlistLimited() { + return ResponseEntity.ok(memberService.getWaitlistLimited()); } @GetMapping("/number/{member_number}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public MemberDto getByMemberNumber(@PathVariable("member_number") Integer memberNumber) { - return memberService.getByMemberNumber(memberNumber); + public ResponseEntity getByMemberNumber(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.getByMemberNumber(memberNumber)); } @GetMapping("/number/{member_number}/incomes") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public Boolean getMemberIncomes(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasIncomes(memberNumber); + public ResponseEntity> getMemberIncomes(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.getIncomes(memberNumber)); } @GetMapping("/number/{member_number}/has-paid") - public Boolean getMemberHasPaid(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasPaid(memberNumber); + public ResponseEntity getMemberHasPaid(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.hasPaid(memberNumber)); } @GetMapping("/number/{member_number}/has-collaborator") - public Boolean getMemberHasCollaborator(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasCollaborator(memberNumber); + public ResponseEntity getMemberHasCollaborator(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.hasCollaborator(memberNumber)); } @GetMapping("/number/{member_number}/has-greenhouse") - public Boolean getMemberHasGreenhouse(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasGreenhouse(memberNumber); + public ResponseEntity getMemberHasGreenhouse(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.hasGreenhouse(memberNumber)); } @GetMapping("/number/{member_number}/has-collaborator-request") - public Boolean getMemberHasCollaboratorRequest(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasCollaboratorRequest(memberNumber); + public ResponseEntity getMemberHasCollaboratorRequest(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.hasCollaboratorRequest(memberNumber)); } @GetMapping("/number/{member_number}/has-greenhouse-request") - public Boolean getMemberHasGreenhouseRequest(@PathVariable("member_number") Integer memberNumber) { - return memberService.hasGreenhouseRequest(memberNumber); + public ResponseEntity getMemberHasGreenhouseRequest(@PathVariable("member_number") Integer memberNumber) { + return ResponseEntity.ok(memberService.hasGreenhouseRequest(memberNumber)); } @GetMapping("/plot/{plot_number}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public MemberDto getByPlotNumber(@PathVariable("plot_number") Integer plotNumber) { - return memberService.getByPlotNumber(plotNumber); + public ResponseEntity getByPlotNumber(@PathVariable("plot_number") Integer plotNumber) { + return ResponseEntity.ok(memberService.getByPlotNumber(plotNumber)); } @GetMapping("/dni/{dni}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") - public MemberDto getByDni(@PathVariable("dni") String dni) { - return memberService.getByDni(dni); + public ResponseEntity getByDni(@PathVariable("dni") String dni) { + return ResponseEntity.ok(memberService.getByDni(dni)); } } \ No newline at end of file diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/PreUserController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/PreUserController.java index 452b2f4..2d84126 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/PreUserController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/PreUserController.java @@ -4,6 +4,8 @@ import net.miarma.backend.huertos.dto.PreUserDto; import net.miarma.backend.huertos.mapper.PreUserMapper; import net.miarma.backend.huertos.model.PreUser; import net.miarma.backend.huertos.service.PreUserService; +import net.miarma.backlib.dto.ApiValidationErrorDto; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.*; @@ -13,7 +15,7 @@ import java.util.Map; import java.util.UUID; @RestController -@RequestMapping("/pre_users") +@RequestMapping("/pre-users") public class PreUserController { private final PreUserService preUserService; @@ -40,6 +42,14 @@ public class PreUserController { return ResponseEntity.ok(PreUserMapper.toResponse(preUser)); } + @PostMapping("/validate") + public ResponseEntity validate(@RequestBody PreUserDto.Request request) { + Map errors = preUserService.validate(request); + if(!errors.isEmpty()) + return ResponseEntity.status(HttpStatus.UNPROCESSABLE_CONTENT).body(new ApiValidationErrorDto(errors)); + return ResponseEntity.ok(new ApiValidationErrorDto(Map.of())); + } + @PutMapping("/{pre_user_id}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity update( diff --git a/huertos/src/main/java/net/miarma/backend/huertos/controller/RequestController.java b/huertos/src/main/java/net/miarma/backend/huertos/controller/RequestController.java index 44fc538..a2151e0 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/controller/RequestController.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/controller/RequestController.java @@ -1,16 +1,10 @@ package net.miarma.backend.huertos.controller; import jakarta.transaction.Transactional; -import net.miarma.backend.huertos.dto.CreateWaitlistDto; -import net.miarma.backend.huertos.dto.PreUserDto; -import net.miarma.backend.huertos.dto.RequestCountDto; -import net.miarma.backend.huertos.dto.RequestDto; +import net.miarma.backend.huertos.dto.*; import net.miarma.backend.huertos.dto.view.VRequestsWithPreUsersDto; -import net.miarma.backend.huertos.mapper.PreUserMapper; import net.miarma.backend.huertos.mapper.RequestMapper; -import net.miarma.backend.huertos.mapper.view.VIncomesWithFullNamesMapper; import net.miarma.backend.huertos.mapper.view.VRequestsWithPreUsersMapper; -import net.miarma.backend.huertos.model.PreUser; import net.miarma.backend.huertos.model.Request; import net.miarma.backend.huertos.model.view.VRequestsWithPreUsers; import net.miarma.backend.huertos.service.PreUserService; @@ -57,6 +51,23 @@ public class RequestController { ); } + @PostMapping + public ResponseEntity create(RequestDto.Request dto) { + return ResponseEntity.ok( + RequestMapper.toResponse( + requestService.createWaitlist( + RequestMapper.toEntity(dto) + ))); + } + + @PostMapping("/waitlist") + @Transactional + public ResponseEntity createWaitlist( + @RequestBody CreateWaitlistDto body) { + RequestDto.Response response = requestService.createWaitlist(body.request(), body.preUser()); + return ResponseEntity.ok(response); + } + @GetMapping("/count") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity getRequestCount() { @@ -119,14 +130,6 @@ public class RequestController { return ResponseEntity.ok(RequestMapper.toResponse(request)); } - @PostMapping - @Transactional - public ResponseEntity create( - @RequestBody CreateWaitlistDto body) { - RequestDto.Response response = requestService.create(body.request(), body.preUser()); - return ResponseEntity.ok(response); - } - @PutMapping("/{request_id}") @PreAuthorize("hasAnyRole('HUERTOS_ROLE_ADMIN', 'HUERTOS_ROLE_DEV')") public ResponseEntity update( diff --git a/huertos/src/main/java/net/miarma/backend/huertos/dto/AnnouncementDto.java b/huertos/src/main/java/net/miarma/backend/huertos/dto/AnnouncementDto.java index 3c8e3fe..85e48bf 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/dto/AnnouncementDto.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/dto/AnnouncementDto.java @@ -39,6 +39,7 @@ public class AnnouncementDto { private String body; private Byte priority; private UUID publishedBy; + private String publishedByName; private Instant createdAt; public UUID getAnnounceId() { @@ -73,6 +74,10 @@ public class AnnouncementDto { this.publishedBy = publishedBy; } + public String getPublishedByName() { return publishedByName; } + + public void setPublishedByName(String publishedByName) { this.publishedByName = publishedByName; } + public Instant getCreatedAt() { return createdAt; } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/dto/MemberProfileDto.java b/huertos/src/main/java/net/miarma/backend/huertos/dto/MemberProfileDto.java new file mode 100644 index 0000000..655e7d1 --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/dto/MemberProfileDto.java @@ -0,0 +1,19 @@ +package net.miarma.backend.huertos.dto; + +import net.miarma.backlib.dto.CredentialDto; +import net.miarma.backlib.dto.UserDto; + +import java.util.List; + +public record MemberProfileDto( + UserDto user, + CredentialDto account, + HuertosUserMetadataDto metadata, + List requests, + List payments, + boolean hasCollaborator, + boolean hasGreenhouse, + boolean hasCollaboratorRequest, + boolean hasGreenhouseRequest +) { +} diff --git a/huertos/src/main/java/net/miarma/backend/huertos/dto/RequestDto.java b/huertos/src/main/java/net/miarma/backend/huertos/dto/RequestDto.java index d6dc42d..316e82e 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/dto/RequestDto.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/dto/RequestDto.java @@ -1,5 +1,7 @@ package net.miarma.backend.huertos.dto; +import jakarta.annotation.Nullable; + import java.time.Instant; import java.util.UUID; @@ -33,15 +35,15 @@ public class RequestDto { this.requestedBy = requestedBy; } - public UUID getTargetUserId() { + public @Nullable UUID getTargetUserId() { return targetUserId; } - public void setTargetUserId(UUID targetUserId) { + public void setTargetUserId(@Nullable UUID targetUserId) { this.targetUserId = targetUserId; } - private UUID targetUserId; + @Nullable private UUID targetUserId; } public static class Response { diff --git a/huertos/src/main/java/net/miarma/backend/huertos/mapper/AnnouncementMapper.java b/huertos/src/main/java/net/miarma/backend/huertos/mapper/AnnouncementMapper.java index 87b7e4b..0d3d109 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/mapper/AnnouncementMapper.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/mapper/AnnouncementMapper.java @@ -14,6 +14,7 @@ public class AnnouncementMapper { dto.setBody(entity.getBody()); dto.setPriority(entity.getPriority()); dto.setPublishedBy(entity.getPublishedBy()); + dto.setPublishedByName(entity.getPublishedByName()); dto.setCreatedAt(entity.getCreatedAt()); return dto; } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/model/Announcement.java b/huertos/src/main/java/net/miarma/backend/huertos/model/Announcement.java index a8a3b25..0c2cf54 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/model/Announcement.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/model/Announcement.java @@ -23,8 +23,14 @@ public class Announcement { @Column(name = "priority", nullable = false) private Byte priority; - @Column(name = "published_by", columnDefinition = "BINARY(16)", nullable = false) - private UUID publishedBy; + @Column(name = "published_by", columnDefinition = "BINARY(16)", nullable = false) + private byte[] publishedByBin; + + @Transient + private UUID publishedBy; + + @Column(name = "published_by_name", nullable = false) + private String publishedByName; @Column(name = "created_at", nullable = false) private Instant createdAt; @@ -35,6 +41,10 @@ public class Announcement { if (announceId != null) { announceIdBin = UuidUtil.uuidToBin(announceId); } + + if (publishedBy != null) { + publishedByBin = UuidUtil.uuidToBin(publishedBy); + } } @PostLoad @@ -42,6 +52,10 @@ public class Announcement { if (announceIdBin != null) { announceId = UuidUtil.binToUUID(announceIdBin); } + + if (publishedByBin != null) { + publishedBy = UuidUtil.binToUUID(publishedByBin); + } } public UUID getAnnounceId() { @@ -76,6 +90,14 @@ public class Announcement { this.publishedBy = publishedBy; } + public String getPublishedByName() { + return publishedByName; + } + + public void setPublishedByName(String publishedByName) { + this.publishedByName = publishedByName; + } + public Instant getCreatedAt() { return createdAt; } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/repository/PreUserRepository.java b/huertos/src/main/java/net/miarma/backend/huertos/repository/PreUserRepository.java index fafd30d..5e5616a 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/repository/PreUserRepository.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/repository/PreUserRepository.java @@ -4,4 +4,7 @@ import net.miarma.backend.huertos.model.PreUser; import org.springframework.data.jpa.repository.JpaRepository; public interface PreUserRepository extends JpaRepository { + boolean existsByDni(String dni); + + boolean existsByEmail(String email); } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/AnnouncementService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/AnnouncementService.java index 5e5df58..302e648 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/AnnouncementService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/AnnouncementService.java @@ -17,9 +17,11 @@ import java.util.UUID; public class AnnouncementService { private final AnnouncementRepository announcementRepository; + private final MemberService memberService; - public AnnouncementService(AnnouncementRepository announcementRepository) { + public AnnouncementService(AnnouncementRepository announcementRepository, MemberService memberService) { this.announcementRepository = announcementRepository; + this.memberService = memberService; } public List getAll() { @@ -36,6 +38,7 @@ public class AnnouncementService { if (announcement.getAnnounceId() == null) { announcement.setAnnounceId(UUID.randomUUID()); } + announcement.setPublishedByName(memberService.getById(announcement.getPublishedBy()).user().getDisplayName()); announcement.setCreatedAt(Instant.now()); return announcementRepository.save(announcement); } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/CoreAuthService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/CoreAuthService.java new file mode 100644 index 0000000..1079b0e --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/CoreAuthService.java @@ -0,0 +1,60 @@ +package net.miarma.backend.huertos.service; + +import net.miarma.backend.huertos.client.HuertosWebClient; +import net.miarma.backlib.dto.LoginRequest; +import net.miarma.backlib.dto.LoginResponse; +import net.miarma.backlib.security.CoreAuthTokenHolder; +import net.miarma.backlib.security.JwtService; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import tools.jackson.databind.ObjectMapper; + +import java.time.Instant; + +@Service +public class CoreAuthService { + + private final RestTemplate authRestTemplate; + private final CoreAuthTokenHolder tokenHolder; + private final JwtService jwtService; + + @Value("${huertos.user}") + private String username; + + @Value("${huertos.password}") + private String password; + + @Value("${core.url}") + private String coreUrl; + + public CoreAuthService(@Qualifier("authRestTemplate") RestTemplate authRestTemplate, + CoreAuthTokenHolder tokenHolder, + JwtService jwtService) { + this.authRestTemplate = authRestTemplate; + this.tokenHolder = tokenHolder; + this.jwtService = jwtService; + } + + public synchronized String getToken() { + if (tokenHolder.getToken() == null || tokenHolder.isExpired()) { + refreshToken(); + } + return tokenHolder.getToken(); + } + + private void refreshToken() { + var req = new LoginRequest(username, password, (byte) 1); + + LoginResponse resp = authRestTemplate.postForObject( + coreUrl + "/auth/login", + req, + LoginResponse.class + ); + + String token = resp.token(); + Instant exp = jwtService.getExpiration(token).toInstant(); + tokenHolder.setToken(token, exp); + } +} diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/ExpenseService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/ExpenseService.java index 6d9b9fa..4d6397b 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/ExpenseService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/ExpenseService.java @@ -35,16 +35,16 @@ public class ExpenseService { public Expense create(Expense expense) { if (expense.getConcept() == null || expense.getConcept().isBlank()) { - throw new ValidationException("Concept is required"); + throw new ValidationException("concept", "Concept is required"); } if (expense.getAmount() == null) { - throw new ValidationException("Amount is required"); + throw new ValidationException("amount", "Amount is required"); } if (expense.getSupplier() == null || expense.getSupplier().isBlank()) { - throw new ValidationException("Supplier is required"); + throw new ValidationException("supplier", "Supplier is required"); } if (expense.getInvoice() == null || expense.getInvoice().isBlank()) { - throw new ValidationException("Invoice is required"); + throw new ValidationException("invoice", "Invoice is required"); } expense.setExpenseId(UUID.randomUUID()); diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/IncomeService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/IncomeService.java index 42e26fa..92e6ca1 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/IncomeService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/IncomeService.java @@ -54,7 +54,7 @@ public class IncomeService { throw new BadRequestException("concept is required"); } if (income.getAmount() == null || income.getAmount().signum() <= 0) { - throw new ValidationException("amount must be positive"); + throw new ValidationException("amount", "amount must be positive"); } income.setIncomeId(UUID.randomUUID()); @@ -72,7 +72,7 @@ public class IncomeService { if (dto.getConcept() != null) income.setConcept(dto.getConcept()); if (dto.getAmount() != null) { if (dto.getAmount().signum() <= 0) { - throw new ValidationException("amount must be positive"); + throw new ValidationException("amount", "amount must be positive"); } income.setAmount(dto.getAmount()); } @@ -106,4 +106,9 @@ public class IncomeService { List incomes = getByUserId(userId); return !incomes.isEmpty() && incomes.stream().allMatch(Income::isPaid); } + + public List getByMemberNumber(Integer memberNumber) { + UUID userId = metadataService.getByMemberNumber(memberNumber).getUserId(); + return getByUserId(userId); + } } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/MemberService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/MemberService.java index 1b6d1f5..07978b8 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/MemberService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/MemberService.java @@ -1,9 +1,11 @@ package net.miarma.backend.huertos.service; import net.miarma.backend.huertos.client.HuertosWebClient; +import net.miarma.backend.huertos.dto.IncomeDto; import net.miarma.backend.huertos.dto.MemberDto; import net.miarma.backend.huertos.dto.WaitlistCensoredDto; import net.miarma.backend.huertos.mapper.HuertosUserMetadataMapper; +import net.miarma.backend.huertos.mapper.IncomeMapper; import net.miarma.backend.huertos.security.NameCensorer; import net.miarma.backlib.dto.UserWithCredentialDto; import net.miarma.backlib.exception.NotFoundException; @@ -31,12 +33,22 @@ public class MemberService { this.metadataService = metadataService; } - public MemberDto getById(UUID userId, Byte serviceId) { - var uwc = huertosWebClient.getUserWithCredential(userId, serviceId); - var meta = metadataService.getById(userId); + public MemberDto getById(UUID userId) { + var uwc = huertosWebClient.getUserWithCredential(userId, (byte)1); + if (uwc == null) { + throw new NotFoundException("User not found in core"); + } - return new MemberDto(uwc.user(), uwc.account(), - HuertosUserMetadataMapper.toDto(meta)); + var meta = metadataService.getById(userId); + if (meta == null) { + throw new NotFoundException("User metadata not found"); + } + + return new MemberDto( + uwc.user(), + uwc.account(), + HuertosUserMetadataMapper.toDto(meta) + ); } public List getAll(Byte serviceId) { @@ -106,8 +118,10 @@ public class MemberService { .orElseThrow(() -> new NotFoundException("Member not found")); } - public Boolean hasIncomes(Integer memberNumber) { - return incomeService.existsByMemberNumber(memberNumber); + public List getIncomes(Integer memberNumber) { + return incomeService.getByMemberNumber(memberNumber).stream() + .map(IncomeMapper::toResponse) + .toList(); } public Boolean hasPaid(Integer memberNumber) { @@ -132,11 +146,11 @@ public class MemberService { .toList(); return plotMembers.stream() - .anyMatch(dto -> dto.metadata().getType().equals((byte)2)); + .anyMatch(dto -> dto.metadata().getType().equals((byte)3)); } public Boolean hasGreenhouse(Integer memberNumber) { - return metadataService.existsByMemberNumber(memberNumber); + return metadataService.getByMemberNumber(memberNumber).getType().equals((byte)2); } public Boolean hasCollaboratorRequest(Integer memberNumber) { diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/PreUserService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/PreUserService.java index 74cc6e9..816fb87 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/PreUserService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/PreUserService.java @@ -2,9 +2,11 @@ package net.miarma.backend.huertos.service; import java.time.Instant; import java.util.List; +import java.util.Map; import java.util.UUID; import net.miarma.backend.huertos.dto.PreUserDto; +import net.miarma.backend.huertos.validation.PreUserValidator; import net.miarma.backlib.exception.BadRequestException; import net.miarma.backlib.exception.NotFoundException; import org.springframework.stereotype.Service; @@ -100,4 +102,18 @@ public class PreUserService { repository.deleteById(idBytes); } + + public Map validate(PreUserDto.Request request) { + Map errors = PreUserValidator.validate(request); + + if (request.getDni() != null && repository.existsByDni(request.getDni())) { + errors.put("dni", "There is already an user with that NIF/NIE"); + } + + if (request.getEmail() != null && repository.existsByEmail(request.getEmail())) { + errors.put("email", "There is already an user with that email"); + } + + return errors; + } } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/service/RequestService.java b/huertos/src/main/java/net/miarma/backend/huertos/service/RequestService.java index 034b38d..829dd92 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/service/RequestService.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/service/RequestService.java @@ -8,7 +8,6 @@ import net.miarma.backend.huertos.dto.PreUserDto; import net.miarma.backend.huertos.dto.RequestDto; import net.miarma.backend.huertos.mapper.PreUserMapper; import net.miarma.backend.huertos.mapper.RequestMapper; -import net.miarma.backend.huertos.model.HuertosUserMetadata; import net.miarma.backend.huertos.model.PreUser; import net.miarma.backend.huertos.repository.PreUserRepository; import net.miarma.backlib.exception.BadRequestException; @@ -64,7 +63,7 @@ public class RequestService { .toList(); } - public Request create(Request request) { + public Request createWaitlist(Request request) { if (request.getType() == null) { throw new BadRequestException("type is required"); } @@ -82,8 +81,8 @@ public class RequestService { } @Transactional - public RequestDto.Response create(RequestDto.Request requestDto, - PreUserDto.Request preUserDto) { + public RequestDto.Response createWaitlist(RequestDto.Request requestDto, + PreUserDto.Request preUserDto) { PreUser preUser = preUserRepository.save(PreUserMapper.toEntity(preUserDto)); Request request = requestRepository.save(RequestMapper.toEntity(requestDto)); return RequestMapper.toResponse(request); diff --git a/huertos/src/main/java/net/miarma/backend/huertos/validation/DniValidator.java b/huertos/src/main/java/net/miarma/backend/huertos/validation/DniValidator.java new file mode 100644 index 0000000..10af1f6 --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/validation/DniValidator.java @@ -0,0 +1,66 @@ +package net.miarma.backend.huertos.validation; + +/** + * Validador de DNI/NIE español. + *

+ * Este validador comprueba si un DNI o NIE es válido según las reglas establecidas por la legislación española. + * Un DNI debe tener 8 dígitos seguidos de una letra, y un NIE debe comenzar con X, Y o Z seguido de 7 dígitos y una letra. + * + * @author José Manuel Amador Gallardo + */ +public class DniValidator { + + /** + * Valida un DNI o NIE español. + * + * @param id El DNI o NIE a validar. + * @return true si el DNI/NIE es válido, false en caso contrario. + */ + public static boolean isValid(String id) { + if (id == null || id.length() != 9) { + return false; + } + + id = id.toUpperCase(); // Pa evitar problemas con minúsculas + String numberPart; + char letterPart = id.charAt(8); + + if (id.startsWith("X") || id.startsWith("Y") || id.startsWith("Z")) { + // NIE + char prefix = id.charAt(0); + String numericPrefix = switch (prefix) { + case 'X' -> "0"; + case 'Y' -> "1"; + case 'Z' -> "2"; + default -> null; + }; + + if (numericPrefix == null) return false; + + numberPart = numericPrefix + id.substring(1, 8); + } else { + // DNI + numberPart = id.substring(0, 8); + } + + if (!numberPart.matches("\\d{8}")) { + return false; + } + + int number = Integer.parseInt(numberPart); + char expectedLetter = calculateLetter(number); + + return letterPart == expectedLetter; + } + + /** + * Calcula la letra correspondiente a un número de DNI. + * + * @param number El número del DNI (8 dígitos). + * @return La letra correspondiente. + */ + private static char calculateLetter(int number) { + String letters = "TRWAGMYFPDXBNJZSQVHLCKE"; + return letters.charAt(number % 23); + } +} diff --git a/huertos/src/main/java/net/miarma/backend/huertos/validation/PreUserValidator.java b/huertos/src/main/java/net/miarma/backend/huertos/validation/PreUserValidator.java new file mode 100644 index 0000000..7fa97b4 --- /dev/null +++ b/huertos/src/main/java/net/miarma/backend/huertos/validation/PreUserValidator.java @@ -0,0 +1,61 @@ +package net.miarma.backend.huertos.validation; + +import net.miarma.backend.huertos.dto.PreUserDto; + +import java.util.HashMap; +import java.util.Map; + +public class PreUserValidator { + + public static Map validate(PreUserDto.Request dto) { + Map errors = new HashMap<>(); + + if (dto.getUserName() == null || dto.getUserName().isBlank()) { + errors.put("userName", "El nombre de usuario es obligatorio"); + } else if (dto.getUserName().length() < 3) { + errors.put("userName", "El nombre de usuario debe tener al menos 3 caracteres"); + } + + if (dto.getDisplayName() == null || dto.getDisplayName().isBlank()) { + errors.put("displayName", "El nombre visible es obligatorio"); + } + + if (dto.getDni() == null || dto.getDni().isBlank()) { + errors.put("dni", "El DNI es obligatorio"); + } else if (!dto.getDni().matches("^[0-9]{8}[A-Za-z]$")) { + errors.put("dni", "DNI inválido"); + } + + if (dto.getEmail() == null || dto.getEmail().isBlank()) { + errors.put("email", "El email es obligatorio"); + } else if (!dto.getEmail().matches("^[\\w-.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) { + errors.put("email", "Email inválido"); + } + + if (dto.getPhone() == null || dto.getPhone().isBlank()) { + errors.put("phone", "El teléfono es obligatorio"); + } else if (!dto.getPhone().matches("^[0-9]{9}$")) { + errors.put("phone", "Teléfono inválido"); + } + + if (dto.getAddress() == null || dto.getAddress().isBlank()) { + errors.put("address", "La dirección es obligatoria"); + } + + if (dto.getZipCode() == null || dto.getZipCode().isBlank()) { + errors.put("zipCode", "El código postal es obligatorio"); + } + + if (dto.getCity() == null || dto.getCity().isBlank()) { + errors.put("city", "La ciudad es obligatoria"); + } + + if (dto.getPassword() == null || dto.getPassword().isBlank()) { + errors.put("password", "La contraseña es obligatoria"); + } else if (dto.getPassword().length() < 6) { + errors.put("password", "La contraseña debe tener al menos 6 caracteres"); + } + + return errors; + } +} \ No newline at end of file