add expired by Admin

This commit is contained in:
Ludy87 2025-04-10 08:14:06 +02:00
parent cfa71e537b
commit 09da81643c
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
9 changed files with 220 additions and 89 deletions

View File

@ -536,6 +536,9 @@ dependencies {
annotationProcessor "org.projectlombok:lombok:$lombokVersion" annotationProcessor "org.projectlombok:lombok:$lombokVersion"
testRuntimeOnly 'org.mockito:mockito-inline:5.2.0' testRuntimeOnly 'org.mockito:mockito-inline:5.2.0'
// https://mvnrepository.com/artifact/org.springframework/spring-webflux
implementation("org.springframework.boot:spring-boot-starter-webflux")
} }
tasks.withType(JavaCompile).configureEach { tasks.withType(JavaCompile).configureEach {

View File

@ -39,14 +39,14 @@ public class EndpointInterceptor implements HandlerInterceptor {
} }
String requestURI = request.getRequestURI(); String requestURI = request.getRequestURI();
if ("GET".equalsIgnoreCase(request.getMethod())) { boolean isApiRequest = requestURI.contains("/api/v1");
if ("GET".equalsIgnoreCase(request.getMethod()) && !isApiRequest) {
Principal principal = request.getUserPrincipal(); Principal principal = request.getUserPrincipal();
boolean isApiRequest = requestURI.contains("/api/v1");
// allowlist for public or static routes // allowlist for public or static routes
if (("/".equals(requestURI) if ("/".equals(requestURI)
|| "/login".equals(requestURI) || "/login".equals(requestURI)
|| "/home".equals(requestURI) || "/home".equals(requestURI)
|| "/home-legacy".equals(requestURI) || "/home-legacy".equals(requestURI)
@ -61,9 +61,9 @@ public class EndpointInterceptor implements HandlerInterceptor {
|| requestURI.endsWith(".js") || requestURI.endsWith(".js")
|| requestURI.endsWith(".png") || requestURI.endsWith(".png")
|| requestURI.endsWith(".webmanifest") || requestURI.endsWith(".webmanifest")
|| requestURI.contains("/files/")) && !isApiRequest) { || requestURI.contains("/files/")) {
return true; return true;
} else if (principal != null && !isApiRequest) { } else if (principal != null) {
if (session == null) { if (session == null) {
session = request.getSession(true); session = request.getSession(true);
} }
@ -71,6 +71,38 @@ public class EndpointInterceptor implements HandlerInterceptor {
final HttpSession finalSession = session; final HttpSession finalSession = session;
String sessionId = finalSession.getId(); String sessionId = finalSession.getId();
boolean isExpiredByAdmin =
sessionsInterface.getAllSessions().stream()
.filter(s -> s.getSessionId().equals(finalSession.getId()))
.anyMatch(s -> s.isExpired());
if (isExpiredByAdmin) {
response.sendRedirect("/logout");
log.info("Session expired. Logging out user {}", principal.getName());
return false;
}
int maxApplicationSessions = sessionsInterface.getMaxApplicationSessions();
Collection<SessionsModelInterface> allSessions = sessionsInterface.getAllSessions();
long totalSessionsNonExpired =
allSessions.stream().filter(s -> !s.isExpired()).count();
List<SessionsModelInterface> activeSessions =
allSessions.stream()
.filter(s -> !s.isExpired())
.sorted(
(s1, s2) ->
Long.compare(
s2.getLastRequest().getTime(),
s1.getLastRequest().getTime()))
.limit(maxApplicationSessions)
.toList();
boolean hasUserActiveSession =
activeSessions.stream().anyMatch(s -> s.getSessionId().equals(sessionId));
final String currentPrincipal = principal.getName(); final String currentPrincipal = principal.getName();
long userSessions = long userSessions =
@ -82,29 +114,21 @@ public class EndpointInterceptor implements HandlerInterceptor {
s.getPrincipalName())) s.getPrincipalName()))
.count(); .count();
long totalSessions =
sessionsInterface.getAllSessions().stream()
.filter(s -> !s.isExpired())
.count();
int maxUserSessions = sessionsInterface.getMaxUserSessions(); int maxUserSessions = sessionsInterface.getMaxUserSessions();
log.info( log.info(
"Active sessions for {}: {} (max: {}) | Total: {} (max: {})", "Active sessions for {}: {} (max: {}) | Total: {} (max: {}) | Active"
+ " sessions: {}",
currentPrincipal, currentPrincipal,
userSessions, userSessions,
maxUserSessions, maxUserSessions,
totalSessions, totalSessionsNonExpired,
sessionsInterface.getMaxApplicationSessions()); maxApplicationSessions,
hasUserActiveSession);
boolean isCurrentSessionRegistered =
sessionsInterface.getAllSessions().stream()
.filter(s -> !s.isExpired())
.anyMatch(s -> s.getSessionId().equals(sessionId));
if ((userSessions >= maxUserSessions if ((userSessions >= maxUserSessions
|| totalSessions >= sessionsInterface.getMaxApplicationSessions()) || totalSessionsNonExpired >= maxApplicationSessions)
&& !isCurrentSessionRegistered) { && !hasUserActiveSession) {
response.sendError( response.sendError(
HttpServletResponse.SC_UNAUTHORIZED, HttpServletResponse.SC_UNAUTHORIZED,
"Max sessions reached for this user. To continue on this device, please" "Max sessions reached for this user. To continue on this device, please"
@ -114,15 +138,15 @@ public class EndpointInterceptor implements HandlerInterceptor {
// If session is not registered yet, register it; otherwise, update the last request // If session is not registered yet, register it; otherwise, update the last request
// timestamp. // timestamp.
if (!isCurrentSessionRegistered) { if (!hasUserActiveSession) {
log.debug("Register session: {}", sessionId); log.info("Register session: {}", sessionId);
sessionsInterface.registerSession(finalSession); sessionsInterface.registerSession(finalSession);
} else { } else {
log.debug("Update session last request: {}", sessionId); log.info("Update session last request: {}", sessionId);
sessionsInterface.updateSessionLastRequest(sessionId); sessionsInterface.updateSessionLastRequest(sessionId);
} }
return true; return true;
} else if (principal == null && !isApiRequest) { } else if (principal == null) {
if (session == null) { if (session == null) {
session = request.getSession(true); session = request.getSession(true);
} }

View File

@ -89,6 +89,10 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
sessionPersistentRegistry.expireSession(sessionId); sessionPersistentRegistry.expireSession(sessionId);
} }
public void expireSession(String sessionId, boolean expiredByAdmin) {
sessionPersistentRegistry.expireSession(sessionId, expiredByAdmin);
}
public int getMaxInactiveInterval() { public int getMaxInactiveInterval() {
return (int) defaultMaxInactiveInterval.getSeconds(); return (int) defaultMaxInactiveInterval.getSeconds();
} }
@ -139,7 +143,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
.toList() .toList()
.size(); .size();
boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled; boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled;
log.debug( log.info(
"all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}", "all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}",
all, all,
allNonExpiredSessions, allNonExpiredSessions,
@ -147,7 +151,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
isAnonymousUserWithoutLogin); isAnonymousUserWithoutLogin);
if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) { if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) {
log.debug("Session {} Expired=TRUE", session.getId()); log.info("Session {} Expired=TRUE", session.getId());
sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.expireSession(session.getId());
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
// if (allNonExpiredSessions > getMaxUserSessions()) { // if (allNonExpiredSessions > getMaxUserSessions()) {
@ -155,12 +159,12 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
// } // }
} else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) { } else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) {
enforceMaxSessionsForPrincipal(principalName); enforceMaxSessionsForPrincipal(principalName);
log.debug("Session {} Expired=TRUE", session.getId()); log.info("Session {} Expired=TRUE", session.getId());
} else if (isAnonymousUserWithoutLogin) { } else if (isAnonymousUserWithoutLogin) {
sessionPersistentRegistry.expireSession(session.getId()); sessionPersistentRegistry.expireSession(session.getId());
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId()); sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
} else { } else {
log.debug("Session created: {}", session.getId()); log.info("Session created: {}", session.getId());
sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName); sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName);
} }
} }

