Minor improvements to invitation email templates (#29498)

Signed-off-by: Pedro Igor <pigor.craveiro@gmail.com>
This commit is contained in:
Pedro Igor 2024-05-14 08:19:02 -03:00 committed by GitHub
parent 51522248a3
commit b5a854b68e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 33 additions and 15 deletions

View file

@ -18,6 +18,7 @@
package org.keycloak.email;
import org.keycloak.events.Event;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
@ -77,7 +78,7 @@ public interface EmailTemplateProvider extends Provider {
void sendVerifyEmail(String link, long expirationInMinutes) throws EmailException;
void sendOrgInviteEmail(String link, long expirationInMinutes) throws EmailException;
void sendOrgInviteEmail(OrganizationModel organization, String link, long expirationInMinutes) throws EmailException;
void sendEmailUpdateConfirmation(String link, long expirationInMinutes, String address) throws EmailException;

View file

@ -37,6 +37,7 @@ import org.keycloak.events.EventType;
import org.keycloak.forms.login.freemarker.model.UrlBean;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.sessions.AuthenticationSessionModel;
@ -163,10 +164,11 @@ public class FreeMarkerEmailTemplateProvider implements EmailTemplateProvider {
}
@Override
public void sendOrgInviteEmail(String link, long expirationInMinutes) throws EmailException {
public void sendOrgInviteEmail(OrganizationModel organization, String link, long expirationInMinutes) throws EmailException {
Map<String, Object> attributes = new HashMap<>(this.attributes);
addLinkInfoIntoAttributes(link, expirationInMinutes, attributes);
send("orgInviteSubject", "org-invite.ftl", attributes);
attributes.put("organization", organization);
send("orgInviteSubject", List.of(organization.getName()), "org-invite.ftl", attributes);
}
@Override

View file

@ -110,7 +110,7 @@ public class OrganizationInvitationResource {
session.getProvider(EmailTemplateProvider.class)
.setRealm(realm)
.setUser(user)
.sendOrgInviteEmail(link, TimeUnit.SECONDS.toMinutes(tokenExpiration));
.sendOrgInviteEmail(organization, link, TimeUnit.SECONDS.toMinutes(tokenExpiration));
} catch (EmailException e) {
ServicesLogger.LOGGER.failedToSendEmail(e);
throw ErrorResponse.error("Failed to send invite email", Status.INTERNAL_SERVER_ERROR);

View file

@ -118,6 +118,7 @@ public class OrganizationMemberResource {
}
@Path("invite-user")
@POST
public Response inviteUser(String email) {
return new OrganizationInvitationResource(session, organization, adminEvent).inviteUser(email);
}

View file

@ -19,12 +19,14 @@ package org.keycloak.testsuite.organization.admin;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;
import jakarta.ws.rs.core.Response;
import org.jboss.arquillian.graphene.page.Page;
@ -33,8 +35,6 @@ import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.common.util.UriUtils;
import org.keycloak.cookie.CookieProvider;
import org.keycloak.cookie.CookieScope;
import org.keycloak.cookie.CookieType;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
@ -46,6 +46,7 @@ import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.RegisterPage;
import org.keycloak.testsuite.util.GreenMailRule;
import org.keycloak.testsuite.util.MailUtils;
import org.keycloak.testsuite.util.MailUtils.EmailBody;
import org.keycloak.testsuite.util.UserBuilder;
@EnableFeature(Feature.ORGANIZATION)
@ -71,7 +72,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
}
@Test
public void testInviteExistingUser() throws IOException {
public void testInviteExistingUser() throws IOException, MessagingException {
UserRepresentation user = UserBuilder.create()
.username("invited")
.email("invited@myemail.com")
@ -88,7 +89,9 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
MimeMessage message = greenMail.getLastReceivedMessage();
Assert.assertNotNull(message);
String link = MailUtils.getPasswordResetEmailLink(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())));
@ -100,7 +103,7 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
}
@Test
public void testInviteNewUserRegistration() throws IOException {
public void testInviteNewUserRegistration() throws IOException, MessagingException {
UserRepresentation user = UserBuilder.create()
.username("invitedUser")
.email("inviteduser@email")
@ -112,7 +115,14 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
MimeMessage message = greenMail.getLastReceivedMessage();
Assert.assertNotNull(message);
String link = MailUtils.getPasswordResetEmailLink(message);
Assert.assertEquals("Invitation to join the " + organizationName + " organization", message.getSubject());
EmailBody body = MailUtils.getBody(message);
String link = MailUtils.getLink(body.getHtml());
String text = body.getHtml();
assertTrue(text.contains("<p>You were invited to join the " + organizationName + " organization. Click the link below to join. </p>"));
assertTrue(text.contains("<a href=\"" + link + "\" rel=\"nofollow\">Link to join the organization</a></p>"));
assertTrue(text.contains("Link to join the organization"));
assertTrue(text.contains("<p>If you dont want to join the organization, just ignore this message.</p>"));
String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null);
Assert.assertNotNull(orgToken);
driver.navigate().to(link.trim());
@ -144,7 +154,8 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
MimeMessage message = greenMail.getLastReceivedMessage();
Assert.assertNotNull(message);
String link = MailUtils.getPasswordResetEmailLink(message);
EmailBody body = MailUtils.getBody(message);
String link = MailUtils.getLink(body.getHtml());
String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null);
Assert.assertNotNull(orgToken);
driver.navigate().to(link.trim());
@ -173,7 +184,8 @@ public class OrganizationInvitationLinkTest extends AbstractOrganizationTest {
setTimeOffset((int) TimeUnit.DAYS.toSeconds(1));
MimeMessage message = greenMail.getLastReceivedMessage();
Assert.assertNotNull(message);
String link = MailUtils.getPasswordResetEmailLink(message);
EmailBody body = MailUtils.getBody(message);
String link = MailUtils.getLink(body.getHtml());
String orgToken = UriUtils.parseQueryParameters(link, false).values().stream().map(strings -> strings.get(0)).findFirst().orElse(null);
Assert.assertNotNull(orgToken);
driver.navigate().to(link.trim());

View file

@ -1,4 +1,4 @@
<#import "template.ftl" as layout>
<@layout.emailLayout>
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))?no_esc}
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))?no_esc}
</@layout.emailLayout>

View file

@ -1,7 +1,9 @@
emailVerificationSubject=Verify email
emailVerificationBody=Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address\n\n{0}\n\nThis link will expire within {3}.\n\nIf you didn''t create this account, just ignore this message.
emailVerificationBodyHtml=<p>Someone has created a {2} account with this email address. If this was you, click the link below to verify your email address</p><p><a href="{0}">Link to e-mail address verification</a></p><p>This link will expire within {3}.</p><p>If you didn''t create this account, just ignore this message.</p>
orgInviteBodyHtml=<p>Someone has invited your account {2} account to join their keycloak 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 {3}.</p><p>If you don't want to join the organization, just ignore this message.</p>
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>
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>

View file

@ -1,2 +1,2 @@
<#ftl output_format="plainText">
${kcSanitize(msg("orgInviteBodyHtml", link, linkExpiration, realmName, linkExpirationFormatter(linkExpiration)))}
${kcSanitize(msg("orgInviteBody", link, linkExpiration, realmName, organization.name, linkExpirationFormatter(linkExpiration)))}