From 4303caaf742ba6f873e50620858e603da5c222d0 Mon Sep 17 00:00:00 2001 From: Jose Date: Fri, 30 Jan 2026 22:37:39 +0100 Subject: [PATCH] only change password, docs and mail left --- TODO | 7 +- .../backlib/dto/ApiValidationErrorDto.java | 22 +++++- .../backlib/http/GlobalExceptionHandler.java | 5 +- .../core/controller/AuthController.java | 70 +++++++++++-------- .../backend/core/service/AuthService.java | 22 +++++- .../huertos/controller/MemberController.java | 2 +- .../backend/huertos/mapper/RequestMapper.java | 1 + .../huertos/service/IncomeService.java | 5 +- .../huertos/service/RequestService.java | 19 +++-- .../huertos/validation/RequestValidator.java | 26 ++++++- 10 files changed, 131 insertions(+), 48 deletions(-) diff --git a/TODO b/TODO index 4541a49..a4ff411 100644 --- a/TODO +++ b/TODO @@ -1,8 +1,6 @@ POR HACER -------------------------------- -- sistema comun de errores en back & front - cambiar contraseña (?) - documentación -- implementar urlParams para filtros - mail wrapper RESUELTO --------------------------------- @@ -10,4 +8,7 @@ RESUELTO --------------------------------- - apuntarse lista espera - aceptar solicitudes LE/Colab (sobre todo por crear preusers) - mejorar queries para no filtrar en memoria -> IMPOSIBLE CON ENDPOINTS INTERNOS DE CORE: RESUELTO CON CACHING -- normalizar el uso de services y repositories desde otros services y repositories \ No newline at end of file +- normalizar el uso de services y repositories desde otros services y repositories +- implementar urlParams para filtros -> NO RESUELTO (DEPRECATED) +- sistema comun de errores en back & front +- nombre del requester diff --git a/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java b/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java index 8437faa..868d8e4 100644 --- a/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java +++ b/backlib/src/main/java/net/miarma/backlib/dto/ApiValidationErrorDto.java @@ -7,11 +7,15 @@ import java.util.List; import java.util.Map; public class ApiValidationErrorDto { + private int status; private Map errors; + private String path; private Instant timestamp; - public ApiValidationErrorDto(Map errors) { + public ApiValidationErrorDto(Map errors, String path) { + this.status = 422; this.errors = errors; + this.path = path; this.timestamp = Instant.now(); } @@ -19,6 +23,22 @@ public class ApiValidationErrorDto { 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 errors) { this.errors = errors; } 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 3028b35..1b9c84e 100644 --- a/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java +++ b/backlib/src/main/java/net/miarma/backlib/http/GlobalExceptionHandler.java @@ -84,9 +84,10 @@ public class GlobalExceptionHandler { } @ExceptionHandler(ValidationException.class) - public ResponseEntity handleValidation(ValidationException ex) { + public ResponseEntity handleValidation( + ValidationException ex, HttpServletRequest req) { Map 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) 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 af50a43..91c1d53 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 @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import net.miarma.backlib.dto.*; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; 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.CredentialService; 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 @RequestMapping("/auth") public class AuthController { - private final CredentialService credentialService; private final JwtService jwtService; - private final PasswordEncoder passwordEncoder; private final AuthService authService; - public AuthController(CredentialService credentialService, JwtService jwtService, - PasswordEncoder passwordEncoder, AuthService authService) { - this.credentialService = credentialService; + public AuthController(JwtService jwtService, AuthService authService) { this.jwtService = jwtService; - this.passwordEncoder = passwordEncoder; this.authService = authService; } @@ -52,12 +44,26 @@ public class AuthController { @PostMapping("/refresh") public ResponseEntity refreshToken(@RequestHeader("Authorization") String authHeader) { 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); 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); @@ -71,40 +77,42 @@ public class AuthController { "serviceId", serviceId )); } - + @PostMapping("/change-password") public ResponseEntity changePassword( @RequestHeader("Authorization") String authHeader, - @Valid @RequestBody ChangePasswordRequest request + @RequestBody ChangePasswordRequest request ) { 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); 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); - Credential cred = credentialService.getByUserId(userId) - .stream() - .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")); + authService.changePassword(userId, request); + return ResponseEntity.ok(Map.of("message", "Contraseña cambiada correctamente")); } + @GetMapping("/validate") public ResponseEntity validate(@RequestHeader("Authorization") String authHeader) { String token = authHeader.substring(7); diff --git a/core/src/main/java/net/miarma/backend/core/service/AuthService.java b/core/src/main/java/net/miarma/backend/core/service/AuthService.java index ca4fdf3..bf7384c 100644 --- a/core/src/main/java/net/miarma/backend/core/service/AuthService.java +++ b/core/src/main/java/net/miarma/backend/core/service/AuthService.java @@ -3,9 +3,7 @@ package net.miarma.backend.core.service; import java.util.UUID; import net.miarma.backlib.dto.*; -import net.miarma.backlib.exception.ConflictException; -import net.miarma.backlib.exception.ForbiddenException; -import net.miarma.backlib.exception.UnauthorizedException; +import net.miarma.backlib.exception.*; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -75,4 +73,22 @@ public class AuthService { 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); + } } 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 33a08d0..ad1887f 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 @@ -41,7 +41,7 @@ public class MemberController { } return ResponseEntity.ok( - memberService.getMyProfile(principal.getUserId()) + memberService.getMyProfile(principal.getUserId()) ); } diff --git a/huertos/src/main/java/net/miarma/backend/huertos/mapper/RequestMapper.java b/huertos/src/main/java/net/miarma/backend/huertos/mapper/RequestMapper.java index b44634b..e7f4a33 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/mapper/RequestMapper.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/mapper/RequestMapper.java @@ -30,6 +30,7 @@ public class RequestMapper { Request entity = new Request(); entity.setType(dto.getType()); entity.setUserId(dto.getUserId()); + entity.setName(dto.getName()); entity.setStatus((byte) 0); if (dto.getMetadata() != null) { 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 7dee775..fdb2efd 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 @@ -47,9 +47,12 @@ public class IncomeService { if (income.getUserId() == null) { 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"); } + 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) { throw new ValidationException("amount", "La cantidad debe ser positiva"); } 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 4a96ba4..ae24e52 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 @@ -60,11 +60,17 @@ public class RequestService { throw new BadRequestException("El tipo de solicitud es obligatorio"); } - if (request.getType() == 2 && hasCollaboratorRequest(request.getUserId())) { // tiene soli de collab + 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() == 1 && hasGreenhouseRequest(request.getUserId())) { // tiene soli de invernadero + 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"); } @@ -119,11 +125,16 @@ public class RequestService { public boolean hasGreenhouseRequest(UUID userId) { return getByUserId(userId).stream() - .anyMatch(r -> r.getType() == 1); + .anyMatch(r -> r.getType() == 4 || r.getType() == 5); } public boolean hasCollaboratorRequest(UUID userId) { return getByUserId(userId).stream() - .anyMatch(r -> r.getType() == 2); + .anyMatch(r -> r.getType() == 2 || r.getType() == 3); + } + + public boolean hasUnregisterRequest(UUID userId) { + return getByUserId(userId).stream() + .anyMatch(r -> r.getType() == 1); } } \ No newline at end of file diff --git a/huertos/src/main/java/net/miarma/backend/huertos/validation/RequestValidator.java b/huertos/src/main/java/net/miarma/backend/huertos/validation/RequestValidator.java index 50d2a46..3e75fb9 100644 --- a/huertos/src/main/java/net/miarma/backend/huertos/validation/RequestValidator.java +++ b/huertos/src/main/java/net/miarma/backend/huertos/validation/RequestValidator.java @@ -2,27 +2,45 @@ 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 es obligatorio"); + 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 username es obligatorio"); + 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) { @@ -46,6 +64,10 @@ public class RequestValidator { throw new BadRequestException("La dirección, código postal y ciudad son obligatorios"); } } + + if (metadata.getPhone() != null && !PHONE_PATTERN.matcher(metadata.getPhone()).matches()) { + throw new ValidationException("phone", "Teléfono inválido (debe tener entre 9 y 15 dígitos, opcional +)"); + } } private static boolean isBlank(String s) {