View File

@ -28,14 +28,18 @@ public class SessionPersistentRegistry implements SessionRegistry {
private final SessionRepository sessionRepository; private final SessionRepository sessionRepository;
private final boolean runningEE; private final boolean runningEE;
private final boolean loginEnabled;
@Value("${server.servlet.session.timeout:30m}") @Value("${server.servlet.session.timeout:30m}")
private Duration defaultMaxInactiveInterval; private Duration defaultMaxInactiveInterval;
public SessionPersistentRegistry( public SessionPersistentRegistry(
SessionRepository sessionRepository, @Qualifier("runningEE") boolean runningEE) { SessionRepository sessionRepository,
@Qualifier("runningEE") boolean runningEE,
@Qualifier("loginEnabled") boolean loginEnabled) {
this.runningEE = runningEE; this.runningEE = runningEE;
this.sessionRepository = sessionRepository; this.sessionRepository = sessionRepository;
this.loginEnabled = loginEnabled;
} }
@Override @Override
@ -86,7 +90,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
if (sessionEntity == null) { if (sessionEntity == null) {
sessionEntity = new SessionEntity(); sessionEntity = new SessionEntity();
sessionEntity.setSessionId(sessionId); sessionEntity.setSessionId(sessionId);
log.debug("Registering new session for principal: {}", principalName); log.info("Registering new session for principal: {}", principalName);
} }
sessionEntity.setPrincipalName(principalName); sessionEntity.setPrincipalName(principalName);
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
@ -188,6 +192,17 @@ public class SessionPersistentRegistry implements SessionRegistry {
} }
} }
public void expireSession(String sessionId, boolean expiredByAdmin) {
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
if (sessionEntityOpt.isPresent()) {
SessionEntity sessionEntity = sessionEntityOpt.get();
sessionEntity.setExpired(true); // Set expired to true
sessionEntity.setAdminExpired(expiredByAdmin);
sessionRepository.save(sessionEntity);
log.debug("Session expired: {}", sessionId);
}
}
// Mark all sessions as expired // Mark all sessions as expired
public void expireAllSessions() { public void expireAllSessions() {
List<SessionEntity> sessionEntities = sessionRepository.findAll(); List<SessionEntity> sessionEntities = sessionRepository.findAll();
@ -283,9 +298,12 @@ public class SessionPersistentRegistry implements SessionRegistry {
// Get the maximum number of user sessions // Get the maximum number of user sessions
public int getMaxUserSessions() { public int getMaxUserSessions() {
if (loginEnabled) {
if (runningEE) { if (runningEE) {
return Integer.MAX_VALUE;
}
return 3; return 3;
} }
return Integer.MAX_VALUE; // (3)
}
return Integer.MAX_VALUE; // (10)
}
} }

View File

@ -15,7 +15,9 @@ public class SessionRegistryConfig {
@Bean @Bean
public SessionPersistentRegistry sessionPersistentRegistry( public SessionPersistentRegistry sessionPersistentRegistry(
SessionRepository sessionRepository, @Qualifier("runningEE") boolean runningEE) { SessionRepository sessionRepository,
return new SessionPersistentRegistry(sessionRepository, runningEE); @Qualifier("runningEE") boolean runningEE,
@Qualifier("loginEnabled") boolean loginEnabled) {
return new SessionPersistentRegistry(sessionRepository, runningEE, loginEnabled);
} }
} }

