Skip to content

Commit 7f51af8

Browse files
committed
feat(portal-rest): wire AcceptUserInvitationUseCase into UsersResource.finalizeRegistration
https://gravitee.atlassian.net/browse/APIM-14130
1 parent 4ec4e4e commit 7f51af8

7 files changed

Lines changed: 285 additions & 70 deletions

File tree

gravitee-apim-rest-api/gravitee-apim-rest-api-management/gravitee-apim-rest-api-management-rest/src/main/java/io/gravitee/rest/api/management/rest/resource/organization/UsersRegistrationResource.java

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -17,21 +17,13 @@
1717

1818
import static io.gravitee.common.http.MediaType.APPLICATION_JSON;
1919

20-
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase;
21-
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase.GroupInvitationAction;
22-
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase.UserRegistrationAction;
23-
import io.gravitee.apim.core.user.model.RawPassword;
24-
import io.gravitee.apim.core.user.service_provider.TokenService;
25-
import io.gravitee.apim.infra.adapter.UserAdapter;
2620
import io.gravitee.common.http.MediaType;
2721
import io.gravitee.rest.api.management.rest.resource.AbstractResource;
2822
import io.gravitee.rest.api.model.NewExternalUserEntity;
2923
import io.gravitee.rest.api.model.RegisterUserEntity;
3024
import io.gravitee.rest.api.model.UserEntity;
3125
import io.gravitee.rest.api.service.UserService;
3226
import io.gravitee.rest.api.service.common.GraviteeContext;
33-
import io.gravitee.rest.api.service.common.JWTHelper;
34-
import io.gravitee.rest.api.service.exceptions.UserStateConflictException;
3527
import io.swagger.v3.oas.annotations.Operation;
3628
import io.swagger.v3.oas.annotations.media.Content;
3729
import io.swagger.v3.oas.annotations.media.Schema;
@@ -46,7 +38,6 @@
4638
import jakarta.ws.rs.container.ResourceContext;
4739
import jakarta.ws.rs.core.Context;
4840
import jakarta.ws.rs.core.Response;
49-
import java.util.Optional;
5041

