Skip to content

Commit fb41115

Browse files
committed
Fixes #389
1 parent a2168c1 commit fb41115

File tree

8 files changed

+141
-34
lines changed

8 files changed

+141
-34
lines changed

client/src/locale/en.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,14 @@ const en = {
211211
disapproved_action: "Disapprove",
212212
name: "Name",
213213
schacHomeOrganization: "Schac home",
214+
isInternal: "Internal",
214215
createdAt: "Created",
215216
memberCount: "# Members",
216217
applicationCount: "# Apps",
217218
status: "Status",
218219
searchPlaceHolder: "Search for organizations...",
219220
confirmation: "Are you sure you want to change the status to <strong>{{status}}</strong> for organization {{name}}?",
221+
manageOrganizationInMutable: "Internal organizations (e.g. IdP's from Manage) are immutable in SURF Access. Only external organizations from external (commercial) service providers are mutable.",
220222
flash: {
221223
updated: "Organization {{name}} now has the status {{status}}",
222224
deleted: "Organization {{name}} has been deleted",
@@ -952,7 +954,12 @@ const en = {
952954
deleteWarning: "If you delete your organization, all of your applications and connections will also be deleted. There is no undo functionality for this action.",
953955
deleteButton: "Delete my organization",
954956
proceedButton: "Save",
955-
flash: "Organization {{name}} is updated"
957+
flash: "Organization {{name}} is updated",
958+
generalInformation: "Algemene gegevens",
959+
entityID: "Entity ID",
960+
name: "name",
961+
keyWords: "Keywords",
962+
keyWordsInfo: "Keywords zijn shortcuts waarmee gebruikers in de WAYF snel deze organisatie terug kan vinden",
956963
},
957964
accessibleApps: {
958965
title: "Gekoppelde apps",

client/src/pages/MyOrganization.jsx

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
import React, {useEffect, useMemo, useRef, useState} from "react";
22
import {useAppStore} from "../stores/AppStore";
33
import {useNavigate, useParams} from "react-router-dom";
4-
import {deleteOrganizationById, organizationById, updateOrganizationMetaData} from "../api/index.js";
4+
import {
5+
deleteOrganizationById,
6+
organizationById,
7+
updateOrganizationMetaData,
8+
updateOrganizationName
9+
} from "../api/index.js";
510
import {isEmpty, stopEvent} from "../utils/Utils.js";
611
import "./MyOrganization.scss";
712
import I18n from "../locale/I18n";
@@ -12,6 +17,8 @@ import {Button, ButtonType, Loader} from "@surfnet/sds";
1217
import {ContactPersons} from "../components/ContactPersons.jsx";
1318
import {contactSectionValid, convertServerApplicationToClient} from "../utils/Application.js";
1419
import {mainMenuItems} from "../utils/MenuItems.js";
20+
import InputField from "../components/InputField.jsx";
21+
import SelectField from "../components/SelectField.jsx";
1522

1623
const sections = {
1724
contactPersons: "contactPersons",
@@ -23,14 +30,17 @@ const MyOrganization = ({refreshUser}) => {
2330
const user = useAppStore(state => state.user);
2431
const setFlash = useAppStore(state => state.setFlash);
2532

33+
const externalUser = user.externalUser;
34+
2635
const {organizationId} = useParams();
2736

2837
const [loading, setLoading] = useState(true);
2938
const [organization, setOrganization] = useState({});
3039
const [confirmation, setConfirmation] = useState({});
31-
const [section, setSection] = useState(user.externalUser ? sections.general : sections.contactPersons);
40+
const [section, setSection] = useState(externalUser ? sections.general : sections.contactPersons);
3241
const [focusedId, setFocusedId] = useState(null);
3342
const [initial, setInitial] = useState(true);
43+
const [dirty, setDirty] = useState(new Date());
3444

3545
const inputRef = useRef(null);
3646

@@ -56,7 +66,7 @@ const MyOrganization = ({refreshUser}) => {
5666
],
5767
activeMenuItem: mainMenuItems.idp
5868
});
59-
}, [navigate, organizationId]);
69+
}, [navigate, organizationId, dirty]);
6070

