Skip to content

Commit

Permalink
hotfix: improve error handling in user service and enhance password r…
Browse files Browse the repository at this point in the history
…ecovery email formatting
  • Loading branch information
LeonardoMeireles55 committed Jan 29, 2025
1 parent baa6d7f commit 65af326
Show file tree
Hide file tree
Showing 3 changed files with 215 additions and 121 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
Expand All @@ -32,10 +34,13 @@ public class UserService {

private void sendRecoveryEmail(RecoveryEmailDTO recoveryEmailDTO) {
String subject = "Password Recovery";
String message = String.format(
"Dear user,\n\nUse the following temporary password to recover your account: %s\n\nBest regards,"
+ "\nYour Team",
recoveryEmailDTO.temporaryPassword());
String message = String.format("""
Dear user,
Use the following temporary password to recover your account: %s
Best regards,
Your Team""", recoveryEmailDTO.temporaryPassword());
log.info("Sending recovery identifier to: {}", recoveryEmailDTO.email());
emailService.sendPlainTextEmail(new EmailDTO(recoveryEmailDTO.email(), subject, message));
}
Expand All @@ -45,7 +50,7 @@ public void recoverPassword(String username, String email) {
var user = userRepository.existsByUsernameAndEmail(username, email);

if (!user) {
throw new CustomGlobalErrorHandling.ResourceNotFoundException(
throw new CustomGlobalErrorHandling.UserNotFoundException(
"User not or invalid arguments");
}

Expand All @@ -57,11 +62,33 @@ public void recoverPassword(String username, String email) {

public void changePassword(String email, String temporaryPassword, String newPassword) {
if (!passwordRecoveryTokenManager.isRecoveryTokenValid(temporaryPassword, email)) {
throw new CustomGlobalErrorHandling.ResourceNotFoundException("Invalid recovery token");
throw new CustomGlobalErrorHandling.RecoveryTokenInvalidException();
}
userRepository.setPasswordWhereByEmail(email, BCryptEncoderComponent.encrypt(newPassword));
}

private TokenJwtDTO authenticateAndGenerateToken(User credential, String password) {
try {
final var authToken =
new UsernamePasswordAuthenticationToken(credential.getUsername(), password);
final var auth = authenticationManager.authenticate(authToken);
final var user = (User) auth.getPrincipal();

if (!auth.isAuthenticated()) {
emailService.notifyFailedUserLogin(user.getUsername(), user.getEmail(),
LocalDateTime.now());
throw new BadCredentialsException(
"Authentication failed for user: " + credential.getUsername());
}

return tokenService.generateToken(user);

} catch (BadCredentialsException e) {
log.error("Authentication failed for user: {}", credential.getUsername(), e);
throw e;
}
}

public User signUp(String username, String email, String password) {

var user =
Expand All @@ -81,22 +108,19 @@ public User signUp(String username, String email, String password) {
}

public TokenJwtDTO signIn(String identifier, String password) {
try {
final var credential = userRepository.findByUsernameOrEmail(identifier, identifier);

final var credential =
userRepository.findByUsernameOrEmail(identifier, identifier).getUsername();

final var authToken = new UsernamePasswordAuthenticationToken(credential, password);
final var auth = authenticationManager.authenticate(authToken);
final var user = (User) auth.getPrincipal();
if (!auth.isAuthenticated()) {
try {
emailService.notifyFailedUserLogin(user.getUsername(), user.getEmail(),
LocalDateTime.now());
} catch (Exception e) {
log.error("Failed to send analytics notification identifier", e);
if (credential == null) {
throw new CustomGlobalErrorHandling.UserNotFoundException();
}

return authenticateAndGenerateToken(credential, password);

} catch (Exception e) {
log.error("Sign-in process failed for identifier: {}", identifier, e);
throw new BadCredentialsException("Sign-in failed for identifier: " + identifier);
}
return tokenService.generateToken(user);
}

public void updateUserPassword(String name, String email, String password, String newPassword) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,100 +20,171 @@
@Slf4j
public class CustomGlobalErrorHandling {

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> handleValidationExceptions(MethodArgumentNotValidException ex,
HttpServletRequest request) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField,
error -> {
error.getDefaultMessage();
return error.getDefaultMessage();
}));

ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, "Validation failed", request.getRequestURI());
apiError.setDetails(new ArrayList<>(errors.values()));

log.error("Validation failed for request to {}: {}", request.getRequestURI(), errors);
return ResponseEntity.badRequest().body(apiError);
}

@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI());

log.error("Resource not found at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(apiError);
}

@ExceptionHandler({BadCredentialsException.class, PasswordNotMatchesException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<ApiError> handleAuthenticationErrors(Exception ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.UNAUTHORIZED, "Authentication failed", request.getRequestURI());

log.error("Authentication failed at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiError);
}

@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<ApiError> handleAccessDenied(HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI());

log.error("Access denied at {}", request.getRequestURI());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(apiError);
}

@ExceptionHandler(UserAlreadyExistException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> handleUserAlreadyExist(UserAlreadyExistException ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, "Username or identifier already exists", request.getRequestURI());

log.error("Username or identifier already exists at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ResponseEntity<ApiError> handleDataIntegrityViolation(DataIntegrityViolationException ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.CONFLICT, "Data integrity violation", request.getRequestURI());

log.error("Data integrity violation at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(apiError);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ApiError> handleAllUncaughtException(Exception ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "An unexpected error occurred", request.getRequestURI());

log.error("Unexpected error occurred at {}: {}", request.getRequestURI(), ex.getMessage(), ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiError);
}


public static class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
}

public static class PasswordNotMatchesException extends RuntimeException {
public PasswordNotMatchesException() {
super();
}
}

public static class DataIntegrityViolationException extends RuntimeException {
public DataIntegrityViolationException() {
super();
}
}

public static class UserAlreadyExistException extends RuntimeException {
public UserAlreadyExistException() {
super();
}
}
}
@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> handleValidationExceptions(MethodArgumentNotValidException ex,
HttpServletRequest request) {
Map<String, String> errors = ex.getBindingResult().getFieldErrors().stream()
.collect(Collectors.toMap(FieldError::getField, error -> {
error.getDefaultMessage();
return error.getDefaultMessage();
}));

ApiError apiError =
new ApiError(HttpStatus.BAD_REQUEST, "Validation failed", request.getRequestURI());
apiError.setDetails(new ArrayList<>(errors.values()));

log.error("Validation failed for request to {}: {}", request.getRequestURI(), errors);
return ResponseEntity.badRequest().body(apiError);
}

@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ApiError> handleNotFound(ResourceNotFoundException ex,
HttpServletRequest request) {
ApiError apiError =
new ApiError(HttpStatus.NOT_FOUND, ex.getMessage(), request.getRequestURI());

log.error("Resource not found at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(apiError);
}

@ExceptionHandler({BadCredentialsException.class, PasswordNotMatchesException.class})
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ResponseEntity<ApiError> handleAuthenticationErrors(Exception ex,
HttpServletRequest request) {
String errorMessage = (ex.getMessage() != null) ? ex.getMessage()
: "Invalid credentials or password does not match";

ApiError apiError =
new ApiError(HttpStatus.UNAUTHORIZED, errorMessage, request.getRequestURI());

log.error("Authentication failed at {}: {}, Exception type: {}", request.getRequestURI(),
errorMessage, ex.getClass().getSimpleName());

return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(apiError);
}

@ExceptionHandler(RecoveryTokenInvalidException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<ApiError> handleRecoveryTokenInvalid(RecoveryTokenInvalidException ex,
HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.FORBIDDEN,
"Recovery token is invalid or expired", request.getRequestURI());

log.error("Invalid recovery token at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(apiError);
}

@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ResponseEntity<ApiError> handleAccessDenied(HttpServletRequest request) {
ApiError apiError =
new ApiError(HttpStatus.FORBIDDEN, "Access denied", request.getRequestURI());

log.error("Access denied at {}", request.getRequestURI());
return ResponseEntity.status(HttpStatus.FORBIDDEN).body(apiError);
}

@ExceptionHandler(UserAlreadyExistException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> handleUserAlreadyExist(UserAlreadyExistException ex,
HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST,
"Username or identifier already exists", request.getRequestURI());

log.error("Username or identifier already exists at {}: {}", request.getRequestURI(),
ex.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(apiError);
}

@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ResponseEntity<ApiError> handleDataIntegrityViolation(DataIntegrityViolationException ex,
HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.CONFLICT, "Data integrity violation",
request.getRequestURI());

log.error("Data integrity violation at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(apiError);
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResponseEntity<ApiError> handleAllUncaughtException(Exception ex,
HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred", request.getRequestURI());

log.error("Unexpected error occurred at {}: {}", request.getRequestURI(), ex.getMessage(),
ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(apiError);
}

@ExceptionHandler(UserNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ResponseEntity<ApiError> handleUserNotFound(UserNotFoundException ex,
HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.NOT_FOUND,
"User not found or invalid credentials", request.getRequestURI());

log.error("User not found at {}: {}", request.getRequestURI(), ex.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(apiError);
}

public static class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException() {
super();
}


public ResourceNotFoundException(String message) {
super(message);
}
}


public static class PasswordNotMatchesException extends RuntimeException {
public PasswordNotMatchesException() {
super();
}
}

public static class DataIntegrityViolationException extends RuntimeException {
public DataIntegrityViolationException() {
super();
}
}

public static class UserAlreadyExistException extends RuntimeException {
public UserAlreadyExistException() {
super();
}
}

public static class UserNotFoundException extends RuntimeException {
public UserNotFoundException() {
super();
}

public UserNotFoundException(String message) {
super(message);
}
}

public static class RecoveryTokenInvalidException extends RuntimeException {
private static final long serialVersionUID = 1L;

public RecoveryTokenInvalidException() {
super("Recovery token is invalid or expired");
}

public RecoveryTokenInvalidException(String message) {
super(message);
}

public RecoveryTokenInvalidException(String message, Throwable cause) {
super(message, cause);
}
}
}
Loading

0 comments on commit 65af326

Please sign in to comment.