View File

@ -9,9 +9,9 @@ import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSession;
@ -20,15 +20,18 @@ import lombok.extern.slf4j.Slf4j;
import stirling.software.SPDF.config.interfaces.SessionsInterface; import stirling.software.SPDF.config.interfaces.SessionsInterface;
import stirling.software.SPDF.config.security.UserUtils; import stirling.software.SPDF.config.security.UserUtils;
import stirling.software.SPDF.config.security.session.CustomHttpSessionListener;
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry; import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
@RestController @Controller
@Slf4j @Slf4j
public class SessionStatusController { public class SessionStatusController {
@Autowired private SessionPersistentRegistry sessionPersistentRegistry; @Autowired private SessionPersistentRegistry sessionPersistentRegistry;
@Autowired private SessionsInterface sessionInterface; @Autowired private SessionsInterface sessionInterface;
@Autowired private CustomHttpSessionListener customHttpSessionListener;
// Returns the current session ID or 401 if no session exists // Returns the current session ID or 401 if no session exists
@GetMapping("/session") @GetMapping("/session")
public ResponseEntity<String> getSession(HttpServletRequest request) { public ResponseEntity<String> getSession(HttpServletRequest request) {
@ -40,6 +43,35 @@ public class SessionStatusController {
} }
} }
@GetMapping("/session/invalidate/{sessionId}")
public String invalidateSession(
HttpServletRequest request,
Authentication authentication,
@PathVariable String sessionId) {
// ist ROLE_ADMIN oder session inhaber
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/login";
}
Object principal = authentication.getPrincipal();
String principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
}
boolean isAdmin =
authentication.getAuthorities().stream()
.anyMatch(role -> "ROLE_ADMIN".equals(role.getAuthority()));
boolean isOwner =
sessionPersistentRegistry.getAllSessions(principalName, false).stream()
.anyMatch(session -> session.getSessionId().equals(sessionId));
if (isAdmin || isOwner) {
customHttpSessionListener.expireSession(sessionId, isAdmin);
return "redirect:/adminSettings?messageType=sessionInvalidated";
} else {
return "redirect:/login";
}
}
// Checks if the session is active and valid according to user session limits // Checks if the session is active and valid according to user session limits
@GetMapping("/session/status") @GetMapping("/session/status")
public ResponseEntity<String> getSessionStatus(HttpServletRequest request) { public ResponseEntity<String> getSessionStatus(HttpServletRequest request) {

View File

@ -215,13 +215,15 @@ public class AccountWebController {
@GetMapping("/adminSettings") @GetMapping("/adminSettings")
public String showAddUserForm( public String showAddUserForm(
HttpServletRequest request, Model model, Authentication authentication) { HttpServletRequest request, Model model, Authentication authentication) {
String currentSessionId = request.getSession().getId();
List<User> allUsers = userRepository.findAll(); List<User> allUsers = userRepository.findAll();
Iterator<User> iterator = allUsers.iterator(); Iterator<User> iterator = allUsers.iterator();
Map<String, String> roleDetails = Role.getAllRoleDetails(); Map<String, String> roleDetails = Role.getAllRoleDetails();
// Map to store session information and user activity status // Map to store session information and user activity status
Map<String, Boolean> userSessions = new HashMap<>(); Map<String, Boolean> userSessions = new HashMap<>();
Map<String, Date> userLastRequest = new HashMap<>(); Map<String, Date> userLastRequest = new HashMap<>();
Map<String, Integer> userActiveSessions = new HashMap<>(); Map<String, List<SessionsModelInterface>> userActiveSessions = new HashMap<>();
int activeUsers = 0; int activeUsers = 0;
int disabledUsers = 0; int disabledUsers = 0;
int maxSessions = customHttpSessionListener.getMaxApplicationSessions(); int maxSessions = customHttpSessionListener.getMaxApplicationSessions();
@ -273,7 +275,7 @@ public class AccountWebController {
} }
List<SessionsModelInterface> sessionInformations = List<SessionsModelInterface> sessionInformations =
customHttpSessionListener.getAllSessions(user.getUsername(), false); customHttpSessionListener.getAllSessions(user.getUsername(), false);
userActiveSessions.put(user.getUsername(), sessionInformations.size()); userActiveSessions.put(user.getUsername(), sessionInformations);
} }
} }
// Sort users by active status and last request date // Sort users by active status and last request date
@ -335,6 +337,7 @@ public class AccountWebController {
} }
model.addAttribute("users", sortedUsers); model.addAttribute("users", sortedUsers);
model.addAttribute("currentSessionId", currentSessionId);
if (authentication != null) { if (authentication != null) {
model.addAttribute("currentUsername", authentication.getName()); model.addAttribute("currentUsername", authentication.getName());
} }

