Skip to content

Commit

Permalink
Fixes #243
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Nov 19, 2024
1 parent 35511b4 commit 587b942
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 12 deletions.
7 changes: 5 additions & 2 deletions client/src/components/InputField.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ export default function InputField({
displayLabel = true,
button = null,
isInteger = false,
isUrl = false
isUrl = false,
customClassName = ""
}) {
const navigate = useNavigate();
placeholder = disabled ? "" : placeholder;
Expand All @@ -40,8 +41,10 @@ export default function InputField({
className += "error ";
}
const validExternalLink = externalLink && !isEmpty(value) && validUrlRegExp.test(value);
const isError = error ? "sds--text-field--status-error" : "";
const topClassName = `input-field sds--text-field ${isError} ${customClassName}`;
return (
<div className={`input-field sds--text-field ${error ? "sds--text-field--status-error" : ""}`}>
<div className={topClassName}>
{(name && displayLabel) && <label htmlFor={name}>{name}
{toolTip && <Tooltip tip={toolTip}/>}
</label>}
Expand Down
7 changes: 7 additions & 0 deletions client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ const en = {
roleExpiryDate: "Role expiry date",
roleExpiryDateQuestion: "Set a custom role expiration period",
roleExpiryDateInfo: "This role will be removed from the user {{expiry}}",
customInviterDisplayNameQuestion: "Set a custom inviter name",
inviterDisplayName: "Custom inviter display name for invitations",
inviterDisplayNamePlaceholder: "e.g. [email protected]",
inviterDisplayNameError: "Custom inviter name is required, unless you use the default inviter name",
customInviterDisplayNameInfoDefault: "Invitations for this role will show the name of the inviter as sender",
customInviterDisplayNameInfo: "Invitations for this role will show a custom name / email as sender",
expiryDateQuestion: "Set a custom invitation expiration period",
expiryDateInfo: "Default an invitation is valid for 1 month",
withinThreeMonths: "Within 1 month",
Expand Down Expand Up @@ -403,6 +409,7 @@ const en = {
eduIDOnlyTooltip: "When checked the invitees will be required to log in with eduID",
roleExpiryDateTooltip: "The end date of this role. After this date the role is removed from the user.",
expiryDateTooltip: "The date on which this invitation expires",
inviterDisplayName: "The functional address which will used in the invitations of the role.<br><br>Default the name of the inviter is show.",
rolesTooltip: "Select all the roles that the invitee will be granted after accepting the invitation",
intendedAuthorityTooltip: "The authority determines the rights the invitee will be granted on accepting the invitation",
inviteesTooltip: "Add email addresses separated by comma, space or semi-colon or on seperate lines. You can also paste a csv file with line-separated email addresses.",
Expand Down
7 changes: 7 additions & 0 deletions client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,12 @@ const nl = {
roleExpiryDate: "Verloopdatum rol",
roleExpiryDateQuestion: "Zet een specifieke verloopdatum voor de rol",
roleExpiryDateInfo: "Deze rol wordt verwijderd van de gebruiker {{expiry}}",
customInviterDisplayNameQuestion: "Zet een specifieke naam voor de uitnodiger",
inviterDisplayName: "Specifieke naam uitnodiger voor in de uitnodiging",
inviterDisplayNamePlaceholder: "Bijv. [email protected]",
inviterDisplayNameError: "Specifieke naam uitnodiger is verplicht, tenzij je ervoor kiest om de default uitnodiger naam te gebruiken",
customInviterDisplayNameInfoDefault: "Uitnodigingen voor deze rol zullen de naam van de uitnodiger tonen.",
customInviterDisplayNameInfo: "Uitnodigingen voor deze rol zullen deze specifieke uitnodiger naam tonen.",
expiryDateQuestion: "Zet een specifieke verloopdatum voor de uitnodiging",
expiryDateInfo: "Standaard verloopt een uitnodiging na 1 maand",
withinThreeMonths: "Binnen 1 maand",
Expand Down Expand Up @@ -402,6 +408,7 @@ const nl = {
eduIDOnlyTooltip: "Indien ingeschakeld moeten de genodigden eduID gebruiken om in te loggen bij het accepteren",
roleExpiryDateTooltip: "De einddatum van deze rol. Na deze datum wordt de rol verwijderd bij de gebruiker.",
expiryDateTooltip: "De datum waarop deze uitnodiging verloopt",
inviterDisplayName: "De specifieke naam van de uitnodiger zal worden getoond in de uitnodiging.<br><br>Standaard tonen we de naam van de daadwerkelijke uitnodiger.",
rolesTooltip: "Alle rollen die de genodigden verkrijgen na het accepteren van de uitnodiging",
intendedAuthorityTooltip: "De autoriteit geeft de rechten aan die de genodigde verwerft na het accepteren van de uitnodiging",
inviteesTooltip: "Geef e-mailadressen op, gescheiden door komma, spatie of puntkomma, of op een eigen regel. Je kunt ook een csv-bestand plakken met daarin op elke regel een e-mailadres.",
Expand Down
32 changes: 31 additions & 1 deletion client/src/pages/RoleForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export const RoleForm = () => {
const [validOrganizationGUID, setValidOrganizationGUID] = useState(true);
const [organizationGUIDIdentityProvider, setOrganizationGUIDIdentityProvider] = useState(null);
const [customRoleExpiryDate, setCustomRoleExpiryDate] = useState(false);
const [customInviterDisplayName, setCustomInviterDisplayName] = useState(false);
const [applications, setApplications] = useState([]);
const [allowedToEditApplication, setAllowedToEditApplication] = useState(true);
const [deletedUserRoles, setDeletedUserRoles] = useState(null);
Expand All @@ -79,6 +80,7 @@ export const RoleForm = () => {
if (!newRole) {
setRole(res[0]);
setCustomRoleExpiryDate(res[0].defaultExpiryDays !== DEFAULT_EXPIRY_DAYS)
setCustomInviterDisplayName(!isEmpty(res[0].inviterDisplayName))
}
let providers;
if (user.superUser) {
Expand Down Expand Up @@ -238,7 +240,8 @@ export const RoleForm = () => {
&& validOrganizationGUID
&& !isEmpty(applications[0])
&& applications.every(app => !app || (!app.invalid && !isEmpty(app.landingPage)))
&& role.defaultExpiryDays > 0;
&& role.defaultExpiryDays > 0
&& (!isEmpty(role.inviterDisplayName) || !customInviterDisplayName);
}

const changeApplication = (index, application) => {
Expand Down Expand Up @@ -424,6 +427,7 @@ export const RoleForm = () => {
info={I18n.t("invitations.roleExpiryDateInfo", {
expiry: displayExpiryDate(futureDate(role.defaultExpiryDays, new Date()))
})}
last={customRoleExpiryDate}
/>
{customRoleExpiryDate && <InputField name={I18n.t("roles.defaultExpiryDays")}
value={role.defaultExpiryDays || 0}
Expand All @@ -435,13 +439,39 @@ export const RoleForm = () => {
{...role, defaultExpiryDays: defaultExpiryDays})
}}
toolTip={I18n.t("tooltips.defaultExpiryDays")}
customClassName="inner-switch"
/>}

{(!initial && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
<ErrorIndicator msg={I18n.t("forms.required", {
attribute: I18n.t("roles.defaultExpiryDays").toLowerCase()
})}/>}

<SwitchField name={"customInviterDisplayName"}
value={customInviterDisplayName}
onChange={() => {
setCustomInviterDisplayName(!customInviterDisplayName);
setRole(
{...role, inviterDisplayName: null})
}}
last={customInviterDisplayName}
label={I18n.t("invitations.customInviterDisplayNameQuestion")}
info={I18n.t(`invitations.${customInviterDisplayName ? "customInviterDisplayNameInfo" : "customInviterDisplayNameInfoDefault"}`)}
/>
{customInviterDisplayName && <InputField name={I18n.t("invitations.inviterDisplayName")}
value={role.inviterDisplayName || ""}
error={!initial && isEmpty(role.inviterDisplayName) && customInviterDisplayName}
onChange={e => {
setRole(
{...role, inviterDisplayName: e.target.value})
}}
placeholder={I18n.t("invitations.inviterDisplayNamePlaceholder")}
toolTip={I18n.t("tooltips.inviterDisplayName")}
customClassName="inner-switch"
/>}
{(!initial && isEmpty(role.inviterDisplayName) && customInviterDisplayName) &&
<ErrorIndicator msg={I18n.t("invitations.inviterDisplayNameError")}/>}

<SwitchField name={"overrideSettingsAllowed"}
value={role.overrideSettingsAllowed}
onChange={value => setRole({...role, overrideSettingsAllowed: value})}
Expand Down
6 changes: 6 additions & 0 deletions client/src/styles/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@

.input-field, .select-field, .date-field, .sds--checkbox-container {
margin-top: 20px;

&.inner-switch {
margin-top: 0;
padding-bottom: 15px;
border-bottom: 1px solid var(--sds--color--gray--200);
}
}

.user-role .sds--checkbox-container {
Expand Down
3 changes: 2 additions & 1 deletion server/src/main/java/access/api/InvitationOperations.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ public ResponseEntity<InvitationResponse> sendInvitation(InvitationRequest invit
}
//We need to assert validations on the roles soo we need to load them
List<Role> requestedRoles = invitationRequest.getRoleIdentifiers().stream()
.map(id -> invitationResource.getRoleRepository().findById(id).orElseThrow(() -> new NotFoundException("Role not found"))).toList();
.map(id -> invitationResource.getRoleRepository().findById(id)
.orElseThrow(() -> new NotFoundException("Role not found"))).toList();

if (user != null) {
UserPermissions.assertValidInvitation(user, intendedAuthority, requestedRoles);
Expand Down
10 changes: 9 additions & 1 deletion server/src/main/java/access/mail/MailBox.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ public void sendInviteMail(Provisionable provisionable, Invitation invitation, L
} else {
variables.put("institutionName", "SURF");
}
variables.put("roles", splitListSemantically(invitation.getRoles().stream().map(invitationRole -> invitationRole.getRole().getName()).toList()));
variables.put("roles", splitListSemantically(invitation.getRoles().stream()
.map(invitationRole -> invitationRole.getRole().getName()).toList()));
if (invitation.getRoles().stream()
.anyMatch(invitationRole -> StringUtils.hasText(invitationRole.getRole().getInviterDisplayName()))) {
variables.put("displaySenderName", splitListSemantically(invitation.getRoles().stream()
.map(invitationRole -> invitationRole.getRole().getInviterDisplayName()).toList()));
} else {
variables.put("displaySenderName", provisionable.getName());
}
if (StringUtils.hasText(invitation.getMessage())) {
variables.put("message", invitation.getMessage().replaceAll("\n", "<br/>"));
}
Expand Down
3 changes: 3 additions & 0 deletions server/src/main/java/access/model/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public class Role implements Serializable, Provisionable {
@Column(name = "remote_api_user")
private String remoteApiUser;

@Column(name = "inviter_display_name")
private String inviterDisplayName;

@Formula(value = "(SELECT COUNT(*) FROM user_roles ur WHERE ur.role_id=id)")
private Long userRoleCount;

Expand Down
1 change: 0 additions & 1 deletion server/src/main/java/access/provision/Provisioning.java
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ public Provisioning(Map<String, Object> provider) {
List<Map<String, String>> applicationMaps = (List<Map<String, String>>) provider.getOrDefault("applications", emptyList());
this.remoteApplications = applicationMaps.stream().map(m -> new ManageIdentifier(m.get("id"), EntityType.valueOf(m.get("type").toUpperCase()))).toList();
this.invariant();

}

private void invariant() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `roles`
add `inviter_display_name` varchar(255) DEFAULT NULL;
4 changes: 2 additions & 2 deletions server/src/main/resources/templates/invitation_en.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</p>
{{/environment}}
<p class="title" style="">Hello,</p>
<p><strong>{{user.name}}</strong> (from <strong>{{institutionName}}</strong>) has invited you for one or
<p><strong>{{displaySenderName}}</strong> (from <strong>{{institutionName}}</strong>) has invited you for one or
more
applications or systems that they use.</p>

Expand All @@ -31,7 +31,7 @@
</ul>
{{/invitation.anyRoles}}
{{#message}}
<p style="margin: 20px 0 0 0;">Personal message from {{user.name}}</p>
<p style="margin: 20px 0 0 0;">Personal message from {{displaySenderName}}</p>
<div class="head" style="background-color: #f5f5f5;padding: 20px;margin:5px 0 15px 0;font-style: italic;">
<p style="margin: 0;">
{{{message}}}
Expand Down
4 changes: 2 additions & 2 deletions server/src/main/resources/templates/invitation_nl.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
</p>
{{/environment}}
<p class="title" style="">Hallo,</p>
<p><strong>{{user.name}}</strong> (van <strong>{{institutionName}}</strong>) heeft je uitgenodigd voor één of meer
<p><strong>{{displaySenderName}}</strong> (van <strong>{{institutionName}}</strong>) heeft je uitgenodigd voor één of meer
applicaties en systemen die zij gebruiken.</p>

<p>Je bent uitgenodigd voor:</p>
Expand All @@ -30,7 +30,7 @@
</ul>
{{/invitation.anyRoles}}
{{#message}}
<p style="margin: 20px 0 0 0;">Persoonlijk bericht van {{user.name}}</p>
<p style="margin: 20px 0 0 0;">Persoonlijk bericht van {{displaySenderName}}</p>
<div class="head" style="background-color: #f5f5f5;padding: 20px;margin:5px 0 15px 0;font-style: italic;">
<p style="margin: 0;">
{{{message}}}
Expand Down
3 changes: 3 additions & 0 deletions server/src/test/java/access/AbstractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -578,10 +578,13 @@ public void doSeed() {
Role wiki =
new Role("Wiki", "Wiki desc",
application("1", EntityType.SAML20_SP), 365, false, false);
wiki.setInviterDisplayName("[email protected]");

Role network =
new Role("Network", "Network desc",
application("2", EntityType.SAML20_SP), 365, false, false);


Set<Application> applications = Set.of(
new Application("3", EntityType.SAML20_SP),
new Application("6", EntityType.OIDC10_RP))
Expand Down
54 changes: 52 additions & 2 deletions server/src/test/java/access/api/InvitationMailControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

import access.AbstractMailTest;
import access.AccessCookieFilter;
import access.mail.MimeMessageParser;
import access.manage.EntityType;
import access.model.Authority;
import access.model.Invitation;
import access.model.*;
import io.restassured.common.mapper.TypeRef;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Map;

import static io.restassured.RestAssured.given;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

class InvitationMailControllerTest extends AbstractMailTest {
Expand All @@ -33,4 +40,47 @@ void resendInviteMail() throws Exception {
assertTrue(htmlContent.contains("SURF bv"));
assertTrue(htmlContent.contains("Mail"));
}

@Test
void newInvitationCustomDisplayName() throws Exception {
//Because the user is changed and provisionings are queried
stubForManageProvisioning(List.of());
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", MANAGE_SUB);

stubForManageProviderById(EntityType.SAML20_SP, "1");
//Wiki role, see AbstractTest#seed
List<Role> roles = roleRepository.findByApplicationUsagesApplicationManageId("1");
List<Long> roleIdentifiers = roles.stream()
.map(Role::getId)
.toList();
InvitationRequest invitationRequest = new InvitationRequest(
Authority.GUEST,
"Message",
Language.en,
true,
false,
false,
false,
List.of("[email protected]"),
roleIdentifiers,
Instant.now().plus(365, ChronoUnit.DAYS),
Instant.now().plus(12, ChronoUnit.DAYS));

given()
.when()
.filter(accessCookieFilter.cookieFilter())
.accept(ContentType.JSON)
.header(accessCookieFilter.csrfToken().getHeaderName(), accessCookieFilter.csrfToken().getToken())
.contentType(ContentType.JSON)
.body(invitationRequest)
.post("/api/v1/invitations")
.then()
.statusCode(201);

List<MimeMessageParser> mimeMessageParsers = super.allMailMessages(1);
String htmlContent = mimeMessageParsers.getFirst().getHtmlContent();

assertTrue(htmlContent.contains("[email protected]"));
}

}

0 comments on commit 587b942

Please sign in to comment.