diff --git a/src/main/java/com/libraryman_api/exception/GlobalExceptionHandler.java b/src/main/java/com/libraryman_api/exception/GlobalExceptionHandler.java index a9202d3..6e637e7 100644 --- a/src/main/java/com/libraryman_api/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/libraryman_api/exception/GlobalExceptionHandler.java @@ -9,42 +9,64 @@ import java.util.Date; /** - * Global exception handler for the LibraryMan API. - * This class provides centralized exception handling across all controllers in the application. - * It handles specific exceptions and returns appropriate HTTP responses. + * Global exception handler for the LibraryMan API. This class provides + * centralized exception handling across all controllers in the application. It + * handles specific exceptions and returns appropriate HTTP responses. */ @ControllerAdvice public class GlobalExceptionHandler { - /** - * Handles {@link ResourceNotFoundException} exceptions. - * This method is triggered when a {@code ResourceNotFoundException} is thrown in the application. - * It constructs an {@link ErrorDetails} object containing the exception details and returns - * a {@link ResponseEntity} with an HTTP status of {@code 404 Not Found}. - * - * @param ex the exception that was thrown. - * @param request the current web request in which the exception was thrown. - * @return a {@link ResponseEntity} containing the {@link ErrorDetails} and an HTTP status of {@code 404 Not Found}. - */ - @ExceptionHandler(ResourceNotFoundException.class) - public ResponseEntity resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) { - ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); - return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); - } - - /** - * Handles {@link InvalidSortFieldException} exceptions. - * This method is triggered when an {@code InvalidSortFieldException} is thrown in the application. - * It constructs an {@link ErrorDetails} object containing the exception details and returns - * a {@link ResponseEntity} with an HTTP status of {@code 400 Bad Request}. - * - * @param ex the exception that was thrown. - * @param request the current web request in which the exception was thrown. - * @return a {@link ResponseEntity} containing the {@link ErrorDetails} and an HTTP status of {@code 400 Bad Request}. - */ - @ExceptionHandler(InvalidSortFieldException.class) - public ResponseEntity invalidSortFieldException(InvalidSortFieldException ex, WebRequest request) { - ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); - return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); - } + /** + * Handles {@link ResourceNotFoundException} exceptions. This method is + * triggered when a {@code ResourceNotFoundException} is thrown in the + * application. It constructs an {@link ErrorDetails} object containing the + * exception details and returns a {@link ResponseEntity} with an HTTP status of + * {@code 404 Not Found}. + * + * @param ex the exception that was thrown. + * @param request the current web request in which the exception was thrown. + * @return a {@link ResponseEntity} containing the {@link ErrorDetails} and an + * HTTP status of {@code 404 Not Found}. + */ + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND); + } + + /** + * Handles {@link InvalidSortFieldException} exceptions. This method is + * triggered when an {@code InvalidSortFieldException} is thrown in the + * application. It constructs an {@link ErrorDetails} object containing the + * exception details and returns a {@link ResponseEntity} with an HTTP status of + * {@code 400 Bad Request}. + * + * @param ex the exception that was thrown. + * @param request the current web request in which the exception was thrown. + * @return a {@link ResponseEntity} containing the {@link ErrorDetails} and an + * HTTP status of {@code 400 Bad Request}. + */ + @ExceptionHandler(InvalidSortFieldException.class) + public ResponseEntity invalidSortFieldException(InvalidSortFieldException ex, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); + } + + /** + * Handles {@link InvalidPasswordException} exceptions. This method is triggered + * when an {@code InvalidPasswordException} is thrown in the application. It + * constructs an {@link ErrorDetails} object containing the exception details + * and returns a {@link ResponseEntity} with an HTTP status of + * {@code 400 Bad Request}. + * + * @param ex the exception that was thrown. + * @param request the current web request in which the exception was thrown. + * @return a {@link ResponseEntity} containing the {@link ErrorDetails} and an + * HTTP status of {@code 400 Bad Request}. + */ + @ExceptionHandler(InvalidPasswordException.class) + public ResponseEntity invalidPasswordException(InvalidPasswordException ex, WebRequest request) { + ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false)); + return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST); + } } diff --git a/src/main/java/com/libraryman_api/exception/InvalidPasswordException.java b/src/main/java/com/libraryman_api/exception/InvalidPasswordException.java new file mode 100644 index 0000000..27013c0 --- /dev/null +++ b/src/main/java/com/libraryman_api/exception/InvalidPasswordException.java @@ -0,0 +1,50 @@ +package com.libraryman_api.exception; + +import java.io.Serial; + +/** + * Custom exception class to handle scenarios where an invalid password is provided + * in the Library Management System. This exception is thrown when a password update + * operation fails due to invalid password criteria. + */ +public class InvalidPasswordException extends RuntimeException { + + /** + * The {@code serialVersionUID} is a unique identifier for each version of a serializable class. + * It is used during the deserialization process to verify that the sender and receiver of a + * serialized object have loaded classes for that object that are compatible with each other. + * + * The {@code serialVersionUID} field is important for ensuring that a serialized class + * (especially when transmitted over a network or saved to disk) can be successfully deserialized, + * even if the class definition changes in later versions. If the {@code serialVersionUID} does not + * match during deserialization, an {@code InvalidClassException} is thrown. + * + * This field is optional, but it is good practice to explicitly declare it to prevent + * automatic generation, which could lead to compatibility issues when the class structure changes. + * + * The {@code @Serial} annotation is used here to indicate that this field is related to + * serialization. This annotation is available starting from Java 14 and helps improve clarity + * regarding the purpose of this field. + */ + @Serial + private static final long serialVersionUID = 1L; + + /** + * Constructs a new {@code InvalidPasswordException} with the specified detail message. + * + * @param message the detail message explaining the reason for the exception + */ + public InvalidPasswordException(String message) { + super(message); + } + + /** + * Constructs a new {@code InvalidPasswordException} with the specified detail message and cause. + * + * @param message the detail message explaining the reason for the exception + * @param cause the cause of the exception (which is saved for later retrieval by the {@link #getCause()} method) + */ + public InvalidPasswordException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/libraryman_api/member/MemberController.java b/src/main/java/com/libraryman_api/member/MemberController.java index 903b9f3..74ff8d3 100644 --- a/src/main/java/com/libraryman_api/member/MemberController.java +++ b/src/main/java/com/libraryman_api/member/MemberController.java @@ -99,4 +99,21 @@ public MembersDto updateMember(@PathVariable int id, @RequestBody UpdateMembersD public void deleteMember(@PathVariable int id) { memberService.deleteMember(id); } + + /** + * Updates the password for a library member. + * If the member is not found or the update fails, an appropriate exception will be thrown. + * + * @param id the ID of the member whose password is to be updated + * @param updatePasswordDto the {@link UpdatePasswordDto} object containing the password details + * @return a {@link ResponseEntity} containing a success message indicating the password was updated successfully + */ + @PutMapping("/{id}/password") + @PreAuthorize("#id == authentication.principal.memberId") + public ResponseEntity updatePassword(@PathVariable int id, + @RequestBody UpdatePasswordDto updatePasswordDto) { + memberService.updatePassword(id, updatePasswordDto); + return ResponseEntity.ok("Password updated successfully."); + } + } diff --git a/src/main/java/com/libraryman_api/member/MemberService.java b/src/main/java/com/libraryman_api/member/MemberService.java index bc5b554..a74566c 100644 --- a/src/main/java/com/libraryman_api/member/MemberService.java +++ b/src/main/java/com/libraryman_api/member/MemberService.java @@ -9,9 +9,11 @@ import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.stereotype.Service; +import com.libraryman_api.exception.InvalidPasswordException; import com.libraryman_api.exception.InvalidSortFieldException; import com.libraryman_api.exception.ResourceNotFoundException; import com.libraryman_api.notification.NotificationService; +import com.libraryman_api.security.config.PasswordEncoder; @@ -35,6 +37,7 @@ public class MemberService { private final MemberRepository memberRepository; private final NotificationService notificationService; + private final PasswordEncoder passwordEncoder; /** * Constructs a new {@code MemberService} with the specified repositories and services. @@ -42,9 +45,10 @@ public class MemberService { * @param memberRepository the repository for managing member records * @param notificationService the service for sending notifications related to member activities */ - public MemberService(MemberRepository memberRepository, NotificationService notificationService) { + public MemberService(MemberRepository memberRepository, NotificationService notificationService, PasswordEncoder passwordEncoder) { this.memberRepository = memberRepository; this.notificationService = notificationService; + this.passwordEncoder = passwordEncoder; } /** @@ -143,6 +147,39 @@ public void deleteMember(int memberId) { memberRepository.delete(member); } + /** + * Updates the password for a library member. + * + *

This method verifies the current password provided by the member, checks if the + * new password is different, and then updates the member's password in the database. + * If the current password is incorrect or the new password is the same as the current + * password, an {@link InvalidPasswordException} is thrown.

+ * + * @param memberId the ID of the member whose password is to be updated + * @param updatePasswordDto the {@link UpdatePasswordDto} object containing the password details + * @throws ResourceNotFoundException if the member with the specified ID is not found + * @throws InvalidPasswordException if the current password is incorrect or the new password is the same as the current password + */ + public void updatePassword(int memberId, UpdatePasswordDto updatePasswordDto) { + Members member = memberRepository.findById(memberId) + .orElseThrow(() -> new ResourceNotFoundException("Member not found")); + + // Check the current password + String currentAuthPassword = member.getPassword(); + + if (!passwordEncoder.bCryptPasswordEncoder().matches(updatePasswordDto.getCurrentPassword(), currentAuthPassword)) { + throw new InvalidPasswordException("Current password is incorrect"); + } + + // Check if new password is different from old password + if (updatePasswordDto.getCurrentPassword().equals(updatePasswordDto.getNewPassword())) { + throw new InvalidPasswordException("New password must be different from the old password"); + } + + member.setPassword(passwordEncoder.bCryptPasswordEncoder().encode(updatePasswordDto.getNewPassword())); + memberRepository.save(member); + } + /** * Converts a MembersDto object to a Members entity. * diff --git a/src/main/java/com/libraryman_api/member/UpdatePasswordDto.java b/src/main/java/com/libraryman_api/member/UpdatePasswordDto.java new file mode 100644 index 0000000..5f593d2 --- /dev/null +++ b/src/main/java/com/libraryman_api/member/UpdatePasswordDto.java @@ -0,0 +1,40 @@ +package com.libraryman_api.member; + +public class UpdatePasswordDto { + + private String currentPassword; + + private String newPassword; + + public UpdatePasswordDto(String currentPassword, String newPassword) { + this.currentPassword = currentPassword; + this.newPassword = newPassword; + } + + public UpdatePasswordDto() { + } + + public String getCurrentPassword() { + return currentPassword; + } + + public void setCurrentPassword(String currentPassword) { + this.currentPassword = currentPassword; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } + + @Override + public String toString() { + return "UpdatePasswordDto{" + + "currentPassword='" + currentPassword + '\'' + + ", newPassword='" + newPassword + '\'' + + '}'; + } +}