Skip to content

Commit

Permalink
AYS-363 | User Password Service Has Been Refactored with New Password…
Browse files Browse the repository at this point in the history
… Flows (#352)
  • Loading branch information
agitrubard authored Aug 1, 2024
1 parent f41c07b commit d51b853
Show file tree
Hide file tree
Showing 8 changed files with 106 additions and 47 deletions.
1 change: 1 addition & 0 deletions src/main/java/org/ays/auth/model/AysUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ public static class Password extends BaseDomainModel {

private String id;
private String value;
private LocalDateTime forgotAt;

}

Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/ays/auth/model/entity/AysUserEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ public static class PasswordEntity extends BaseEntity {
@JoinColumn(name = "USER_ID", referencedColumnName = "ID")
private AysUserEntity user;

@Column(name = "FORGOT_AT")
private LocalDateTime forgotAt;

}


Expand Down
19 changes: 17 additions & 2 deletions src/main/java/org/ays/auth/service/AysUserPasswordService.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package org.ays.auth.service;

import org.ays.auth.model.request.AysForgotPasswordRequest;
import org.ays.auth.util.exception.AysEmailAddressNotValidException;
import org.ays.auth.util.exception.AysUserPasswordCannotChangedException;
import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException;

/**
* Service interface for handling user password operations.
Expand All @@ -11,15 +14,27 @@ public interface AysUserPasswordService {
/**
* Handles the forgot password request by sending an email to the user
* with instructions to create a new password.
* <p>
* This method checks if a user exists with the provided email address.
* If the user exists and has no password set, a new temp password is generated.
* If the user already has a password, the forgot password timestamp is updated.
* In both cases, an email is sent to the user with instructions to create a new password.
*
* @param forgotPasswordRequest the request containing the user's email address.
* @throws AysEmailAddressNotValidException if no user is found with the provided email address.
*/
void forgotPassword(AysForgotPasswordRequest forgotPasswordRequest);

/**
* Checks the existence and validity of a password reset token by its ID.
* Checks the validity of changing the user's password.
* <p>
* This method verifies if the password change request is valid by checking the
* existence and expiry of the password change request associated with the given password ID.
* It throws an exception if the password cannot be changed due to invalid conditions.
*
* @param passwordId the ID of the password reset token to be checked.
* @param passwordId The ID of the password to check for change validity.
* @throws AysUserPasswordDoesNotExistException if no password is found for the given ID.
* @throws AysUserPasswordCannotChangedException if the password cannot be changed due to invalid conditions.
*/
void checkPasswordChangingValidity(String passwordId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.ays.auth.util.exception.AysUserPasswordCannotChangedException;
import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException;
import org.ays.common.util.AysRandomUtil;
import org.ays.common.util.AysUUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -37,15 +36,16 @@ class AysUserPasswordServiceImpl implements AysUserPasswordService {


/**
* Handles the forgot password process for a user.
* Handles the forgot password request by sending an email to the user
* with instructions to create a new password.
* <p>
* This method is triggered when a user requests to reset their password. It checks if the user's email
* address is valid and then sends a password reset email. If the user does not have an existing password,
* a new temporary password is generated and saved.
* </p>
* This method checks if a user exists with the provided email address.
* If the user exists and has no password set, a new temp password is generated.
* If the user already has a password, the forgot password timestamp is updated.
* In both cases, an email is sent to the user with instructions to create a new password.
*
* @param forgotPasswordRequest The request object containing the user's email address.
* @throws AysEmailAddressNotValidException if the email address provided does not exist in the system.
* @param forgotPasswordRequest The request containing the user's email address.
* @throws AysEmailAddressNotValidException if no user is found with the provided email address.
*/
@Override
public void forgotPassword(final AysForgotPasswordRequest forgotPasswordRequest) {
Expand All @@ -54,29 +54,31 @@ public void forgotPassword(final AysForgotPasswordRequest forgotPasswordRequest)
final AysUser user = userReadPort.findByEmailAddress(emailAddress)
.orElseThrow(() -> new AysEmailAddressNotValidException(emailAddress));

if (user.getPassword() != null) {
userMailService.sendPasswordCreateEmail(user);
return;
if (user.getPassword() == null) {
final AysUser.Password password = AysUser.Password.builder()
.value(AysRandomUtil.generateUUID())
.forgotAt(LocalDateTime.now())
.build();
user.setPassword(password);
} else {
user.getPassword().setForgotAt(LocalDateTime.now());
}

final AysUser.Password password = AysUser.Password.builder()
.value(AysRandomUtil.generateUUID())
.build();
user.setPassword(password);
AysUser savedUser = userSavePort.save(user);

final AysUser savedUser = userSavePort.save(user);
userMailService.sendPasswordCreateEmail(savedUser);
}


/**
* Validates the provided password ID to ensure it is valid for password reset.
* This method checks if the password ID exists, if the password value is in UUID format,
* and if the password was updated within the last 2 hours.
* Checks the validity of changing the user's password.
* <p>
* This method verifies if the password change request is valid by checking the
* existence and expiry of the password change request associated with the given password ID.
* It throws an exception if the password cannot be changed due to invalid conditions.
*
* @param passwordId the ID of the password to be checked.
* @throws AysUserPasswordDoesNotExistException if the password ID does not exist.
* @throws AysUserPasswordCannotChangedException if the password value is not in UUID format or if the password update time exceeds the allowed limit.
* @param passwordId The ID of the password to check for change validity.
* @throws AysUserPasswordDoesNotExistException if no password is found for the given ID.
* @throws AysUserPasswordCannotChangedException if the password cannot be changed due to invalid conditions.
*/
@Override
public void checkPasswordChangingValidity(final String passwordId) {
Expand All @@ -85,17 +87,31 @@ public void checkPasswordChangingValidity(final String passwordId) {
.orElseThrow(() -> new AysUserPasswordDoesNotExistException(passwordId))
.getPassword();

boolean isUUID = AysUUID.isValid(password.getValue());
if (!isUUID) {
throw new AysUserPasswordCannotChangedException(passwordId);
this.checkChangingValidity(password);
}


/**
* Checks the validity of changing the password.
* <p>
* This method verifies if the password change request is valid by checking if the password change request
* was initiated within the allowable time frame. It throws an exception if the password cannot be changed.
*
* @param password The AysUser.Password object representing the user's password.
* @throws AysUserPasswordCannotChangedException if the password cannot be changed due to invalid conditions.
*/
private void checkChangingValidity(final AysUser.Password password) {

Optional<LocalDateTime> forgotAt = Optional
.ofNullable(password.getForgotAt());

if (forgotAt.isEmpty()) {
throw new AysUserPasswordCannotChangedException(password.getId());
}

LocalDateTime passwordChangedAt = Optional
.ofNullable(password.getUpdatedAt())
.orElse(password.getCreatedAt());
boolean isExpired = LocalDateTime.now().minusHours(2).isBefore(passwordChangedAt);
boolean isExpired = LocalDateTime.now().minusHours(2).isBefore(forgotAt.get());
if (!isExpired) {
throw new AysUserPasswordCannotChangedException(passwordId);
throw new AysUserPasswordCannotChangedException(password.getId());
}

}
Expand Down
1 change: 1 addition & 0 deletions src/main/resources/db/changelog/changes/1-ays-ddl.xml
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
<column name="VALUE" type="VARCHAR(60)">
<constraints nullable="false"/>
</column>
<column name="FORGOT_AT" type="TIMESTAMP"/>
<column name="CREATED_USER" type="VARCHAR(255)" defaultValue="AYS">
<constraints nullable="false"/>
</column>
Expand Down
16 changes: 15 additions & 1 deletion src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.ays.auth.model.response.AysTokenResponse;
import org.ays.auth.model.response.AysTokenResponseBuilder;
import org.ays.auth.port.AysRoleReadPort;
import org.ays.auth.port.AysUserReadPort;
import org.ays.auth.port.AysUserSavePort;
import org.ays.common.model.response.AysResponse;
import org.ays.common.model.response.AysResponseBuilder;
Expand All @@ -22,19 +23,24 @@
import org.ays.util.AysMockMvcRequestBuilders;
import org.ays.util.AysMockResultMatchersBuilders;
import org.ays.util.AysValidTestData;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import java.time.LocalDateTime;
import java.util.List;

class AysAuthEndToEndTest extends AysEndToEndTest {

@Autowired
private AysUserSavePort userSavePort;

@Autowired
private AysUserReadPort userReadPort;

@Autowired
private AysRoleReadPort roleReadPort;

Expand Down Expand Up @@ -142,6 +148,14 @@ void givenValidForgotPasswordRequest_whenSendPasswordCreateMail_thenReturnSucces
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.doesNotExist());

// Verify
AysUser user = userReadPort.findByEmailAddress(AysValidTestData.User.EMAIL_ADDRESS)
.orElseThrow();

Assertions.assertNotNull(user.getPassword());
Assertions.assertNotNull(user.getPassword().getForgotAt());
Assertions.assertTrue(user.getPassword().getForgotAt().isAfter(LocalDateTime.now().minusMinutes(1)));
}


Expand All @@ -160,7 +174,7 @@ void givenValidId_whenCheckPasswordIdSuccessfully_thenReturnSuccessResponse() th

AysUser.Password password = new AysUserBuilder.PasswordBuilder()
.withoutId()
.withValue("ba9c1156-05d6-410e-a3af-fe6a36935430")
.withForgotAt(LocalDateTime.now().minusMinutes(15))
.build();

AysUser user = userSavePort.save(
Expand Down
4 changes: 2 additions & 2 deletions src/test/java/org/ays/auth/model/AysUserBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ public PasswordBuilder withValue(String value) {
return this;
}

public PasswordBuilder withUpdatedAt(LocalDateTime updatedAt) {
data.setUpdatedAt(updatedAt);
public PasswordBuilder withForgotAt(LocalDateTime forgotAt) {
data.setForgotAt(forgotAt);
return this;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import org.ays.auth.util.exception.AysEmailAddressNotValidException;
import org.ays.auth.util.exception.AysUserPasswordCannotChangedException;
import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException;
import org.ays.util.AysValidTestData;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
Expand All @@ -37,7 +36,7 @@ class AysUserPasswordServiceImplTest extends AysUnitTest {


@Test
void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSendPasswordCreateEmail() {
void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSetPasswordForgotAtAndSendPasswordCreateEmail() {
// Given
AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder()
.withValidValues()
Expand All @@ -52,6 +51,17 @@ void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSendPasswordC
Mockito.when(userReadPort.findByEmailAddress(Mockito.anyString()))
.thenReturn(Optional.of(mockUser));

AysUser mockSavedUser = new AysUserBuilder()
.withValidValues()
.withId(mockUser.getId())
.withEmailAddress(mockUser.getEmailAddress())
.withPhoneNumber(mockUser.getPhoneNumber())
.withPassword(mockUser.getPassword())
.build();
mockSavedUser.getPassword().setForgotAt(LocalDateTime.now());
Mockito.when(userSavePort.save(Mockito.any(AysUser.class)))
.thenReturn(mockSavedUser);

Mockito.doNothing()
.when(userMailService)
.sendPasswordCreateEmail(Mockito.any(AysUser.class));
Expand All @@ -63,7 +73,7 @@ void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSendPasswordC
Mockito.verify(userReadPort, Mockito.times(1))
.findByEmailAddress(Mockito.anyString());

Mockito.verify(userSavePort, Mockito.never())
Mockito.verify(userSavePort, Mockito.times(1))
.save(Mockito.any(AysUser.class));

Mockito.verify(userMailService, Mockito.times(1))
Expand Down Expand Up @@ -143,16 +153,15 @@ void givenValidForgotPasswordRequest_whenEmailDoesNotExist_thenThrowAysEmailAddr


@Test
void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInTwoHours_thenDoNothing() {
void givenValidId_whenPasswordExistAndForgotInTwoHours_thenDoNothing() {
// Given
String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1";

// When
AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder()
.withValidValues()
.withId(mockId)
.withValue("9fccaabc-2d00-4128-923e-d415b319a57f")
.withUpdatedAt(LocalDateTime.now().minusMinutes(5))
.withForgotAt(LocalDateTime.now().minusMinutes(5))
.build();
AysUser mockUser = new AysUserBuilder()
.withValidValues()
Expand Down Expand Up @@ -190,16 +199,16 @@ void givenId_whenPasswordDoesExist_thenThrowUserPasswordDoesNotExistException()
}

@Test
void givenValidId_whenPasswordExistAndValueHasNotUUIDFormatAndUpdatedInTwoHours_thenThrowUserPasswordCannotChangedException() {
void givenValidId_whenPasswordExistAndForgotAtDoesNotExist_thenThrowUserPasswordCannotChangedException() {
// Given
String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1";

// When
AysUser.Password mockPassword = new AysUserBuilder.PasswordBuilder()
.withValidValues()
.withId(mockId)
.withValue(AysValidTestData.PASSWORD_ENCRYPTED)
.withUpdatedAt(LocalDateTime.now().minusMinutes(5))
.withValue("608a15a8-5e82-4fd8-ac74-308068393e53")
.withForgotAt(null)
.build();
AysUser mockUser = new AysUserBuilder()
.withValidValues()
Expand All @@ -220,7 +229,7 @@ void givenValidId_whenPasswordExistAndValueHasNotUUIDFormatAndUpdatedInTwoHours_
}

@Test
void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInThreeHours_thenThrowUserPasswordCannotChangedException() {
void givenValidId_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCannotChangedException() {
// Given
String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1";

Expand All @@ -229,7 +238,7 @@ void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInThreeHours_t
.withValidValues()
.withId(mockId)
.withValue("608a15a8-5e82-4fd8-ac74-308068393e53")
.withUpdatedAt(LocalDateTime.now().minusHours(3))
.withForgotAt(LocalDateTime.now().minusHours(3))
.build();
AysUser mockUser = new AysUserBuilder()
.withValidValues()
Expand Down

0 comments on commit d51b853

Please sign in to comment.