Skip to content

Commit

Permalink
All invitations for super user
Browse files Browse the repository at this point in the history
  • Loading branch information
oharsta committed Aug 23, 2023
1 parent a0ba964 commit 5775411
Show file tree
Hide file tree
Showing 11 changed files with 92 additions and 33 deletions.
4 changes: 4 additions & 0 deletions client/src/api/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ export function deleteInvitation(invitationId) {
return fetchDelete(`/api/v1/invitations/${invitationId}`);
}

export function allInvitations() {
return fetchJson(`/api/v1/invitations/all`, {}, {}, false);
}

//Manage
export function allProviders() {
return fetchJson("/api/v1/manage/providers");
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/Page.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import "./Page.scss";

export const Page = ({Icon, label, name, children}) => {
export const Page = ({children}) => {

return (
<div className="page">
Expand Down
2 changes: 1 addition & 1 deletion client/src/pages/Role.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const Role = () => {
label={I18n.t("tabs.invitations")}
Icon={InvitationLogo}>
<Invitations role={res[0]}
invitations={res[2]}/>
preloadedInvitations={res[2]}/>
</Page>
];
setTabs(newTabs);
Expand Down
14 changes: 13 additions & 1 deletion client/src/pages/System.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ import React, {useEffect, useState} from "react";
import I18n from "../locale/I18n";
import "./System.scss";
import {Loader} from "@surfnet/sds";
import {useParams} from "react-router-dom";
import {useNavigate, useParams} from "react-router-dom";
import {useAppStore} from "../stores/AppStore";
import {ReactComponent as CronLogo} from "@surfnet/sds/icons/illustrative-icons/database-check.svg";
import Tabs from "../components/Tabs";
import {Page} from "../components/Page";
import {Cron} from "../tabs/Cron";
import {Invitations} from "../tabs/Invitations";
import {ReactComponent as InvitationLogo} from "@surfnet/sds/icons/functional-icons/id-1.svg";


export const System = () => {
const navigate = useNavigate();
const {tab = "cron"} = useParams();
const [loading, setLoading] = useState(false);
const [currentTab, setCurrentTab] = useState(tab);
Expand All @@ -29,7 +33,14 @@ export const System = () => {
label={I18n.t("tabs.cron")}
Icon={CronLogo}>
<Cron/>
</Page>,
<Page key="invitations"
name="invitations"
label={I18n.t("tabs.invitations")}
Icon={InvitationLogo}>
<Invitations standAlone={true}/>
</Page>

];
setTabs(newTabs);
setLoading(false);
Expand All @@ -38,6 +49,7 @@ export const System = () => {

const tabChanged = (name) => {
setCurrentTab(name);
navigate(`/system/${name}`);
}

if (loading) {
Expand Down
59 changes: 32 additions & 27 deletions client/src/tabs/Invitations.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useEffect, useState} from "react";
import React, {useEffect, useRef, useState} from "react";
import I18n from "../locale/I18n";
import "./Invitations.scss";
import {Button, ButtonSize, ButtonType, Checkbox, Chip, Loader, Tooltip} from "@surfnet/sds";
Expand All @@ -8,44 +8,49 @@ import {shortDateFromEpoch} from "../utils/Date";

import {chipTypeForInvitationStatus, chipTypeForUserRole} from "../utils/Authority";
import {useNavigate} from "react-router-dom";
import {deleteInvitation, resendInvitation} from "../api";
import {allInvitations, deleteInvitation, resendInvitation} from "../api";
import ConfirmationDialog from "../components/ConfirmationDialog";
import {useAppStore} from "../stores/AppStore";
import {isEmpty, pseudoGuid} from "../utils/Utils";
import {allowedToDeleteInvitation, INVITATION_STATUS} from "../utils/UserRole";


export const Invitations = ({role, invitations}) => {
export const Invitations = ({role, preloadedInvitations, standAlone = false}) => {
const navigate = useNavigate();
const {user, setFlash} = useAppStore(state => state);

const invitations = useRef();
const [selectedInvitations, setSelectedInvitations] = useState({});
const [allSelected, setAllSelected] = useState(false);
const [resultAfterSearch, setResultAfterSearch] = useState(invitations)
const [resultAfterSearch, setResultAfterSearch] = useState([])
const [loading, setLoading] = useState(true);
const [confirmation, setConfirmation] = useState({});
const [confirmationOpen, setConfirmationOpen] = useState(false);

useEffect(() => {
invitations.forEach(invitation => {
invitation.intendedRoles = invitation.roles
.sort((r1, r2) => r1.role.name.localeCompare(r2.role.name))
.map(role => role.role.name).join(", ");
const now = new Date();
invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status;
});
setSelectedInvitations(invitations
.reduce((acc, invitation) => {
acc[invitation.id] = {
selected: false,
ref: invitation,
allowed: allowedToDeleteInvitation(user, invitation)
};
return acc;
}, {}));
setLoading(false);
const promise = standAlone ? allInvitations() : Promise.resolve(preloadedInvitations);
promise.then(res => {
res.forEach(invitation => {
invitation.intendedRoles = invitation.roles
.sort((r1, r2) => r1.role.name.localeCompare(r2.role.name))
.map(role => role.role.name).join(", ");
const now = new Date();
invitation.status = new Date(invitation.expiryDate * 1000) < now ? INVITATION_STATUS.EXPIRED : invitation.status;
});
setSelectedInvitations(res
.reduce((acc, invitation) => {
acc[invitation.id] = {
selected: false,
ref: invitation,
allowed: allowedToDeleteInvitation(user, invitation)
};
return acc;
}, {}));
invitations.current = res;
setResultAfterSearch(res);
setLoading(false);
})
},
[invitations, user])
[invitations, user]) // eslint-disable-line react-hooks/exhaustive-deps

const onCheck = invitation => e => {
const checked = e.target.checked;
Expand Down Expand Up @@ -219,7 +224,7 @@ export const Invitations = ({role, invitations}) => {
mapper: invitation => shortDateFromEpoch(invitation.roleExpiryDate)
}];

const countInvitations = invitations.length;
const countInvitations = invitations.current.length;
const hasEntities = countInvitations > 0;
let title = "";

Expand All @@ -237,14 +242,14 @@ export const Invitations = ({role, invitations}) => {
confirmationTxt={confirmation.confirmationTxt}
question={confirmation.question}/>}

<Entities entities={invitations}
<Entities entities={invitations.current}
modelName="invitations"
defaultSort="name"
columns={columns}
title={title}
newLabel={I18n.t("invitations.new")}
showNew={true}
newEntityFunc={() => navigate("/invitation/new", {state: role.id})}
showNew={!!role}
newEntityFunc={role ? () => navigate("/invitation/new", {state: role.id}) : null}
hideTitle={true}
customNoEntities={I18n.t(`invitations.noResults`)}
loading={false}
Expand Down
2 changes: 2 additions & 0 deletions client/src/tabs/Invitations.scss
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
.mod-invitations {
background-color: white;

table.invitations {

thead {
Expand Down
7 changes: 7 additions & 0 deletions server/src/main/java/access/api/InvitationController.java
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ public ResponseEntity<MetaInvitation> getInvitation(@RequestParam("hash") String
return ResponseEntity.ok(new MetaInvitation(invitation, providers));
}

@GetMapping("all")
public ResponseEntity<List<Invitation>> all(@Parameter(hidden = true) User user) {
LOG.debug("/all invitations");
UserPermissions.assertSuperUser(user);
return ResponseEntity.ok(invitationRepository.findByStatus(Status.OPEN));
}


@PostMapping("accept")
public ResponseEntity<Map<String, Integer>> accept(@Validated @RequestBody AcceptInvitation acceptInvitation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public interface InvitationRepository extends JpaRepository<Invitation, Long> {
attributePaths = {"inviter", "roles", "roles.role"})
Optional<Invitation> findByHash(String hash);

Optional<Invitation> findByIdAndStatus(Long id, Status status);
List<Invitation> findByStatus(Status status);

List<Invitation> findByStatusAndRoles_role(Status status, Role role);
}
5 changes: 3 additions & 2 deletions server/src/test/java/access/AbstractTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.util.MultiValueMap;
import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponentsBuilder;

import java.io.IOException;
Expand Down Expand Up @@ -216,7 +217,7 @@ protected AccessCookieFilter openIDConnectFlow(String path, String sub, Consumer

Map<String, Object> userInfo = objectMapper.readValue(new ClassPathResource("user-info.json").getInputStream(), new TypeReference<>() {
});
userInfo.put("sub", sub);
userInfo.put("sub", StringUtils.hasText(sub) ? sub : "sub");
userInfo.put("email", sub);
userInfo.put("eduperson_principal_name", sub);
String userInfoResult = objectMapper.writeValueAsString(userInfo);
Expand Down Expand Up @@ -288,7 +289,7 @@ protected JWTClaimsSet getJwtClaimsSet(String clientId, String sub, String redir
.jwtID(UUID.randomUUID().toString())
.issuer(clientId)
.issueTime(Date.from(instant))
.subject(sub)
.subject(StringUtils.hasText(sub) ? sub : "sub")
.notBeforeTime(new Date(System.currentTimeMillis()))
.claim("redirect_uri", redirectURI)
.claim("eduperson_principal_name", sub)
Expand Down
13 changes: 13 additions & 0 deletions server/src/test/java/access/api/InvitationControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -316,4 +316,17 @@ void byRole() throws Exception {
assertEquals(2, invitations.size());
}

@Test
void all() throws Exception {
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", SUPER_SUB);
List<Invitation> invitations = given()
.when()
.filter(accessCookieFilter.cookieFilter())
.accept(ContentType.JSON)
.contentType(ContentType.JSON)
.get("/api/v1/invitations/all")
.as(new TypeRef<>() {
});
assertEquals(5, invitations.size());
}
}
15 changes: 15 additions & 0 deletions server/src/test/java/access/api/UserControllerTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,21 @@ void configNewUserWithOauth2Login() throws Exception {
assertEquals("John Doe", res.get("name"));
}

@Test
void configMissingAttributes() throws Exception {
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/login", "");

Map res = given()
.when()
.filter(accessCookieFilter.cookieFilter())
.accept(ContentType.JSON)
.contentType(ContentType.JSON)
.get("/api/v1/users/config")
.as(Map.class);
assertFalse((Boolean) res.get("authenticated"));
assertEquals(2, ((List)res.get("missingAttributes")).size());
}

@Test
void meWithOauth2Login() throws Exception {
AccessCookieFilter accessCookieFilter = openIDConnectFlow("/api/v1/users/me", "urn:collab:person:example.com:admin");
Expand Down

0 comments on commit 5775411

Please sign in to comment.