only change password, docs and mail left
This commit is contained in:
5
TODO
5
TODO
@@ -1,8 +1,6 @@
|
|||||||
POR HACER --------------------------------
|
POR HACER --------------------------------
|
||||||
- sistema comun de errores en back & front
|
|
||||||
- cambiar contraseña (?)
|
- cambiar contraseña (?)
|
||||||
- documentación
|
- documentación
|
||||||
- implementar urlParams para filtros
|
|
||||||
- mail wrapper
|
- mail wrapper
|
||||||
|
|
||||||
RESUELTO ---------------------------------
|
RESUELTO ---------------------------------
|
||||||
@@ -11,3 +9,6 @@ RESUELTO ---------------------------------
|
|||||||
- aceptar solicitudes LE/Colab (sobre todo por crear preusers)
|
- 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
|
- 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
|
||||||
|
- implementar urlParams para filtros -> NO RESUELTO (DEPRECATED)
|
||||||
|
- sistema comun de errores en back & front
|
||||||
|
- nombre del requester
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +44,26 @@ public class AuthController {
|
|||||||
@PostMapping("/refresh")
|
@PostMapping("/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);
|
||||||
@@ -75,35 +81,37 @@ public class AuthController {
|
|||||||
@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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ 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) {
|
||||||
|
|||||||
@@ -47,9 +47,12 @@ 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");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,11 +60,17 @@ public class RequestService {
|
|||||||
throw new BadRequestException("El tipo de solicitud es obligatorio");
|
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");
|
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");
|
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) {
|
public boolean hasGreenhouseRequest(UUID userId) {
|
||||||
return getByUserId(userId).stream()
|
return getByUserId(userId).stream()
|
||||||
.anyMatch(r -> r.getType() == 1);
|
.anyMatch(r -> r.getType() == 4 || r.getType() == 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasCollaboratorRequest(UUID userId) {
|
public boolean hasCollaboratorRequest(UUID userId) {
|
||||||
return getByUserId(userId).stream()
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,27 +2,45 @@ package net.miarma.backend.huertos.validation;
|
|||||||
|
|
||||||
import net.miarma.backend.huertos.model.RequestMetadata;
|
import net.miarma.backend.huertos.model.RequestMetadata;
|
||||||
import net.miarma.backlib.exception.BadRequestException;
|
import net.miarma.backlib.exception.BadRequestException;
|
||||||
|
import net.miarma.backlib.exception.ValidationException;
|
||||||
|
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class RequestValidator {
|
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) {
|
public static void validate(RequestMetadata metadata, Byte requestType) {
|
||||||
if (metadata.getRequestId() == null) {
|
if (metadata.getRequestId() == null) {
|
||||||
throw new BadRequestException("Estos metadatos deben pertenecer a una solicitud (falta ID)");
|
throw new BadRequestException("Estos metadatos deben pertenecer a una solicitud (falta ID)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isBlank(metadata.getDisplayName())) {
|
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())) {
|
if (isBlank(metadata.getDni())) {
|
||||||
throw new BadRequestException("El DNI es obligatorio");
|
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())) {
|
if (isBlank(metadata.getEmail())) {
|
||||||
throw new BadRequestException("El email es obligatorio");
|
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())) {
|
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) {
|
if (metadata.getType() == null) {
|
||||||
@@ -46,6 +64,10 @@ public class RequestValidator {
|
|||||||
throw new BadRequestException("La dirección, código postal y ciudad son obligatorios");
|
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) {
|
private static boolean isBlank(String s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user