From c94ddbd952a5e76f9d1c4aef1fc735041df687a0 Mon Sep 17 00:00:00 2001 From: Guhapriya01 Date: Sun, 20 Oct 2024 12:02:52 +0530 Subject: [PATCH 1/3] feat: secure member update endpoint and add UpdateMembersDto --- .../member/MemberController.java | 3 +- .../libraryman_api/member/MemberService.java | 5 +- .../member/UpdateMembersDto.java | 52 +++++++++++++++++++ 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/libraryman_api/member/UpdateMembersDto.java diff --git a/src/main/java/com/libraryman_api/member/MemberController.java b/src/main/java/com/libraryman_api/member/MemberController.java index 2f921a8..903b9f3 100644 --- a/src/main/java/com/libraryman_api/member/MemberController.java +++ b/src/main/java/com/libraryman_api/member/MemberController.java @@ -83,7 +83,8 @@ public ResponseEntity getMemberById(@PathVariable int id) { * @return the updated {@link Members} object */ @PutMapping("/{id}") - public MembersDto updateMember(@PathVariable int id, @RequestBody MembersDto membersDtoDetails) { + @PreAuthorize("hasRole('LIBRARIAN') or hasRole('ADMIN') or (hasRole('USER') and #id == authentication.principal.memberId)") + public MembersDto updateMember(@PathVariable int id, @RequestBody UpdateMembersDto membersDtoDetails) { return memberService.updateMember(id, membersDtoDetails); } diff --git a/src/main/java/com/libraryman_api/member/MemberService.java b/src/main/java/com/libraryman_api/member/MemberService.java index 2554db5..bc5b554 100644 --- a/src/main/java/com/libraryman_api/member/MemberService.java +++ b/src/main/java/com/libraryman_api/member/MemberService.java @@ -109,15 +109,12 @@ public MembersDto addMember(MembersDto membersDto) { */ @CacheEvict(value = "members", key = "#memberId") - public MembersDto updateMember(int memberId, MembersDto membersDtoDetails) { + public MembersDto updateMember(int memberId, UpdateMembersDto membersDtoDetails) { Members member = memberRepository.findById(memberId) .orElseThrow(() -> new ResourceNotFoundException("Member not found")); member.setName(membersDtoDetails.getName()); member.setUsername(membersDtoDetails.getUsername()); member.setEmail(membersDtoDetails.getEmail()); - member.setPassword(membersDtoDetails.getPassword()); - member.setRole(membersDtoDetails.getRole()); - member.setMembershipDate(membersDtoDetails.getMembershipDate()); member = memberRepository.save(member); if(member!=null) notificationService.accountDetailsUpdateNotification(member); diff --git a/src/main/java/com/libraryman_api/member/UpdateMembersDto.java b/src/main/java/com/libraryman_api/member/UpdateMembersDto.java new file mode 100644 index 0000000..a3e6692 --- /dev/null +++ b/src/main/java/com/libraryman_api/member/UpdateMembersDto.java @@ -0,0 +1,52 @@ +package com.libraryman_api.member; + +public class UpdateMembersDto { + + private String name; + + private String username; + + private String email; + + public UpdateMembersDto(String name, String username, String email) { + this.name = name; + this.username = username; + this.email = email; + } + + public UpdateMembersDto() { + } + + public String getName() { + return name; + } + + public String getUsername() { + return username; + } + + public void setName(String name) { + this.name = name; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + return "UpdateMembersDto{" + + "name='" + name + '\'' + + ", username='" + username + '\'' + + ", email='" + email + '\'' + + '}'; + } +} From 390774429a315a124341da4673e5f817e31c3b91 Mon Sep 17 00:00:00 2001 From: Guhapriya01 Date: Sun, 20 Oct 2024 12:38:29 +0530 Subject: [PATCH 2/3] feat: implement password update endpoint with DTO and exception handling --- .../exception/GlobalExceptionHandler.java | 90 ++++++++++++------- .../exception/InvalidPasswordException.java | 50 +++++++++++ .../member/MemberController.java | 17 ++++ .../libraryman_api/member/MemberService.java | 39 +++++++- .../member/UpdatePasswordDto.java | 40 +++++++++ 5 files changed, 201 insertions(+), 35 deletions(-) create mode 100644 src/main/java/com/libraryman_api/exception/InvalidPasswordException.java create mode 100644 src/main/java/com/libraryman_api/member/UpdatePasswordDto.java 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 + '\'' + + '}'; + } +} From 60781eeea32c07a8be6cf075136911927f448248 Mon Sep 17 00:00:00 2001 From: Guhapriya01 Date: Sun, 20 Oct 2024 12:48:54 +0530 Subject: [PATCH 3/3] refactor: move Member DTOs to member.dto package for better organization --- .../borrowing/BorrowingService.java | 2 +- .../libraryman_api/borrowing/BorrowingsDto.java | 2 +- .../libraryman_api/member/MemberController.java | 16 +++++++++++++--- .../com/libraryman_api/member/MemberService.java | 3 +++ .../member/{ => dto}/MembersDto.java | 4 +++- .../member/{ => dto}/UpdateMembersDto.java | 2 +- .../member/{ => dto}/UpdatePasswordDto.java | 2 +- 7 files changed, 23 insertions(+), 8 deletions(-) rename src/main/java/com/libraryman_api/member/{ => dto}/MembersDto.java (96%) rename src/main/java/com/libraryman_api/member/{ => dto}/UpdateMembersDto.java (96%) rename src/main/java/com/libraryman_api/member/{ => dto}/UpdatePasswordDto.java (95%) diff --git a/src/main/java/com/libraryman_api/borrowing/BorrowingService.java b/src/main/java/com/libraryman_api/borrowing/BorrowingService.java index 35cec20..7a1c510 100644 --- a/src/main/java/com/libraryman_api/borrowing/BorrowingService.java +++ b/src/main/java/com/libraryman_api/borrowing/BorrowingService.java @@ -9,7 +9,7 @@ import com.libraryman_api.fine.FineRepository; import com.libraryman_api.member.MemberService; import com.libraryman_api.member.Members; -import com.libraryman_api.member.MembersDto; +import com.libraryman_api.member.dto.MembersDto; import com.libraryman_api.notification.NotificationService; import org.springframework.data.domain.Page; diff --git a/src/main/java/com/libraryman_api/borrowing/BorrowingsDto.java b/src/main/java/com/libraryman_api/borrowing/BorrowingsDto.java index 28b36b3..7c1b392 100644 --- a/src/main/java/com/libraryman_api/borrowing/BorrowingsDto.java +++ b/src/main/java/com/libraryman_api/borrowing/BorrowingsDto.java @@ -2,7 +2,7 @@ import com.libraryman_api.book.BookDto; import com.libraryman_api.fine.Fines; -import com.libraryman_api.member.MembersDto; +import com.libraryman_api.member.dto.MembersDto; import java.util.Date; diff --git a/src/main/java/com/libraryman_api/member/MemberController.java b/src/main/java/com/libraryman_api/member/MemberController.java index 74ff8d3..20a729a 100644 --- a/src/main/java/com/libraryman_api/member/MemberController.java +++ b/src/main/java/com/libraryman_api/member/MemberController.java @@ -1,7 +1,5 @@ package com.libraryman_api.member; -import com.libraryman_api.exception.ResourceNotFoundException; - import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -9,7 +7,19 @@ import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.access.prepost.PreAuthorize; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.member.dto.MembersDto; +import com.libraryman_api.member.dto.UpdateMembersDto; +import com.libraryman_api.member.dto.UpdatePasswordDto; /** * REST controller for managing library members. diff --git a/src/main/java/com/libraryman_api/member/MemberService.java b/src/main/java/com/libraryman_api/member/MemberService.java index a74566c..8f053c8 100644 --- a/src/main/java/com/libraryman_api/member/MemberService.java +++ b/src/main/java/com/libraryman_api/member/MemberService.java @@ -12,6 +12,9 @@ import com.libraryman_api.exception.InvalidPasswordException; import com.libraryman_api.exception.InvalidSortFieldException; import com.libraryman_api.exception.ResourceNotFoundException; +import com.libraryman_api.member.dto.MembersDto; +import com.libraryman_api.member.dto.UpdateMembersDto; +import com.libraryman_api.member.dto.UpdatePasswordDto; import com.libraryman_api.notification.NotificationService; import com.libraryman_api.security.config.PasswordEncoder; diff --git a/src/main/java/com/libraryman_api/member/MembersDto.java b/src/main/java/com/libraryman_api/member/dto/MembersDto.java similarity index 96% rename from src/main/java/com/libraryman_api/member/MembersDto.java rename to src/main/java/com/libraryman_api/member/dto/MembersDto.java index 93a7c73..7f97d8a 100644 --- a/src/main/java/com/libraryman_api/member/MembersDto.java +++ b/src/main/java/com/libraryman_api/member/dto/MembersDto.java @@ -1,7 +1,9 @@ -package com.libraryman_api.member; +package com.libraryman_api.member.dto; import java.util.Date; +import com.libraryman_api.member.Role; + public class MembersDto { private int memberId; diff --git a/src/main/java/com/libraryman_api/member/UpdateMembersDto.java b/src/main/java/com/libraryman_api/member/dto/UpdateMembersDto.java similarity index 96% rename from src/main/java/com/libraryman_api/member/UpdateMembersDto.java rename to src/main/java/com/libraryman_api/member/dto/UpdateMembersDto.java index a3e6692..8859bb3 100644 --- a/src/main/java/com/libraryman_api/member/UpdateMembersDto.java +++ b/src/main/java/com/libraryman_api/member/dto/UpdateMembersDto.java @@ -1,4 +1,4 @@ -package com.libraryman_api.member; +package com.libraryman_api.member.dto; public class UpdateMembersDto { diff --git a/src/main/java/com/libraryman_api/member/UpdatePasswordDto.java b/src/main/java/com/libraryman_api/member/dto/UpdatePasswordDto.java similarity index 95% rename from src/main/java/com/libraryman_api/member/UpdatePasswordDto.java rename to src/main/java/com/libraryman_api/member/dto/UpdatePasswordDto.java index 5f593d2..a7be20a 100644 --- a/src/main/java/com/libraryman_api/member/UpdatePasswordDto.java +++ b/src/main/java/com/libraryman_api/member/dto/UpdatePasswordDto.java @@ -1,4 +1,4 @@ -package com.libraryman_api.member; +package com.libraryman_api.member.dto; public class UpdatePasswordDto {