Skip to content

Commit

Permalink
AYS-329 | Check Password Changing Validity Flow Has Been Created (#348)
Browse files Browse the repository at this point in the history
  • Loading branch information
agitrubard authored Jul 26, 2024
1 parent 347900a commit fadfd4f
Show file tree
Hide file tree
Showing 16 changed files with 497 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ SecurityFilterChain filterChain(HttpSecurity httpSecurity,
.authorizeHttpRequests(customizer -> customizer
.requestMatchers(HttpMethod.GET, "/public/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/authentication/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/authentication/password/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/v1/admin-registration-application/*/summary").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/admin-registration-application/*/complete").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/emergency-evacuation-application").permitAll()
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/ays/auth/controller/AysAuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
import org.ays.auth.service.AysAuthService;
import org.ays.auth.service.AysUserPasswordService;
import org.ays.common.model.response.AysResponse;
import org.hibernate.validator.constraints.UUID;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -20,6 +24,7 @@
/**
* Auth controller to perform authentication api operations.
*/
@Validated
@RestController
@RequestMapping("/api/v1/authentication")
@RequiredArgsConstructor
Expand Down Expand Up @@ -86,4 +91,21 @@ public AysResponse<Void> forgotPassword(@RequestBody @Valid AysForgotPasswordReq
return AysResponse.SUCCESS;
}


/**
* Endpoint for checking the existence and validity of a password reset token.
* <p>
* This endpoint handles the verification of a password reset token by its ID.
* It delegates the check to the user password service.
* </p>
*
* @param id The ID of the password reset token to be checked.
* @return AysResponse with a success message and no data.
*/
@GetMapping("/password/{id}/validity")
public AysResponse<Void> checkPasswordChangingValidity(@PathVariable @UUID String id) {
userPasswordService.checkPasswordChangingValidity(id);
return AysResponse.SUCCESS;
}

}
18 changes: 13 additions & 5 deletions src/main/java/org/ays/auth/port/AysUserReadPort.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,15 @@
*/
public interface AysUserReadPort {

/**
* Finds all users with pagination and optional filtering.
*
* @param aysPageable the pagination configuration
* @param filter the filter for users
* @return a paginated list of users
*/
AysPage<AysUser> findAll(AysPageable aysPageable, AysUserFilter filter);

/**
* Retrieves a {@link AysUser} by its ID.
*
Expand All @@ -38,13 +47,12 @@ public interface AysUserReadPort {
Optional<AysUser> findByPhoneNumber(AysPhoneNumber phoneNumber);

/**
* Finds all users with pagination and optional filtering.
* Finds a user by their password ID.
*
* @param aysPageable the pagination configuration
* @param filter the filter for users
* @return a paginated list of users
* @param passwordId the ID of the password to search for.
* @return an Optional containing the found user, or empty if no user was found with the given password ID.
*/
AysPage<AysUser> findAll(AysPageable aysPageable, AysUserFilter filter);
Optional<AysUser> findByPasswordId(String passwordId);

/**
* Checks if a user with the given email address exists in the repository.
Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/ays/auth/port/impl/AysUserAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,19 @@ public Optional<AysUser> findByPhoneNumber(AysPhoneNumber phoneNumber) {
}


/**
* Finds a user by their password ID.
*
* @param passwordId the ID of the password to search for.
* @return an Optional containing the found user, or empty if no user was found with the given password ID.
*/
@Override
public Optional<AysUser> findByPasswordId(final String passwordId) {
Optional<AysUserEntity> userEntity = userRepository.findByPasswordId(passwordId);
return userEntity.map(userEntityToDomainMapper::map);
}


/**
* Checks if a user with the given phone number exists in the repository.
*
Expand Down
8 changes: 8 additions & 0 deletions src/main/java/org/ays/auth/repository/AysUserRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ public interface AysUserRepository extends JpaRepository<AysUserEntity, String>,
*/
Optional<AysUserEntity> findByCountryCodeAndLineNumber(String countryCode, String lineNumber);

/**
* Finds a user entity by the password ID.
*
* @param passwordId the ID of the password to search for.
* @return an Optional containing the found user entity, or empty if no user entity was found with the given password ID.
*/
Optional<AysUserEntity> findByPasswordId(String passwordId);

/**
* Checks if an {@link AysUserEntity} exists with the given email.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,11 @@ public interface AysUserPasswordService {
*/
void forgotPassword(AysForgotPasswordRequest forgotPasswordRequest);

/**
* Checks the existence and validity of a password reset token by its ID.
*
* @param passwordId the ID of the password reset token to be checked.
*/
void checkPasswordChangingValidity(String passwordId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,16 @@
import org.ays.auth.service.AysUserMailService;
import org.ays.auth.service.AysUserPasswordService;
import org.ays.auth.util.exception.AysEmailAddressNotValidException;
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;

import java.time.LocalDateTime;
import java.util.Optional;

/**
* Service implementation for managing user password operations.
* <p>
Expand Down Expand Up @@ -62,4 +68,36 @@ public void forgotPassword(final AysForgotPasswordRequest forgotPasswordRequest)
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.
*
* @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.
*/
@Override
public void checkPasswordChangingValidity(final String passwordId) {

final AysUser.Password password = userReadPort.findByPasswordId(passwordId)
.orElseThrow(() -> new AysUserPasswordDoesNotExistException(passwordId))
.getPassword();

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

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

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.ays.auth.util.exception;

import org.ays.common.util.exception.AysAuthException;

import java.io.Serial;

/**
* Exception thrown when a user password cannot be changed.
*/
public final class AysUserPasswordCannotChangedException extends AysAuthException {

/**
* Unique serial version ID.
*/
@Serial
private static final long serialVersionUID = -2214328005741759939L;

/**
* Constructs a new {@link AysUserPasswordCannotChangedException} with the given password ID.
*
* @param passwordId the ID of the password that cannot be changed
*/
public AysUserPasswordCannotChangedException(String passwordId) {
super("user password cannot be changed! passwordId:" + passwordId);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.ays.auth.util.exception;

import org.ays.common.util.exception.AysAuthException;

import java.io.Serial;

/**
* Exception thrown when a user password does not exist.
*/
public final class AysUserPasswordDoesNotExistException extends AysAuthException {

/**
* Unique serial version ID.
*/
@Serial
private static final long serialVersionUID = -9023497278913148659L;

/**
* Constructs a new {@link AysUserPasswordDoesNotExistException} with the given password ID.
*
* @param passwordId the ID of the password that does not exist
*/
public AysUserPasswordDoesNotExistException(String passwordId) {
super("user password does not exist! passwordId:" + passwordId);
}

}
26 changes: 26 additions & 0 deletions src/main/java/org/ays/common/util/AysUUID.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.ays.common.util;

import lombok.experimental.UtilityClass;

import java.util.UUID;

@UtilityClass
public class AysUUID {

/**
* Checks if the given string is a valid UUID (Universally Unique Identifier).
* The method attempts to create a UUID object from the given string and returns true if successful, false otherwise.
*
* @param value the string to check
* @return true if the string is a valid UUID, false otherwise
*/
public static boolean isValid(String value) {
try {
UUID.fromString(value);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}

}
54 changes: 54 additions & 0 deletions src/test/java/org/ays/auth/controller/AysAuthControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,58 @@ void givenForgotPasswordRequestWithoutEmailAddress_whenEmailIsNull_thenReturnVal
.forgotPassword(Mockito.any(AysForgotPasswordRequest.class));
}


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

// When
Mockito.doNothing()
.when(userPasswordService)
.checkPasswordChangingValidity(Mockito.anyString());

// Then
String endpoint = BASE_PATH.concat("/password/").concat(mockId).concat("/validity");
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint);

AysResponse<Void> mockResponse = AysResponseBuilder.SUCCESS;

aysMockMvc.perform(mockHttpServletRequestBuilder, mockResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isOk())
.andExpect(AysMockResultMatchersBuilders.response()
.doesNotExist());

// Verify
Mockito.verify(userPasswordService, Mockito.times(1))
.checkPasswordChangingValidity(Mockito.anyString());
}

@ParameterizedTest
@ValueSource(strings = {
"A",
"493268349068342"
})
void givenId_whenIdDoesNotValid_thenReturnValidationError(String invalidId) throws Exception {

// Then
String endpoint = BASE_PATH.concat("/password/").concat(invalidId).concat("/validity");
MockHttpServletRequestBuilder mockHttpServletRequestBuilder = AysMockMvcRequestBuilders
.get(endpoint);

AysErrorResponse mockErrorResponse = AysErrorBuilder.VALIDATION_ERROR;

aysMockMvc.perform(mockHttpServletRequestBuilder, mockErrorResponse)
.andExpect(AysMockResultMatchersBuilders.status()
.isBadRequest())
.andExpect(AysMockResultMatchersBuilders.subErrors()
.isNotEmpty());

// Verify
Mockito.verify(userPasswordService, Mockito.never())
.checkPasswordChangingValidity(Mockito.anyString());
}

}
Loading

0 comments on commit fadfd4f

Please sign in to comment.