Skip to content

Commit

Permalink
feat: allow login using either email or username
Browse files Browse the repository at this point in the history
- Updated the signIn method to check if the identifier is an email or username.
- Modified the authentication process to handle both email and username.
  • Loading branch information
LeonardoMeireles55 committed Jan 28, 2025
1 parent ea5378f commit 9abf7c4
Show file tree
Hide file tree
Showing 9 changed files with 50 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ public ResponseEntity<Void> signUp(@Valid @RequestBody final SignUpUsersDTO sign
@PostMapping("/sign-in")
public ResponseEntity<TokenJwtDTO> singIn(
@RequestBody @Valid final LoginUserDTO loginUserDTO) {
final var token = userService.signIn(loginUserDTO.email(),
final var token = userService.signIn(loginUserDTO.identifier(),
loginUserDTO.password());
return ResponseEntity.ok(token);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

import jakarta.validation.constraints.NotNull;

public record LoginUserDTO(@NotNull String email,
public record LoginUserDTO(@NotNull String identifier,
@NotNull String password) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
UserDetails findByUsername(String userName);
UserDetails findByEmail(String email);

UserDetails getReferenceByUsernameAndEmail(String userName, String Email);

Expand All @@ -23,7 +24,7 @@ public interface UserRepository extends JpaRepository<User, Long> {

@Transactional
@Modifying
@Query("UPDATE users u SET u.password = ?2 WHERE u.email = ?1")
@Query("UPDATE users u SET u.password = ?2 WHERE u.identifier = ?1")
void setPasswordWhereByEmail(String email, String newPassword);


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ public void saveNewAnalyticsRecords(List<AnalyticsDTO> valuesOfLevelsList) {
var content = controlRulesValidators.validateRules(notPassedList);
emailService.sendFailedAnalyticsNotification(notPassedList, content);
} catch (Exception e) {
log.error("Error sending email notification: {}", e.getMessage());
log.error("Error sending identifier notification: {}", e.getMessage());
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public class EmailService {
@Value("${spring.mail.username}")
String emailFrom;

@Value("${email.to.send.list}")
@Value("${identifier.to.send.list}")
String emailListString;


Expand All @@ -46,7 +46,7 @@ private void init() {
: List.of();

if (emailList.isEmpty()) {
log.warn("No email recipients configured in email.to.send.list");
log.warn("No identifier recipients configured in identifier.to.send.list");
}
}

Expand All @@ -67,18 +67,18 @@ public void sendHtmlEmail(EmailDTO emailDTO) {
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
helper.setFrom(emailFrom);

// Convert email list to InternetAddress array
// Convert identifier list to InternetAddress array
InternetAddress[] internetAddresses = emailList.stream().map(emailAddress -> {
try {
return new InternetAddress(emailAddress);
} catch (AddressException e) {
log.error("Invalid email address: {}", emailAddress, e);
log.error("Invalid identifier address: {}", emailAddress, e);
return null;
}
}).filter(Objects::nonNull).toArray(InternetAddress[]::new);

if (internetAddresses.length == 0) {
log.error("No valid email addresses found");
log.error("No valid identifier addresses found");
return;
}

Expand All @@ -87,10 +87,10 @@ public void sendHtmlEmail(EmailDTO emailDTO) {
helper.setText(buildEmailBody(emailDTO.body()), true);

javaMailSender.send(mimeMessage);
log.info("HTML email sent successfully to {} recipients", internetAddresses.length);
log.info("HTML identifier sent successfully to {} recipients", internetAddresses.length);

} catch (MessagingException e) {
log.error("Failed to send HTML email: {}", e.getMessage(), e);
log.error("Failed to send HTML identifier: {}", e.getMessage(), e);
throw new RuntimeException("Email sending failed", e);
}
}
Expand All @@ -114,7 +114,7 @@ public void sendFailedAnalyticsNotification(List<AnalyticsDTO> failedRecords,
try {
return new InternetAddress(email);
} catch (AddressException e) {
log.error("Invalid email address: {}", email, e);
log.error("Invalid identifier address: {}", email, e);
return null;
}
}).filter(Objects::nonNull).toArray(InternetAddress[]::new);
Expand All @@ -126,7 +126,7 @@ public void sendFailedAnalyticsNotification(List<AnalyticsDTO> failedRecords,

log.info("Failed analytics notification sent for {} records", failedRecords.size());
} catch (MessagingException e) {
log.error("Failed to send analytics notification email", e);
log.error("Failed to send analytics notification identifier", e);
throw new RuntimeException("Failed to send analytics notification", e);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
Expand All @@ -36,7 +37,7 @@ private void sendRecoveryEmail(RecoveryEmailDTO recoveryEmailDTO) {
"Dear user,\n\nUse the following temporary password to recover your account: %s\n\nBest regards,"
+ "\nYour Team",
recoveryEmailDTO.temporaryPassword());
log.info("Sending recovery email to: {}", recoveryEmailDTO.email());
log.info("Sending recovery identifier to: {}", recoveryEmailDTO.email());
emailService.sendPlainTextEmail(new EmailDTO(recoveryEmailDTO.email(), subject, message));
}

Expand Down Expand Up @@ -75,18 +76,25 @@ public User signUp(String username, String email, String password) {
}


public TokenJwtDTO signIn(String email, String password) {
public TokenJwtDTO signIn(String identifier, String password) {
User user;
if (identifier.contains("@")) {
user = (User) userRepository.findByEmail(identifier);
} else {
user = (User) userRepository.findByUsername(identifier);
}

final var authToken = new UsernamePasswordAuthenticationToken(email, password);
if (user == null) {
throw new UsernameNotFoundException("User not found: " + identifier);
}

final var authToken = new UsernamePasswordAuthenticationToken(user.getEmail(), password);
final var auth = authenticationManager.authenticate(authToken);
final var user = (User) auth.getPrincipal();
if (!auth.isAuthenticated()) {
emailService.notifyFailedUserLogin(user.getUsername(), user.getEmail(),
LocalDateTime.now());
emailService.notifyFailedUserLogin(user.getUsername(), user.getEmail(), LocalDateTime.now());
}
return tokenService.generateToken(user);
}

public void updateUserPassword(String name, String email, String password, String newPassword) {
var oldPass = userRepository.getReferenceByUsernameAndEmail(name, email);
if (!BCryptEncoderComponent.decrypt(password, oldPass.getPassword())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,9 +68,9 @@ public ResponseEntity<ApiError> handleAccessDenied(HttpServletRequest request) {
@ExceptionHandler(UserAlreadyExistException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ResponseEntity<ApiError> handleUserAlreadyExist(UserAlreadyExistException ex, HttpServletRequest request) {
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, "Username or email already exists", request.getRequestURI());
ApiError apiError = new ApiError(HttpStatus.BAD_REQUEST, "Username or identifier already exists", request.getRequestURI());

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ void signUp_return_204() throws Exception {
.content(usersRecordJacksonTester.write(usersDTO).getJson()))
.andExpect(status().isNoContent())
.andExpect(jsonPath("$.username").value("testUser"))
.andExpect(jsonPath("$.email").value("[email protected]"));
.andExpect(jsonPath("$.identifier").value("[email protected]"));

verify(userService).signUp(usersDTO.username(), usersDTO.password(), usersDTO.email());
}
Expand All @@ -102,7 +102,7 @@ void signIn_shouldReturn200AndCallUserService() throws Exception {
LoginUserDTO loginRecord = new LoginUserDTO("[email protected]", "password");
TokenJwtDTO tokenJwtDTO = new TokenJwtDTO("TokenJwt", dateExp);

when(userService.signIn(loginRecord.email(), loginRecord.password()))
when(userService.signIn(loginRecord.identifier(), loginRecord.password()))
.thenReturn(tokenJwtDTO);

mockMvc.perform(post("/users/sign-in").contentType(MediaType.APPLICATION_JSON)
Expand Down Expand Up @@ -170,7 +170,7 @@ void updatePassword_return_204() throws Exception {
@Test
@DisplayName("Should return 400 when signing up with invalid data")
void signUp_with_invalid_data_return_400() throws Exception {
UsersDTO invalidRecord = new UsersDTO("", "", "invalid-email");
UsersDTO invalidRecord = new UsersDTO("", "", "invalid-identifier");

mockMvc.perform(post("/users/sign-up").contentType(MediaType.APPLICATION_JSON)
.content(usersRecordJacksonTester.write(invalidRecord).getJson()))
Expand All @@ -189,6 +189,6 @@ void signIn_with_invalid_credentials_return_401() throws Exception {
.content(loginRecordJacksonTester.write(loginRecord).getJson()))
.andExpect(status().isUnauthorized());

verify(userService, times(1)).signIn(loginRecord.email(), loginRecord.password());
verify(userService, times(1)).signIn(loginRecord.identifier(), loginRecord.password());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,9 @@ void testRecoverPassword_UserExists() {
when(userRepository.existsByUsernameAndEmail(anyString(), anyString())).thenReturn(true);
when(passwordRecoveryTokenManager.generateTemporaryPassword()).thenReturn("tempPassword");

userService.recoverPassword("username", "email@example.com");
userService.recoverPassword("username", "identifier@example.com");

verify(passwordRecoveryTokenManager).generateAndStoreToken("email@example.com",
verify(passwordRecoveryTokenManager).generateAndStoreToken("identifier@example.com",
"tempPassword");
verify(emailService).sendPlainTextEmail(any());
}
Expand All @@ -52,18 +52,18 @@ void testRecoverPassword_UserDoesNotExist() {
when(userRepository.existsByUsernameAndEmail(anyString(), anyString())).thenReturn(false);

assertThrows(CustomGlobalErrorHandling.ResourceNotFoundException.class,
() -> userService.recoverPassword("username", "email@example.com"));
() -> userService.recoverPassword("username", "identifier@example.com"));
}

@Test
void testChangePassword_ValidToken() {
User user = new User("username", BCryptEncoderComponent.encrypt("newPassword"),
"email@example.com", UserRoles.USER);
"identifier@example.com", UserRoles.USER);
when(passwordRecoveryTokenManager.isRecoveryTokenValid(anyString(), anyString()))
.thenReturn(true);
userService.changePassword("email@example.com", "tempPassword", "newPassword");
userService.changePassword("identifier@example.com", "tempPassword", "newPassword");
assertThat(passwordRecoveryTokenManager.isRecoveryTokenValid("tempPassword",
"email@example.com")).isTrue();
"identifier@example.com")).isTrue();
}

@Test
Expand All @@ -72,52 +72,52 @@ void testChangePassword_InvalidToken() {
.thenReturn(false);

assertThrows(CustomGlobalErrorHandling.ResourceNotFoundException.class, () -> userService
.changePassword("email@example.com", "tempPassword", "newPassword"));
.changePassword("identifier@example.com", "tempPassword", "newPassword"));
}

@Test
void testSignUp_UserAlreadyExists() {
when(userRepository.existsByEmail(anyString())).thenReturn(true);

assertThrows(CustomGlobalErrorHandling.UserAlreadyExistException.class,
() -> userService.signUp("username", "password", "email@example.com"));
() -> userService.signUp("username", "password", "identifier@example.com"));
}

@Test
void testSignUp_NewUser() {
when(userRepository.existsByEmail(anyString())).thenReturn(false);
when(userRepository.save(any(User.class))).thenReturn(
new User("username", "encryptedPassword", "email@example.com", UserRoles.USER));
new User("username", "encryptedPassword", "identifier@example.com", UserRoles.USER));

User user = userService.signUp("username", "password", "email@example.com");
User user = userService.signUp("username", "password", "identifier@example.com");

assertNotNull(user);
assertEquals("username", user.getUsername());
assertEquals("email@example.com", user.getEmail());
assertEquals("identifier@example.com", user.getEmail());
}

@Test
void should_return_error_with_testUpdateUserPassword_PasswordMatches() {
User user = new User("username", BCryptEncoderComponent.encrypt("newPassword"),
"email@example.com", UserRoles.USER);
"identifier@example.com", UserRoles.USER);

when(userRepository.getReferenceByUsernameAndEmail(anyString(), anyString()))
.thenReturn(user);

assertThrows(CustomGlobalErrorHandling.PasswordNotMatchesException.class, () -> userService
.updateUserPassword("username", "email@example.com", "oldPassword", "newPassword"));
.updateUserPassword("username", "identifier@example.com", "oldPassword", "newPassword"));
verify(userRepository, never()).setPasswordWhereByUsername(anyString(), anyString());
}

@Test
void testUpdateUserPassword_PasswordDoesNotMatch() {
User user = new User("username", BCryptEncoderComponent.encrypt("oldPassword"),
"email@example.com", UserRoles.USER);
"identifier@example.com", UserRoles.USER);
when(userRepository.getReferenceByUsernameAndEmail(anyString(), anyString()))
.thenReturn(user);

assertThrows(CustomGlobalErrorHandling.PasswordNotMatchesException.class,
() -> userService.updateUserPassword("username", "email@example.com",
() -> userService.updateUserPassword("username", "identifier@example.com",
"wrongPassword", "newPassword"));
}
}

0 comments on commit 9abf7c4

Please sign in to comment.