View File

@ -20,6 +20,8 @@ public class SessionEntity implements Serializable, SessionsModelInterface {
private Date lastRequest; private Date lastRequest;
private boolean expired; private boolean expired;
private Boolean adminExpired = false;
@Override @Override
public String getSessionId() { public String getSessionId() {
return sessionId; return sessionId;

View File

@ -66,7 +66,7 @@
<div class="my-4"> <div class="my-4">
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong> <strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
<span th:text="${totalUsers}"></span> <span th:text="${totalUsers}"></span>
<span th:if="${@runningProOrHigher}" th:text="'| ' + ${maxPaidUsers}"></span> <span th:if="${@runningProOrHigher}" th:text="' | ' + ${maxPaidUsers}"></span>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong> <strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
<span th:text="${activeUsers}"></span> <span th:text="${activeUsers}"></span>
@ -75,7 +75,8 @@
<span th:text="${disabledUsers}"></span> <span th:text="${disabledUsers}"></span>
<th:block> <th:block>
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong> <strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
<span th:text="${sessionCount}"></span><span th:text="' | ' + ${maxSessions}"></span> <span th:if="${@runningProOrHigher}" th:text="${sessionCount}"></span>
<span th:text="' | ' + ${maxSessions}"></span>
</th:block> </th:block>
</div> </div>
</div> </div>
@ -107,45 +108,55 @@
th:text="#{adminUserSettings.authenticated}">Authenticated</th> th:text="#{adminUserSettings.authenticated}">Authenticated</th>
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" <th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow"
th:text="#{adminUserSettings.lastRequest}">Last Request</th> th:text="#{adminUserSettings.lastRequest}">Last Request</th>
<th scope="col" th:title="#{adminUserSettings.userSessions}" th:text="#{adminUserSettings.userSessions}"> <th scope="col" class="text-center" th:title="#{adminUserSettings.userSessions}"
User Sessions</th> th:text="#{adminUserSettings.userSessions}">User Sessions</th>
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2"> <th scope="col" class="text-center" th:title="#{adminUserSettings.actions}"
Actions</th> th:text="#{adminUserSettings.actions}" colspan="2">Actions</th>
<!-- <th scope="col"></th> --> <!-- <th scope="col"></th> -->
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr th:each="user : ${users}"> <th:block th:each="user, iterStat : ${users}">
<th scope="row" style="align-content: center;" th:text="${user.id}"></th> <tr>
<td style="align-content: center;" th:text="${user.username}" <th scope="row" th:text="${user.id}"></th>
th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td> <td th:text="${user.username}" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
<td style="align-content: center;" th:text="#{${user.roleName}}"></td> <td th:text="#{${user.roleName}}"></td>
<td style="align-content: center;" th:text="${user.authenticationType}"></td> <td th:text="${user.authenticationType}"></td>
<td style="align-content: center;" <td
th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"> th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}">
</td> </td>
<th:block th:if="${@runningProOrHigher}"> <th:block th:if="${@runningProOrHigher}">
<td style="align-content: center;" <td class="text-center">
th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0}"> <button th:if="${@enableAlphaFunctionality}" type="button"
th:text="${#lists.size(userActiveSessions[user.username])}" th:data-bs-toggle="'collapse'"
th:data-bs-target="'#sessions__' + ${iterStat.index}"
th:aria-controls="'sessions__' + ${iterStat.index}"
th:class="${#lists.isEmpty(userActiveSessions[user.username])} ? 'btn btn-sm btn-outline-secondary disabled' : 'btn btn-sm btn-outline-secondary'"
th:aria-disabled="${#lists.isEmpty(userActiveSessions[user.username])} ? 'true' : 'false'">
0
</button>
<span th:if="${!@enableAlphaFunctionality}"
th:text="${#lists.size(userActiveSessions[user.username])}"></span> |
<span th:text="${maxUserSessions}"></span>
</td> </td>
</th:block> </th:block>
<th:block th:if="${!@runningProOrHigher}"> <th:block th:if="${!@runningProOrHigher}">
<td style="align-content: center;" <td class="text-center" th:text="${#lists.size(userActiveSessions[user.username])}">0</td>
th:text="${userActiveSessions[user.username] != null ? userActiveSessions[user.username] : 0} + '/' + ${maxUserSessions}">
</td>
</th:block> </th:block>
<td style="align-content: center;"> <td class="text-center">
<form th:if="${user.username != currentUsername}" <form th:if="${user.username != currentUsername}"
th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post"
onsubmit="return confirmDeleteUser()"> onsubmit="return confirmDeleteUser()">
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm"><span <button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm">
class="material-symbols-rounded">person_remove</span></button> <span class="material-symbols-rounded">person_remove</span>
</button>
</form> </form>
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" <a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}"
th:href="@{'/account'}" class="btn btn-outline-success btn-sm"><span th:href="@{'/account'}" class="btn btn-outline-success btn-sm">
class="material-symbols-rounded">edit</span></a> <span class="material-symbols-rounded">edit</span>
</a>
</td> </td>
<td style="align-content: center;"> <td>
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" <form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post"
onsubmit="return confirmChangeUserStatus()"> onsubmit="return confirmChangeUserStatus()">
<input type="hidden" name="enabled" th:value="!${user.enabled}" /> <input type="hidden" name="enabled" th:value="!${user.enabled}" />
@ -160,6 +171,38 @@
</form> </form>
</td> </td>
</tr> </tr>
<tr th:if="${@enableAlphaFunctionality}">
<td colspan="8" class="p-0 border-0">
<div th:id="'sessions__' + ${iterStat.index}" class="collapse">
<table class="table table-striped table-hover table-sm mb-0">
<tbody>
<tr th:each="s : ${userActiveSessions[user.username]}">
<td scope="row" colspan="4">
<span th:text="${s.sessionId}"></span>
<span th:if="${s.sessionId == currentSessionId}" class="text-warning ms-2"
title="Aktuelle Sitzung">
⚠️
</span>
</td>
<td colspan="2" th:text="${#dates.format(s.lastRequest, 'yyyy-MM-dd HH:mm:ss')}"></td>
<td colspan="2">
<form th:action="@{'/session/invalidate/' + ${s.sessionId}}" method="get"
onsubmit="return confirm('Session wirklich beenden?')">
<input type="hidden" name="_method" value="DELETE" />
<button class="btn btn-danger btn-sm"><span
class="material-symbols-rounded">remove_circle_outline</span></button>
</form>
</td>
</tr>
<tr th:if="${#lists.isEmpty(userActiveSessions[user.username])}">
<td colspan="3" class="text-center text-muted">Keine aktiven Sessions</td>
</tr>
</tbody>
</table>
</div>
</td>
</tr>
</th:block>
</tbody> </tbody>
</table> </table>
</div> </div>