Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Member Management with Secure Updates and Password Management #82

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
36 changes: 32 additions & 4 deletions src/main/java/com/libraryman_api/member/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
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;
import org.springframework.data.domain.Sort;
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.
Expand Down Expand Up @@ -83,7 +93,8 @@ public ResponseEntity<MembersDto> 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);
}

Expand All @@ -98,4 +109,21 @@ public MembersDto updateMember(@PathVariable int id, @RequestBody MembersDto mem
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.");
}

}
47 changes: 42 additions & 5 deletions src/main/java/com/libraryman_api/member/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,14 @@
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.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;



Expand All @@ -35,16 +40,18 @@ 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.
*
* @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;
}

/**
Expand Down Expand Up @@ -109,15 +116,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);
Expand Down Expand Up @@ -146,6 +150,39 @@ public void deleteMember(int memberId) {
memberRepository.delete(member);
}

/**
* Updates the password for a library member.
*
* <p>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.</p>
*
* @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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
52 changes: 52 additions & 0 deletions src/main/java/com/libraryman_api/member/dto/UpdateMembersDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.libraryman_api.member.dto;

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 + '\'' +
'}';
}
}
Loading
Loading