general fixes and changes

This commit is contained in:
Anthony Stirling 2025-06-11 00:52:03 +01:00
parent 71b126104e
commit 1fac660bf8
7 changed files with 89 additions and 39 deletions

View File

@ -0,0 +1,20 @@
package stirling.software.proprietary.model.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
public class TeamWithUserCountDTO {
private Long id;
private String name;
private Long userCount;
// Constructor for JPQL projection
public TeamWithUserCountDTO(Long id, String name, Long userCount) {
this.id = id;
this.name = name;
this.userCount = userCount;
}
}

View File

@ -33,6 +33,7 @@ import stirling.software.common.model.ApplicationProperties;
import stirling.software.common.model.enumeration.Role; import stirling.software.common.model.enumeration.Role;
import stirling.software.common.model.exception.UnsupportedProviderException; import stirling.software.common.model.exception.UnsupportedProviderException;
import stirling.software.proprietary.model.Team; import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.AuthenticationType; import stirling.software.proprietary.security.model.AuthenticationType;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
import stirling.software.proprietary.security.model.api.user.UsernameAndPass; import stirling.software.proprietary.security.model.api.user.UsernameAndPass;
@ -54,7 +55,7 @@ public class UserController {
private final SessionPersistentRegistry sessionRegistry; private final SessionPersistentRegistry sessionRegistry;
private final ApplicationProperties applicationProperties; private final ApplicationProperties applicationProperties;
private final TeamRepository teamRepository; private final TeamRepository teamRepository;
private final UserRepository userRepository;
@PreAuthorize("!hasAuthority('ROLE_DEMO_USER')") @PreAuthorize("!hasAuthority('ROLE_DEMO_USER')")
@PostMapping("/register") @PostMapping("/register")
public String register(@ModelAttribute UsernameAndPass requestModel, Model model) public String register(@ModelAttribute UsernameAndPass requestModel, Model model)
@ -210,6 +211,7 @@ public class UserController {
@RequestParam(name = "username", required = true) String username, @RequestParam(name = "username", required = true) String username,
@RequestParam(name = "password", required = false) String password, @RequestParam(name = "password", required = false) String password,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
@RequestParam(name = "authType") String authType, @RequestParam(name = "authType") String authType,
@RequestParam(name = "forceChange", required = false, defaultValue = "false") @RequestParam(name = "forceChange", required = false, defaultValue = "false")
boolean forceChange) boolean forceChange)
@ -243,14 +245,23 @@ public class UserController {
// If the role ID is not valid, redirect with an error message // If the role ID is not valid, redirect with an error message
return new RedirectView("/adminSettings?messageType=invalidRole", true); return new RedirectView("/adminSettings?messageType=invalidRole", true);
} }
Team team = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
// Use teamId if provided, otherwise use default team
Long effectiveTeamId = teamId;
if (effectiveTeamId == null) {
Team defaultTeam = teamRepository.findByName(TeamService.DEFAULT_TEAM_NAME).orElse(null);
if (defaultTeam != null) {
effectiveTeamId = defaultTeam.getId();
}
}
if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) { if (authType.equalsIgnoreCase(AuthenticationType.SSO.toString())) {
userService.saveUser(username, AuthenticationType.SSO, team, role); userService.saveUser(username, AuthenticationType.SSO, effectiveTeamId, role);
} else { } else {
if (password.isBlank()) { if (password.isBlank()) {
return new RedirectView("/adminSettings?messageType=invalidPassword", true); return new RedirectView("/adminSettings?messageType=invalidPassword", true);
} }
userService.saveUser(username, password, team, role, forceChange); userService.saveUser(username, password, effectiveTeamId, role, forceChange);
} }
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/adminSettings", // Redirect to account page after adding the user
@ -262,6 +273,7 @@ public class UserController {
public RedirectView changeRole( public RedirectView changeRole(
@RequestParam(name = "username") String username, @RequestParam(name = "username") String username,
@RequestParam(name = "role") String role, @RequestParam(name = "role") String role,
@RequestParam(name = "teamId", required = false) Long teamId,
Authentication authentication) Authentication authentication)
throws SQLException, UnsupportedProviderException { throws SQLException, UnsupportedProviderException {
Optional<User> userOpt = userService.findByUsernameIgnoreCase(username); Optional<User> userOpt = userService.findByUsernameIgnoreCase(username);
@ -289,6 +301,15 @@ public class UserController {
return new RedirectView("/adminSettings?messageType=invalidRole", true); return new RedirectView("/adminSettings?messageType=invalidRole", true);
} }
User user = userOpt.get(); User user = userOpt.get();
// Update the team if a teamId is provided
if (teamId != null) {
teamRepository.findById(teamId).ifPresent(team -> {
user.setTeam(team);
userRepository.save(user);
});
}
userService.changeRole(user, role); userService.changeRole(user, role);
return new RedirectView( return new RedirectView(
"/adminSettings", // Redirect to account page after adding the user "/adminSettings", // Redirect to account page after adding the user

View File

@ -2,7 +2,6 @@ package stirling.software.proprietary.security.controller.web;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -14,8 +13,10 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import stirling.software.proprietary.model.Team; import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
import stirling.software.proprietary.security.database.repository.SessionRepository; import stirling.software.proprietary.security.database.repository.SessionRepository;
import stirling.software.proprietary.security.database.repository.UserRepository; import stirling.software.proprietary.security.database.repository.UserRepository;
import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.model.User;
@ -24,17 +25,18 @@ import stirling.software.proprietary.security.repository.TeamRepository;
@Controller @Controller
@RequestMapping("/teams") @RequestMapping("/teams")
@RequiredArgsConstructor @RequiredArgsConstructor
@Slf4j
public class TeamWebController { public class TeamWebController {
private final TeamRepository teamRepository; private final TeamRepository teamRepository;
private final UserRepository userRepository;
private final SessionRepository sessionRepository; private final SessionRepository sessionRepository;
private final UserRepository userRepository;
@GetMapping @GetMapping
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
public String listTeams(Model model) { public String listTeams(Model model) {
// Get all teams with their users // Get teams with user counts using a DTO projection
List<Team> teams = teamRepository.findAllWithUsers(); List<TeamWithUserCountDTO> teamsWithCounts = teamRepository.findAllTeamsWithUserCount();
// Get the latest activity for each team // Get the latest activity for each team
List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam(); List<Object[]> teamActivities = sessionRepository.findLatestActivityByTeam();
@ -42,45 +44,41 @@ public class TeamWebController {
// Convert the query results to a map for easy access in the view // Convert the query results to a map for easy access in the view
Map<Long, Date> teamLastRequest = new HashMap<>(); Map<Long, Date> teamLastRequest = new HashMap<>();
for (Object[] result : teamActivities) { for (Object[] result : teamActivities) {
// For JPQL query with aliases
Long teamId = (Long) result[0]; // teamId alias Long teamId = (Long) result[0]; // teamId alias
Date lastActivity = (Date) result[1]; // lastActivity alias Date lastActivity = (Date) result[1]; // lastActivity alias
teamLastRequest.put(teamId, lastActivity); teamLastRequest.put(teamId, lastActivity);
} }
model.addAttribute("teams", teams); // Add data to the model
model.addAttribute("teamsWithCounts", teamsWithCounts);
model.addAttribute("teamLastRequest", teamLastRequest); model.addAttribute("teamLastRequest", teamLastRequest);
return "enterprise/teams"; return "enterprise/teams";
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@PreAuthorize("hasRole('ROLE_ADMIN')") @PreAuthorize("hasRole('ROLE_ADMIN')")
public String viewTeamDetails(@PathVariable("id") Long id, Model model) { public String viewTeamDetails(@PathVariable("id") Long id, Model model) {
// Get the team with its users // Get the team
Team team = Team team = teamRepository.findById(id)
teamRepository .orElseThrow(() -> new RuntimeException("Team not found"));
.findById(id)
.orElseThrow(() -> new RuntimeException("Team not found")); // Get users for this team directly using the direct query
List<User> teamUsers = userRepository.findAllByTeamId(id);
List<User> members = userRepository.findAllByTeam(team);
team.setUsers(new HashSet<>(members));
// Get the latest session for each user in the team // Get the latest session for each user in the team
List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id); List<Object[]> userSessions = sessionRepository.findLatestSessionByTeamId(id);
// Create a map of username to last request date // Create a map of username to last request date
Map<String, Date> userLastRequest = new HashMap<>(); Map<String, Date> userLastRequest = new HashMap<>();
// Process results from JPQL query
for (Object[] result : userSessions) { for (Object[] result : userSessions) {
String username = (String) result[0]; // username alias String username = (String) result[0]; // username alias
Date lastRequest = (Date) result[1]; // lastRequest alias Date lastRequest = (Date) result[1]; // lastRequest alias
userLastRequest.put(username, lastRequest); userLastRequest.put(username, lastRequest);
} }
model.addAttribute("team", team); model.addAttribute("team", team);
model.addAttribute("teamUsers", teamUsers);
model.addAttribute("userLastRequest", userLastRequest); model.addAttribute("userLastRequest", userLastRequest);
return "enterprise/team-details"; return "enterprise/team-details";
} }

View File

@ -27,8 +27,11 @@ public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u WHERE u.team IS NULL") @Query("SELECT u FROM User u WHERE u.team IS NULL")
List<User> findAllWithoutTeam(); List<User> findAllWithoutTeam();
@Query("SELECT u FROM User u LEFT JOIN FETCH u.team") @Query(value = "SELECT u FROM User u LEFT JOIN FETCH u.team")
List<User> findAllWithTeam(); List<User> findAllWithTeam();
@Query("SELECT u FROM User u JOIN FETCH u.authorities WHERE u.team.id = :teamId")
List<User> findAllByTeamId(@Param("teamId") Long teamId);
long countByTeam(Team team); long countByTeam(Team team);

View File

@ -5,16 +5,19 @@ import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import stirling.software.proprietary.model.Team; import stirling.software.proprietary.model.Team;
import stirling.software.proprietary.model.dto.TeamWithUserCountDTO;
@Repository @Repository
public interface TeamRepository extends JpaRepository<Team, Long> { public interface TeamRepository extends JpaRepository<Team, Long> {
Optional<Team> findByName(String name); Optional<Team> findByName(String name);
@Query("SELECT t FROM Team t LEFT JOIN FETCH t.users") @Query("SELECT new stirling.software.proprietary.model.dto.TeamWithUserCountDTO(t.id, t.name, COUNT(u)) " +
List<Team> findAllWithUsers(); "FROM Team t LEFT JOIN t.users u GROUP BY t.id, t.name")
List<TeamWithUserCountDTO> findAllTeamsWithUserCount();
boolean existsByNameIgnoreCase(String name); boolean existsByNameIgnoreCase(String name);
} }

View File

@ -24,13 +24,9 @@
<div class="data-body"> <div class="data-body">
<div class="data-stats"> <div class="data-stats">
<div class="data-stat-card">
<div class="data-stat-label">Team ID:</div>
<div class="data-stat-value" th:text="${team.id}">1</div>
</div>
<div class="data-stat-card"> <div class="data-stat-card">
<div class="data-stat-label">Total Members:</div> <div class="data-stat-label">Total Members:</div>
<div class="data-stat-value" th:text="${team.users.size()}">1</div> <div class="data-stat-value" th:text="${teamUsers.size()}">1</div>
</div> </div>
</div> </div>
@ -55,7 +51,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="user : ${team.users}"> <tr th:each="user : ${teamUsers}">
<td th:text="${user.id}">1</td> <td th:text="${user.id}">1</td>
<td th:text="${user.username}">username</td> <td th:text="${user.username}">username</td>
<td th:text="#{${user.roleName}}">Role</td> <td th:text="#{${user.roleName}}">Role</td>
@ -76,7 +72,7 @@
</div> </div>
<!-- Empty state for when there are no team members --> <!-- Empty state for when there are no team members -->
<div th:if="${team.users.empty}" class="data-empty"> <div th:if="${teamUsers.empty}" class="data-empty">
<span class="material-symbols-rounded data-empty-icon">person_off</span> <span class="material-symbols-rounded data-empty-icon">person_off</span>
<p class="data-empty-text">This team has no members yet.</p> <p class="data-empty-text">This team has no members yet.</p>
<a th:href="@{'/admin/users'}" class="data-btn data-btn-primary"> <a th:href="@{'/admin/users'}" class="data-btn data-btn-primary">

View File

@ -23,6 +23,14 @@
</div> </div>
<div class="data-body"> <div class="data-body">
<!-- Back Button -->
<div class="data-actions data-actions-start">
<a href="/adminSettings" class="data-btn data-btn-secondary">
<span class="material-symbols-rounded">arrow_back</span>
<span>Back to Settings</span>
</a>
</div>
<!-- Create New Team Button --> <!-- Create New Team Button -->
<div class="data-actions"> <div class="data-actions">
<a href="#" <a href="#"
@ -47,18 +55,19 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="team : ${teams}"> <!-- Try approach 1 - DTO projection -->
<td th:text="${team.name}"></td> <tr th:each="teamDto : ${teamsWithCounts}">
<td th:text="${team.users.size()}"></td> <td th:text="${teamDto.name}"></td>
<td th:text="${@runningProOrHigher} ? (${teamLastRequest[team.id] != null ? #dates.format(teamLastRequest[team.id], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'"></td> <td th:text="${teamDto.userCount}"></td>
<td th:text="${@runningProOrHigher} ? (${teamLastRequest[teamDto.id] != null ? #dates.format(teamLastRequest[teamDto.id], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}) : 'hidden'"></td>
<td> <td>
<div class="data-action-cell"> <div class="data-action-cell">
<a th:href="@{'/teams/' + ${team.id}}" class="data-btn data-btn-secondary data-btn-sm" th:title="#{adminUserSettings.viewTeam}"> <a th:href="@{'/teams/' + ${teamDto.id}}" class="data-btn data-btn-secondary data-btn-sm" th:title="#{adminUserSettings.viewTeam}">
<span class="material-symbols-rounded">search</span> <span th:text="#{view}">View</span> <span class="material-symbols-rounded">search</span> <span th:text="#{view}">View</span>
</a> </a>
<form th:action="@{'/api/v1/team/delete'}" method="post" style="display:inline-block" <form th:action="@{'/api/v1/team/delete'}" method="post" style="display:inline-block"
onsubmit="return confirmDeleteTeam()"> onsubmit="return confirmDeleteTeam()">
<input type="hidden" name="teamId" th:value="${team.id}" /> <input type="hidden" name="teamId" th:value="${teamDto.id}" />
<button type="submit" class="data-btn data-btn-danger data-btn-sm" <button type="submit" class="data-btn data-btn-danger data-btn-sm"
th:disabled="${!@runningProOrHigher}" th:disabled="${!@runningProOrHigher}"
th:classappend="${!@runningProOrHigher} ? 'disabled' : ''" th:classappend="${!@runningProOrHigher} ? 'disabled' : ''"