diff --git a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java index 108d6434c..35a19b649 100644 --- a/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java +++ b/src/main/java/run/halo/app/core/extension/endpoint/UserEndpoint.java @@ -16,7 +16,9 @@ import java.util.stream.Collectors; import org.springdoc.webflux.core.fn.SpringdocRouteBuilder; import org.springframework.http.MediaType; import org.springframework.lang.NonNull; +import org.springframework.security.core.Authentication; import org.springframework.security.core.context.ReactiveSecurityContextHolder; +import org.springframework.security.core.context.SecurityContext; import org.springframework.stereotype.Component; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.ServerRequest; @@ -52,6 +54,12 @@ public class UserEndpoint implements CustomEndpoint { .description("Get current user detail") .tag(tag) .response(responseBuilder().implementation(User.class))) + .PUT("/users/-", this::updateProfile, + builder -> builder.operationId("UpdateCurrentUser") + .description("Update current user profile, but password.") + .tag(tag) + .requestBody(requestBodyBuilder().required(true).implementation(User.class)) + .response(responseBuilder().implementation(User.class))) .POST("/users/{name}/permissions", this::grantPermission, builder -> builder.operationId("GrantPermission") .description("Grant permissions to user") @@ -89,6 +97,32 @@ public class UserEndpoint implements CustomEndpoint { .build(); } + private Mono updateProfile(ServerRequest request) { + return ReactiveSecurityContextHolder.getContext() + .map(SecurityContext::getAuthentication) + .map(Authentication::getName) + .flatMap(currentUserName -> client.get(User.class, currentUserName)) + .flatMap(currentUser -> request.bodyToMono(User.class) + .filter(user -> + Objects.equals(user.getMetadata().getName(), + currentUser.getMetadata().getName())) + .switchIfEmpty( + Mono.error(() -> new ServerWebInputException("Username didn't match."))) + .map(user -> { + var spec = currentUser.getSpec(); + var newSpec = user.getSpec(); + spec.setAvatar(newSpec.getAvatar()); + spec.setBio(newSpec.getBio()); + spec.setDisplayName(newSpec.getDisplayName()); + spec.setTwoFactorAuthEnabled(newSpec.getTwoFactorAuthEnabled()); + spec.setEmail(newSpec.getEmail()); + spec.setPhone(newSpec.getPhone()); + return currentUser; + })) + .flatMap(client::update) + .flatMap(updatedUser -> ServerResponse.ok().bodyValue(updatedUser)); + } + Mono changePassword(ServerRequest request) { final var nameInPath = request.pathVariable("name"); return ReactiveSecurityContextHolder.getContext() diff --git a/src/main/resources/extensions/role-template-authenticated.yaml b/src/main/resources/extensions/role-template-authenticated.yaml index 822fc4d01..86dec94ae 100644 --- a/src/main/resources/extensions/role-template-authenticated.yaml +++ b/src/main/resources/extensions/role-template-authenticated.yaml @@ -31,7 +31,7 @@ rules: - apiGroups: [ "api.console.halo.run" ] resources: [ "users" ] resourceNames: [ "-" ] - verbs: [ "list", "get" ] + verbs: [ "get", "update" ] --- apiVersion: v1alpha1 kind: "Role" diff --git a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java index a00f6904c..33b407462 100644 --- a/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java +++ b/src/test/java/run/halo/app/core/extension/endpoint/UserEndpointTest.java @@ -98,6 +98,65 @@ class UserEndpointTest { } } + @Nested + @DisplayName("UpdateProfile") + class UpdateProfileTest { + + @Test + void shouldUpdateProfileCorrectly() { + var currentUser = createUser("fake-user"); + var updatedUser = createUser("fake-user"); + var requestUser = createUser("fake-user"); + + when(client.get(User.class, "fake-user")).thenReturn(Mono.just(currentUser)); + when(client.update(currentUser)).thenReturn(Mono.just(updatedUser)); + + webClient.put().uri("/apis/api.console.halo.run/v1alpha1/users/-") + .bodyValue(requestUser) + .exchange() + .expectStatus().isOk() + .expectBody(User.class) + .isEqualTo(updatedUser); + + verify(client).get(User.class, "fake-user"); + verify(client).update(currentUser); + } + + @Test + void shouldGetErrorIfUsernameMismatch() { + var currentUser = createUser("fake-user"); + var updatedUser = createUser("fake-user"); + var requestUser = createUser("another-fake-user"); + + when(client.get(User.class, "fake-user")).thenReturn(Mono.just(currentUser)); + when(client.update(currentUser)).thenReturn(Mono.just(updatedUser)); + + webClient.put().uri("/apis/api.console.halo.run/v1alpha1/users/-") + .bodyValue(requestUser) + .exchange() + .expectStatus().isBadRequest(); + + verify(client).get(User.class, "fake-user"); + verify(client, never()).update(currentUser); + } + + User createUser(String name) { + var spec = new User.UserSpec(); + spec.setEmail("hi@halo.run"); + spec.setBio("Fake bio"); + spec.setDisplayName("Faker"); + spec.setPassword("fake-password"); + + var metadata = new Metadata(); + metadata.setName(name); + + var user = new User(); + user.setSpec(spec); + user.setMetadata(metadata); + return user; + } + } + @Nested @DisplayName("ChangePassword") class ChangePasswordTest {