diff --git a/teams-gui/src/App.js b/teams-gui/src/App.js index 931f0e7a..94ef592b 100644 --- a/teams-gui/src/App.js +++ b/teams-gui/src/App.js @@ -41,8 +41,9 @@ const App = () => { } else { //shortcut notation currentUser.superAdmin = currentUser.person.superAdmin; - currentUser.superAdminModus = false; + currentUser.superAdminModus = currentUser.superAdmin; setUser(currentUser); + setSuperAdmin(currentUser.superAdminModus); setLoading(false); } }) diff --git a/teams-gui/src/api/index.js b/teams-gui/src/api/index.js index 744ba074..1162e79a 100644 --- a/teams-gui/src/api/index.js +++ b/teams-gui/src/api/index.js @@ -197,6 +197,14 @@ export function changeRole(membershipProperties) { return postPutJson("memberships", membershipProperties); } +export function teamInviteAppDetails(id) { + return fetchJson(`invite-app/${id}`) +} + +export function teamInviteAppMigrate(id) { + return postPutJson("invite-app/migrate", {id: id}, "PUT"); +} + export function changeExpiryDate(membershipExpiryDate) { return postPutJson("memberships/expiry-date", membershipExpiryDate); } diff --git a/teams-gui/src/components/MigrateTeamForm.js b/teams-gui/src/components/MigrateTeamForm.js new file mode 100644 index 00000000..6a3feee8 --- /dev/null +++ b/teams-gui/src/components/MigrateTeamForm.js @@ -0,0 +1,92 @@ +import I18n from "i18n-js"; +import {React, useEffect, useState} from "react"; +import "./MigrateTeamForm.scss"; +import {ReactComponent as BackIcon} from "../icons/arrow-left-1.svg"; +import {teamInviteAppDetails} from "../api"; +import {SpinnerField} from "./SpinnerField"; +import {Button} from "./Button"; + +export const MigrateTeamForm = ({migrateTeam, user, team, setShowForm}) => { + + const [loaded, setLoaded] = useState(false); + const [teamDetails, setTeamDetails] = useState(false); + + useEffect(() => { + teamInviteAppDetails(team.id).then(res => { + setTeamDetails(res); + setLoaded(true); + }) + }, [team]) + + if (!loaded) { + return ; + } + + return ( +
+ +

{I18n.t("migrateTeam.header")}

+

{I18n.t("migrateTeam.info")}

+ + + + + + + + + + {teamDetails.applications.map(application => <> + + + + + + + + + + + + + + + + )} + {teamDetails.memberships.map(membership => <> + + + + + + + + + + + + + + + + + + + + ) } + +
{I18n.t("migrateTeam.table.attribute")}{I18n.t("migrateTeam.table.value")}
{I18n.t("migrateTeam.table.application")}
{I18n.t("migrateTeam.table.manageId")}{application.manageId}
{I18n.t("migrateTeam.table.manageType")}{application.manageType}
{I18n.t("migrateTeam.table.landingPage")}{application.landingPage}
{I18n.t("migrateTeam.table.membership", {name: membership.person.name})}
{I18n.t("migrateTeam.table.role")}{membership.role}
{I18n.t("migrateTeam.table.urn")}{membership.person.urn}
{I18n.t("migrateTeam.table.schacHome")}{membership.person.schacHomeOrganization}
{I18n.t("migrateTeam.table.email")}{membership.person.email}
+
+
+ + +
+ ); +}; diff --git a/teams-gui/src/components/MigrateTeamForm.scss b/teams-gui/src/components/MigrateTeamForm.scss new file mode 100644 index 00000000..61f03967 --- /dev/null +++ b/teams-gui/src/components/MigrateTeamForm.scss @@ -0,0 +1,84 @@ +@import "../styles/vars.scss"; + +div.migrate-teams-form-container { + display: flex; + flex-direction: column; + width: 75%; + + @media (max-width: $width-app) { + width: 100%; + padding: 0 10px 10px 10px; + } + + .back { + margin: 25px 0; + cursor: pointer; + display: flex; + align-items: center; + color: $primary-blue; + background-color: transparent; + border: none; + font-family: "Source Sans Pro", sans-serif; + font-size: 16px; + max-width: 100px; + + svg { + width: 16px; + height: auto; + margin-right: 14px; + } + } + + p { + margin: 25px 0 0 0; + + &.last { + margin: 25px 0; + } + } + + table { + margin: 20px 0 40px 0; + + th, td { + padding: 10px 0; + + &.attr { + width: 35%; + } + + &.value { + width: 65%; + } + + } + + thead { + tr { + border-bottom: 1px solid black; + } + } + + tbody { + tr { + border-bottom: 1px solid $primary-grey; + td { + &.instance { + font-weight: 600; + } + } + } + } + + } + + div.actions { + display: flex; + margin-bottom: 25px; + + a { + margin: 0 25px 25px auto; + } + } +} + diff --git a/teams-gui/src/locale/en.js b/teams-gui/src/locale/en.js index de4cbafe..98380914 100644 --- a/teams-gui/src/locale/en.js +++ b/teams-gui/src/locale/en.js @@ -18,6 +18,7 @@ I18n.translations.en = { "teams": "Join request", "join-request": "Join request", "add-members": "Add members", + "migrate": "Migrate team", "include-team" : "Include team", "missing-attributes": "Missing attributes", }, @@ -32,7 +33,8 @@ I18n.translations.en = { myteams: { tabs: { myTeams: "My teams", - publicTeams: "Public teams" + publicTeams: "Public teams", + migrations: "Invite migration" }, columns: { title: "Team name", @@ -198,12 +200,14 @@ I18n.translations.en = { manager: "Manager", admin: "Admin", owner: "Owner", + superUser: "Superuser", title: "You're {{role}}" }, details: { leave: "Leave team", delete: "Delete team", edit: "Edit team", + migrate: "Migrate", confirmations: { leave: "Are you sure you want to leave this team?", delete: "Are you sure you want to delete this team?" @@ -349,7 +353,25 @@ I18n.translations.en = { email: "Email" } }, - + migrateTeam: { + header: "Migration", + info: "Review the applications and schacHome values of the persons and hit 'Migrate' to transfer this team to the invite application. The team will be deleted after an successful migration.", + migrate: "Migrate", + table: { + attribute: "Attribute", + value: "Value", + name: "Name", + application: "Application", + manageId: "Manage identifier", + manageType: "Entity type", + landingPage: "Landing page", + membership: "Membership {{name}}", + role: "Role", + urn: "Unspecified urn", + schacHome: "SchacHome organization", + email: "Email" + } + } }; export default I18n.translations.en; diff --git a/teams-gui/src/locale/nl.js b/teams-gui/src/locale/nl.js index ca09dc2d..c19d8c72 100644 --- a/teams-gui/src/locale/nl.js +++ b/teams-gui/src/locale/nl.js @@ -18,6 +18,7 @@ I18n.translations.nl = { "teams": "Toetredingsverzoek", "join-request": "Toetredingsverzoek", "add-members": "Leden toevoegen", + "migrate": "Migreer team", "include-team" : "Team koppelen", "missing-attributes": "Missende attributes", }, @@ -32,7 +33,8 @@ I18n.translations.nl = { myteams: { tabs: { myTeams: "Mijn teams", - publicTeams: "Publieke teams" + publicTeams: "Publieke teams", + migrations: "Invite migratie" }, columns: { title: "Teamnaam", @@ -198,12 +200,14 @@ I18n.translations.nl = { manager: "Manager", admin: "Beheerder", owner: "Eigenaar", + superUser: "Superuser", title: "Je bent {{role}}" }, details: { leave: "Verlaat team", delete: "Verwijder team", edit: "Bewerk team", + migrate: "Migreer", confirmations: { leave: "Weet je zeker dat je dit team wilt verlaten?", delete: "Weet je zeker dat je dit team wilt verwijderen?" diff --git a/teams-gui/src/pages/TeamDetails.js b/teams-gui/src/pages/TeamDetails.js index 431fbcaa..f54b110e 100644 --- a/teams-gui/src/pages/TeamDetails.js +++ b/teams-gui/src/pages/TeamDetails.js @@ -21,6 +21,7 @@ import { getTeamDetailByPublicLink, rejectJoinRequest, resetPublicLink, + teamInviteAppMigrate, } from "../api"; import I18n from "i18n-js"; import {ActionMenu} from "../components/ActionMenu"; @@ -51,6 +52,7 @@ import {setFlash} from "../flash/events"; import TeamWelcomeDialog from "../components/TeamWelcomeDialog"; import {MarkDown} from "../components/MarkDown"; import {DateField} from "../components/DateField"; +import {MigrateTeamForm} from "../components/MigrateTeamForm"; let currentExpiryDate; @@ -69,6 +71,7 @@ const TeamDetail = ({user, showMembers = false}) => { }); const [isNewTeam, setIsNewTeam] = useState(showMembers); const [showAddMembersForm, setShowAddMembersForm] = useState(showMembers); + const [showMigrateForm, setShowShowMigrateForm] = useState(showMembers); const [alerts, setAlerts] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [memberList, setMembersList] = useState([]); @@ -687,6 +690,19 @@ const TeamDetail = ({user, showMembers = false}) => { navigate(`/edit-team/${team.id}`); } + const startMigrationForm = () => { + addHistoryState(); + setShowAddMembersForm(false); + setShowAddAdminsButton(false); + setShowShowMigrateForm(true); + document.title = I18n.t("headerTitles.index", {page: I18n.t("headerTitles.migrate")}); + } + + const migrateTeam = () => { + teamInviteAppMigrate(team.id) + .then(() => navigate("/")); + } + const processDeleteTeam = (showConfirmation) => { if (showConfirmation) { setConfirmation({ @@ -724,6 +740,12 @@ const TeamDetail = ({user, showMembers = false}) => { action: () => processDeleteTeam(true), }); } + if (user.superAdminModus) { + actions.push({ + name: I18n.t("details.migrate"), + action: () => startMigrationForm(), + }) + } return actions; }; @@ -826,7 +848,8 @@ const TeamDetail = ({user, showMembers = false}) => { /> )} {renderAlertBanners()} - {(!showAddMembersForm && !selectedJoinRequest && !selectedInvitation && !showExternalTeams && !invitationInvalid) && ( + {(!showAddMembersForm && !selectedJoinRequest && !selectedInvitation && !showExternalTeams && !invitationInvalid + && !showMigrateForm) && (
{!team.hideMembers &&

{I18n.t("teamDetails.members")} ({memberList.length})

} {team.hideMembers &&

{I18n.t("teamDetails.hideMembers")}

} @@ -891,6 +914,11 @@ const TeamDetail = ({user, showMembers = false}) => { user={user} team={team} setShowForm={setShowExternalTeams}/>} + + {showMigrateForm && } ); }; diff --git a/teams-gui/src/utils/roles.js b/teams-gui/src/utils/roles.js index 5a48b7bb..10080098 100644 --- a/teams-gui/src/utils/roles.js +++ b/teams-gui/src/utils/roles.js @@ -52,6 +52,9 @@ export function isOnlyAdmin(team, currentUser) { } export const actionDropDownTitle = (team, user) => { + if (user.superAdminModus) { + return I18n.t("roles.title", {role: I18n.t("roles.superUser")}); + } const membership = getMembership(team, user); if (!membership) { return I18n.t("roles.title", {role: I18n.t("roles.guest")}); diff --git a/teams-server/src/main/java/teams/api/InviteController.java b/teams-server/src/main/java/teams/api/InviteController.java index 864b90b9..7bee967b 100644 --- a/teams-server/src/main/java/teams/api/InviteController.java +++ b/teams-server/src/main/java/teams/api/InviteController.java @@ -13,6 +13,7 @@ import teams.domain.FederatedUser; import teams.domain.Team; import teams.exception.NotAllowedException; +import teams.exception.ResourceNotFoundException; import teams.repository.TeamRepository; import java.util.Map; @@ -52,8 +53,9 @@ public ResponseEntity teamDetails(@PathVariable("id") Long id) { } private ResponseEntity doTeamDetails(Long id) { - Team team = teamRepository.findFirstById(id); + Team team = teamRepository.findById(id).orElseThrow(ResourceNotFoundException::new); team.getApplications().forEach(Application::getLandingPage); + team.getMemberships().forEach(membership -> membership.getPerson().getSchacHomeOrganization()); return ResponseEntity.ok(team); } @@ -69,13 +71,13 @@ public ResponseEntity migrateTeam(@RequestBody Map teamIdent } private ResponseEntity doMigrate(Map teamIdentifier) { - Team team = teamRepository.findFirstById(teamIdentifier.get("id")); + //We must avoid hibernateLazyInitializer errors, so do not use the repository + Team team = doTeamDetails(teamIdentifier.get("id")).getBody(); //Must ensure the migration will work Set applications = team.getApplications(); if (CollectionUtils.isEmpty(applications)) { throw new IllegalArgumentException("No applications"); } - restTemplate.put(inviteUrl, team); teamRepository.delete(team); diff --git a/teams-server/src/main/java/teams/domain/Person.java b/teams-server/src/main/java/teams/domain/Person.java index b77dbcfb..8c6eb3c4 100644 --- a/teams-server/src/main/java/teams/domain/Person.java +++ b/teams-server/src/main/java/teams/domain/Person.java @@ -39,6 +39,9 @@ public class Person implements Serializable { @Column private Instant lastLoginDate; + @Column + private String schacHomeOrganization; + @Column private boolean guest; diff --git a/teams-server/src/main/java/teams/domain/Team.java b/teams-server/src/main/java/teams/domain/Team.java index c5b84f83..029d57d7 100644 --- a/teams-server/src/main/java/teams/domain/Team.java +++ b/teams-server/src/main/java/teams/domain/Team.java @@ -1,6 +1,7 @@ package teams.domain; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -27,6 +28,7 @@ @Getter @Setter @NoArgsConstructor +@JsonIgnoreProperties({"hibernateLazyInitializer", "handler"}) public class Team implements HashGenerator, Serializable { @Id diff --git a/teams-server/src/main/java/teams/exception/ResourceNotFoundException.java b/teams-server/src/main/java/teams/exception/ResourceNotFoundException.java index c4ca3801..bdd3da1c 100644 --- a/teams-server/src/main/java/teams/exception/ResourceNotFoundException.java +++ b/teams-server/src/main/java/teams/exception/ResourceNotFoundException.java @@ -1,9 +1,11 @@ package teams.exception; +import lombok.NoArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value = HttpStatus.NOT_FOUND) +@NoArgsConstructor public class ResourceNotFoundException extends RuntimeException { public ResourceNotFoundException(String message) { diff --git a/teams-server/src/main/java/teams/shibboleth/mock/MockShibbolethFilter.java b/teams-server/src/main/java/teams/shibboleth/mock/MockShibbolethFilter.java index 4eabd949..7df941c4 100644 --- a/teams-server/src/main/java/teams/shibboleth/mock/MockShibbolethFilter.java +++ b/teams-server/src/main/java/teams/shibboleth/mock/MockShibbolethFilter.java @@ -15,8 +15,7 @@ public class MockShibbolethFilter extends GenericFilterBean { private final boolean test; - private final String userUrn = "urn:collab:person:surfnet.nl:jdoe"; - //"urn:collab:person:surfnet.nl:super_admin" + private final String userUrn = "urn:collab:person:surfnet.nl:super_admin";//"urn:collab:person:surfnet.nl:jdoe"; public MockShibbolethFilter(boolean test) { this.test = test; diff --git a/teams-server/src/test/java/teams/api/InviteControllerTest.java b/teams-server/src/test/java/teams/api/InviteControllerTest.java index cb420f68..3236ebde 100644 --- a/teams-server/src/test/java/teams/api/InviteControllerTest.java +++ b/teams-server/src/test/java/teams/api/InviteControllerTest.java @@ -67,5 +67,21 @@ public void migrateTeam() { @Test public void migrateTeamExternal() { + stubFor(put( + urlEqualTo("/api/teams")) + .willReturn(aResponse().withStatus(200))); + + assertTrue(super.teamRepository.findById(2L).isPresent()); + + given() + .auth() + .preemptive() + .basic("teams", "secret") + .contentType(ContentType.JSON) + .body(Map.of("id", 2)) + .when() + .put("/api/v1/external/invite-app/migrate") + .then() + .statusCode(201); } } \ No newline at end of file