Skip to content

Commit 3d5ccec

Browse files
committed
WIP for #360
1 parent a699374 commit 3d5ccec

File tree

8 files changed

+276
-4
lines changed

8 files changed

+276
-4
lines changed

client/src/App.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import ApplicationOverview from "./pages/ApplicationOverview.jsx";
4242
import Profile from "./pages/Profile.jsx";
4343
import {UserFeedbackWidget} from "./components/UserFeedbackWidget.jsx";
4444
import Policies from "./pages/Policies.jsx";
45+
import ManageDetail from "./pages/ManageDetail.jsx";
4546

4647
const App = () => {
4748

@@ -144,6 +145,8 @@ const App = () => {
144145
<Route path="/external/:app?" element={<ExternalApplication/>}/>
145146
<Route path="/application-detail/:manageType/:manageId/:page?/:policyId?"
146147
element={<ApplicationDetail anonymous={false} refreshUser={refreshUser}/>}/>
148+
<Route path="/manage/details/:manageType/:manageId"
149+
element={<ManageDetail />}/>
147150
<Route path="/refresh-route/:path" element={<RefreshRoute/>}/>
148151
<Route path="/feedback" element={<Feedback/>}/>
149152
<Route path="/idp/:organizationId" element={<MyOrganization refreshUser={refreshUser}/>}/>

client/src/api/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@ export function getConnectionById(connectionId) {
286286
return fetchJson(`/api/v1/connections/${connectionId}`);
287287
}
288288

289+
export function getByManageIdentifiers(entityType, manageIdentifier) {
290+
return fetchJson(`/api/v1/connections/${entityType}/${manageIdentifier}`);
291+
}
292+
289293
export function deleteConnectionById(connectionId) {
290294
return fetchDelete(`/api/v1/connections/${connectionId}`);
291295
}

client/src/locale/en.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,10 @@ const en = {
11751175
oidc10_rp: "OIDC Relying Party",
11761176
saml20_sp: "SAML Service Provider",
11771177
searchPlaceHolder: "Search for Manage entities"
1178+
},
1179+
manageDetail: {
1180+
backToSystem: "← Back to manage apps",
1181+
import: "Import"
11781182
}
11791183

11801184
}

client/src/pages/ManageDetail.jsx

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import "./ManageDetail.scss";
2+
import "../styles/access_card.scss";
3+
import React, {useEffect, useState} from "react";
4+
import {getByManageIdentifiers, publicServiceProviderByDetail} from "../api/index.js";
5+
import I18n from "../locale/I18n.js";
6+
import {useNavigate, useParams} from "react-router-dom";
7+
import {Button, Loader} from "@surfnet/sds";
8+
import PlaceHolderImage from "@surfnet/sds/icons/placeholder-image.svg";
9+
import {providerName, providerOrganizationName} from "../utils/Manage.js";
10+
import {isEmpty, stopEvent} from "../utils/Utils.js";
11+
import ConfirmationDialog from "../components/ConfirmationDialog.jsx";
12+
13+
const ManageDetail = () => {
14+
15+
const {manageType, manageId} = useParams();
16+
const navigate = useNavigate();
17+
const [loading, setLoading] = useState(true);
18+
const [serviceProvider, setServiceProvider] = useState({});
19+
const [connection, setConnection] = useState({});
20+
const [section, setSection] = useState({});
21+
const [confirmation, setConfirmation] = useState({});
22+
23+
useEffect(() => {
24+
publicServiceProviderByDetail(manageType, manageId)
25+
.then(res => {
26+
setServiceProvider(res);
27+
//We can't combine the two calls, as getByManageIdentifiers might throw a 404
28+
getByManageIdentifiers(manageType, manageId)
29+
.then(conn => setConnection(conn))
30+
.catch(() => true);
31+
setLoading(false);
32+
})
33+
.catch(() => {
34+
navigate("/404");
35+
});
36+
}, [manageType, manageId]);// eslint-disable-line react-hooks/exhaustive-deps
37+
38+
if (loading) {
39+
return <Loader/>
40+
}
41+
42+
const renderCurrentSection = () => {
43+
switch (section) {
44+
case "migrate": {
45+
return "Migrate";
46+
}
47+
case "import": {
48+
return "Import";
49+
}
50+
}
51+
return <code>{JSON.stringify(connection)}</code>
52+
}
53+
54+
const backToSystem = e => {
55+
stopEvent(e);
56+
navigate("/system/manage");
57+
}
58+
59+
const renderLogo = metaDataFields => {
60+
const logoUrl = metaDataFields["logo:0:url"];
61+
return isEmpty(logoUrl) ? <PlaceHolderImage/> : <img src={logoUrl} alt=""/>
62+
}
63+
64+
/**
65+
* TODO. There are two options:
66+
*
67+
* 1) Not present in Access database, choose organization, import as application or as connection under application X?
68+
* 2) Present in Access database, choose other organization, move the application of the connection to different organization?
69+
*
70+
* First provide status in Chip, then based on 1/ or 2/, show the information needs to be filled in with dynamic components
71+
*/
72+
73+
const renderApp = () => {
74+
return (
75+
<>
76+
<div className="manage-detail-top">
77+
<a href="/#" onClick={backToSystem}>{I18n.t("manageDetail.backToSystem")}</a>
78+
</div>
79+
<div className="inner-manage-detail-container">
80+
<div className="manage-detail">
81+
<div className="meta-data">
82+
{renderLogo(serviceProvider.data.metaDataFields)}
83+
<div className="meta-data-name">
84+
<p className="organization">
85+
{providerOrganizationName(I18n.locale, serviceProvider)}
86+
</p>
87+
<p className="name">
88+
{providerName(I18n.locale, serviceProvider)}
89+
</p>
90+
</div>
91+
92+
<Button onClick={() => alert("todo")}
93+
txt={I18n.t("manageDetail.import")}/>
94+
</div>
95+
{renderCurrentSection()}
96+
</div>
97+
</div>
98+
</>
99+
);
100+
}
101+
102+
const {open, cancel, isError, action, question, title, okButton} = confirmation;
103+
104+
return (
105+
<div className={`manage-detail-container`}>
106+
{open && <ConfirmationDialog confirm={action}
107+
cancel={cancel}
108+
isError={isError}
109+
confirmationTxt={okButton}
110+
confirmationHeader={title}
111+
question={question}>
112+
</ConfirmationDialog>}
113+
<div className="inner-manage-detail-container">
114+
{renderApp()}
115+
</div>
116+
</div>
117+
);
118+
}
119+
120+
export default ManageDetail;

client/src/pages/ManageDetail.scss

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
@use "../index";
2+
@use "../styles/vars.scss" as *;
3+
4+
div.manage-detail-container {
5+
width: 100%;
6+
7+
$width: 920px;
8+
9+
.inner-manage-detail-container {
10+
width: 100%;
11+
background-color: white;
12+
}
13+
14+
.manage-detail-top {
15+
background-color: white;
16+
padding: 0 25px;
17+
18+
a {
19+
color: var(--sds--color--gray--500);
20+
text-decoration: none;
21+
}
22+
}
23+
24+
.manage-detail {
25+
@include index.page;
26+
margin: 0 25px;
27+
background-color: white;
28+
max-width: $width;
29+
padding: 25px 0;
30+
31+
&.stand-alone {
32+
margin: 0 25px;
33+
}
34+
35+
.meta-data {
36+
display: flex;
37+
align-items: center;
38+
padding-bottom: 25px;
39+
margin-bottom: 15px;
40+
border-bottom: 1px solid var(--sds--color--gray--300);
41+
42+
img {
43+
width: 140px;
44+
height: auto;
45+
border: 1px solid var(--sds--color--gray--200);
46+
padding: 12px;
47+
border-radius: 8px;
48+
margin-right: 25px;
49+
}
50+
51+
.meta-data-name {
52+
53+
display: flex;
54+
flex-direction: column;
55+
gap: 12px;
56+
57+
p.organization {
58+
color: var(--sds--color--blue--400);
59+
font-size: 20px;
60+
}
61+
62+
p.name {
63+
font-weight: 600;
64+
font-size: 24px;
65+
}
66+
}
67+
68+
button {
69+
margin-left: auto;
70+
}
71+
}
72+
}
73+
74+
75+
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import access.model.ConnectionStatus;
1515
import access.model.EntityType;
1616
import access.model.Environment;
17+
import access.model.Organization;
1718
import access.model.User;
1819
import access.repository.ApplicationRepository;
1920
import access.repository.ConnectionRepository;
@@ -111,6 +112,25 @@ public ResponseEntity<Connection> find(User user, @PathVariable("connectionId")
111112
return ResponseEntity.ok(connection);
112113
}
113114

115+
@GetMapping({"/{manageType}/{manageIdentifier}"})
116+
public ResponseEntity<?> findByManage(User user,
117+
@PathVariable("manageType") EntityType entityType,
118+
@PathVariable String manageIdentifier) {
119+
LOG.debug("/find findByManage for " + user.getEmail());
120+
121+
return connectionRepository.findByProtocolAndManageIdentifier(entityType, manageIdentifier)
122+
.map(connection -> {
123+
Application application = connection.getApplication();
124+
Organization organization = application.getOrganization();
125+
return ResponseEntity.ok(Map.of(
126+
"connection", connection,
127+
"application", application,
128+
"organization", organization
129+
));
130+
})
131+
.orElse(ResponseEntity.notFound().build());
132+
}
133+
114134
@PostMapping({"", "/"})
115135
public ResponseEntity<Connection> create(User user, @Validated @RequestBody Connection connection) {
116136
LOG.debug("/create connection by " + user.getEmail());

server/src/main/java/access/repository/ConnectionRepository.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package access.repository;
22

33
import access.model.Connection;
4+
import access.model.EntityType;
45
import org.springframework.data.jpa.repository.EntityGraph;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Modifying;
@@ -17,6 +18,9 @@ public interface ConnectionRepository extends JpaRepository<Connection, Long> {
1718
@EntityGraph(attributePaths = {"application.organization"})
1819
Optional<Connection> findDetailsById(Long id);
1920

21+
@EntityGraph(attributePaths = {"application.organization"})
22+
Optional<Connection> findByProtocolAndManageIdentifier(EntityType entityType, String manageIdentifier);
23+
2024
@Modifying
2125
@Query(value = "DELETE FROM connections WHERE id = ?1", nativeQuery = true)
2226
@Transactional(isolation = Isolation.SERIALIZABLE)

server/src/test/java/access/api/ConnectionControllerTest.java

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,18 @@
22

33
import access.AbstractTest;
44
import access.AccessCookieFilter;
5-
import access.model.*;
5+
import access.model.Application;
6+
import access.model.Connection;
7+
import access.model.ConnectionStatus;
8+
import access.model.EntityType;
9+
import access.model.Environment;
10+
import access.model.GrantType;
11+
import access.model.State;
612
import com.fasterxml.jackson.core.type.TypeReference;
713
import io.restassured.common.mapper.TypeRef;
814
import io.restassured.http.ContentType;
915
import lombok.SneakyThrows;
1016
import org.apache.commons.io.IOUtils;
11-
import org.junit.jupiter.api.Disabled;
1217
import org.junit.jupiter.api.Test;
1318
import org.springframework.core.io.ClassPathResource;
1419
import org.springframework.http.HttpStatus;
@@ -202,6 +207,42 @@ void updateAndCreateChangeRequest() {
202207
assertEquals(2, changeRequests.size());
203208
}
204209

210+
@Test
211+
void find() {
212+
AccessCookieFilter accessCookieFilter = mockLoginFlow(MANAGE_SUB);
213+
Connection connection = connectionRepository.findById(seedIdentifiers.get(BUDDY_CHECK_PROD)).get();
214+
215+
Connection fetchedConnection = given()
216+
.when()
217+
.filter(accessCookieFilter.cookieFilter())
218+
.header(csrfHeader(accessCookieFilter))
219+
.accept(ContentType.JSON)
220+
.contentType(ContentType.JSON)
221+
.pathParam("manageType", connection.getProtocol())
222+
.pathParam("manageIdentifier", connection.getManageIdentifier())
223+
.get("/api/v1/connections/{manageType}/{manageIdentifier}")
224+
.as(Connection.class);
225+
226+
assertEquals(connection.getId(), fetchedConnection.getId());
227+
}
228+
229+
@Test
230+
void find404() {
231+
AccessCookieFilter accessCookieFilter = mockLoginFlow(MANAGE_SUB);
232+
given()
233+
.when()
234+
.filter(accessCookieFilter.cookieFilter())
235+
.header(csrfHeader(accessCookieFilter))
236+
.accept(ContentType.JSON)
237+
.contentType(ContentType.JSON)
238+
.pathParam("manageType", EntityType.oidc10_rp)
239+
.pathParam("manageIdentifier", "nope")
240+
.get("/api/v1/connections")
241+
.then()
242+
.statusCode(HttpStatus.NOT_FOUND.value());
243+
244+
}
245+
205246
@SneakyThrows
206247
@Test
207248
void findAndSyncWithManage() {
@@ -226,7 +267,7 @@ void findAndSyncWithManage() {
226267
//See /manage/playground_rp.json
227268
assertEquals(244, connection.get("manageVersion"));
228269
assertEquals(ConnectionStatus.PROD_READY.name(), connection.get("status"));
229-
assertEquals(2, ((List)connection.get("changeRequests")).size());
270+
assertEquals(2, ((List) connection.get("changeRequests")).size());
230271
}
231272

232273
@SneakyThrows
@@ -252,7 +293,8 @@ void findChangeRequests() {
252293
});
253294
assertEquals(2, changeRequests.size());
254295
}
255-
@Test
296+
297+
@Test
256298
void resetSecret() {
257299
AccessCookieFilter accessCookieFilter = mockLoginFlow(MANAGE_SUB);
258300
Map<String, String> newSecret = given()

0 commit comments

Comments
 (0)