Skip to content

Commit cea9d33

Browse files
committed
Merge branch 'feature/fixed-role-end-date-564'
2 parents e9767c8 + d0f4ae2 commit cea9d33

16 files changed

Lines changed: 205 additions & 52 deletions

File tree

.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: 7 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",
@@ -316,6 +321,7 @@ const en = {
316321
no: "No",
317322
ok: "OK",
318323
or: "or ",
324+
fixed: "or set a fixed date",
319325
edit: "Edit",
320326
reset: "Reset",
321327
cancel: "Cancel",

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/InvitationForm.jsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ import ErrorIndicator from "../components/ErrorIndicator";
2929
import SelectField from "../components/SelectField";
3030
import {DateField} from "../components/DateField";
3131
import EmailField from "../components/EmailField";
32-
import {displayExpiryDate, futureDate} from "../utils/Date";
32+
import {deriveExpirationDate, displayExpiryDate, futureDate} from "../utils/Date";
3333
import SwitchField from "../components/SwitchField";
3434
import {InvitationRoleCard} from "../components/InvitationRoleCard";
3535
import DOMPurify from "dompurify";
@@ -144,27 +144,26 @@ export const InvitationForm = () => {
144144
const initialRole = markedRoles.find(role => role.value === location.state);
145145
if (initialRole) {
146146
// See markAndFilterRoles - we are mixing up userRoles and roles
147-
const defaultExpiryDays = initialRole.isUserRole ? initialRole.role.defaultExpiryDays : initialRole.defaultExpiryDays;
148147
setSelectedRoles([initialRole])
149148
setInvitation({
150149
...invitation,
151150
intendedAuthority: isGuest ? AUTHORITIES.GUEST : AUTHORITIES.INVITER,
152151
enforceEmailEquality: initialRole.enforceEmailEquality,
153152
eduIDOnly: initialRole.eduIDOnly,
154-
roleExpiryDate: futureDate(defaultExpiryDays)
153+
roleExpiryDate: deriveExpirationDate(initialRole.isUserRole ? initialRole.role : initialRole)
155154
})
156155
setOriginalRoleId(initialRole.isUserRole ? initialRole.role.id : initialRole.id);
157156
} else {
158-
let defaultExpiryDays = 366;
157+
let roleExpiryDate = futureDate(366);
159158
if (markedRoles.length === 1) {
160159
const role = markedRoles[0]
161-
defaultExpiryDays = role.isUserRole ? role.role.defaultExpiryDays : role.defaultExpiryDays;
160+
roleExpiryDate = deriveExpirationDate(role.isUserRole ? role.role : role);
162161
setSelectedRoles(markedRoles);
163162
}
164163
setInvitation({
165164
...invitation,
166165
intendedAuthority: isGuest ? AUTHORITIES.GUEST : AUTHORITIES.INVITER,
167-
roleExpiryDate: futureDate(defaultExpiryDays)
166+
roleExpiryDate: roleExpiryDate
168167
})
169168
}
170169
}
@@ -215,14 +214,11 @@ export const InvitationForm = () => {
215214
}
216215

217216
const defaultRoleExpiryDate = newRoles => {
218-
const allDefaultExpiryDays = (newRoles || [])
219-
.filter(role => role.defaultExpiryDays)
220-
.map(role => role.defaultExpiryDays)
221-
.sort();
222-
if (invitation.intendedAuthority === AUTHORITIES.GUEST) {
223-
return futureDate(isEmpty(allDefaultExpiryDays) ? 365 : allDefaultExpiryDays[0]);
224-
}
225-
return invitation.roleExpiryDate;
217+
const allDefaultExpiryDates = (newRoles || [])
218+
.map(role => deriveExpirationDate(role));
219+
220+
return isEmpty(allDefaultExpiryDates) ? futureDate(365, new Date()) :
221+
new Date(Math.max(...allDefaultExpiryDates.map(d => d.getTime())));
226222
}
227223

228224
const validateOrganizationGUID = e => {
@@ -431,7 +427,7 @@ export const InvitationForm = () => {
431427
<InviterContainer isInviter={isInviter}>
432428
{renderFormElements(authorityOptions)}
433429
</InviterContainer>
434-
430+
<h1>roleExpiryDate: {invitation.roleExpiryDate.toString()}</h1>
435431
<InviterContainer isInviter={isInviter}>
436432
{!displayAdvancedSettings &&
437433
<a className={`advanced-settings ${isInviter ? "inviter" : ""}`} href="/#"

client/src/pages/RoleForm.jsx

Lines changed: 78 additions & 18 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,10 +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";
34+
import {DateField} from "../components/DateField";
3335

3436
const DEFAULT_EXPIRY_DAYS = 365;
3537
const CUT_OFF_DELETED_USER = 5;
3638

39+
const removeByOptions = ["after", "on"].map(val => ({value: val, label: I18n.t(`invitations.${val}`)}))
40+
3741
export const RoleForm = () => {
3842
const location = useLocation();
3943
const navigate = useNavigate();
@@ -62,6 +66,7 @@ export const RoleForm = () => {
6266
const [applications, setApplications] = useState([]);
6367
const [provisionings, setProvisionings] = useState({});
6468
const [deletedUserRoles, setDeletedUserRoles] = useState(null);
69+
const [removeRoleBy, setRemoveRoleBy] = useState(removeByOptions[0]);
6570

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

@@ -81,6 +86,10 @@ export const RoleForm = () => {
8186
}
8287
Promise.all(promises).then(res => {
8388
if (!newRole) {
89+
if (res[0].defaultExpiryDate !== 0) {
90+
res[0].defaultExpiryDate = new Date(res[0].defaultExpiryDate * 1000);
91+
setRemoveRoleBy(removeByOptions[1]);
92+
}
8493
setRole(res[0]);
8594
setCustomRoleExpiryDate(res[0].defaultExpiryDays !== DEFAULT_EXPIRY_DAYS)
8695
setCustomInviterDisplayName(!isEmpty(res[0].inviterDisplayName))
@@ -178,6 +187,15 @@ export const RoleForm = () => {
178187
})
179188
}
180189

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+
181199
const updateUserIfNecessary = (path, flashMessage) => {
182200
if (user.userRoles.some(userRole => userRole.role.id === role.id)) {
183201
//We need to refresh the roles of the User to ensure 100% consistency
@@ -243,7 +261,7 @@ export const RoleForm = () => {
243261
&& validOrganizationGUID
244262
&& !isEmpty(applications[0])
245263
&& applications.every(app => !app || (!app.invalid && !isEmpty(app.landingPage)))
246-
&& role.defaultExpiryDays > 0
264+
&& (role.defaultExpiryDays > 0 || role.defaultExpiryDate !== null)
247265
&& (!isEmpty(role.inviterDisplayName) || !customInviterDisplayName);
248266
}
249267

@@ -277,6 +295,12 @@ export const RoleForm = () => {
277295
setApplications([...applications]);
278296
}
279297

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+
};
280304
const renderForm = () => {
281305
const valid = isValid();
282306
const disabledSubmit = (!valid && !initial) || !validOrganizationGUID;
@@ -439,27 +463,63 @@ export const RoleForm = () => {
439463

440464
<SwitchField name={"roleExpiryDate"}
441465
value={customRoleExpiryDate}
442-
onChange={() => setCustomRoleExpiryDate(!customRoleExpiryDate)}
443-
label={I18n.t("invitations.roleExpiryDateQuestion")}
444-
info={I18n.t("invitations.roleExpiryDateInfo", {
445-
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
446481
})}
447482
last={customRoleExpiryDate}
448483
/>
449-
{customRoleExpiryDate && <InputField name={I18n.t("roles.defaultExpiryDays")}
450-
value={role.defaultExpiryDays || 0}
451-
isInteger={true}
452-
onChange={e => {
453-
const val = parseInt(e.target.value);
454-
const defaultExpiryDays = Number.isInteger(val) && val > 0 ? val : 0;
455-
setRole(
456-
{...role, defaultExpiryDays: defaultExpiryDays})
457-
}}
458-
toolTip={I18n.t("tooltips.defaultExpiryDays")}
459-
customClassName="inner-switch"
460-
/>}
484+
{customRoleExpiryDate &&
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+
}
517+
518+
</div>
519+
</div>
520+
}
461521

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

client/src/pages/RoleForm.scss

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

101+
.role-expiry-date-container {
102+
grid-column-start: first;
103+
display: flex;
104+
flex-direction: column;
105+
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+
}
112+
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+
}
128+
}
129+
130+
div.input-field.inner-switch {
131+
padding-bottom: 0;
132+
border-bottom: none;
133+
}
134+
}
135+
101136
}
102137

103138
}

client/src/utils/Date.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ export const futureDate = (daysAhead, fromDate = new Date()) => {
99
return new Date(time);
1010
}
1111

12+
export const deriveExpirationDate = role => {
13+
if (!isEmpty(role.defaultExpiryDate)) {
14+
//When the role is serialized from the Server, then the defaultExpiryDate in epoch / 1000, else a genuine Date
15+
return role.defaultExpiryDate.getTime ? role.defaultExpiryDate : new Date(role.defaultExpiryDate * 1000);
16+
}
17+
return futureDate(role.defaultExpiryDays);
18+
}
19+
1220
const formatOptions = {month: "short", day: "numeric", year: "numeric"};
1321

1422
export const shortDateFromEpoch = (epoch, needsMultiplier = true) => {

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/RoleController.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,11 +130,13 @@ public ResponseEntity<List<Role>> rolesPerApplicationId(@PathVariable("manageId"
130130
LOG.debug(String.format("/rolesPerApplicationId for user %s", user.getEduPersonPrincipalName()));
131131

132132
UserPermissions.assertAuthority(user, Authority.INSTITUTION_ADMIN);
133-
134-
if (!user.isSuperUser()) {
133+
List<Role> roles;
134+
if (user.isSuperUser()) {
135+
roles = roleRepository.findByApplicationUsagesApplicationManageId(manageId);
136+
} else {
135137
Set<String> applicationManageIdentifiers = user.getApplications().stream().map(m -> (String) m.get("id")).collect(Collectors.toSet());
136138
Set<String> roleManageIdentifiers = user.getUserRoles().stream()
137-
//If the user has an userRole as Inviter, then we must exclude those
139+
//If the user has a userRole as Inviter, then we must exclude those
138140
.filter(userRole -> userRole.getAuthority().hasEqualOrHigherRights(Authority.MANAGER))
139141
.map(userRole -> userRole.getRole().applicationsUsed())
140142
.flatMap(Collection::stream)
@@ -144,8 +146,8 @@ public ResponseEntity<List<Role>> rolesPerApplicationId(@PathVariable("manageId"
144146
if (!applicationManageIdentifiers.contains(manageId)) {
145147
throw new UserRestrictionException();
146148
}
149+
roles = roleRepository.findByOrganizationGUIDAndApplicationUsagesApplicationManageId(user.getOrganizationGUID(), manageId);
147150
}
148-
List<Role> roles = roleRepository.findByApplicationUsagesApplicationManageId(manageId);
149151
return ResponseEntity.ok(manage.addManageMetaData(roles));
150152
}
151153

0 commit comments

Comments
 (0)