From d51b853169aaab18db8e1633554fc962c88b22b3 Mon Sep 17 00:00:00 2001 From: Agit Rubar Demir <61833677+agitrubard@users.noreply.github.com> Date: Thu, 1 Aug 2024 09:48:50 +0300 Subject: [PATCH] AYS-363 | User Password Service Has Been Refactored with New Password Flows (#352) --- src/main/java/org/ays/auth/model/AysUser.java | 1 + .../ays/auth/model/entity/AysUserEntity.java | 3 + .../auth/service/AysUserPasswordService.java | 19 ++++- .../impl/AysUserPasswordServiceImpl.java | 78 +++++++++++-------- .../db/changelog/changes/1-ays-ddl.xml | 1 + .../auth/controller/AysAuthEndToEndTest.java | 16 +++- .../org/ays/auth/model/AysUserBuilder.java | 4 +- .../impl/AysUserPasswordServiceImplTest.java | 31 +++++--- 8 files changed, 106 insertions(+), 47 deletions(-) diff --git a/src/main/java/org/ays/auth/model/AysUser.java b/src/main/java/org/ays/auth/model/AysUser.java index 822c97296..94f823091 100644 --- a/src/main/java/org/ays/auth/model/AysUser.java +++ b/src/main/java/org/ays/auth/model/AysUser.java @@ -111,6 +111,7 @@ public static class Password extends BaseDomainModel { private String id; private String value; + private LocalDateTime forgotAt; } diff --git a/src/main/java/org/ays/auth/model/entity/AysUserEntity.java b/src/main/java/org/ays/auth/model/entity/AysUserEntity.java index c2e079d85..01184dac0 100644 --- a/src/main/java/org/ays/auth/model/entity/AysUserEntity.java +++ b/src/main/java/org/ays/auth/model/entity/AysUserEntity.java @@ -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; + } diff --git a/src/main/java/org/ays/auth/service/AysUserPasswordService.java b/src/main/java/org/ays/auth/service/AysUserPasswordService.java index 7d7a60921..a67fe726c 100644 --- a/src/main/java/org/ays/auth/service/AysUserPasswordService.java +++ b/src/main/java/org/ays/auth/service/AysUserPasswordService.java @@ -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. @@ -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. + *

+ * 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. + *

+ * 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); diff --git a/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java b/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java index 2314cdcd1..c1bfc6ac2 100644 --- a/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java +++ b/src/main/java/org/ays/auth/service/impl/AysUserPasswordServiceImpl.java @@ -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; @@ -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. *

- * 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. - *

+ * 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) { @@ -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. + *

+ * 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) { @@ -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. + *

+ * 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 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()); } } diff --git a/src/main/resources/db/changelog/changes/1-ays-ddl.xml b/src/main/resources/db/changelog/changes/1-ays-ddl.xml index 5ba5cae0f..8a5e43dad 100644 --- a/src/main/resources/db/changelog/changes/1-ays-ddl.xml +++ b/src/main/resources/db/changelog/changes/1-ays-ddl.xml @@ -346,6 +346,7 @@ + diff --git a/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java b/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java index bbabc4b9c..d4774faa5 100644 --- a/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java +++ b/src/test/java/org/ays/auth/controller/AysAuthEndToEndTest.java @@ -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; @@ -22,12 +23,14 @@ 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 { @@ -35,6 +38,9 @@ class AysAuthEndToEndTest extends AysEndToEndTest { @Autowired private AysUserSavePort userSavePort; + @Autowired + private AysUserReadPort userReadPort; + @Autowired private AysRoleReadPort roleReadPort; @@ -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))); } @@ -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( diff --git a/src/test/java/org/ays/auth/model/AysUserBuilder.java b/src/test/java/org/ays/auth/model/AysUserBuilder.java index be7e129d5..349e69aa8 100644 --- a/src/test/java/org/ays/auth/model/AysUserBuilder.java +++ b/src/test/java/org/ays/auth/model/AysUserBuilder.java @@ -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; } diff --git a/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java b/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java index d849a9153..a45ad3bb1 100644 --- a/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java +++ b/src/test/java/org/ays/auth/service/impl/AysUserPasswordServiceImplTest.java @@ -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; @@ -37,7 +36,7 @@ class AysUserPasswordServiceImplTest extends AysUnitTest { @Test - void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSendPasswordCreateEmail() { + void givenValidForgotPasswordRequest_whenUserExistWithPassword_thenSetPasswordForgotAtAndSendPasswordCreateEmail() { // Given AysForgotPasswordRequest mockForgotPasswordRequest = new AysForgotPasswordRequestBuilder() .withValidValues() @@ -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)); @@ -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)) @@ -143,7 +153,7 @@ void givenValidForgotPasswordRequest_whenEmailDoesNotExist_thenThrowAysEmailAddr @Test - void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInTwoHours_thenDoNothing() { + void givenValidId_whenPasswordExistAndForgotInTwoHours_thenDoNothing() { // Given String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; @@ -151,8 +161,7 @@ void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInTwoHours_the 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() @@ -190,7 +199,7 @@ void givenId_whenPasswordDoesExist_thenThrowUserPasswordDoesNotExistException() } @Test - void givenValidId_whenPasswordExistAndValueHasNotUUIDFormatAndUpdatedInTwoHours_thenThrowUserPasswordCannotChangedException() { + void givenValidId_whenPasswordExistAndForgotAtDoesNotExist_thenThrowUserPasswordCannotChangedException() { // Given String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; @@ -198,8 +207,8 @@ void givenValidId_whenPasswordExistAndValueHasNotUUIDFormatAndUpdatedInTwoHours_ 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() @@ -220,7 +229,7 @@ void givenValidId_whenPasswordExistAndValueHasNotUUIDFormatAndUpdatedInTwoHours_ } @Test - void givenValidId_whenPasswordExistAndValueHasUUIDFormatAndUpdatedInThreeHours_thenThrowUserPasswordCannotChangedException() { + void givenValidId_whenPasswordExistAndForgotInThreeHours_thenThrowUserPasswordCannotChangedException() { // Given String mockId = "40fb7a46-40bd-46cb-b44f-1f47162133b1"; @@ -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()