From 0cd81fd2b6ce9a5a6e174a4ed03ddf1a9a150441 Mon Sep 17 00:00:00 2001 From: Arturo Gomez Date: Mon, 1 Jun 2026 20:02:21 -0600 Subject: [PATCH] =?UTF-8?q?feat(validation):=20a=C3=B1adir=20l=C3=ADmites?= =?UTF-8?q?=20de=20inputs=20y=20mensajes=20de=20error=20claros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - storyPoints acotado a 1-21 (TaskRequest) - actualHours: positivo, máx 999.99, hasta 2 decimales (StatusChangeRequest) - búsqueda por email y velocity sprints (1-50) validados con @Validated - password de login limitado a máx 100 caracteres - tope de paginación: max-page-size=500, default-page-size=20 - GlobalExceptionHandler antepone el campo y maneja ConstraintViolationException (400 en vez de 500) Co-Authored-By: Claude Opus 4.8 --- .../MyTodoList/auth/LoginRequest.java | 3 ++- .../exception/GlobalExceptionHandler.java | 22 ++++++++++++++++++- .../dashboard/DashboardController.java | 8 ++++++- .../MyTodoList/task/StatusChangeRequest.java | 6 +++++ .../MyTodoList/task/TaskRequest.java | 4 ++++ .../MyTodoList/user/UserController.java | 8 ++++++- .../src/main/resources/application.yml | 5 +++++ 7 files changed, 52 insertions(+), 4 deletions(-) diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/auth/LoginRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/auth/LoginRequest.java index c1eab5151..a33fd7609 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/auth/LoginRequest.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/auth/LoginRequest.java @@ -2,6 +2,7 @@ import jakarta.validation.constraints.Email; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.Getter; import lombok.Setter; @@ -10,6 +11,6 @@ public class LoginRequest { @NotBlank @Email private String email; - @NotBlank + @NotBlank @Size(max = 100, message = "must not exceed 100 characters") private String password; } diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/common/exception/GlobalExceptionHandler.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/common/exception/GlobalExceptionHandler.java index 83d561fde..8d5388fd6 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/common/exception/GlobalExceptionHandler.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/common/exception/GlobalExceptionHandler.java @@ -1,6 +1,8 @@ package com.springboot.MyTodoList.common.exception; import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; @@ -70,13 +72,31 @@ public ResponseEntity handleBadCredentials(BadCredentialsExceptio @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidation(MethodArgumentNotValidException ex, HttpServletRequest req) { String message = ex.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage) + .map(fe -> fe.getField() + ": " + fe.getDefaultMessage()) .collect(Collectors.joining(", ")); log.warn("[400] {} {}: {}", req.getMethod(), req.getRequestURI(), message); return ResponseEntity.status(HttpStatus.BAD_REQUEST) .body(new ErrorResponse("VALIDATION_ERROR", message, 400)); } + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolation(ConstraintViolationException ex, HttpServletRequest req) { + String message = ex.getConstraintViolations().stream() + .map(this::formatViolation) + .collect(Collectors.joining(", ")); + log.warn("[400] {} {}: {}", req.getMethod(), req.getRequestURI(), message); + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(new ErrorResponse("VALIDATION_ERROR", message, 400)); + } + + private String formatViolation(ConstraintViolation violation) { + String path = violation.getPropertyPath().toString(); + // Property path for method params looks like "method.paramName"; keep only the last node. + int lastDot = path.lastIndexOf('.'); + String field = lastDot >= 0 ? path.substring(lastDot + 1) : path; + return field + ": " + violation.getMessage(); + } + @ExceptionHandler(Exception.class) public ResponseEntity handleGeneric(Exception ex, HttpServletRequest req) { log.error("[500] {} {} — {}", req.getMethod(), req.getRequestURI(), ex.getMessage(), ex); diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java index d64613248..fb5a8d0ea 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/project/dashboard/DashboardController.java @@ -3,9 +3,12 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -13,6 +16,7 @@ @RestController @RequestMapping("/api/v1/projects/{projectId}/dashboard") @RequiredArgsConstructor +@Validated @Tag(name = "Dashboard") @SecurityRequirement(name = "bearerAuth") public class DashboardController { @@ -30,7 +34,9 @@ public ResponseEntity sprintSummary(@PathVariable Long pr @GetMapping("/velocity") @Operation(summary = "SP completed per closed sprint for the last N sprints, oldest to newest") public ResponseEntity> velocity(@PathVariable Long projectId, - @RequestParam(defaultValue = "5") int sprints, + @RequestParam(defaultValue = "5") + @Min(value = 1, message = "must be between 1 and 50") + @Max(value = 50, message = "must be between 1 and 50") int sprints, Authentication auth) { return ResponseEntity.ok(dashboardService.getVelocity(uid(auth), projectId, sprints)); } diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/StatusChangeRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/StatusChangeRequest.java index bdb3c8234..2b44788d4 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/StatusChangeRequest.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/StatusChangeRequest.java @@ -1,7 +1,10 @@ package com.springboot.MyTodoList.task; import com.springboot.MyTodoList.common.enums.TaskStatus; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.Digits; import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Positive; import lombok.Getter; import lombok.Setter; @@ -12,5 +15,8 @@ public class StatusChangeRequest { @NotNull private TaskStatus status; + @Positive(message = "must be greater than 0") + @DecimalMax(value = "999.99", message = "must not exceed 999.99") + @Digits(integer = 4, fraction = 2, message = "allows at most 2 decimal places") private BigDecimal actualHours; } diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRequest.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRequest.java index 9ca327516..67ba22268 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRequest.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/task/TaskRequest.java @@ -2,6 +2,8 @@ import com.springboot.MyTodoList.common.enums.TaskPriority; import com.springboot.MyTodoList.common.enums.TaskStatus; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -19,6 +21,8 @@ public class TaskRequest { private TaskStatus status; private TaskPriority priority; @NotNull + @Min(value = 1, message = "must be between 1 and 21") + @Max(value = 21, message = "must be between 1 and 21") private Integer storyPoints; private Long assignedTo; private Long sprintId; diff --git a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/user/UserController.java b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/user/UserController.java index 10aae0ff5..d7dedc5e5 100644 --- a/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/user/UserController.java +++ b/MtdrSpring/backend/src/main/java/com/springboot/MyTodoList/user/UserController.java @@ -4,14 +4,19 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/v1/users") @RequiredArgsConstructor +@Validated @Tag(name = "Users") @SecurityRequirement(name = "bearerAuth") public class UserController { @@ -32,7 +37,8 @@ public ResponseEntity updateMe(@Valid @RequestBody UserRequest req @GetMapping("/search") @Operation(summary = "Search user by email") - public ResponseEntity searchByEmail(@RequestParam String email) { + public ResponseEntity searchByEmail( + @RequestParam @NotBlank @Email @Size(max = 100, message = "must be a valid address (max 100 characters)") String email) { return ResponseEntity.ok(userService.findByEmail(email)); } diff --git a/MtdrSpring/backend/src/main/resources/application.yml b/MtdrSpring/backend/src/main/resources/application.yml index 9a44e5a08..1aaf9d999 100644 --- a/MtdrSpring/backend/src/main/resources/application.yml +++ b/MtdrSpring/backend/src/main/resources/application.yml @@ -12,6 +12,11 @@ spring: properties: hibernate: format_sql: false + data: + web: + pageable: + default-page-size: 20 + max-page-size: 500 jwt: secret: ${JWT_SECRET}