Skip to content

Commit 8f6b1ea

Browse files
committed
WIP for integration Invite and Connection requests
1 parent 10757cc commit 8f6b1ea

35 files changed

Lines changed: 664 additions & 95 deletions

client/src/App.jsx

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,6 @@ const App = () => {
4949
const navigate = useNavigate();
5050
const currentLocation = useLocation();
5151

52-
const sharedRoutes = () => {
53-
return (
54-
<>
55-
<Route path="/home" element={<Home/>}/>
56-
<Route path="/institutions" element={<Institutions/>}/>
57-
<Route path="/connect" element={<Connect/>}/>
58-
<Route path="/applications" element={<Applications/>}/>
59-
<Route path="/login-info" element={<LoginInfo/>}/>
60-
</>
61-
);
62-
}
63-
6452
const refreshUser = () => {
6553
me().then(user => {
6654
const currentOrganization = useAppStore.getState().currentOrganization
@@ -148,14 +136,13 @@ const App = () => {
148136
<Route path="/system/:tab?" element={<System/>}/>
149137
<Route path="/profile" element={<Profile setIsAuthenticated={setIsAuthenticated}/>}/>
150138
<Route path="/external/:app?" element={<ExternalApplication/>}/>
151-
<Route path="/application-detail/:manageType/:manageId" element={<ApplicationDetail/>}/>
139+
<Route path="/application-detail/:manageType/:manageId" element={<ApplicationDetail anonymous={false}/>}/>
152140
<Route path="/refresh-route/:path" element={<RefreshRoute/>}/>
153141
<Route path="/feedback" element={<Feedback/>}/>
154142
<Route path="/idp/:organizationId" element={<MyOrganization refreshUser={refreshUser}/>}/>
155143
<Route path="/authentication-switch" element={<AuthenticationSwitch/>}/>
156-
<Route path="/accessible-apps" element={<ApplicationOverview accessible={true}/>}/>
157-
<Route path="/catalogue" element={<ApplicationOverview accessible={false}/>}/>
158-
{/*{sharedRoutes()}*/}
144+
<Route path="/accessible-apps" element={<ApplicationOverview accessible={true} />}/>
145+
<Route path="/catalogue" element={<ApplicationOverview accessible={false} />}/>
159146
<Route path="*" element={<NotFound/>}/>
160147
</Routes>
161148
</div>
@@ -168,8 +155,12 @@ const App = () => {
168155
<Routes>
169156
<Route path="/" element={<Navigate replace to="/home"/>}/>
170157
<Route path="/landing" element={<Navigate replace to="/home"/>}/>
171-
{sharedRoutes()}
172-
<Route path="/application-detail/:manageType/:manageId" element={<ApplicationDetail/>}/>
158+
<Route path="/home" element={<Home/>}/>
159+
<Route path="/institutions" element={<Institutions/>}/>
160+
<Route path="/connect" element={<Connect/>}/>
161+
<Route path="/applications" element={<Applications/>}/>
162+
<Route path="/login-info" element={<LoginInfo/>}/>
163+
<Route path="/application-detail/:manageType/:manageId" element={<ApplicationDetail anonymous={true}/>}/>
173164
<Route path="/authentication-switch" element={<AuthenticationSwitch/>}/>
174165
<Route path="/*" element={<LoginRedirect/>}/>
175166
</Routes>

client/src/locale/en.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1018,7 +1018,11 @@ const en = {
10181018
internalUser: "Internal user",
10191019
internalUserTooltip: "An internal user - as opposed to an external user - is a user who has logged in with an institutional account."
10201020
},
1021-
1021+
applicationConnect: {
1022+
connect: "Connect",
1023+
request: "Request connection",
1024+
back: "← Back to other apps"
1025+
}
10221026
}
10231027

10241028
export default en;

client/src/pages/ApplicationDetail.jsx

Lines changed: 72 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import {APPLICATION_LINKS, providerDescription, providerName, providerOrganizati
1111
import {isEmpty, stopEvent} from "../utils/Utils.js";
1212
import {useAppStore} from "../stores/AppStore.js";
1313
import {useShallow} from "zustand/react/shallow";
14+
import ConfirmationDialog from "../components/ConfirmationDialog.jsx";
1415

15-
const ApplicationDetail = () => {
16+
const ApplicationDetail = ({anonymous}) => {
1617

17-
const {arp, privacy} = useAppStore(useShallow(state => ({
18+
const {arp, privacy, user} = useAppStore(useShallow(state => ({
1819
arp: state.arp,
19-
privacy: state.privacy
20+
privacy: state.privacy,
21+
user: state.user
2022
})));
2123

2224
const navigate = useNavigate();
@@ -26,6 +28,7 @@ const ApplicationDetail = () => {
2628
const [serviceProvider, setServiceProvider] = useState([]);
2729
const [showAttributes, setShowAttributes] = useState(false);
2830
const [showPrivacy, setShowPrivacy] = useState(false);
31+
const [confirmation, setConfirmation] = useState({});
2932

3033
useEffect(() => {
3134
publicServiceProviderByDetail(manageType, manageId)
@@ -76,19 +79,75 @@ const ApplicationDetail = () => {
7679
return arp.attributes.find(attr => attr.urn === urn);
7780
}
7881

82+
const mayConnectWithoutInteraction = () => {
83+
const connectOption = metaData["coin:dashboard_connect_option"] || "connect_with_interaction";
84+
const sameInstitution = !isEmpty(metaData["coin:institution_guid"]) &&
85+
metaData["coin:institution_guid"] === user.identityProvider.data.metaDataFields["coin:institution_guid"]
86+
return connectOption !== "connect_with_interaction" || sameInstitution;
87+
}
88+
89+
const openConnectDialog = () => {
90+
setConfirmation({
91+
open: true,
92+
cancel: () => setConfirmation({open: false}),
93+
action: () => doDelete(invitation, false),
94+
question: I18n.t("invitationsManagement.deleteConfirmation", {email: invitation.inviter.name}),
95+
okButton: I18n.t("invitationsManagement.revoke")
96+
});
97+
}
98+
99+
const doRequestConnection = () => {
100+
101+
}
102+
103+
const openRequestConnectionDialog = () => {
104+
setConfirmation({
105+
open: true,
106+
cancel: () => setConfirmation({open: false}),
107+
action: () => doDelete(invitation, false),
108+
question: I18n.t("invitationsManagement.deleteConfirmation", {email: invitation.inviter.name}),
109+
okButton: I18n.t("invitationsManagement.revoke")
110+
});
111+
}
112+
113+
const goBack = e => {
114+
stopEvent(e);
115+
navigate(-1);
116+
}
117+
118+
const connectButtonText = () => {
119+
//Is the app already connected, may the app be connected without interaction, or is there already an outstanding change request?
120+
return I18n.t(`applicationConnect.${mayConnectWithoutInteraction() ? "connect" : "request"}`)
121+
}
122+
123+
const {open, cancel, action, question, okButton, children} = confirmation;
124+
79125
return (
80126
<div className="application-detail-container">
81-
<div className="application-detail-header-container">
127+
{open && <ConfirmationDialog confirm={action}
128+
cancel={cancel}
129+
confirmationHeader={I18n.t("forms.submit")}
130+
confirmationTxt={okButton}
131+
question={question}>
132+
{children}
133+
</ConfirmationDialog>
134+
}
135+
{anonymous && <div className="application-detail-header-container">
82136
<div className="application-detail-header">
83137
<div className="left">
84138
<h1 className="large">{I18n.t("applicationDetail.title")}</h1>
85139
<p>{I18n.t("applicationDetail.subTitle")}</p>
86140
</div>
87141
<img src={StudentPng} alt="student"/>
88142
</div>
89-
</div>
143+
</div>}
144+
{!anonymous &&
145+
<div className="application-detail-top">
146+
<a href={"/"} onClick={goBack}>{I18n.t("applicationConnect.back")}</a>
147+
</div>
148+
}
90149
<div className="inner-application-detail-container">
91-
<div className="application-detail">
150+
<div className={`application-detail ${anonymous ? "" : "stand-alone"}`}>
92151
<div className="meta-data">
93152
{metaData["logo:0:url"] && <img src={metaData["logo:0:url"]} alt=""/>}
94153
{!metaData["logo:0:url"] && <PlaceHolderImage/>}
@@ -100,11 +159,13 @@ const ApplicationDetail = () => {
100159
{providerName(I18n.locale, serviceProvider)}
101160
</p>
102161
</div>
103-
<Button type={ButtonType.Secondary}
104-
icon={<ArrowLeftIcon/>}
105-
iconPlacement={ButtonIconPlacement.Left}
106-
onClick={() => navigate(-1)}
107-
txt={I18n.t("applicationDetail.back")}/>
162+
{anonymous && <Button type={ButtonType.Secondary}
163+
icon={<ArrowLeftIcon/>}
164+
iconPlacement={ButtonIconPlacement.Left}
165+
onClick={goBack}
166+
txt={I18n.t("applicationDetail.back")}/>}
167+
{!anonymous && <Button onClick={() => alert("Todo")}
168+
txt={connectButtonText()}/>}
108169
</div>
109170
<div className="details">
110171
<div className="left">

client/src/pages/ApplicationDetail.scss

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,21 @@ div.application-detail-container {
3838
background-color: white;
3939
}
4040

41+
.application-detail-top {
42+
background-color: white;
43+
padding: 0 25px;
44+
}
45+
4146
.application-detail {
4247
@include index.page;
4348
background-color: white;
4449
max-width: $width;
4550
padding: 25px 0;
4651

52+
&.stand-alone {
53+
margin: 0 25px;
54+
}
55+
4756
.meta-data {
4857
display: flex;
4958
align-items: center;

client/src/pages/ApplicationOverview.jsx

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,15 @@ const ApplicationOverview = ({accessible}) => {
3939

4040
publicServiceProviders()
4141
.then(res => {
42-
//If the IdP of the user
43-
const allowedAll = user.identityProvider?.data?.allowedall || true;
44-
if (!allowedAll && accessible) {
42+
//Scope the services on the allowed-entities of the IdP of the user
43+
if (accessible) {
44+
const allowedAll = user.identityProvider.data.allowedall;
4545
const allowedEntities = user.identityProvider.data.allowedEntities.map(entity => entity.name);
46-
res = res.filter(entity => allowedEntities.includes(entity.data.entityid))
46+
res = res.filter(entity => allowedAll || allowedEntities.includes(entity.data.entityid))
47+
} else {
48+
//In the case of eduID / external user, we don't have an identityProvider
49+
const allowedEntities = (user.identityProvider?.data?.allowedEntities || []).map(entity => entity.name);
50+
res = res.filter(entity => !allowedEntities.includes(entity.data.entityid))
4751
}
4852
res.forEach(entity => {
4953
entity.name = providerName(I18n.locale, entity);

client/src/utils/Login.js

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import {isEmpty, sanitizeURL} from "./Utils.js";
2+
import {dictToQueryString} from "./QueryParameters.js";
23

34
export const SESSION_STORAGE_LOCATION = "session_storage_location";
45

5-
export const login = (config, force = true, useEduID = false) => {
6-
let params = force ? "?force=true" : "";
7-
if (useEduID) {
8-
params += (force ? "&eduId=true" : "?eduId=true");
6+
export const login = (config, force = true, useEduID = false, upgradeLoa = false) => {
7+
const params = {
8+
force: force ? "true" : null,
9+
eduId: useEduID ? "true" : null,
10+
upgradeLoa: upgradeLoa ? "true" : null
911
}
1012
let serverUrl = config.serverUrl;
13+
const queryString = dictToQueryString(params);
1114
if (isEmpty(serverUrl)) {
1215
const local = window.location.hostname === "localhost";
1316
serverUrl = local ? "http://localhost:8886" :
1417
`${window.location.protocol}//${window.location.host}`
1518
}
16-
window.location.href = sanitizeURL(`${serverUrl}/api/v1/users/login${params}`);
19+
window.location.href = sanitizeURL(`${serverUrl}/api/v1/users/login?${queryString}`);
1720
}

client/src/utils/QueryParameters.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {isEmpty} from "./Utils.js";
2+
13
export function replaceQueryParameter(windowLocationSearch, name, value) {
24
const urlSearchParams = new URLSearchParams(windowLocationSearch);
35
urlSearchParams.set(name, value);
@@ -7,4 +9,19 @@ export function replaceQueryParameter(windowLocationSearch, name, value) {
79
export function getParameterByName(name, windowLocationSearch) {
810
const urlSearchParams = new URLSearchParams(windowLocationSearch);
911
return urlSearchParams.get(name);
12+
}
13+
14+
15+
export function dictToQueryString(params) {
16+
const searchParams = new URLSearchParams();
17+
Object.entries(params).forEach(([key, value]) => {
18+
if (!isEmpty(value)) {
19+
if (Array.isArray(value)) {
20+
value.forEach(v => searchParams.append(key, v));
21+
} else {
22+
searchParams.append(key, value);
23+
}
24+
}
25+
});
26+
return searchParams.toString();
1027
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ public ResponseEntity<Application> update(User user, @Validated @RequestBody App
182182
}
183183

184184
@DeleteMapping({"", "/{applicationId}"})
185-
public ResponseEntity<Map<String, Integer>> delete(User user, @PathVariable("applicationId") Long applicationId) {
185+
public ResponseEntity<Map<String, Object>> delete(User user, @PathVariable("applicationId") Long applicationId) {
186186
LOG.debug("/delete application by " + user.getEmail());
187187

188188
Application application = applicationRepository.findById(applicationId)

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public ResponseEntity<ApplicationMembership> create(
6262
}
6363

6464
@DeleteMapping({"/{membership_id}"})
65-
public ResponseEntity<Map<String, Integer>> delete(User user, @PathVariable("membership_id") Long membershipId) {
65+
public ResponseEntity<Map<String, Object>> delete(User user, @PathVariable("membership_id") Long membershipId) {
6666
LOG.debug("/delete");
6767
ApplicationMembership applicationMembership = this.applicationMembershipRepository.findById(membershipId)
6868
.orElseThrow(() -> new NotFoundException("ApplicationMembership not found"));

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -199,7 +199,7 @@ public ResponseEntity<Map<String, Object>> requestProductionStatus(User user,
199199
}
200200

201201
@DeleteMapping({"", "/{connectionId}"})
202-
public ResponseEntity<Map<String, Integer>> delete(User user, @PathVariable("connectionId") Long connectionId) {
202+
public ResponseEntity<Map<String, Object>> delete(User user, @PathVariable("connectionId") Long connectionId) {
203203
LOG.debug("/delete connection by " + user.getEmail());
204204

205205
Connection connection = findConnectionForAuthorizedUser(user, connectionId);

0 commit comments

Comments
 (0)