Provide an additional endpoint to allow sending both invitation and registration links depending on the email being associated with an user or not
Closes #29482 Signed-off-by: Martin Kanis <mkanis@redhat.com>
This commit is contained in:
parent
7182bc2125
commit
97cd5f3b8d
8 changed files with 85 additions and 30 deletions
|
@ -20,6 +20,7 @@ package org.keycloak.admin.client.resource;
|
|||
import java.util.List;
|
||||
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.FormParam;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
|
@ -76,11 +77,13 @@ public interface OrganizationMembersResource {
|
|||
|
||||
@POST
|
||||
@Path("invite-user")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response inviteUser(String email);
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
Response inviteUser(@FormParam("email") String email,
|
||||
@FormParam("first-name") String firstName,
|
||||
@FormParam("last-name") String lastName);
|
||||
|
||||
@POST
|
||||
@Path("invite-existing-user")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
Response inviteExistingUser(String id);
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
Response inviteExistingUser(@FormParam("id") String id);
|
||||
}
|
||||
|
|
|
@ -168,6 +168,10 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
|
|||
Map<String, Object> attributes = new HashMap<>(this.attributes);
|
||||
addLinkInfoIntoAttributes(link, expirationInMinutes, attributes);
|
||||
attributes.put("organization", organization);
|
||||
if (user.getFirstName() != null && user.getLastName() != null) {
|
||||
attributes.put("firstName", user.getFirstName());
|
||||
attributes.put("lastName", user.getLastName());
|
||||
}
|
||||
send("orgInviteSubject", List.of(organization.getName()), "org-invite.ftl", attributes);
|
||||
}
|
||||
|
||||
|
|
|
@ -66,7 +66,7 @@ public class OrganizationInvitationResource {
|
|||
this.tokenExpiration = getTokenExpiration();
|
||||
}
|
||||
|
||||
public Response inviteUser(String email) {
|
||||
public Response inviteUser(String email, String firstName, String lastName) {
|
||||
if (StringUtil.isBlank(email)) {
|
||||
throw ErrorResponse.error("To invite a member you need to provide an email", Status.BAD_REQUEST);
|
||||
}
|
||||
|
@ -76,15 +76,19 @@ public class OrganizationInvitationResource {
|
|||
if (user != null) {
|
||||
OrganizationModel org = provider.getByMember(user);
|
||||
|
||||
if (org.equals(organization)) {
|
||||
if (org != null && org.equals(organization)) {
|
||||
throw ErrorResponse.error("User already a member of the organization", Status.CONFLICT);
|
||||
}
|
||||
|
||||
throw ErrorResponse.error("User already exists with this e-mail", Status.BAD_REQUEST);
|
||||
return sendInvitation(user);
|
||||
}
|
||||
|
||||
user = new InMemoryUserAdapter(session, realm, null);
|
||||
user.setEmail(email);
|
||||
if (firstName != null && lastName != null) {
|
||||
user.setFirstName(firstName);
|
||||
user.setLastName(lastName);
|
||||
}
|
||||
|
||||
return sendInvitation(user);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ import java.util.stream.Stream;
|
|||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.DELETE;
|
||||
import jakarta.ws.rs.DefaultValue;
|
||||
import jakarta.ws.rs.FormParam;
|
||||
import jakarta.ws.rs.GET;
|
||||
import jakarta.ws.rs.NotFoundException;
|
||||
import jakarta.ws.rs.POST;
|
||||
|
@ -111,18 +112,22 @@ public class OrganizationMemberResource {
|
|||
|
||||
@Path("invite-user")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
|
||||
@Operation(summary = "Invites a new user to the organization using the specified e-mail address")
|
||||
public Response inviteUser(String email) {
|
||||
return new OrganizationInvitationResource(session, organization, adminEvent).inviteUser(email);
|
||||
@Operation(summary = "Invites an existing user or sends a registration link to a new user, based on the provided e-mail address.",
|
||||
description = "If the user with the given e-mail address exists, it sends an invitation link, otherwise it sends a registration link.")
|
||||
public Response inviteUser(@FormParam("email") String email,
|
||||
@FormParam("first-name") String firstName,
|
||||
@FormParam("last-name") String lastName) {
|
||||
return new OrganizationInvitationResource(session, organization, adminEvent).inviteUser(email, firstName, lastName);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("invite-existing-user")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
@Tag(name = KeycloakOpenAPI.Admin.Tags.ORGANIZATIONS)
|
||||
@Operation(summary = "Invites an existing user to the organization, using the specified user id")
|
||||
public Response inviteExistingUser(String id) {
|
||||
public Response inviteExistingUser(@FormParam("id") String id) {
|
||||
return new OrganizationInvitationResource(session, organization, adminEvent).inviteExistingUser(id);
|
||||
}
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit;
|
|||
import jakarta.mail.MessagingException;
|
||||
import jakarta.mail.internet.MimeMessage;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.jboss.arquillian.graphene.page.Page;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
|
@ -87,19 +88,26 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
|
||||
organization.members().inviteExistingUser(user.getId()).close();
|
||||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
Assert.assertNotNull(message);
|
||||
Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject());
|
||||
EmailBody body = MailUtils.getBody(message);
|
||||
String link = MailUtils.getLink(body.getHtml());
|
||||
driver.navigate().to(link.trim());
|
||||
// not yet a member
|
||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||
// confirm the intent of membership
|
||||
infoPage.clickToContinue();
|
||||
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
||||
// now a member
|
||||
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
||||
acceptInvitation(organization, user);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInviteExistingUserWithEmail() throws IOException, MessagingException {
|
||||
UserRepresentation user = UserBuilder.create()
|
||||
.username("invitedWithMatchingEmail")
|
||||
.email("invitedWithMatchingEmail@myemail.com")
|
||||
.password("password")
|
||||
.enabled(true)
|
||||
.build();
|
||||
try (Response response = testRealm().users().create(user)) {
|
||||
user.setId(ApiUtil.getCreatedId(response));
|
||||
}
|
||||
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
|
||||
organization.members().inviteUser(user.getEmail(), "Homer", "Simpson").close();
|
||||
|
||||
acceptInvitation(organization, user);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -111,7 +119,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
.build();
|
||||
// User isn't created when we send the invite
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
organization.members().inviteUser(user.getEmail()).close();
|
||||
organization.members().inviteUser(user.getEmail(), null, null).close();
|
||||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
Assert.assertNotNull(message);
|
||||
|
@ -150,7 +158,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
.build();
|
||||
// User isn't created when we send the invite
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
organization.members().inviteUser(user.getEmail()).close();
|
||||
organization.members().inviteUser(user.getEmail(), null, null).close();
|
||||
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
Assert.assertNotNull(message);
|
||||
|
@ -178,7 +186,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
.build();
|
||||
// User isn't created when we send the invite
|
||||
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
|
||||
organization.members().inviteUser(user.getEmail()).close();
|
||||
organization.members().inviteUser(user.getEmail(), "Homer", "Simpson").close();
|
||||
|
||||
try {
|
||||
setTimeOffset((int) TimeUnit.DAYS.toSeconds(1));
|
||||
|
@ -201,4 +209,23 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
|
|||
resetTimeOffset();
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptInvitation(OrganizationResource organization, UserRepresentation user) throws MessagingException, IOException {
|
||||
MimeMessage message = greenMail.getLastReceivedMessage();
|
||||
Assert.assertNotNull(message);
|
||||
Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject());
|
||||
EmailBody body = MailUtils.getBody(message);
|
||||
if (user.getFirstName() != null && user.getLastName() != null) {
|
||||
assertThat(body.getText(), Matchers.containsString(user.getFirstName() + " " + user.getLastName()));
|
||||
}
|
||||
String link = MailUtils.getLink(body.getHtml());
|
||||
driver.navigate().to(link.trim());
|
||||
// not yet a member
|
||||
Assert.assertFalse(organization.members().getAll().stream().anyMatch(actual -> user.getId().equals(actual.getId())));
|
||||
// confirm the intent of membership
|
||||
infoPage.clickToContinue();
|
||||
assertThat(infoPage.getInfo(), containsString("Your account has been updated."));
|
||||
// now a member
|
||||
Assert.assertNotNull(organization.members().member(user.getId()).toRepresentation());
|
||||
}
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
<#import "template.ftl" as layout>
|
||||
<@layout.emailLayout>
|
||||
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||
<#if firstName?? && lastName??>
|
||||
${kcSanitize(msg("orgInviteBodyPersonalizedHtml", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration), firstName, lastName))?no_esc}
|
||||
<#else>
|
||||
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))?no_esc}
|
||||
</#if>
|
||||
</@layout.emailLayout>
|
||||
|
|
|
@ -4,6 +4,8 @@ emailVerificationBodyHtml=<p>Someone has created a {2} account with this email a
|
|||
orgInviteSubject=Invitation to join the {0} organization
|
||||
orgInviteBody=You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||
orgInviteBodyHtml=<p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||
orgInviteBodyPersonalized="{5}" "{6}"\n\n You were invited to join the "{3}" organization. Click the link below to join.\n\n{0}\n\nThis link will expire within {4}.\n\nIf you don't want to join the organization, just ignore this message.
|
||||
orgInviteBodyPersonalizedHtml=<p>{5} {6}</p><p>You were invited to join the {3} organization. Click the link below to join. </p><p><a href="{0}">Link to join the organization</a></p><p>This link will expire within {4}.</p><p>If you don't want to join the organization, just ignore this message.</p>
|
||||
emailUpdateConfirmationSubject=Verify new email
|
||||
emailUpdateConfirmationBody=To update your {2} account with email address {1}, click the link below\n\n{0}\n\nThis link will expire within {3}.\n\nIf you don''t want to proceed with this modification, just ignore this message.
|
||||
emailUpdateConfirmationBodyHtml=<p>To update your {2} account with email address {1}, click the link below</p><p><a href="{0}">{0}</a></p><p>This link will expire within {3}.</p><p>If you don''t want to proceed with this modification, just ignore this message.</p>
|
||||
|
|
|
@ -1,2 +1,8 @@
|
|||
<#ftl output_format="plainText">
|
||||
${kcSanitize(msg("orgInviteBody", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))}
|
||||
|
||||
<#if firstName?? && lastName??>
|
||||
${kcSanitize(msg("orgInviteBodyPersonalized", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration), firstName, lastName))}
|
||||
<#else>
|
||||
${kcSanitize(msg("orgInviteBody", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))}
|
||||
</#if>
|
||||
|
||||
|
|
Loading…
Reference in a new issue