Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add update password API endpoint and functionality #165

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions src/main/java/com/digitalsanctuary/spring/user/api/UserAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.digitalsanctuary.spring.user.audit.AuditEvent;
import com.digitalsanctuary.spring.user.dto.PasswordDto;
import com.digitalsanctuary.spring.user.dto.UserDto;
import com.digitalsanctuary.spring.user.event.OnRegistrationCompleteEvent;
import com.digitalsanctuary.spring.user.exceptions.InvalidOldPasswordException;
import com.digitalsanctuary.spring.user.exceptions.UserAlreadyExistException;
import com.digitalsanctuary.spring.user.persistence.model.User;
import com.digitalsanctuary.spring.user.service.DSUserDetails;
Expand Down Expand Up @@ -146,6 +148,40 @@ public ResponseEntity<JSONResponse> resetPassword(@Valid @RequestBody UserDto us
return buildSuccessResponse("If account exists, password reset email has been sent!", forgotPasswordPendingURI);
}

/**
* Updates the user's password. This is used when the user is logged in and wants to change their password.
*
* @param userDetails the authenticated user details
* @param passwordDto the password data transfer object containing the old and new passwords
* @param request the HTTP servlet request
* @param locale the locale
* @return a ResponseEntity containing a JSONResponse with the password update result
*/
@PostMapping("/updatePassword")
public ResponseEntity<JSONResponse> updatePassword(@AuthenticationPrincipal DSUserDetails userDetails,
@Valid @RequestBody PasswordDto passwordDto, HttpServletRequest request, Locale locale) {
validateAuthenticatedUser(userDetails);
User user = userDetails.getUser();

try {
if (!userService.checkIfValidOldPassword(user, passwordDto.getOldPassword())) {
throw new InvalidOldPasswordException("Invalid old password");
}

userService.changeUserPassword(user, passwordDto.getNewPassword());
logAuditEvent("PasswordUpdate", "Success", "User password updated", user, request);

return buildSuccessResponse(messages.getMessage("message.update-password.success", null, locale), null);
} catch (InvalidOldPasswordException ex) {
logAuditEvent("PasswordUpdate", "Failure", "Invalid old password", user, request);
return buildErrorResponse(messages.getMessage("message.update-password.invalid-old", null, locale), 1, HttpStatus.BAD_REQUEST);
} catch (Exception ex) {
log.error("Unexpected error during password update.", ex);
logAuditEvent("PasswordUpdate", "Failure", ex.getMessage(), user, request);
return buildErrorResponse("System Error!", 5, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

/**
* Deletes the user's account. This is used when the user wants to delete their account. This will either delete the account or disable it based
* on the configuration of the actuallyDeleteAccount property. After the account is disabled or deleted, the user will be logged out.
Expand Down
2 changes: 2 additions & 0 deletions src/main/resources/messages/dsspringusermessages.properties
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ email.signature=Best regards, <br /><em>The DigitalSanctuary Team</em>

# Messages
message.update-user.success=Your profile has been successfully updated.
message.update-password.success=Your password has been successfully updated.
message.update-password.invalid-old=The old password is incorrect.

message.account.verified=Your account has been successfully verified.
message.logout.success=You logged out successfully
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@
import com.digitalsanctuary.spring.user.api.data.Response;
import com.digitalsanctuary.spring.user.api.helper.AssertionsHelper;
import com.digitalsanctuary.spring.user.api.provider.ApiTestRegistrationArgumentsProvider;
import com.digitalsanctuary.spring.user.api.provider.ApiTestUpdatePasswordArgumentsProvider;
import com.digitalsanctuary.spring.user.api.provider.holder.ApiTestArgumentsHolder;
import com.digitalsanctuary.spring.user.dto.PasswordDto;
import com.digitalsanctuary.spring.user.dto.UserDto;
import com.digitalsanctuary.spring.user.jdbc.Jdbc;
import com.digitalsanctuary.spring.user.persistence.model.User;
Expand Down Expand Up @@ -75,6 +77,37 @@ public void resetPassword() throws Exception {
AssertionsHelper.compareResponses(actual, excepted);
}

/**
* Tests the update password functionality with valid and invalid password combinations.
*
* @param argumentsHolder Contains test data for password updates (valid/invalid scenarios)
* @throws Exception if any error occurs during test execution
*/
@ParameterizedTest
@ArgumentsSource(ApiTestUpdatePasswordArgumentsProvider.class)
@Order(3)
public void updatePassword(ApiTestArgumentsHolder argumentsHolder) throws Exception {
// Register and login test user first
login(baseTestUser);

PasswordDto passwordDto = argumentsHolder.getPasswordDto();

ResultActions action = perform(MockMvcRequestBuilders.post(URL + "/updatePassword")
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
.content(buildUrlEncodedFormEntity(passwordDto)));

if (argumentsHolder.getStatus() == DataStatus.VALID) {
action.andExpect(status().isOk());
}
if (argumentsHolder.getStatus() == DataStatus.INVALID) {
action.andExpect(status().isBadRequest());
}

MockHttpServletResponse actual = action.andReturn().getResponse();
Response expected = argumentsHolder.getResponse();
AssertionsHelper.compareResponses(actual, expected);
}



protected void login(UserDto userDto) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,13 +73,13 @@ public static Response userUpdateSuccess() {
}
public static Response passwordUpdateSuccess() {
return new Response(true, 0, null,
new String[]{"Password updated successfully"}, null
new String[]{"Your password has been successfully updated."}, null
);
}

public static Response passwordUpdateFailry() {
return new Response(false, 1, null,
new String[]{"Invalid Old Password"}, null
new String[]{"The old password is incorrect."}, null
);
}
public static Response successDeleteAccount() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,5 +101,24 @@ void checkIfValidOldPassword_returnTrueIfValid() {
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(true);
Assertions.assertTrue(userService.checkIfValidOldPassword(testUser, testUser.getPassword()));
}

@Test
void checkIfValidOldPassword_returnFalseIfInvalid() {
when(passwordEncoder.matches(anyString(), anyString())).thenReturn(false);
Assertions.assertFalse(userService.checkIfValidOldPassword(testUser, "wrongPassword"));
}

@Test
void changeUserPassword_encodesAndSavesNewPassword() {
String newPassword = "newTestPassword";
String encodedPassword = "encodedNewPassword";

when(passwordEncoder.encode(newPassword)).thenReturn(encodedPassword);
when(userRepository.save(any(User.class))).thenReturn(testUser);

userService.changeUserPassword(testUser, newPassword);

Assertions.assertEquals(encodedPassword, testUser.getPassword());
}

}