From fd634ff6534b06c9b29ccdec95ebabf0fc6b8e76 Mon Sep 17 00:00:00 2001 From: Tidiane Date: Tue, 7 Jul 2026 09:22:12 +0000 Subject: [PATCH 1/3] Cloning Project + Changing the db port --- apigateway/src/main/resources/application.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apigateway/src/main/resources/application.yaml b/apigateway/src/main/resources/application.yaml index 2afdad9..08500d5 100644 --- a/apigateway/src/main/resources/application.yaml +++ b/apigateway/src/main/resources/application.yaml @@ -2,12 +2,12 @@ spring: application: name: apigateway r2dbc: - url: r2dbc:postgresql://localhost:5432/auth_gateway_db + url: r2dbc:postgresql://localhost:5433/auth_gateway_db username: postgres password: password sql: init: - mode: never # Use 'always' only in dev/test profiles; handle prod schema via migrations + mode: always # Use 'always' only in dev/test profiles; handle prod schema via migrations platform: postgresql jwt: From a837c88120ac2362780500c9a23d137db6e894fd Mon Sep 17 00:00:00 2001 From: Tidiane Date: Tue, 7 Jul 2026 11:39:53 +0000 Subject: [PATCH 2/3] feat: implement /auth/change-password endpoint (#32) --- apigateway/pom.xml | 2 +- .../controller/GatewayAuthController.java | 5 +- .../dto/request/ChangePasswordRequest.java | 13 ++ .../apigateway/service/auth/AuthService.java | 23 +++ ...tewayAuthControllerChangePasswordTest.java | 135 ++++++++++++++++++ 5 files changed, 175 insertions(+), 3 deletions(-) create mode 100644 apigateway/src/main/java/vaultweb/apigateway/dto/request/ChangePasswordRequest.java create mode 100644 apigateway/src/test/java/vaultweb/apigateway/controller/GatewayAuthControllerChangePasswordTest.java diff --git a/apigateway/pom.xml b/apigateway/pom.xml index d3f4818..80ed445 100644 --- a/apigateway/pom.xml +++ b/apigateway/pom.xml @@ -155,7 +155,7 @@ - check + apply verify diff --git a/apigateway/src/main/java/vaultweb/apigateway/controller/GatewayAuthController.java b/apigateway/src/main/java/vaultweb/apigateway/controller/GatewayAuthController.java index 5f9240d..edb8af4 100644 --- a/apigateway/src/main/java/vaultweb/apigateway/controller/GatewayAuthController.java +++ b/apigateway/src/main/java/vaultweb/apigateway/controller/GatewayAuthController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; +import vaultweb.apigateway.dto.request.ChangePasswordRequest; import vaultweb.apigateway.dto.request.LoginRequest; import vaultweb.apigateway.dto.request.UserRegistrationRequest; import vaultweb.apigateway.dto.response.AuthResponse; @@ -70,8 +71,8 @@ public String changeEmail() { } @PostMapping("change-password") - public String changePassword() { - return "changePassword"; + public Mono changePassword(@Valid @RequestBody ChangePasswordRequest request) { + return authService.changePassword(request); } @PostMapping("/reset-password") diff --git a/apigateway/src/main/java/vaultweb/apigateway/dto/request/ChangePasswordRequest.java b/apigateway/src/main/java/vaultweb/apigateway/dto/request/ChangePasswordRequest.java new file mode 100644 index 0000000..8768c4e --- /dev/null +++ b/apigateway/src/main/java/vaultweb/apigateway/dto/request/ChangePasswordRequest.java @@ -0,0 +1,13 @@ +package vaultweb.apigateway.dto.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; + +public record ChangePasswordRequest( + @NotEmpty(message = "Your old password logic that it cannot be empty") String oldPassword, + @NotEmpty(message = "Your new password logic that it cannot be empty") + @Pattern( + regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$", + message = + "password must be at least 8 characters long and include at least one uppercase letter, one lowercase letter, one digit, and one special character") + String newPassword) {} diff --git a/apigateway/src/main/java/vaultweb/apigateway/service/auth/AuthService.java b/apigateway/src/main/java/vaultweb/apigateway/service/auth/AuthService.java index aa638d7..ce3aef0 100644 --- a/apigateway/src/main/java/vaultweb/apigateway/service/auth/AuthService.java +++ b/apigateway/src/main/java/vaultweb/apigateway/service/auth/AuthService.java @@ -2,6 +2,7 @@ import org.springframework.stereotype.Service; +import vaultweb.apigateway.dto.request.ChangePasswordRequest; import vaultweb.apigateway.dto.request.LoginRequest; import vaultweb.apigateway.dto.request.UserRegistrationRequest; import vaultweb.apigateway.dto.response.AuthResponse; @@ -105,6 +106,28 @@ public Mono login(LoginRequest request) { }); } + public Mono changePassword(ChangePasswordRequest request) { + return securityContextUtil + .getAuthenticatedUsername() + .flatMap(username -> userRepository.findByUsername(username)) + .switchIfEmpty( + Mono.error( + new DefaultException( + "username from token has no registered user", + DefaultExceptionLevels.AUTHENTICATION_EXCEPTION))) + .flatMap( + user -> { + if (!BcryptUtil.matches(request.oldPassword(), user.getPassword())) { + return Mono.error( + new DefaultException( + "Invalid current password", + DefaultExceptionLevels.AUTHENTICATION_EXCEPTION)); + } + user.setPassword(BcryptUtil.encode(request.newPassword())); + return userRepository.save(user).then(); + }); + } + public Mono switchToken(String token) { // find refresh return refreshTokenRepository diff --git a/apigateway/src/test/java/vaultweb/apigateway/controller/GatewayAuthControllerChangePasswordTest.java b/apigateway/src/test/java/vaultweb/apigateway/controller/GatewayAuthControllerChangePasswordTest.java new file mode 100644 index 0000000..9e8eabc --- /dev/null +++ b/apigateway/src/test/java/vaultweb/apigateway/controller/GatewayAuthControllerChangePasswordTest.java @@ -0,0 +1,135 @@ +package vaultweb.apigateway.controller; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.reactive.server.WebTestClient; + +/** + * Integration tests for the protected {@code /auth/change-password} endpoint exposed by {@link + * GatewayAuthController}. Each test registers and logs in a fresh user to obtain a valid access + * token before exercising the endpoint, so the full controller -> service -> repository path is + * covered end to end. + */ +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@AutoConfigureWebTestClient +@ActiveProfiles("test") +class GatewayAuthControllerChangePasswordTest { + + private static final String VALID_PASSWORD = "Test@1234"; + private static final String NEW_VALID_PASSWORD = "NewTest@5678"; + + @Autowired private WebTestClient webTestClient; + + private void register(String name, String username, String email, String password) { + webTestClient + .post() + .uri("/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("name", name, "username", username, "email", email, "password", password)) + .exchange() + .expectStatus() + .isCreated(); + } + + private String loginAndGetAccessToken(String emailUsername, String password) { + byte[] responseBody = + webTestClient + .post() + .uri("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("emailUsername", emailUsername, "password", password)) + .exchange() + .expectStatus() + .isOk() + .expectBody() + .returnResult() + .getResponseBody(); + + return readJsonField(responseBody, "accessToken"); + } + + private WebTestClient.ResponseSpec changePassword( + String accessToken, String oldPassword, String newPassword) { + WebTestClient.RequestBodySpec request = + webTestClient.post().uri("/auth/change-password").contentType(MediaType.APPLICATION_JSON); + + if (accessToken != null) { + request = request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken); + } + + return request + .bodyValue(Map.of("oldPassword", oldPassword, "newPassword", newPassword)) + .exchange(); + } + + @Test + void changePassword_withValidCredentials_returnsOk() { + register("Karl", "karl", "karl@example.com", VALID_PASSWORD); + String accessToken = loginAndGetAccessToken("karl@example.com", VALID_PASSWORD); + + changePassword(accessToken, VALID_PASSWORD, NEW_VALID_PASSWORD).expectStatus().isOk(); + } + + @Test + void changePassword_withoutToken_isUnauthorized() { + changePassword(null, VALID_PASSWORD, NEW_VALID_PASSWORD).expectStatus().isUnauthorized(); + } + + @Test + void changePassword_withWrongOldPassword_isUnauthorized() { + register("Liam", "liam", "liam@example.com", VALID_PASSWORD); + String accessToken = loginAndGetAccessToken("liam@example.com", VALID_PASSWORD); + + changePassword(accessToken, "Wrong@1234", NEW_VALID_PASSWORD).expectStatus().isUnauthorized(); + } + + @Test + void changePassword_withWeakNewPassword_isRejected() { + register("Mona", "mona", "mona@example.com", VALID_PASSWORD); + String accessToken = loginAndGetAccessToken("mona@example.com", VALID_PASSWORD); + + changePassword(accessToken, VALID_PASSWORD, "weak").expectStatus().isBadRequest(); + } + + @Test + void changePassword_persistsNewPassword_oldPasswordNoLongerWorks() { + register("Nina", "nina", "nina@example.com", VALID_PASSWORD); + String accessToken = loginAndGetAccessToken("nina@example.com", VALID_PASSWORD); + + changePassword(accessToken, VALID_PASSWORD, NEW_VALID_PASSWORD).expectStatus().isOk(); + + webTestClient + .post() + .uri("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("emailUsername", "nina@example.com", "password", NEW_VALID_PASSWORD)) + .exchange() + .expectStatus() + .isOk(); + + webTestClient + .post() + .uri("/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("emailUsername", "nina@example.com", "password", VALID_PASSWORD)) + .exchange() + .expectStatus() + .isUnauthorized(); + } + + private static String readJsonField(byte[] json, String field) { + try { + return new com.fasterxml.jackson.databind.ObjectMapper().readTree(json).get(field).asText(); + } catch (Exception e) { + throw new IllegalStateException("Could not read field '" + field + "' from response", e); + } + } +} From 97e6cec5d61e0076dc30cc69b279b013536b0ae3 Mon Sep 17 00:00:00 2001 From: Tidiane Date: Thu, 9 Jul 2026 10:09:08 +0000 Subject: [PATCH 3/3] fix: revert unrelated build/config changes flagged in review - Restore goal to 'check' - Restore default DB port (5432) and sql.init.mode (never) --- apigateway/pom.xml | 2 +- apigateway/src/main/resources/application.yaml | 4 ++-- docker-compose.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apigateway/pom.xml b/apigateway/pom.xml index 80ed445..d3f4818 100644 --- a/apigateway/pom.xml +++ b/apigateway/pom.xml @@ -155,7 +155,7 @@ - apply + check verify diff --git a/apigateway/src/main/resources/application.yaml b/apigateway/src/main/resources/application.yaml index 08500d5..2afdad9 100644 --- a/apigateway/src/main/resources/application.yaml +++ b/apigateway/src/main/resources/application.yaml @@ -2,12 +2,12 @@ spring: application: name: apigateway r2dbc: - url: r2dbc:postgresql://localhost:5433/auth_gateway_db + url: r2dbc:postgresql://localhost:5432/auth_gateway_db username: postgres password: password sql: init: - mode: always # Use 'always' only in dev/test profiles; handle prod schema via migrations + mode: never # Use 'always' only in dev/test profiles; handle prod schema via migrations platform: postgresql jwt: diff --git a/docker-compose.yml b/docker-compose.yml index 9306509..4b6da8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: POSTGRES_PASSWORD: password POSTGRES_DB: auth_gateway_db ports: - - "5432:5432" + - "5433:5432" volumes: - postgres_data:/var/lib/postgresql/data