add session UI for Users

This commit is contained in:
Ludy87 2025-04-13 20:56:02 +02:00
parent 95f289b9a3
commit 1343e41149
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
6 changed files with 161 additions and 11 deletions

View File

@ -46,10 +46,12 @@ public class EndpointInterceptor implements HandlerInterceptor {
Principal principal = request.getUserPrincipal();
// allowlist for public or static routes
if ("/".equals(requestURI)
|| "/login".equals(requestURI)
|| "/home".equals(requestURI)
|| "/home-legacy".equals(requestURI)
if ("/login".equals(requestURI)
// || "/".equals(requestURI)
// || "/home".equals(requestURI)
// || "/home-legacy".equals(requestURI)
|| "/userSession".equals(requestURI)
|| requestURI.contains("/userSession/invalidate/")
|| requestURI.contains("/js/")
|| requestURI.contains("/css/")
|| requestURI.contains("/fonts/")
@ -76,10 +78,22 @@ public class EndpointInterceptor implements HandlerInterceptor {
.filter(s -> s.getSessionId().equals(finalSession.getId()))
.anyMatch(s -> s.isExpired());
if (isExpiredByAdmin) {
if (isExpiredByAdmin
&& !"/".equals(requestURI)
&& !"/home".equals(requestURI)
&& !"/home-legacy".equals(requestURI)) {
response.sendRedirect("/logout");
log.info("Session expired. Logging out user {}", principal.getName());
return false;
} else if (isExpiredByAdmin
&& ("/".equals(requestURI)
|| "/home".equals(requestURI)
|| "/home-legacy".equals(requestURI))) {
log.info(
"Max sessions reached for this user. To continue on this device, please"
+ " close your session in another browser. ");
response.sendError(HttpServletResponse.SC_EXPECTATION_FAILED);
return false;
}
int maxApplicationSessions = sessionsInterface.getMaxApplicationSessions();
@ -101,7 +115,15 @@ public class EndpointInterceptor implements HandlerInterceptor {
.toList();
boolean hasUserActiveSession =
activeSessions.stream().anyMatch(s -> s.getSessionId().equals(sessionId));
// activeSessions.stream().anyMatch(s ->
// s.getSessionId().equals(sessionId));
activeSessions.stream()
.anyMatch(
s ->
s.getSessionId().equals(sessionId)
// && !s.isExpired()
&& s.getPrincipalName()
.equals(principal.getName()));
final String currentPrincipal = principal.getName();

View File

@ -83,9 +83,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
int sessionUserCount = getAllSessions(principalName, false).size();
if (sessionUserCount >= getMaxUserSessions()) {
return;
}
SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId);
if (sessionEntity == null) {
sessionEntity = new SessionEntity();
@ -94,7 +91,12 @@ public class SessionPersistentRegistry implements SessionRegistry {
}
sessionEntity.setPrincipalName(principalName);
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
sessionEntity.setExpired(false);
if (sessionUserCount >= getMaxUserSessions()) {
sessionEntity.setExpired(true);
} else {
sessionEntity.setExpired(false);
}
sessionRepository.save(sessionEntity);
sessionRepository.flush();
}

View File

@ -10,9 +10,11 @@ import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
@ -43,6 +45,72 @@ public class SessionStatusController {
}
}
// list all sessions from authentication user, return String redirect userSession.html
@GetMapping("/userSession")
public String getUserSessions(
HttpServletRequest request, Model model, Authentication authentication) {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/login";
}
HttpSession session = request.getSession(false);
if (session != null) {
Object principal = authentication.getPrincipal();
String principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
}
boolean isSessionValid =
sessionPersistentRegistry.getAllSessions(principalName, false).stream()
.allMatch(
sessionEntity ->
sessionEntity.getSessionId().equals(session.getId()));
if (isSessionValid) {
return "redirect:/";
}
// Get all sessions for the user
List<SessionInformation> sessionList =
sessionPersistentRegistry.getAllSessions(principalName, false).stream()
.filter(
sessionEntity ->
!sessionEntity.getSessionId().equals(session.getId()))
.toList();
model.addAttribute("sessionList", sessionList);
return "userSession";
}
return "redirect:/login";
}
@GetMapping("/userSession/invalidate/{sessionId}")
public String invalidateUserSession(
HttpServletRequest request,
Authentication authentication,
@PathVariable String sessionId)
throws ServletException {
if (authentication == null || !authentication.isAuthenticated()) {
return "redirect:/login";
}
Object principal = authentication.getPrincipal();
String principalName = UserUtils.getUsernameFromPrincipal(principal);
if (principalName == null) {
return "redirect:/login";
}
boolean isOwner =
sessionPersistentRegistry.getAllSessions(principalName, false).stream()
.anyMatch(session -> session.getSessionId().equals(sessionId));
if (isOwner) {
customHttpSessionListener.expireSession(sessionId, false);
sessionPersistentRegistry.registerNewSession(
request.getRequestedSessionId().split(".node0")[0], principal);
// return "redirect:/userSession?messageType=sessionInvalidated"
return "redirect:/userSession";
} else {
return "redirect:/login";
}
}
@GetMapping("/session/invalidate/{sessionId}")
public String invalidateSession(
HttpServletRequest request,

View File

@ -14,6 +14,10 @@
<h1 class="display-2" th:text="#{oops}"></h1>
<p class="lead" th:if="${param.status == '404'}" th:text="#{error.404.1}"></p>
<p class="lead" th:unless="${param.status == '404'}" th:text="#{error.404.2}"></p>
<p class="lead" th:if="${status == 417}">
<a th:href="@{'/userSession'}" th:text="#{session.maxUserSession}">Max sessions reached for this user. To continue on this device, please close your
session in another browser.</a>
</p>
<br>
<h2 th:text="#{error.needHelp}"></h2>
<p th:text="#{error.contactTip}"></p>
@ -21,7 +25,7 @@
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
</div>
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
</div>
</div>
</div>

View File

@ -259,6 +259,8 @@
<div class="modal-footer">
<a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a>
<a class="btn btn-danger" role="button"
th:text="#{settings.userSessions}" th:href="@{'/userSession'}">Sign Out</a>
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
</div>
</div>

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}"
xmlns:th="https://www.thymeleaf.org">
<head>
<th:block th:insert="~{fragments/common :: head(title=#{session.title}, header=#{session.header})}"></th:block>
</head>
<body>
<div id="page-container">
<div id="content-wrap">
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
<br><br>
<div class="container">
<div class="row justify-content-center">
<div class="col-md-9 bg-card">
<div class="tool-header">
<span class="material-symbols-rounded tool-header-icon organize">key</span>
<span class="tool-header-text" th:text="#{session.user}">User Session</span>
</div>
<div class="bg-card mt-3 mb-3">
<span th:text="#{session.maxUserSession}">Max sessions reached for this user. To continue on this device, please close your session in another browser.</span>
<table class="table table-striped table-hover mb-0">
<thead>
<tr>
<th scope="col" th:text="#{session.sessionId}">Session ID</th>
<th scope="col" th:text="#{session.lastRequest}">last Request</th>
<th scope="col" th:text="#{session.invalidate}">Logout</th>
</tr>
</thead>
<tbody>
<tr th:each="userSession : ${sessionList}">
<td th:text="${userSession.getSessionId}"></td>
<td th:text="${userSession.getLastRequest}"></td>
<td><a th:href="@{/userSession/invalidate/{id}(id=${userSession.getSessionId})}"
class="btn btn-sm btn-danger">
<span class="material-symbols-rounded">logout</span>
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
</div>
</body>
</html>