Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ spieldata
teams-api-calls.md
.run/
client/locale_parser.js

PlayGroundTest.java
8 changes: 7 additions & 1 deletion client/src/locale/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,13 @@ const en = {
expiryDate: "Valid till",
acceptedAt: "Date accepted",
roleExpiryDate: "Role expiry date",
roleExpiryDateQuestion: "Set a custom role expiration period",
roleExpiryDateQuestion: "Set a custom role expiration",
roleExpiryDateInfo: "This role will be removed from the user {{expiry}}",
roleExpiryDateInfoDefault: "By default this role will be removed from a user after {{days}} days",
removeRole: "Remove role",
after: "After",
on: "On",
days: "Days",
customInviterDisplayNameQuestion: "Set a custom inviter name",
inviterDisplayName: "Custom inviter display name for invitations",
inviterDisplayNamePlaceholder: "e.g. working@home.university.nl",
Expand Down Expand Up @@ -316,6 +321,7 @@ const en = {
no: "No",
ok: "OK",
or: "or ",
fixed: "or set a fixed date",
edit: "Edit",
reset: "Reset",
cancel: "Cancel",
Expand Down
5 changes: 5 additions & 0 deletions client/src/locale/nl.js
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,11 @@ const nl = {
roleExpiryDate: "Verloopdatum rol",
roleExpiryDateQuestion: "Zet een specifieke verloopdatum voor de rol",
roleExpiryDateInfo: "Deze rol wordt verwijderd van de gebruiker {{expiry}}",
roleExpiryDateInfoDefault: "Default wordt deze rol verwijderd van de gebruiker na 365 dagen",
removeRole: "Verwijder rol",
after: "Na",
on: "Op",
days: "dagen",
customInviterDisplayNameQuestion: "Zet een specifieke naam voor de uitnodiger",
inviterDisplayName: "Specifieke naam uitnodiger voor in de uitnodiging",
inviterDisplayNamePlaceholder: "Bijv. werken@thuis.universiteit.nl",
Expand Down
96 changes: 78 additions & 18 deletions client/src/pages/RoleForm.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {useLocation, useNavigate, useParams} from "react-router-dom";
import {useAppStore} from "../stores/AppStore";
import I18n from "../locale/I18n";
import {AUTHORITIES, isUserAllowed, urnFromRole} from "../utils/UserRole";
import Select from "react-select";
import {
allProviders,
consequencesRoleDeletion,
Expand Down Expand Up @@ -30,10 +31,13 @@ import SwitchField from "../components/SwitchField";
import {dateFromEpoch, displayExpiryDate, futureDate} from "../utils/Date";
import DOMPurify from "dompurify";
import WarningIndicator from "../components/WarningIndicator";
import {DateField} from "../components/DateField";

const DEFAULT_EXPIRY_DAYS = 365;
const CUT_OFF_DELETED_USER = 5;

const removeByOptions = ["after", "on"].map(val => ({value: val, label: I18n.t(`invitations.${val}`)}))

export const RoleForm = () => {
const location = useLocation();
const navigate = useNavigate();
Expand Down Expand Up @@ -62,6 +66,7 @@ export const RoleForm = () => {
const [applications, setApplications] = useState([]);
const [provisionings, setProvisionings] = useState({});
const [deletedUserRoles, setDeletedUserRoles] = useState(null);
const [removeRoleBy, setRemoveRoleBy] = useState(removeByOptions[0]);

const allowedToEditApplication = useState(isUserAllowed(AUTHORITIES.INSTITUTION_ADMIN, user));

Expand All @@ -81,6 +86,10 @@ export const RoleForm = () => {
}
Promise.all(promises).then(res => {
if (!newRole) {
if (res[0].defaultExpiryDate !== 0) {
res[0].defaultExpiryDate = new Date(res[0].defaultExpiryDate * 1000);
setRemoveRoleBy(removeByOptions[1]);
}
setRole(res[0]);
setCustomRoleExpiryDate(res[0].defaultExpiryDays !== DEFAULT_EXPIRY_DAYS)
setCustomInviterDisplayName(!isEmpty(res[0].inviterDisplayName))
Expand Down Expand Up @@ -178,6 +187,15 @@ export const RoleForm = () => {
})
}

const toggleRemoveBy = option => {
setRemoveRoleBy(option);
if (option.value === "after") {
setRole({...role, defaultExpiryDays: DEFAULT_EXPIRY_DAYS, defaultExpiryDate: null});
} else {
setRole({...role, defaultExpiryDays: 0, defaultExpiryDate: futureDate(365, new Date())});
}
}

const updateUserIfNecessary = (path, flashMessage) => {
if (user.userRoles.some(userRole => userRole.role.id === role.id)) {
//We need to refresh the roles of the User to ensure 100% consistency
Expand Down Expand Up @@ -243,7 +261,7 @@ export const RoleForm = () => {
&& validOrganizationGUID
&& !isEmpty(applications[0])
&& applications.every(app => !app || (!app.invalid && !isEmpty(app.landingPage)))
&& role.defaultExpiryDays > 0
&& (role.defaultExpiryDays > 0 || role.defaultExpiryDate !== null)
&& (!isEmpty(role.inviterDisplayName) || !customInviterDisplayName);
}

Expand Down Expand Up @@ -277,6 +295,12 @@ export const RoleForm = () => {
setApplications([...applications]);
}

const deriveExpiryDate = () => {
console.log(`defaultExpityDate ${role.defaultExpiryDate}, defaultExpiryDays: ${role.defaultExpiryDays}`)
const expiryDate = isEmpty(role.defaultExpiryDate) ? futureDate(role.defaultExpiryDays, new Date()) : role.defaultExpiryDate;
return displayExpiryDate(expiryDate);

};
const renderForm = () => {
const valid = isValid();
const disabledSubmit = (!valid && !initial) || !validOrganizationGUID;
Expand Down Expand Up @@ -439,27 +463,63 @@ export const RoleForm = () => {

<SwitchField name={"roleExpiryDate"}
value={customRoleExpiryDate}
onChange={() => setCustomRoleExpiryDate(!customRoleExpiryDate)}
label={I18n.t("invitations.roleExpiryDateQuestion")}
info={I18n.t("invitations.roleExpiryDateInfo", {
expiry: displayExpiryDate(futureDate(role.defaultExpiryDays, new Date()))
onChange={() => {
if (customRoleExpiryDate) {
setRole({
...role,
defaultExpiryDays: DEFAULT_EXPIRY_DAYS,
defaultExpiryDate: null
});
setRemoveRoleBy(removeByOptions[0]);
}
setCustomRoleExpiryDate(!customRoleExpiryDate);
}}
label={I18n.t(`invitations.roleExpiryDateQuestion`)}
info={I18n.t(`invitations.roleExpiryDateInfo${role.defaultExpiryDays === DEFAULT_EXPIRY_DAYS ? "Default" : ""}`, {
expiry: deriveExpiryDate(),
days: DEFAULT_EXPIRY_DAYS
})}
last={customRoleExpiryDate}
/>
{customRoleExpiryDate && <InputField name={I18n.t("roles.defaultExpiryDays")}
value={role.defaultExpiryDays || 0}
isInteger={true}
onChange={e => {
const val = parseInt(e.target.value);
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0;
setRole(
{...role, defaultExpiryDays: defaultExpiryDays})
}}
toolTip={I18n.t("tooltips.defaultExpiryDays")}
customClassName="inner-switch"
/>}
{customRoleExpiryDate &&
<div className="role-expiry-date-container">
<p className="label">{I18n.t("invitations.removeRole")}</p>
<div className="role-expiry-date">
<Select className="input-select-inner"
classNamePrefix={"select-inner"}
value={removeRoleBy}
options={removeByOptions}
onChange={toggleRemoveBy}
/>
{removeRoleBy.value === "after" &&
<>
<InputField value={role.defaultExpiryDays || DEFAULT_EXPIRY_DAYS}
isInteger={true}
onChange={e => {
const val = parseInt(e.target.value);
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0;
setRole(
{...role, defaultExpiryDays: defaultExpiryDays})
}}
customClassName="inner-switch"/>
<span>{I18n.t("invitations.days")}</span>
</>
}
{removeRoleBy.value === "on" &&
<DateField value={role.defaultExpiryDate || futureDate(365, new Date())}
onChange={e => setRole({...role, defaultExpiryDate: e})}
showYearDropdown={true}
pastDatesAllowed={config.pastDateAllowed}
allowNull={false}
minDate={futureDate(1, new Date())}
/>
}

</div>
</div>
}

{(!initial && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
{(!initial && removeRoleBy.value === "after" && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
<ErrorIndicator msg={I18n.t("forms.required", {
attribute: I18n.t("roles.defaultExpiryDays").toLowerCase()
})}/>}
Expand Down
35 changes: 35 additions & 0 deletions client/src/pages/RoleForm.scss
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,41 @@
margin-top: 32px;
}

.role-expiry-date-container {
grid-column-start: first;
display: flex;
flex-direction: column;
border-bottom: 1px solid var(--sds--color--gray--200);
padding-bottom: 15px;

p.label {
font-weight: 600;
margin: 12px var(--sds--space--1) 0 0;
}

.role-expiry-date {
display: flex;
gap: 15px;
align-items: center;
.input-select-inner {
width: 150px;

.select-inner__control {
height: 48px;
}
}

.date-field {
margin-top: 0;
}
}

div.input-field.inner-switch {
padding-bottom: 0;
border-bottom: none;
}
}

}

}
21 changes: 19 additions & 2 deletions server/src/main/java/invite/api/InvitationOperations.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import invite.validation.EmailFormatValidator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jetbrains.annotations.NotNull;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.CollectionUtils;
Expand Down Expand Up @@ -62,8 +63,8 @@ public ResponseEntity<InvitationResponse> sendInvitation(InvitationRequest invit
invitationRequest.setEduIDOnly(requestedRoles.stream().anyMatch(Role::isEduIDOnly));
invitationRequest.setEnforceEmailEquality(requestedRoles.stream().anyMatch(Role::isEnforceEmailEquality));
if (intendedAuthority.equals(Authority.GUEST)) {
Integer defaultExpiryDays = requestedRoles.stream().max(Comparator.comparingInt(Role::getDefaultExpiryDays)).get().getDefaultExpiryDays();
invitationRequest.setRoleExpiryDate(Instant.now().plus(defaultExpiryDays, ChronoUnit.DAYS));
Instant latest = calculateInvitationExpiry(requestedRoles);
invitationRequest.setRoleExpiryDate(latest);
}
}
List<Invite> invites = invitationRequest.getInvitesWithInternalPlaceholderIdentifiers();
Expand Down Expand Up @@ -131,6 +132,22 @@ public ResponseEntity<InvitationResponse> sendInvitation(InvitationRequest invit
return ResponseEntity.status(HttpStatus.CREATED).body(new InvitationResponse(HttpStatus.CREATED.value(), recipientInvitationURLs));
}

public static Instant calculateInvitationExpiry(List<Role> requestedRoles) {
Integer defaultExpiryDays = requestedRoles.stream()
.filter(role -> role.getDefaultExpiryDays() != null)
.max(Comparator.comparingInt(Role::getDefaultExpiryDays))
.map(Role::getDefaultExpiryDays)
.orElse(0);
Instant now = Instant.now();
Instant defaultExpiryDate = requestedRoles.stream()
.filter(role -> role.getDefaultExpiryDate() != null)
.max(Comparator.comparing(Role::getDefaultExpiryDate))
.map(Role::getDefaultExpiryDate)
.orElse(now);
Instant expiryByDays = now.plus(defaultExpiryDays, ChronoUnit.DAYS);
return expiryByDays.isAfter(defaultExpiryDate) ? expiryByDays : defaultExpiryDate;
}

public ResponseEntity<Map<String, Integer>> resendInvitation(Long id,
User user,
RemoteUser remoteUser) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/main/java/invite/api/UserRoleController.java
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public ResponseEntity<User> userRoleProvisioning(@Validated @RequestBody UserRol
role,
userRoleProvisioning.intendedAuthority,
userRoleProvisioning.guestRoleIncluded,
Instant.now().plus(role.getDefaultExpiryDays(), ChronoUnit.DAYS)))
role.deriveExpirationDate()))
: null)
.filter(Objects::nonNull)
.toList();
Expand Down
8 changes: 2 additions & 6 deletions server/src/main/java/invite/model/Invitation.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import invite.api.InvitationOperations;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotEmpty;
import lombok.Getter;
Expand Down Expand Up @@ -131,12 +132,7 @@ private Instant roleExpiryDate(@NotEmpty Set<InvitationRole> roles, Instant role
if (roleExpiryDate != null || !intendedAuthority.equals(Authority.GUEST)) {
return roleExpiryDate;
}
Integer days = roles.stream()
.map(invitationRole -> invitationRole.getRole().getDefaultExpiryDays())
.filter(Objects::nonNull)
.min(Comparator.naturalOrder())
.orElse(365);
return Instant.now().plus(days, ChronoUnit.DAYS);
return InvitationOperations.calculateInvitationExpiry(roles.stream().map(InvitationRole::getRole).toList());
}

//used in the mustache templates
Expand Down
15 changes: 15 additions & 0 deletions server/src/main/java/invite/model/Role.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
import org.hibernate.annotations.Formula;

import java.io.Serializable;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
import java.util.stream.Collectors;

Expand Down Expand Up @@ -41,6 +43,9 @@ public class Role implements Serializable, Provisionable {
@Column(name = "default_expiry_days")
private Integer defaultExpiryDays;

@Column(name = "default_expiry_date")
private Instant defaultExpiryDate;

@Column(name = "enforce_email_equality")
private boolean enforceEmailEquality;

Expand Down Expand Up @@ -138,6 +143,7 @@ public Role(RoleRequest roleRequest) {
this.name = roleRequest.getName();
this.description = roleRequest.getDescription();
this.defaultExpiryDays = roleRequest.getDefaultExpiryDays();
this.defaultExpiryDate = roleRequest.getDefaultExpiryDate();
this.enforceEmailEquality = roleRequest.isEnforceEmailEquality();
this.eduIDOnly = roleRequest.isEduIDOnly();
this.blockExpiryDate = roleRequest.isBlockExpiryDate();
Expand All @@ -162,6 +168,15 @@ public Set<Application> applicationsUsed() {
.map(ApplicationUsage::getApplication).collect(Collectors.toSet());
}

@Transient
@JsonIgnore
public Instant deriveExpirationDate() {
if (this.defaultExpiryDate != null) {
return this.defaultExpiryDate;
}
return Instant.now().plus(this.defaultExpiryDays, ChronoUnit.DAYS);
}

public void setApplicationUsages(Set<ApplicationUsage> applicationUsages) {
this.applicationUsages = applicationUsages;
this.applicationUsages.forEach(applicationUsage -> applicationUsage.setRole(this));
Expand Down
3 changes: 3 additions & 0 deletions server/src/main/java/invite/model/RoleRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.hibernate.annotations.Formula;

import java.io.Serializable;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;

Expand All @@ -29,6 +30,8 @@ public class RoleRequest implements Serializable{

private Integer defaultExpiryDays;

private Instant defaultExpiryDate;

private boolean enforceEmailEquality;

private boolean eduIDOnly;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE `roles`
add `default_expiry_date` DATETIME DEFAULT NULL;
Loading
Loading