diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java b/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java index a601a5354..5bd992967 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/config/AccountWebController.java @@ -45,6 +45,7 @@ import stirling.software.proprietary.security.model.SessionEntity; import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamRepository; import stirling.software.proprietary.security.saml2.CustomSaml2AuthenticatedPrincipal; +import stirling.software.proprietary.security.service.TeamService; import stirling.software.proprietary.security.session.SessionPersistentRegistry; @Controller @@ -226,14 +227,27 @@ public class AccountWebController { while (iterator.hasNext()) { User user = iterator.next(); if (user != null) { + boolean shouldRemove = false; + + // Check if user is an INTERNAL_API_USER for (Authority authority : user.getAuthorities()) { if (authority.getAuthority().equals(Role.INTERNAL_API_USER.getRoleId())) { - iterator.remove(); + shouldRemove = true; roleDetails.remove(Role.INTERNAL_API_USER.getRoleId()); - // Break out of the inner loop once the user is removed break; } } + + // Also check if user is part of the Internal team + if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + shouldRemove = true; + } + + // Remove the user if either condition is true + if (shouldRemove) { + iterator.remove(); + continue; + } // Determine the user's session status and last request time int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval(); boolean hasActiveSession = false; @@ -336,7 +350,11 @@ public class AccountWebController { model.addAttribute("activeUsers", activeUsers); model.addAttribute("disabledUsers", disabledUsers); - List allTeams = teamRepository.findAll(); + // Get all teams but filter out the Internal team + List allTeams = teamRepository.findAll() + .stream() + .filter(team -> !team.getName().equals(stirling.software.proprietary.security.service.TeamService.INTERNAL_TEAM_NAME)) + .toList(); model.addAttribute("teams", allTeams); model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers()); diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java index 347c8b514..be0dda2b7 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/TeamController.java @@ -17,7 +17,9 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.proprietary.model.Team; import stirling.software.proprietary.security.config.PremiumEndpoint; import stirling.software.proprietary.security.database.repository.UserRepository; +import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamRepository; +import stirling.software.proprietary.security.service.TeamService; @Controller @RequestMapping("/api/v1/team") @@ -54,6 +56,12 @@ public class TeamController { return new RedirectView("/adminSettings?messageType=teamNameExists"); } Team team = existing.get(); + + // Prevent renaming the Internal team + if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible"); + } + team.setName(newName); teamRepository.save(team); return new RedirectView("/adminSettings?messageType=teamRenamed"); @@ -69,6 +77,12 @@ public class TeamController { } Team team = teamOpt.get(); + + // Prevent deleting the Internal team + if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible"); + } + long memberCount = userRepository.countByTeam(team); if (memberCount > 0) { return new RedirectView("/adminSettings?messageType=teamHasUsers"); @@ -77,4 +91,37 @@ public class TeamController { teamRepository.delete(team); return new RedirectView("/adminSettings?messageType=teamDeleted"); } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/addUser") + @Transactional + public RedirectView addUserToTeam( + @RequestParam("teamId") Long teamId, + @RequestParam("userId") Long userId) { + + // Find the team + Team team = teamRepository.findById(teamId) + .orElseThrow(() -> new RuntimeException("Team not found")); + + // Prevent adding users to the Internal team + if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/teams?error=internalTeamNotAccessible"); + } + + // Find the user + User user = userRepository.findById(userId) + .orElseThrow(() -> new RuntimeException("User not found")); + + // Check if user is in the Internal team - prevent moving them + if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/teams/" + teamId + "?error=cannotMoveInternalUsers"); + } + + // Assign user to team + user.setTeam(team); + userRepository.save(user); + + // Redirect back to team details page + return new RedirectView("/teams/" + teamId + "?messageType=userAdded"); + } } diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java index c2fbcce79..eb5e6ee67 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/controller/api/UserController.java @@ -253,6 +253,12 @@ public class UserController { if (defaultTeam != null) { effectiveTeamId = defaultTeam.getId(); } + } else { + // Check if the selected team is Internal - prevent assigning to it + Team selectedTeam = teamRepository.findById(effectiveTeamId).orElse(null); + if (selectedTeam != null && selectedTeam.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true); + } } if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { @@ -304,10 +310,21 @@ public class UserController { // Update the team if a teamId is provided if (teamId != null) { - teamRepository.findById(teamId).ifPresent(team -> { + Team team = teamRepository.findById(teamId).orElse(null); + if (team != null) { + // Prevent assigning to Internal team + if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/adminSettings?messageType=internalTeamNotAccessible", true); + } + + // Prevent moving users from Internal team + if (user.getTeam() != null && user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return new RedirectView("/adminSettings?messageType=cannotMoveInternalUsers", true); + } + user.setTeam(team); userRepository.save(user); - }); + } } userService.changeRole(user, role); diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java b/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java index 68a7035c7..96832c0ab 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/controller/web/TeamWebController.java @@ -21,6 +21,7 @@ import stirling.software.proprietary.security.database.repository.SessionReposit import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.repository.TeamRepository; +import stirling.software.proprietary.security.service.TeamService; @Controller @RequestMapping("/teams") @@ -36,7 +37,12 @@ public class TeamWebController { @PreAuthorize("hasRole('ROLE_ADMIN')") public String listTeams(Model model) { // Get teams with user counts using a DTO projection - List teamsWithCounts = teamRepository.findAllTeamsWithUserCount(); + List allTeamsWithCounts = teamRepository.findAllTeamsWithUserCount(); + + // Filter out the Internal team + List teamsWithCounts = allTeamsWithCounts.stream() + .filter(team -> !team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) + .toList(); // Get the latest activity for each team List teamActivities = sessionRepository.findLatestActivityByTeam(); @@ -63,9 +69,22 @@ public class TeamWebController { Team team = teamRepository.findById(id) .orElseThrow(() -> new RuntimeException("Team not found")); + // Prevent access to Internal team + if (team.getName().equals(TeamService.INTERNAL_TEAM_NAME)) { + return "redirect:/teams?error=internalTeamNotAccessible"; + } + // Get users for this team directly using the direct query List teamUsers = userRepository.findAllByTeamId(id); + // Get all users not in this team for the Add User to Team dropdown + // Exclude users that are in the Internal team + List allUsers = userRepository.findAllWithTeam(); + List availableUsers = allUsers.stream() + .filter(user -> (user.getTeam() == null || !user.getTeam().getId().equals(id)) && + (user.getTeam() == null || !user.getTeam().getName().equals(TeamService.INTERNAL_TEAM_NAME))) + .toList(); + // Get the latest session for each user in the team List userSessions = sessionRepository.findLatestSessionByTeamId(id); @@ -79,6 +98,7 @@ public class TeamWebController { model.addAttribute("team", team); model.addAttribute("teamUsers", teamUsers); + model.addAttribute("availableUsers", availableUsers); model.addAttribute("userLastRequest", userLastRequest); return "enterprise/team-details"; } diff --git a/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java b/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java index 6420e6c09..517998ec2 100644 --- a/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java +++ b/proprietary/src/main/java/stirling/software/proprietary/security/database/repository/UserRepository.java @@ -30,7 +30,7 @@ public interface UserRepository extends JpaRepository { @Query(value = "SELECT u FROM User u LEFT JOIN FETCH u.team") List findAllWithTeam(); - @Query("SELECT u FROM User u JOIN FETCH u.authorities WHERE u.team.id = :teamId") + @Query("SELECT u FROM User u JOIN FETCH u.authorities JOIN FETCH u.team WHERE u.team.id = :teamId") List findAllByTeamId(@Param("teamId") Long teamId); long countByTeam(Team team); diff --git a/proprietary/src/main/resources/static/css/modern-tables.css b/proprietary/src/main/resources/static/css/modern-tables.css index 8acd8360a..66bdd2aaf 100644 --- a/proprietary/src/main/resources/static/css/modern-tables.css +++ b/proprietary/src/main/resources/static/css/modern-tables.css @@ -330,6 +330,32 @@ gap: 0.75rem; } +/* Modal close button styling */ +.data-btn-close { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 50%; + background-color: var(--md-sys-color-surface-variant); + color: var(--md-sys-color-on-surface-variant); + border: none; + cursor: pointer; + transition: all 0.2s ease; + padding: 0; + margin: 0; +} + +.data-btn-close:hover { + background-color: var(--md-sys-color-surface-container-high); + color: var(--md-sys-color-on-surface); +} + +.data-btn-close .material-symbols-rounded { + font-size: 1.25rem; +} + .data-modal-body { padding: 1.5rem; } diff --git a/proprietary/src/main/resources/templates/enterprise/team-details.html b/proprietary/src/main/resources/templates/enterprise/team-details.html index de10011b4..81154c42d 100644 --- a/proprietary/src/main/resources/templates/enterprise/team-details.html +++ b/proprietary/src/main/resources/templates/enterprise/team-details.html @@ -33,7 +33,7 @@ @@ -75,16 +75,121 @@
person_off

This team has no members yet.

- + +
+ + +
+
+ + + + + + diff --git a/proprietary/src/main/resources/templates/enterprise/teams.html b/proprietary/src/main/resources/templates/enterprise/teams.html index deeccdda0..eb81d207e 100644 --- a/proprietary/src/main/resources/templates/enterprise/teams.html +++ b/proprietary/src/main/resources/templates/enterprise/teams.html @@ -27,7 +27,7 @@ diff --git a/stirling-pdf/src/main/resources/messages_en_GB.properties b/stirling-pdf/src/main/resources/messages_en_GB.properties index 9b6940c36..7dc6cb571 100644 --- a/stirling-pdf/src/main/resources/messages_en_GB.properties +++ b/stirling-pdf/src/main/resources/messages_en_GB.properties @@ -1,43 +1,3 @@ -account.adminTitle=Administrator Tools -account.adminNotif=You have admin privileges. Access system settings and user management. -view=View -cancel=Cancel -adminUserSettings.teams=View/Edit Teams -adminUserSettings.team=Team -adminUserSettings.manageTeams=Manage Teams -adminUserSettings.createTeam=Create Team -adminUserSettings.viewTeam=View Team -adminUserSettings.deleteTeam=Delete Team -adminUserSettings.teamName=Team Name -adminUserSettings.teamExists=Team already exists -adminUserSettings.teamCreated=Team created successfully -adminUserSettings.teamChanged=User's team was updated -adminUserSettings.totalMembers=Total Members - -teamCreated=Team created successfully -teamExists=A team with that name already exists -teamNameExists=Another team with that name already exists -teamNotFound=Team not found -teamDeleted=Team deleted -teamHasUsers=Cannot delete a team with users assigned -teamRenamed=Team renamed successfully -merge.generateToc=Generate table of contents in the merged file? -# Table of Contents Feature -home.editTableOfContents.title=Edit Table of Contents -home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents -editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline -editTableOfContents.title=Edit Table of Contents -editTableOfContents.header=Add or Edit PDF Table of Contents -editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing) -editTableOfContents.editorTitle=Bookmark Editor -editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks. -editTableOfContents.addBookmark=Add New Bookmark -editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document. -editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks. -editTableOfContents.desc.3=Each bookmark requires a title and target page number. -editTableOfContents.submit=Apply Table of Contents - - ########### # Generic # ########### @@ -259,6 +219,12 @@ addToDoc=Add to Document reset=Reset apply=Apply noFileSelected=No file selected. Please upload one. +view=View +cancel=Cancel + +back.toSettings=Back to Settings +back.toHome=Back to Home +back.toAdmin=Back to Admin legal.privacy=Privacy Policy legal.terms=Terms and Conditions @@ -380,6 +346,8 @@ account.property=Property account.webBrowserSettings=Web Browser Setting account.syncToBrowser=Sync Account -> Browser account.syncToAccount=Sync Account <- Browser +account.adminTitle=Administrator Tools +account.adminNotif=You have admin privileges. Access system settings and user management. adminUserSettings.title=User Control Settings @@ -411,6 +379,39 @@ adminUserSettings.disabledUsers=Disabled Users: adminUserSettings.totalUsers=Total Users: adminUserSettings.lastRequest=Last Request adminUserSettings.usage=View Usage +adminUserSettings.teams=View/Edit Teams +adminUserSettings.team=Team +adminUserSettings.manageTeams=Manage Teams +adminUserSettings.createTeam=Create Team +adminUserSettings.viewTeam=View Team +adminUserSettings.deleteTeam=Delete Team +adminUserSettings.teamName=Team Name +adminUserSettings.teamExists=Team already exists +adminUserSettings.teamCreated=Team created successfully +adminUserSettings.teamChanged=User's team was updated +adminUserSettings.totalMembers=Total Members +adminUserSettings.confirmDeleteTeam=Are you sure you want to delete this team? + +teamCreated=Team created successfully +teamExists=A team with that name already exists +teamNameExists=Another team with that name already exists +teamNotFound=Team not found +teamDeleted=Team deleted +teamHasUsers=Cannot delete a team with users assigned +teamRenamed=Team renamed successfully + +# Team user management +team.addUser=Add User to Team +team.selectUser=Select User +team.warning.moveUser=Warning: This will move the user from "{0}" team to "{1}" team. Are you sure? +team.confirm.moveUser=Are you sure you want to move this user from "{0}" team to "{1}" team? +team.userAdded=User successfully added to team +team.back=Back to Teams +team.internal=Internal Team +team.internalTeamNotAccessible=The Internal team is a system team and cannot be accessed +team.cannotMoveInternalUsers=Users in the Internal team cannot be moved to other teams + + endpointStatistics.title=Endpoint Statistics endpointStatistics.header=Endpoint Statistics @@ -1199,6 +1200,7 @@ merge.header=Merge multiple PDFs (2+) merge.sortByName=Sort by name merge.sortByDate=Sort by date merge.removeCertSign=Remove digital signature in the merged file? +merge.generateToc=Generate table of contents in the merged file? merge.submit=Merge @@ -1683,3 +1685,22 @@ fakeScan.blur=Blur fakeScan.noise=Noise fakeScan.yellowish=Yellowish (simulate old paper) fakeScan.resolution=Resolution (DPI) + + +# Table of Contents Feature +home.editTableOfContents.title=Edit Table of Contents +home.editTableOfContents.desc=Add or edit bookmarks and table of contents in PDF documents + +editTableOfContents.tags=bookmarks,toc,navigation,index,table of contents,chapters,sections,outline +editTableOfContents.title=Edit Table of Contents +editTableOfContents.header=Add or Edit PDF Table of Contents +editTableOfContents.replaceExisting=Replace existing bookmarks (uncheck to append to existing) +editTableOfContents.editorTitle=Bookmark Editor +editTableOfContents.editorDesc=Add and arrange bookmarks below. Click + to add child bookmarks. +editTableOfContents.addBookmark=Add New Bookmark +editTableOfContents.desc.1=This tool allows you to add or edit the table of contents (bookmarks) in a PDF document. +editTableOfContents.desc.2=You can create a hierarchical structure by adding child bookmarks to parent bookmarks. +editTableOfContents.desc.3=Each bookmark requires a title and target page number. +editTableOfContents.submit=Apply Table of Contents + +