Skip to content

Commit 9c54361

Browse files
committed
WIP for #564
1 parent b3cbef3 commit 9c54361

File tree

12 files changed

+162
-60
lines changed

12 files changed

+162
-60
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,4 @@ spieldata
4747
teams-api-calls.md
4848
.run/
4949
client/locale_parser.js
50-
50+
PlayGroundTest.java

client/src/locale/en.js

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,8 +266,13 @@ const en = {
266266
expiryDate: "Valid till",
267267
acceptedAt: "Date accepted",
268268
roleExpiryDate: "Role expiry date",
269-
roleExpiryDateQuestion: "Set a custom role expiration period",
269+
roleExpiryDateQuestion: "Set a custom role expiration",
270270
roleExpiryDateInfo: "This role will be removed from the user {{expiry}}",
271+
roleExpiryDateInfoDefault: "By default this role will be removed from a user after {{days}} days",
272+
removeRole: "Remove role",
273+
after: "After",
274+
on: "On",
275+
days: "Days",
271276
customInviterDisplayNameQuestion: "Set a custom inviter name",
272277
inviterDisplayName: "Custom inviter display name for invitations",
273278
inviterDisplayNamePlaceholder: "e.g. working@home.university.nl",

client/src/locale/nl.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,11 @@ const nl = {
268268
roleExpiryDate: "Verloopdatum rol",
269269
roleExpiryDateQuestion: "Zet een specifieke verloopdatum voor de rol",
270270
roleExpiryDateInfo: "Deze rol wordt verwijderd van de gebruiker {{expiry}}",
271+
roleExpiryDateInfoDefault: "Default wordt deze rol verwijderd van de gebruiker na 365 dagen",
272+
removeRole: "Verwijder rol",
273+
after: "Na",
274+
on: "Op",
275+
days: "dagen",
271276
customInviterDisplayNameQuestion: "Zet een specifieke naam voor de uitnodiger",
272277
inviterDisplayName: "Specifieke naam uitnodiger voor in de uitnodiging",
273278
inviterDisplayNamePlaceholder: "Bijv. werken@thuis.universiteit.nl",

client/src/pages/RoleForm.jsx

Lines changed: 76 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {useLocation, useNavigate, useParams} from "react-router-dom";
33
import {useAppStore} from "../stores/AppStore";
44
import I18n from "../locale/I18n";
55
import {AUTHORITIES, isUserAllowed, urnFromRole} from "../utils/UserRole";
6+
import Select from "react-select";
67
import {
78
allProviders,
89
consequencesRoleDeletion,
@@ -30,12 +31,13 @@ import SwitchField from "../components/SwitchField";
3031
import {dateFromEpoch, displayExpiryDate, futureDate} from "../utils/Date";
3132
import DOMPurify from "dompurify";
3233
import WarningIndicator from "../components/WarningIndicator";
33-
import DatePicker from "react-datepicker";
34-
import {MinimalDateField} from "../components/MinimalDateField";
34+
import {DateField} from "../components/DateField";
3535

3636
const DEFAULT_EXPIRY_DAYS = 365;
3737
const CUT_OFF_DELETED_USER = 5;
3838

39+
const removeByOptions = ["after", "on"].map(val => ({value: val, label: I18n.t(`invitations.${val}`)}))
40+
3941
export const RoleForm = () => {
4042
const location = useLocation();
4143
const navigate = useNavigate();
@@ -64,6 +66,7 @@ export const RoleForm = () => {
6466
const [applications, setApplications] = useState([]);
6567
const [provisionings, setProvisionings] = useState({});
6668
const [deletedUserRoles, setDeletedUserRoles] = useState(null);
69+
const [removeRoleBy, setRemoveRoleBy] = useState(removeByOptions[0]);
6770

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

@@ -83,6 +86,10 @@ export const RoleForm = () => {
8386
}
8487
Promise.all(promises).then(res => {
8588
if (!newRole) {
89+
if (res[0].defaultExpiryDate !== 0) {
90+
res[0].defaultExpiryDate = new Date(res[0].defaultExpiryDate * 1000);
91+
setRemoveRoleBy(removeByOptions[1]);
92+
}
8693
setRole(res[0]);
8794
setCustomRoleExpiryDate(res[0].defaultExpiryDays !== DEFAULT_EXPIRY_DAYS)
8895
setCustomInviterDisplayName(!isEmpty(res[0].inviterDisplayName))
@@ -180,6 +187,15 @@ export const RoleForm = () => {
180187
})
181188
}
182189

190+
const toggleRemoveBy = option => {
191+
setRemoveRoleBy(option);
192+
if (option.value === "after") {
193+
setRole({...role, defaultExpiryDays: DEFAULT_EXPIRY_DAYS, defaultExpiryDate: null});
194+
} else {
195+
setRole({...role, defaultExpiryDays: 0, defaultExpiryDate: futureDate(365, new Date())});
196+
}
197+
}
198+
183199
const updateUserIfNecessary = (path, flashMessage) => {
184200
if (user.userRoles.some(userRole => userRole.role.id === role.id)) {
185201
//We need to refresh the roles of the User to ensure 100% consistency
@@ -245,7 +261,7 @@ export const RoleForm = () => {
245261
&& validOrganizationGUID
246262
&& !isEmpty(applications[0])
247263
&& applications.every(app => !app || (!app.invalid && !isEmpty(app.landingPage)))
248-
&& role.defaultExpiryDays > 0
264+
&& (role.defaultExpiryDays > 0 || role.defaultExpiryDate !== null)
249265
&& (!isEmpty(role.inviterDisplayName) || !customInviterDisplayName);
250266
}
251267

@@ -279,6 +295,12 @@ export const RoleForm = () => {
279295
setApplications([...applications]);
280296
}
281297

298+
const deriveExpiryDate = () => {
299+
console.log(`defaultExpityDate ${role.defaultExpiryDate}, defaultExpiryDays: ${role.defaultExpiryDays}`)
300+
const expiryDate = isEmpty(role.defaultExpiryDate) ? futureDate(role.defaultExpiryDays, new Date()) : role.defaultExpiryDate;
301+
return displayExpiryDate(expiryDate);
302+
303+
};
282304
const renderForm = () => {
283305
const valid = isValid();
284306
const disabledSubmit = (!valid && !initial) || !validOrganizationGUID;
@@ -441,48 +463,63 @@ export const RoleForm = () => {
441463

442464
<SwitchField name={"roleExpiryDate"}
443465
value={customRoleExpiryDate}
444-
onChange={() => setCustomRoleExpiryDate(!customRoleExpiryDate)}
445-
label={I18n.t("invitations.roleExpiryDateQuestion")}
446-
info={I18n.t("invitations.roleExpiryDateInfo", {
447-
expiry: displayExpiryDate(futureDate(role.defaultExpiryDays, new Date()))
466+
onChange={() => {
467+
if (customRoleExpiryDate) {
468+
setRole({
469+
...role,
470+
defaultExpiryDays: DEFAULT_EXPIRY_DAYS,
471+
defaultExpiryDate: null
472+
});
473+
setRemoveRoleBy(removeByOptions[0]);
474+
}
475+
setCustomRoleExpiryDate(!customRoleExpiryDate);
476+
}}
477+
label={I18n.t(`invitations.roleExpiryDateQuestion`)}
478+
info={I18n.t(`invitations.roleExpiryDateInfo${role.defaultExpiryDays === DEFAULT_EXPIRY_DAYS ? "Default" : ""}`, {
479+
expiry: deriveExpiryDate(),
480+
days: DEFAULT_EXPIRY_DAYS
448481
})}
449482
last={customRoleExpiryDate}
450483
/>
451484
{customRoleExpiryDate &&
452-
<div className="role-expiry-date">
453-
<InputField name={I18n.t("roles.defaultExpiryDays")}
454-
value={role.defaultExpiryDays || 0}
455-
isInteger={true}
456-
onChange={e => {
457-
const val = parseInt(e.target.value);
458-
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0;
459-
setRole(
460-
{...role, defaultExpiryDays: defaultExpiryDays})
461-
}}
462-
toolTip={I18n.t("tooltips.defaultExpiryDays")}
463-
customClassName="inner-switch"/>
464-
<span className="separator">{I18n.t("forms.fixed")}</span>
465-
<DatePicker
466-
name={"custom-expiry-date"}
467-
id={"custom-expiry-date"}
468-
selected={null}
469-
dateFormat={"dd/MM/yyyy"}
470-
onChange={() => true}
471-
showWeekNumbers
472-
isClearable={true}
473-
showIcon={true}
474-
showYearDropdown={true}
475-
weekLabel="Week"
476-
disabled={false}
477-
todayButton={null}
478-
maxDate={null}
479-
minDate={new Date()}
480-
/>
481-
</div>
485+
<div className="role-expiry-date-container">
486+
<p className="label">{I18n.t("invitations.removeRole")}</p>
487+
<div className="role-expiry-date">
488+
<Select className="input-select-inner"
489+
classNamePrefix={"select-inner"}
490+
value={removeRoleBy}
491+
options={removeByOptions}
492+
onChange={toggleRemoveBy}
493+
/>
494+
{removeRoleBy.value === "after" &&
495+
<>
496+
<InputField value={role.defaultExpiryDays || DEFAULT_EXPIRY_DAYS}
497+
isInteger={true}
498+
onChange={e => {
499+
const val = parseInt(e.target.value);
500+
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0;
501+
setRole(
502+
{...role, defaultExpiryDays: defaultExpiryDays})
503+
}}
504+
customClassName="inner-switch"/>
505+
<span>{I18n.t("invitations.days")}</span>
506+
</>
507+
}
508+
{removeRoleBy.value === "on" &&
509+
<DateField value={role.defaultExpiryDate || futureDate(365, new Date())}
510+
onChange={e => setRole({...role, defaultExpiryDate: e})}
511+
showYearDropdown={true}
512+
pastDatesAllowed={config.pastDateAllowed}
513+
allowNull={false}
514+
minDate={futureDate(1, new Date())}
515+
/>
516+
}
482517

483-
}
518+
</div>
519+
</div>
520+
}
484521

485-
{(!initial && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
522+
{(!initial && removeRoleBy.value === "after" && (isEmpty(role.defaultExpiryDays) || role.defaultExpiryDays < 1)) &&
486523
<ErrorIndicator msg={I18n.t("forms.required", {
487524
attribute: I18n.t("roles.defaultExpiryDays").toLowerCase()
488525
})}/>}

client/src/pages/RoleForm.scss

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,16 +98,33 @@
9898
margin-top: 32px;
9999
}
100100

101-
.role-expiry-date {
101+
.role-expiry-date-container {
102102
grid-column-start: first;
103103
display: flex;
104-
align-items: flex-end;
105-
justify-content: space-between;
106-
padding-bottom: 15px;
104+
flex-direction: column;
107105
border-bottom: 1px solid var(--sds--color--gray--200);
106+
padding-bottom: 15px;
107+
108+
p.label {
109+
font-weight: 600;
110+
margin: 12px var(--sds--space--1) 0 0;
111+
}
108112

109-
span.separator {
110-
margin-bottom: 15px;
113+
.role-expiry-date {
114+
display: flex;
115+
gap: 15px;
116+
align-items: center;
117+
.input-select-inner {
118+
width: 150px;
119+
120+
.select-inner__control {
121+
height: 48px;
122+
}
123+
}
124+
125+
.date-field {
126+
margin-top: 0;
127+
}
111128
}
112129

113130
div.input-field.inner-switch {

server/src/main/java/invite/api/InvitationOperations.java

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import invite.validation.EmailFormatValidator;
1515
import org.apache.commons.logging.Log;
1616
import org.apache.commons.logging.LogFactory;
17+
import org.jetbrains.annotations.NotNull;
1718
import org.springframework.http.HttpStatus;
1819
import org.springframework.http.ResponseEntity;
1920
import org.springframework.util.CollectionUtils;
@@ -62,8 +63,8 @@ public ResponseEntity<InvitationResponse> sendInvitation(InvitationRequest invit
6263
invitationRequest.setEduIDOnly(requestedRoles.stream().anyMatch(Role::isEduIDOnly));
6364
invitationRequest.setEnforceEmailEquality(requestedRoles.stream().anyMatch(Role::isEnforceEmailEquality));
6465
if (intendedAuthority.equals(Authority.GUEST)) {
65-
Integer defaultExpiryDays = requestedRoles.stream().max(Comparator.comparingInt(Role::getDefaultExpiryDays)).get().getDefaultExpiryDays();
66-
invitationRequest.setRoleExpiryDate(Instant.now().plus(defaultExpiryDays, ChronoUnit.DAYS));
66+
Instant latest = calculateInvitationExpiry(requestedRoles);
67+
invitationRequest.setRoleExpiryDate(latest);
6768
}
6869
}
6970
List<Invite> invites = invitationRequest.getInvitesWithInternalPlaceholderIdentifiers();
@@ -131,6 +132,22 @@ public ResponseEntity<InvitationResponse> sendInvitation(InvitationRequest invit
131132
return ResponseEntity.status(HttpStatus.CREATED).body(new InvitationResponse(HttpStatus.CREATED.value(), recipientInvitationURLs));
132133
}
133134

135+
public static Instant calculateInvitationExpiry(List<Role> requestedRoles) {
136+
Integer defaultExpiryDays = requestedRoles.stream()
137+
.filter(role -> role.getDefaultExpiryDays() != null)
138+
.max(Comparator.comparingInt(Role::getDefaultExpiryDays))
139+
.map(Role::getDefaultExpiryDays)
140+
.orElse(0);
141+
Instant now = Instant.now();
142+
Instant defaultExpiryDate = requestedRoles.stream()
143+
.filter(role -> role.getDefaultExpiryDate() != null)
144+
.max(Comparator.comparing(Role::getDefaultExpiryDate))
145+
.map(Role::getDefaultExpiryDate)
146+
.orElse(now);
147+
Instant expiryByDays = now.plus(defaultExpiryDays, ChronoUnit.DAYS);
148+
return expiryByDays.isAfter(defaultExpiryDate) ? expiryByDays : defaultExpiryDate;
149+
}
150+
134151
public ResponseEntity<Map<String, Integer>> resendInvitation(Long id,
135152
User user,
136153
RemoteUser remoteUser) {

server/src/main/java/invite/api/UserRoleController.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ public ResponseEntity<User> userRoleProvisioning(@Validated @RequestBody UserRol
185185
role,
186186
userRoleProvisioning.intendedAuthority,
187187
userRoleProvisioning.guestRoleIncluded,
188-
Instant.now().plus(role.getDefaultExpiryDays(), ChronoUnit.DAYS)))
188+
role.deriveExpirationDate()))
189189
: null)
190190
.filter(Objects::nonNull)
191191
.toList();

server/src/main/java/invite/model/Invitation.java

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.fasterxml.jackson.annotation.JsonIgnore;
44
import com.fasterxml.jackson.annotation.JsonProperty;
5+
import invite.api.InvitationOperations;
56
import jakarta.persistence.*;
67
import jakarta.validation.constraints.NotEmpty;
78
import lombok.Getter;
@@ -131,12 +132,7 @@ private Instant roleExpiryDate(@NotEmpty Set<InvitationRole> roles, Instant role
131132
if (roleExpiryDate != null || !intendedAuthority.equals(Authority.GUEST)) {
132133
return roleExpiryDate;
133134
}
134-
Integer days = roles.stream()
135-
.map(invitationRole -> invitationRole.getRole().getDefaultExpiryDays())
136-
.filter(Objects::nonNull)
137-
.min(Comparator.naturalOrder())
138-
.orElse(365);
139-
return Instant.now().plus(days, ChronoUnit.DAYS);
135+
return InvitationOperations.calculateInvitationExpiry(roles.stream().map(InvitationRole::getRole).toList());
140136
}
141137

142138
//used in the mustache templates

server/src/main/java/invite/model/Role.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import org.hibernate.annotations.Formula;
1212

1313
import java.io.Serializable;
14+
import java.time.Instant;
15+
import java.time.temporal.ChronoUnit;
1416
import java.util.*;
1517
import java.util.stream.Collectors;
1618

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

46+
@Column(name = "default_expiry_date")
47+
private Instant defaultExpiryDate;
48+
4449
@Column(name = "enforce_email_equality")
4550
private boolean enforceEmailEquality;
4651

@@ -138,6 +143,7 @@ public Role(RoleRequest roleRequest) {
138143
this.name = roleRequest.getName();
139144
this.description = roleRequest.getDescription();
140145
this.defaultExpiryDays = roleRequest.getDefaultExpiryDays();
146+
this.defaultExpiryDate = roleRequest.getDefaultExpiryDate();
141147
this.enforceEmailEquality = roleRequest.isEnforceEmailEquality();
142148
this.eduIDOnly = roleRequest.isEduIDOnly();
143149
this.blockExpiryDate = roleRequest.isBlockExpiryDate();
@@ -162,6 +168,15 @@ public Set<Application> applicationsUsed() {
162168
.map(ApplicationUsage::getApplication).collect(Collectors.toSet());
163169
}
164170

171+
@Transient
172+
@JsonIgnore
173+
public Instant deriveExpirationDate() {
174+
if (this.defaultExpiryDate != null) {
175+
return this.defaultExpiryDate;
176+
}
177+
return Instant.now().plus(this.defaultExpiryDays, ChronoUnit.DAYS))
178+
}
179+
165180
public void setApplicationUsages(Set<ApplicationUsage> applicationUsages) {
166181
this.applicationUsages = applicationUsages;
167182
this.applicationUsages.forEach(applicationUsage -> applicationUsage.setRole(this));

server/src/main/java/invite/model/RoleRequest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.hibernate.annotations.Formula;
1313

1414
import java.io.Serializable;
15+
import java.time.Instant;
1516
import java.util.*;
1617
import java.util.stream.Collectors;
1718

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

3031
private Integer defaultExpiryDays;
3132

33+
private Instant defaultExpiryDate;
34+
3235
private boolean enforceEmailEquality;
3336

3437
private boolean eduIDOnly;

0 commit comments

Comments
 (0)