6171
useEffect(() => {
6272
if (inputRef.current) {
@@ -65,9 +75,10 @@ const MyOrganization = ({refreshUser}) => {
6575
}, [focusedId]);
6676

6777
const availableSections = useMemo(() => {
78+
const isExternalUser = externalUser;
6879
return Object.values(sections)
69-
.filter(s => s !== sections.delete || (user.externalUser && (user.superUser || isOrganizationAdmin(user, organization))))
70-
.filter(s => s !== sections.contactPersons || !user.externalUser)
80+
.filter(s => s !== sections.delete || (isExternalUser && (user.superUser || isOrganizationAdmin(user, organization))))
81+
.filter(s => s !== sections.contactPersons || !isExternalUser)
7182
}, [organization, user])
7283

7384
if (loading) {
@@ -106,8 +117,46 @@ const MyOrganization = ({refreshUser}) => {
106117
initial={initial}/>
107118
}
108119

109-
const renderGeneralSection = () => {
110-
return <span>renderGeneralSection-TODO</span>
120+
const changeKeyWords = options => {
121+
const newMetaData = {...organization.metaData, keyWords: (options || []).map(option => option.value)}
122+
setOrganization({...organization, metaData: newMetaData});
123+
}
124+
125+
const renderInternalGeneralSection = () => {
126+
return (
127+
<section className="inner-right">
128+
<h3>{I18n.t("myOrganization.generalInformation")}</h3>
129+
<InputField name={I18n.t("myOrganization.name")}
130+
value={organization.name}
131+
disabled={true}/>
132+
133+
<InputField name={I18n.t("myOrganization.entityID")}
134+
value={organization.metaData.entityID}
135+
disabled={true}/>
136+
137+
<SelectField name={I18n.t("myOrganization.keyWords")}
138+
value={(organization.metaData.keyWords || []).map(word => ({
139+
label: word,
140+
value: word
141+
}))}
142+
onChange={changeKeyWords}
143+
isMulti={true}
144+
creatable={true}
145+
/>
146+
<p className="info">{I18n.t("myOrganization.keyWordsInfo")}</p>
147+
</section>
148+
)
149+
}
150+
151+
const renderExternalGeneralSection = () => {
152+
return (
153+
<section className="inner-right">
154+
<h3>{I18n.t("myOrganization.generalInformation")}</h3>
155+
<InputField name={I18n.t("myOrganization.name")}
156+
value={organization.name}
157+
onChange={e => setOrganization({...organization, name: e.target.value})}/>
158+
</section>
159+
)
111160
}
112161

113162
const renderDeleteSection = () => {
@@ -125,33 +174,46 @@ const MyOrganization = ({refreshUser}) => {
125174
);
126175
}
127176

128-
const saveOrganization = () => {
177+
const saveInternalOrganization = () => {
129178
setInitial(false);
130179
if (contactSectionValid(organization)) {
131180
setLoading(true);
132-
updateOrganizationMetaData(organization.id, {contactPersons: organization.contactPersons})
181+
updateOrganizationMetaData(organization.id, {
182+
contactPersons: organization.contactPersons,
183+
keyWords: organization.metaData.keyWords
184+
})
185+
.then(() => {
186+
setDirty(new Date());
187+
setLoading(false);
188+
setFlash(I18n.t("myOrganization.flash", {name: organization.name}));
189+
});
190+
191+
}
192+
}
193+
194+
const saveExternalOrganization = () => {
195+
setInitial(false);
196+
if (!isEmpty(organization.name)) {
197+
setLoading(true);
198+
updateOrganizationName(organization.id, organization.name)
133199
.then(() => {
134-
//Re-fetch everything so there can be no mismatch is data
135-
organizationById(organizationId, true)
136-
.then(res => {
137-
const convertedOrganization = convertServerApplicationToClient(res);
138-
setOrganization(convertedOrganization);
139-
setLoading(false);
140-
setFlash(I18n.t("myOrganization.flash", {name: organization.name}));
141-
})
200+
setDirty(new Date());
201+
setLoading(false);
202+
setFlash(I18n.t("myOrganization.flash", {name: organization.name}));
142203
});
143204

144205
}
145206

146207
}
147208

209+
148210
const renderCurrentSection = () => {
149211
switch (section) {
150212
case sections.contactPersons: {
151213
return renderContactPersonsSection();
152214
}
153215
case sections.general: {
154-
return renderGeneralSection();
216+
return externalUser ? renderExternalGeneralSection() : renderInternalGeneralSection();
155217
}
156218
case sections.delete: {
157219
return renderDeleteSection();
@@ -193,11 +255,10 @@ const MyOrganization = ({refreshUser}) => {
193255
</div>
194256
{section !== sections.delete &&
195257
<div className="actions proceed">
196-
<Button onClick={saveOrganization}
197-
disabled={!initial && !contactSectionValid(organization)}
258+
<Button onClick={() => externalUser ? saveExternalOrganization() : saveInternalOrganization()}
259+
disabled={!initial && !contactSectionValid(organization) && isEmpty(organization.name)}
198260
txt={I18n.t("myOrganization.proceedButton")}
199261
/>
200-
201262
</div>}
202263

203264
</div>

client/src/pages/MyOrganization.scss

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,11 @@ div.my-organization-outer-container {
6363
margin: 0 0 25px 0;
6464
}
6565

66+
p.info {
67+
margin-top: 5px;
68+
color:var(--sds--color--gray--400);
69+
}
70+
6671
}
6772

6873
.actions {

client/src/pages/Organizations.jsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import React, {useEffect, useRef, useState} from "react";
22
import "./Organizations.scss";
33
import I18n from "../locale/I18n";
44
import "../components/Entities.scss";
5-
import {Loader, Tooltip} from "@surfnet/sds";
5+
import {Loader, Tooltip, Checkbox} from "@surfnet/sds";
66
import {Entities} from "../components/Entities";
77
import {isEmpty} from "../utils/Utils";
88
import {useAppStore} from "../stores/AppStore";
@@ -242,6 +242,11 @@ export const Organizations = ({pendingApproval, tab}) => {
242242
header: I18n.t("organizations.schacHomeOrganization"),
243243
mapper: org => <span>{org.schacHomeOrganization}</span>
244244
},
245+
{
246+
key: "manageIdentifier",
247+
header: I18n.t("organizations.isInternal"),
248+
mapper: org => <div className="wrapper"><Checkbox value={!isEmpty(org.manageIdentifier)} disabled={true}/></div>
249+
},
245250
{
246251
key: "applicationCount",
247252
header: I18n.t("organizations.applicationCount"),
@@ -266,7 +271,7 @@ export const Organizations = ({pendingApproval, tab}) => {
266271
key: "buttons",
267272
header: "",
268273
nonSortable: true,
269-
mapper: org =>
274+
mapper: org => isEmpty(org.manageIdentifier) ?
270275
<div className="top-header"
271276
tabIndex={1}
272277
onBlur={() => setTimeout(() => setDropDownActive(-1), 175)}>
@@ -275,7 +280,7 @@ export const Organizations = ({pendingApproval, tab}) => {
275280
<MenuIcon/>
276281
{dropDownActive === org.id && renderMenu(org)}
277282
</span>
278-
</div>
283+
</div> : <Tooltip standalone={true} tip={I18n.t("organizations.manageOrganizationInMutable")}/>
279284
}
280285
];
281286

client/src/pages/Organizations.scss

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,48 +18,57 @@
1818
display: flex;
1919
position: relative;
2020
width: 100%;
21+
2122
.input-field {
2223
width: 100%;
2324
}
25+
2426
.action-icons {
2527
display: flex;
2628
gap: 5px;
2729
position: absolute;
2830
right: 8px;
2931
top: 12px;
32+
3033
span {
3134
cursor: pointer;
3235
}
3336
}
3437
}
38+
3539
table.organizations {
3640

3741
thead {
3842
th {
3943
&.name {
40-
width: 22%;
44+
width: 20%;
4145
}
4246

4347
&.schac_home_organization, &.schacHomeOrganization {
44-
width: 16%;
48+
width: 14%;
4549
}
4650

4751
&.createdAt {
4852
width: 12%;
4953
}
5054

55+
&.manageIdentifier {
56+
text-align: center;
57+
width: 8%;
58+
}
59+
5160
&.applicationCount {
5261
text-align: center;
53-
width: 12%;
62+
width: 11%;
5463
}
5564

5665
&.memberCount {
5766
text-align: center;
58-
width: 12%;
67+
width: 11%;
5968
}
6069

6170
&.status {
62-
width: 14%;
71+
width: 12%;
6372
}
6473

6574
th.buttons {
@@ -70,8 +79,20 @@
7079

7180
tbody {
7281
td {
82+
&.manageIdentifier {
83+
text-align: center;
84+
85+
.wrapper {
86+
display: flex;
87+
88+
.sds--checkbox-container {
89+
margin: auto;
90+
}
91+
}
92+
}
93+
7394
&.applicationCount {
74-
text-align: center;
95+
text-align: center;
7596
}
7697

7798
&.memberCount {

server/src/main/java/access/api/UserController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,8 @@ public ResponseEntity<User> me(@Parameter(hidden = true) User user, Authenticati
116116
String manageIdentifier = (String) identityProvider.get("_id");
117117
Integer manageVersion = (Integer) identityProvider.get("version");
118118
Organization organization = new Organization(organizationName, schacHomeOrganization, manageIdentifier, manageVersion);
119+
//Organizations created based on the IdP's in Manage are approved automatically
120+
organization.setStatus(OrganizationStatus.APPROVED);
119121
organizationRepository.save(organization);
120122
userFromDB.addOrganizationMembership(new OrganizationMembership(userFromDB, organization, authority));
121123
}

server/src/main/java/access/manage/RemoteManage.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import java.util.HashMap;
2929
import java.util.List;
3030
import java.util.Map;
31+
import java.util.stream.Collectors;
3132
import java.util.stream.Stream;
3233

3334
import static access.manage.ManageData.getData;
@@ -112,10 +113,13 @@ public Map<String, Object> providerById(EntityType entityType, String manageIden
112113
@Override
113114
public Map<String, Object> saveIdentityProvider(Organization organization) {
114115
Map<String, Object> provider = providerById(EntityType.saml20_idp, organization.getManageIdentifier(), Environment.PROD);
115-
Map<String, Object> metaDataFields = getMetaDataFields (getData(provider));
116+
Map<String, Object> metaDataFields = getMetaDataFields(getData(provider));
116117

117118
Map<String, Object> metaDataOrganization = organization.getMetaData();
118119
converter.convertContactPersons(metaDataOrganization, metaDataFields);
120+
String keyWords = String.join(" ", ((List<String>) metaDataOrganization.getOrDefault("keyWords", List.of())));
121+
metaDataFields.put("keywords:0:nl", keyWords);
122+
metaDataFields.put("keywords:0:en", keyWords);
119123

120124
RestTemplate restTemplate = environmentRestTemplate(Environment.PROD);
121125
String url = environmentUrl(Environment.PROD);
@@ -236,7 +240,7 @@ public Map<String, Object> identityProviderByEntityID(String entityID) {
236240
url,
237241
baseQuery, List.class);
238242
if (identityProviders.isEmpty()) {
239-
throw new NotFoundException("No identityProviders found for entityID: "+entityID);
243+
throw new NotFoundException("No identityProviders found for entityID: " + entityID);
240244
}
241245
return identityProviders.getFirst();
242246
}
@@ -248,7 +252,7 @@ public List<Map<String, Object>> serviceProvidersByEntityID(List<String> entityI
248252
Map<String, Object> baseQuery = getBaseQuery(true);
249253
baseQuery.put("entityid", entityIdentifiers);
250254
return Stream.of(EntityType.oidc10_rp, EntityType.saml20_sp)
251-
.flatMap( entityType -> {
255+
.flatMap(entityType -> {
252256
String url = String.format("%s/manage/api/internal/search/%s",
253257
environmentUrl(activeEnvironment),
254258
entityType.name());

0 commit comments

Comments
 (0)