5142
/**
5243
* Defines the REST resources to manage users registration.
@@ -65,12 +56,6 @@ public class UsersRegistrationResource extends AbstractResource {
6556
@Inject
6657
private UserService userService;
6758

68-
@Inject
69-
private TokenService tokenService;
70-
71-
@Inject
72-
private AcceptUserInvitationUseCase acceptUserInvitationUseCase;
73-
7459
/**
7560
* Register a new user.
7661
* Generate a token and send it in an email to allow a user to create an account.
@@ -106,27 +91,11 @@ public Response registerUser(@Valid NewExternalUserEntity newExternalUserEntity)
10691
)
10792
@ApiResponse(responseCode = "500", description = "Internal server error")
10893
public Response finalizeUserRegistration(@Valid RegisterUserEntity registerUserEntity) {
109-
var decoded = tokenService.decode(registerUserEntity.getToken());
110-
111-
if (JWTHelper.ACTION.RESET_PASSWORD.name().equals(decoded.action())) {
112-
throw new UserStateConflictException("Reset password forbidden on this resource");
94+
UserEntity newUser = userService.finalizeRegistration(GraviteeContext.getExecutionContext(), registerUserEntity);
95+
if (newUser != null) {
96+
return Response.ok().entity(newUser).build();
11397
}
11498

115-
var executionContext = GraviteeContext.getExecutionContext();
116-
var action = JWTHelper.ACTION.GROUP_INVITATION.name().equals(decoded.action())
117-
? new GroupInvitationAction(decoded.email(), decoded.subject())
118-
: new UserRegistrationAction(decoded.email(), decoded.subject());
119-
120-
var password = Optional.ofNullable(registerUserEntity.getPassword()).map(RawPassword::new);
121-
var input = new AcceptUserInvitationUseCase.Input(
122-
executionContext,
123-
action,
124-
password,
125-
Optional.ofNullable(registerUserEntity.getFirstname()),
126-
Optional.ofNullable(registerUserEntity.getLastname())
127-
);
128-
129-
var output = acceptUserInvitationUseCase.execute(input);
130-
return Response.ok().entity(UserAdapter.INSTANCE.toUserEntity(output.user())).build();
99+
return Response.serverError().build();
131100
}
132101
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Copyright © 2015 The Gravitee team (http://gravitee.io)
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.gravitee.rest.api.portal.rest.mapper;
17+
18+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase;
19+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase.GroupInvitationAction;
20+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase.UserRegistrationAction;
21+
import io.gravitee.apim.core.user.model.DecodedToken;
22+
import io.gravitee.apim.core.user.model.RawPassword;
23+
import io.gravitee.rest.api.portal.rest.model.FinalizeRegistrationInput;
24+
import io.gravitee.rest.api.service.common.ExecutionContext;
25+
import io.gravitee.rest.api.service.common.JWTHelper;
26+
import java.util.Optional;
27+
import org.springframework.stereotype.Component;
28+
29+
@Component
30+
public class FinalizeRegistrationMapper {
31+
32+
public AcceptUserInvitationUseCase.Input toUseCaseInput(
33+
ExecutionContext executionContext,
34+
DecodedToken decoded,
35+
FinalizeRegistrationInput input
36+
) {
37+
var action = JWTHelper.ACTION.GROUP_INVITATION.name().equals(decoded.action())
38+
? new GroupInvitationAction(decoded.email(), decoded.subject())
39+
: new UserRegistrationAction(decoded.email(), decoded.subject());
40+
41+
return new AcceptUserInvitationUseCase.Input(
42+
executionContext,
43+
action,
44+
Optional.ofNullable(input.getPassword()).map(RawPassword::new),
45+
Optional.ofNullable(input.getFirstname()),
46+
Optional.ofNullable(input.getLastname())
47+
);
48+
}
49+
}

gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/main/java/io/gravitee/rest/api/portal/rest/resource/UsersResource.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@
1818
import static io.gravitee.rest.api.model.permissions.RolePermissionAction.READ;
1919
import static java.util.function.Predicate.not;
2020

21+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase;
2122
import io.gravitee.apim.core.user.model.UserSearchQuery;
23+
import io.gravitee.apim.core.user.service_provider.TokenService;
2224
import io.gravitee.apim.core.user.use_case.SearchUsersUseCase;
25+
import io.gravitee.apim.infra.adapter.UserAdapter;
2326
import io.gravitee.common.http.MediaType;
2427
import io.gravitee.rest.api.model.InlinePictureEntity;
2528
import io.gravitee.rest.api.model.PictureEntity;
2629
import io.gravitee.rest.api.model.UrlPictureEntity;
2730
import io.gravitee.rest.api.model.UserEntity;
2831
import io.gravitee.rest.api.model.permissions.RolePermission;
32+
import io.gravitee.rest.api.portal.rest.mapper.FinalizeRegistrationMapper;
2933
import io.gravitee.rest.api.portal.rest.mapper.UserMapper;
3034
import io.gravitee.rest.api.portal.rest.mapper.UsersSearchQueryMapper;
3135
import io.gravitee.rest.api.portal.rest.model.*;
@@ -35,9 +39,11 @@
3539
import io.gravitee.rest.api.rest.annotation.Permissions;
3640
import io.gravitee.rest.api.service.UserService;
3741
import io.gravitee.rest.api.service.common.GraviteeContext;
42+
import io.gravitee.rest.api.service.common.JWTHelper;
3843
import io.gravitee.rest.api.service.exceptions.AbstractManagementException;
3944
import io.gravitee.rest.api.service.exceptions.ForbiddenAccessException;
4045
import io.gravitee.rest.api.service.exceptions.PasswordAlreadyResetException;
46+
import io.gravitee.rest.api.service.exceptions.UserStateConflictException;
4147
import jakarta.inject.Inject;
4248
import jakarta.validation.Valid;
4349
import jakarta.validation.constraints.NotNull;
@@ -63,6 +69,15 @@ public class UsersResource extends AbstractResource {
6369
@Inject
6470
private UserService userService;
6571

72+
@Inject
73+
private TokenService tokenService;
74+
75+
@Inject
76+
private AcceptUserInvitationUseCase acceptUserInvitationUseCase;
77+
78+
@Inject
79+
private FinalizeRegistrationMapper finalizeRegistrationMapper;
80+
6681
@Inject
6782
private SearchUsersUseCase searchUsersUseCase;
6883

@@ -96,15 +111,16 @@ public Response registerUser(@Valid @NotNull(message = "Input must not be null."
96111
public Response finalizeRegistration(
97112
@Valid @NotNull(message = "Input must not be null.") FinalizeRegistrationInput finalizeRegistrationInput
98113
) {
99-
UserEntity newUser = userService.finalizeRegistration(
100-
GraviteeContext.getExecutionContext(),
101-
userMapper.convert(finalizeRegistrationInput)
102-
);
103-
if (newUser != null) {
104-
return Response.ok().entity(userMapper.convert(newUser)).build();
114+
var decoded = tokenService.decode(finalizeRegistrationInput.getToken());
115+
116+
if (JWTHelper.ACTION.RESET_PASSWORD.name().equals(decoded.action())) {
117+
throw new UserStateConflictException("Reset password forbidden on this resource");
105118
}
106119

107-
return Response.serverError().build();
120+
var executionContext = GraviteeContext.getExecutionContext();
121+
var useCaseInput = finalizeRegistrationMapper.toUseCaseInput(executionContext, decoded, finalizeRegistrationInput);
122+
var output = acceptUserInvitationUseCase.execute(useCaseInput);
123+
return Response.ok().entity(userMapper.convert(UserAdapter.INSTANCE.toUserEntity(output.user()))).build();
108124
}
109125

110126
@POST

gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/AbstractResourceTest.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.gravitee.apim.core.application_certificate.use_case.GetClientCertificatesUseCase;
2424
import io.gravitee.apim.core.application_certificate.use_case.UpdateClientCertificateUseCase;
2525
import io.gravitee.apim.core.application_certificate.use_case.ValidateClientCertificateUseCase;
26+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase;
2627
import io.gravitee.apim.core.subscription.use_case.CreateSubscriptionUseCase;
2728
import io.gravitee.apim.core.subscription_form.domain_service.SubscriptionFormSchemaGenerator;
2829
import io.gravitee.rest.api.portal.rest.JerseySpringTest;
@@ -121,6 +122,12 @@
121122
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
122123
public abstract class AbstractResourceTest extends JerseySpringTest {
123124

125+
@Autowired
126+
protected AcceptUserInvitationUseCase acceptUserInvitationUseCase;
127+
128+
@Autowired
129+
protected io.gravitee.apim.core.user.service_provider.TokenService registrationTokenService;
130+
124131
@Autowired
125132
protected CreateSubscriptionUseCase createSubscriptionUseCase;
126133

gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/resource/UsersResourceTest.java

Lines changed: 50 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
import static org.mockito.ArgumentMatchers.eq;
2222
import static org.mockito.Mockito.*;
2323

24+
import io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase;
25+
import io.gravitee.apim.core.user.model.BaseUserEntity;
26+
import io.gravitee.apim.core.user.model.DecodedToken;
2427
import io.gravitee.apim.core.user.model.UserSearchQuery;
2528
import io.gravitee.apim.core.user.use_case.SearchUsersUseCase;
2629
import io.gravitee.common.http.HttpStatusCode;
@@ -30,7 +33,9 @@
3033
import io.gravitee.rest.api.model.UserEntity;
3134
import io.gravitee.rest.api.portal.rest.model.*;
3235
import io.gravitee.rest.api.service.common.GraviteeContext;
36+
import io.gravitee.rest.api.service.common.JWTHelper;
3337
import io.gravitee.rest.api.service.exceptions.UserNotFoundException;
38+
import io.gravitee.rest.api.service.exceptions.UserStateConflictException;
3439
import jakarta.ws.rs.client.Entity;
3540
import jakarta.ws.rs.core.Response;
3641
import java.io.IOException;
@@ -61,6 +66,8 @@ protected String contextPath() {
6166
public void init() {
6267
resetAllMocks();
6368
reset(searchUsersUseCase);
69+
reset(acceptUserInvitationUseCase);
70+
reset(registrationTokenService);
6471

6572
searchUser = io.gravitee.apim.core.user.model.User.builder()
6673
.id("my-user-id")
@@ -269,47 +276,62 @@ public void shouldHaveBadRequestWhileFinalizingRegistrationWithEmpty() {
269276
}
270277

271278
@Test
272-
public void shouldFinalizeRegistration() {
273-
// init
274-
FinalizeRegistrationInput input = new FinalizeRegistrationInput()
275-
.token("token")
276-
.password("P4s5vv0Rd")
277-
.firstname("Firstname")
278-
.lastname("LASTNAME");
279-
280-
RegisterUserEntity registerUserEntity = new RegisterUserEntity();
281-
doReturn(registerUserEntity).when(userMapper).convert(input);
279+
public void should_finalize_registration_for_user_registration_action() {
280+
var input = new FinalizeRegistrationInput().token("my-jwt").password("P4s5vv0Rd").firstname("John").lastname("Doe");
281+
var decoded = new DecodedToken(JWTHelper.ACTION.USER_REGISTRATION.name(), "user@example.com", Optional.empty());
282+
var user = BaseUserEntity.builder().id("user-id").email("user@example.com").build();
282283

283-
doReturn(new UserEntity()).when(userService).finalizeRegistration(GraviteeContext.getExecutionContext(), registerUserEntity);
284+
doReturn(decoded).when(registrationTokenService).decode("my-jwt");
285+
doReturn(new AcceptUserInvitationUseCase.Output(user)).when(acceptUserInvitationUseCase).execute(any());
286+
doReturn(new io.gravitee.rest.api.portal.rest.model.User()).when(userMapper).convert(any(UserEntity.class));
284287

285-
// test
286288
final Response response = target("registration/_finalize").request().post(Entity.json(input));
289+
287290
assertEquals(HttpStatusCode.OK_200, response.getStatus());
291+
verify(registrationTokenService).decode("my-jwt");
292+
verify(acceptUserInvitationUseCase).execute(any());
293+
}
288294

289-
Mockito.verify(userMapper).convert(input);
290-
Mockito.verify(userService).finalizeRegistration(GraviteeContext.getExecutionContext(), registerUserEntity);
295+
@Test
296+
public void should_finalize_registration_for_group_invitation_action() {
297+
var input = new FinalizeRegistrationInput().token("my-jwt").password("P4s5vv0Rd").firstname("John").lastname("Doe");
298+
var decoded = new DecodedToken(JWTHelper.ACTION.GROUP_INVITATION.name(), "user@example.com", Optional.of("user-id"));
299+
var user = BaseUserEntity.builder().id("user-id").email("user@example.com").build();
300+
301+
doReturn(decoded).when(registrationTokenService).decode("my-jwt");
302+
doReturn(new AcceptUserInvitationUseCase.Output(user)).when(acceptUserInvitationUseCase).execute(any());
303+
doReturn(new io.gravitee.rest.api.portal.rest.model.User()).when(userMapper).convert(any(UserEntity.class));
304+
305+
final Response response = target("registration/_finalize").request().post(Entity.json(input));
306+
307+
assertEquals(HttpStatusCode.OK_200, response.getStatus());
308+
verify(acceptUserInvitationUseCase).execute(any());
291309
}
292310

293311
@Test
294-
public void shouldNotFinalizeRegistration() {
295-
// init
296-
FinalizeRegistrationInput input = new FinalizeRegistrationInput()
297-
.token("token")
298-
.password("P4s5vv0Rd")
299-
.firstname("Firstname")
300-
.lastname("LASTNAME");
312+
public void should_return_conflict_when_reset_password_action() {
313+
var input = new FinalizeRegistrationInput().token("my-jwt").password("P4s5vv0Rd").firstname("John").lastname("Doe");
314+
var decoded = new DecodedToken(JWTHelper.ACTION.RESET_PASSWORD.name(), "user@example.com", Optional.empty());
301315

302-
RegisterUserEntity registerUserEntity = new RegisterUserEntity();
303-
doReturn(registerUserEntity).when(userMapper).convert(input);
316+
doReturn(decoded).when(registrationTokenService).decode("my-jwt");
304317

305-
doReturn(null).when(userService).finalizeRegistration(GraviteeContext.getExecutionContext(), registerUserEntity);
318+
final Response response = target("registration/_finalize").request().post(Entity.json(input));
319+
320+
assertEquals(HttpStatusCode.CONFLICT_409, response.getStatus());
321+
verify(acceptUserInvitationUseCase, never()).execute(any());
322+
}
323+
324+
@Test
325+
public void should_propagate_token_decode_error() {
326+
var input = new FinalizeRegistrationInput().token("invalid-jwt").password("P4s5vv0Rd").firstname("John").lastname("Doe");
327+
doThrow(new com.auth0.jwt.exceptions.JWTVerificationException("invalid token"))
328+
.when(registrationTokenService)
329+
.decode("invalid-jwt");
306330

307-
// test
308331
final Response response = target("registration/_finalize").request().post(Entity.json(input));
309-
assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
310332

311-
Mockito.verify(userMapper).convert(input);
312-
Mockito.verify(userService).finalizeRegistration(GraviteeContext.getExecutionContext(), registerUserEntity);
333+
assertEquals(HttpStatusCode.INTERNAL_SERVER_ERROR_500, response.getStatus());
334+
verify(acceptUserInvitationUseCase, never()).execute(any());
313335
}
314336

315337
@Test

gravitee-apim-rest-api/gravitee-apim-rest-api-portal/gravitee-apim-rest-api-portal-rest/src/test/java/io/gravitee/rest/api/portal/rest/spring/ResourceContextConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -620,6 +620,17 @@ public UserMapper userMapper() {
620620
return mock(UserMapper.class);
621621
}
622622

623+
@Bean
624+
public io.gravitee.rest.api.portal.rest.mapper.FinalizeRegistrationMapper finalizeRegistrationMapper() {
625+
return new io.gravitee.rest.api.portal.rest.mapper.FinalizeRegistrationMapper();
626+
}
627+
628+
@Bean
629+
@org.springframework.context.annotation.Primary
630+
public io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase primaryAcceptUserInvitationUseCase() {
631+
return mock(io.gravitee.apim.core.invitation.use_case.AcceptUserInvitationUseCase.class);
632+
}
633+
623634
@Bean
624635
public AnalyticsMapper analyticsMapper() {
625636
return mock(AnalyticsMapper.class);

0 commit comments

Comments
 (0)