Skip to content

Commit

Permalink
AYS-328 | Create Password Flow Has Been Created (#358)
Browse files Browse the repository at this point in the history
  • Loading branch information
agitrubard authored Aug 4, 2024
1 parent c6dac35 commit b36f181
Show file tree
Hide file tree
Showing 11 changed files with 608 additions and 52 deletions.
27 changes: 24 additions & 3 deletions src/main/java/org/ays/auth/controller/AysAuthController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
import lombok.RequiredArgsConstructor;
import org.ays.auth.model.AysToken;
import org.ays.auth.model.mapper.AysTokenToResponseMapper;
import org.ays.auth.model.request.AysForgotPasswordRequest;
import org.ays.auth.model.request.AysLoginRequest;
import org.ays.auth.model.request.AysPasswordCreateRequest;
import org.ays.auth.model.request.AysPasswordForgotRequest;
import org.ays.auth.model.request.AysTokenInvalidateRequest;
import org.ays.auth.model.request.AysTokenRefreshRequest;
import org.ays.auth.model.response.AysTokenResponse;
Expand Down Expand Up @@ -44,7 +45,7 @@ class AysAuthController {
* @return An AysResponse containing an AysTokenResponse object and the HTTP status code (200 OK).
*/
@PostMapping("/token")
public AysResponse<AysTokenResponse> landingAuthenticate(@RequestBody @Valid AysLoginRequest loginRequest) {
public AysResponse<AysTokenResponse> authenticate(@RequestBody @Valid AysLoginRequest loginRequest) {
final AysToken token = authService.authenticate(loginRequest);
final AysTokenResponse tokenResponse = tokenToTokenResponseMapper.map(token);
return AysResponse.successOf(tokenResponse);
Expand Down Expand Up @@ -86,7 +87,7 @@ public AysResponse<Void> invalidateTokens(@RequestBody @Valid AysTokenInvalidate
* @return An AysResponse indicating the success of the password create request.
*/
@PostMapping("/password/forgot")
public AysResponse<Void> forgotPassword(@RequestBody @Valid AysForgotPasswordRequest forgotPasswordRequest) {
public AysResponse<Void> forgotPassword(@RequestBody @Valid AysPasswordForgotRequest forgotPasswordRequest) {
userPasswordService.forgotPassword(forgotPasswordRequest);
return AysResponse.SUCCESS;
}
Expand All @@ -108,4 +109,24 @@ public AysResponse<Void> checkPasswordChangingValidity(@PathVariable @UUID Strin
return AysResponse.SUCCESS;
}


/**
* Handles the request to create a new password for a user.
* <p>
* This endpoint processes a request to set a new password for a user identified
* by the provided password ID. It validates the request and updates the user's
* password if the request meets the required criteria.
*
* @param id The unique identifier of the user for whom the password is being created.
* @param createRequest The request body containing the new password details.
* @return A response indicating the success of the operation.
*/
@PostMapping("/password/{id}")
public AysResponse<Void> forgotPassword(@PathVariable @UUID String id,
@RequestBody @Valid AysPasswordCreateRequest createRequest) {

userPasswordService.createPassword(id, createRequest);
return AysResponse.SUCCESS;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package org.ays.auth.model.request;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.validation.constraints.AssertTrue;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;

@Getter
@Setter
public class AysPasswordCreateRequest {

@NotBlank
private String password;

@NotBlank
private String passwordRepeat;


@JsonIgnore
@AssertTrue(message = "passwords must be equal")
@SuppressWarnings("This method is unused by the application directly but Spring is using it in the background.")
private boolean isPasswordsEqual() {

if (StringUtils.isEmpty(this.password) || StringUtils.isEmpty(this.passwordRepeat)) {
return true;
}

return this.password.equals(this.passwordRepeat);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

@Getter
@Setter
public class AysForgotPasswordRequest {
public class AysPasswordForgotRequest {

@EmailAddress
@NotBlank
Expand Down
16 changes: 14 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,7 @@
package org.ays.auth.service;

import org.ays.auth.model.request.AysForgotPasswordRequest;
import org.ays.auth.model.request.AysPasswordCreateRequest;
import org.ays.auth.model.request.AysPasswordForgotRequest;
import org.ays.auth.util.exception.AysEmailAddressNotValidException;
import org.ays.auth.util.exception.AysUserPasswordCannotChangedException;
import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException;
Expand All @@ -23,7 +24,7 @@ public interface AysUserPasswordService {
* @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);
void forgotPassword(AysPasswordForgotRequest forgotPasswordRequest);

/**
* Checks the validity of changing the user's password.
Expand All @@ -38,4 +39,15 @@ public interface AysUserPasswordService {
*/
void checkPasswordChangingValidity(String passwordId);

/**
* Creates a new password for the user.
* <p>
* This method updates the user's password with the new one provided in the request.
* It validates the password change request before updating the password.
*
* @param passwordId The unique identifier of the password to be created.
* @param createRequest The request containing the new password details.
*/
void createPassword(String passwordId, AysPasswordCreateRequest createRequest);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import lombok.RequiredArgsConstructor;
import org.ays.auth.model.AysUser;
import org.ays.auth.model.request.AysForgotPasswordRequest;
import org.ays.auth.model.request.AysPasswordCreateRequest;
import org.ays.auth.model.request.AysPasswordForgotRequest;
import org.ays.auth.port.AysUserReadPort;
import org.ays.auth.port.AysUserSavePort;
import org.ays.auth.service.AysUserMailService;
Expand All @@ -11,6 +12,7 @@
import org.ays.auth.util.exception.AysUserPasswordCannotChangedException;
import org.ays.auth.util.exception.AysUserPasswordDoesNotExistException;
import org.ays.common.util.AysRandomUtil;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -33,6 +35,7 @@ class AysUserPasswordServiceImpl implements AysUserPasswordService {
private final AysUserReadPort userReadPort;
private final AysUserSavePort userSavePort;
private final AysUserMailService userMailService;
private final PasswordEncoder passwordEncoder;


/**
Expand All @@ -48,21 +51,17 @@ class AysUserPasswordServiceImpl implements AysUserPasswordService {
* @throws AysEmailAddressNotValidException if no user is found with the provided email address.
*/
@Override
public void forgotPassword(final AysForgotPasswordRequest forgotPasswordRequest) {
public void forgotPassword(final AysPasswordForgotRequest forgotPasswordRequest) {

final String emailAddress = forgotPasswordRequest.getEmailAddress();
final AysUser user = userReadPort.findByEmailAddress(emailAddress)
.orElseThrow(() -> new AysEmailAddressNotValidException(emailAddress));

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())
.forgotAt(LocalDateTime.now())
.build();
user.setPassword(password);

final AysUser savedUser = userSavePort.save(user);
userMailService.sendPasswordCreateEmail(savedUser);
Expand Down Expand Up @@ -91,11 +90,45 @@ public void checkPasswordChangingValidity(final String passwordId) {
}


/**
* Creates a new password for a user identified by the given password ID.
* <p>
* This method updates the user's password with the new password provided in the request.
* It first verifies if the password change request is valid based on the time elapsed since
* the password change request was initiated. If the request is valid, it updates the password
* and saves the changes.
*
* @param passwordId The unique identifier of the user whose password is being updated.
* @param createRequest The request object containing the new password details.
* @throws AysUserPasswordDoesNotExistException if no user is found with the provided password ID.
* @throws AysUserPasswordCannotChangedException if the password change request is invalid due to
* the elapsed time or other conditions that prevent the password from being changed.
*/
@Override
public void createPassword(final String passwordId,
final AysPasswordCreateRequest createRequest) {

final AysUser user = userReadPort.findByPasswordId(passwordId)
.orElseThrow(() -> new AysUserPasswordDoesNotExistException(passwordId));

this.checkChangingValidity(user.getPassword());

AysUser.Password password = AysUser.Password.builder()
.value(passwordEncoder.encode(createRequest.getPassword()))
.build();
user.setPassword(password);

userSavePort.save(user);
}


/**
* 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.
* This method verifies if the password change request is valid based on the time elapsed since the
* password change request was initiated or the password was created. It distinguishes between
* a newly created password and an existing one to determine if the change request is 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.
Expand All @@ -105,15 +138,38 @@ private void checkChangingValidity(final AysUser.Password password) {
Optional<LocalDateTime> forgotAt = Optional
.ofNullable(password.getForgotAt());

boolean isFirstCreation = forgotAt.isEmpty() && password.getUpdatedAt() == null;
if (isFirstCreation) {
this.checkExpiration(password.getId(), password.getCreatedAt());
return;
}

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

boolean isExpired = LocalDateTime.now().minusHours(2).isBefore(forgotAt.get());
this.checkExpiration(password.getId(), forgotAt.get());
}

/**
* Checks if the password change request has expired.
* <p>
* This method determines if the time elapsed since the provided timestamp exceeds the allowable time frame
* for changing the password. It throws an exception if the password change request is deemed expired.
*
* @param id The unique identifier of the password being validated.
* @param date The timestamp used to check if the password change request has expired.
* @throws AysUserPasswordCannotChangedException if the time elapsed since the timestamp exceeds the allowable time frame.
*/
private void checkExpiration(final String id,
final LocalDateTime date) {

boolean isExpired = LocalDateTime.now()
.minusHours(2)
.isBefore(date);
if (!isExpired) {
throw new AysUserPasswordCannotChangedException(password.getId());
throw new AysUserPasswordCannotChangedException(id);
}

}

}
Loading

0 comments on commit b36f181

Please sign in to comment.