Provide an endpoint to update user profile (#3067)

#### What type of PR is this?

/kind feature
/area core
/milestone 2.1.x

#### What this PR does / why we need it:

Provide an endpoint to update current user profile instead of whole user data.

##### Request example

```bash
curl -X 'PUT' \
  'http://localhost:8090/apis/api.console.halo.run/v1alpha1/users/-' \
  -H 'accept: */*' \
  -H 'Content-Type: */*' \
  -d '{
  "spec": {
    "displayName": "JohnNiang",
    "email": "johnniang@halo.run",
    "password": "xxx",
    "registeredAt": "2022-12-19T03:46:54.809770900Z",
    "twoFactorAuthEnabled": false,
    "disabled": false
  },
  "status": {
    "permalink": "http://localhost:8090/authors/admin"
  },
  "apiVersion": "v1alpha1",
  "kind": "User",
  "metadata": {
    "finalizers": [
      "user-protection"
    ],
    "name": "admin",
    "annotations": {
      "rbac.authorization.halo.run/role-names": "[\"super-role\"]"
    },
    "version": 3,
    "creationTimestamp": "2022-12-19T03:46:54.911951800Z"
  }
}'
```

##### Response example

```json
{
  "spec": {
    "displayName": "JohnNiang",
    "email": "johnniang@halo.run",
    "password": "{bcrypt}$2a$10$IBV8/q7Q6Fj78Ls5AG1eBO0bCQ.rM6vli5pAVexf/gqu.hNfjJxaq",
    "registeredAt": "2022-12-19T03:46:54.809770900Z",
    "twoFactorAuthEnabled": false,
    "disabled": false
  },
  "status": {
    "permalink": "http://localhost:8090/authors/admin"
  },
  "apiVersion": "v1alpha1",
  "kind": "User",
  "metadata": {
    "finalizers": [
      "user-protection"
    ],
    "name": "admin",
    "annotations": {
      "rbac.authorization.halo.run/role-names": "[\"super-role\"]"
    },
    "version": 5,
    "creationTimestamp": "2022-12-19T03:46:54.911951800Z"
  }
}
```

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/3035

#### Does this PR introduce a user-facing change?

```release-note
提供更新当前登录用户信息功能
```
pull/3068/head^2
John Niang 2022-12-29 11:16:33 +08:00 committed by GitHub
parent da55532777
commit 313605d52c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 94 additions and 1 deletions

View File

@ -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<ServerResponse> 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<ServerResponse> changePassword(ServerRequest request) {
final var nameInPath = request.pathVariable("name");
return ReactiveSecurityContextHolder.getContext()

View File

@ -31,7 +31,7 @@ rules:
- apiGroups: [ "api.console.halo.run" ]
resources: [ "users" ]
resourceNames: [ "-" ]
verbs: [ "list", "get" ]
verbs: [ "get", "update" ]
---
apiVersion: v1alpha1
kind: "Role"

View File

@ -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 {