mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-04-19 11:11:18 +00:00
Merge fa8df329dfcd0299d533fd6dd4e92a52975a6564 into 6906344178f102e5aa5273a10fe6a92db1389761
This commit is contained in:
commit
1e1a78cded
@ -65,6 +65,8 @@ sourceSets {
|
||||
exclude "stirling/software/SPDF/model/SessionEntity.java"
|
||||
exclude "stirling/software/SPDF/model/User.java"
|
||||
exclude "stirling/software/SPDF/repository/**"
|
||||
} else {
|
||||
exclude "stirling/software/SPDF/config/anonymus/**"
|
||||
}
|
||||
|
||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||
|
@ -1,28 +1,225 @@
|
||||
package stirling.software.SPDF.config;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsInterface;
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
private final SessionsInterface sessionsInterface;
|
||||
|
||||
public EndpointInterceptor(EndpointConfiguration endpointConfiguration) {
|
||||
public EndpointInterceptor(
|
||||
EndpointConfiguration endpointConfiguration, SessionsInterface sessionsInterface) {
|
||||
this.endpointConfiguration = endpointConfiguration;
|
||||
this.sessionsInterface = sessionsInterface;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean preHandle(
|
||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
session = request.getSession(true);
|
||||
}
|
||||
String requestURI = request.getRequestURI();
|
||||
|
||||
boolean isApiRequest = requestURI.contains("/api/v1");
|
||||
|
||||
if ("GET".equalsIgnoreCase(request.getMethod()) && !isApiRequest) {
|
||||
|
||||
Principal principal = request.getUserPrincipal();
|
||||
|
||||
// allowlist for public or static routes
|
||||
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/")
|
||||
|| requestURI.contains("/images/")
|
||||
|| requestURI.contains("/favicon")
|
||||
|| requestURI.contains("/pdfjs-legacy/")
|
||||
|| requestURI.contains("/error")
|
||||
|| requestURI.contains("/session")
|
||||
|| requestURI.endsWith(".js")
|
||||
|| requestURI.endsWith(".png")
|
||||
|| requestURI.endsWith(".webmanifest")
|
||||
|| requestURI.contains("/files/")) {
|
||||
return true;
|
||||
} else if (principal != null) {
|
||||
if (session == null) {
|
||||
session = request.getSession(true);
|
||||
}
|
||||
|
||||
final HttpSession finalSession = session;
|
||||
String sessionId = finalSession.getId();
|
||||
|
||||
boolean isExpiredByAdmin =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(s -> s.getSessionId().equals(finalSession.getId()))
|
||||
.anyMatch(s -> s.isExpired());
|
||||
|
||||
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();
|
||||
|
||||
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));
|
||||
activeSessions.stream()
|
||||
.anyMatch(
|
||||
s ->
|
||||
s.getSessionId().equals(sessionId)
|
||||
// && !s.isExpired()
|
||||
&& s.getPrincipalName()
|
||||
.equals(principal.getName()));
|
||||
|
||||
final String currentPrincipal = principal.getName();
|
||||
|
||||
long userSessions =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(
|
||||
s ->
|
||||
!s.isExpired()
|
||||
&& currentPrincipal.equals(
|
||||
s.getPrincipalName()))
|
||||
.count();
|
||||
|
||||
int maxUserSessions = sessionsInterface.getMaxUserSessions();
|
||||
|
||||
log.info(
|
||||
"Active sessions for {}: {} (max: {}) | Total: {} (max: {}) | Active"
|
||||
+ " sessions: {}",
|
||||
currentPrincipal,
|
||||
userSessions,
|
||||
maxUserSessions,
|
||||
totalSessionsNonExpired,
|
||||
maxApplicationSessions,
|
||||
hasUserActiveSession);
|
||||
|
||||
if ((userSessions >= maxUserSessions
|
||||
|| totalSessionsNonExpired >= maxApplicationSessions)
|
||||
&& !hasUserActiveSession) {
|
||||
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;
|
||||
}
|
||||
|
||||
// If session is not registered yet, register it; otherwise, update the last request
|
||||
// timestamp.
|
||||
if (!hasUserActiveSession) {
|
||||
log.info("Register session: {}", sessionId);
|
||||
sessionsInterface.registerSession(finalSession);
|
||||
} else {
|
||||
log.info("Update session last request: {}", sessionId);
|
||||
sessionsInterface.updateSessionLastRequest(sessionId);
|
||||
}
|
||||
return true;
|
||||
} else if (principal == null) {
|
||||
if (session == null) {
|
||||
session = request.getSession(true);
|
||||
}
|
||||
final HttpSession finalSession = session;
|
||||
String sessionId = finalSession.getId();
|
||||
|
||||
int maxApplicationSessions = sessionsInterface.getMaxApplicationSessions();
|
||||
|
||||
Collection<SessionsModelInterface> allSessions = sessionsInterface.getAllSessions();
|
||||
|
||||
long totalSessions = 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));
|
||||
|
||||
log.info(
|
||||
"Active sessions for anonymous: Total: {} (max: {}) | Active sessions: {}",
|
||||
totalSessions,
|
||||
maxApplicationSessions,
|
||||
hasUserActiveSession);
|
||||
|
||||
if (totalSessions >= maxApplicationSessions && !hasUserActiveSession) {
|
||||
sessionsInterface.removeSession(finalSession);
|
||||
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;
|
||||
}
|
||||
if (!hasUserActiveSession) {
|
||||
log.debug("Register session: {}", sessionId);
|
||||
sessionsInterface.registerSession(finalSession);
|
||||
} else {
|
||||
log.debug("Update session last request: {}", sessionId);
|
||||
sessionsInterface.updateSessionLastRequest(sessionId);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean isEnabled;
|
||||
|
||||
// Extract the specific endpoint name (e.g: /api/v1/general/remove-pages -> remove-pages)
|
||||
|
@ -0,0 +1,55 @@
|
||||
package stirling.software.SPDF.config.anonymus.session;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import lombok.ToString;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
|
||||
@Setter
|
||||
@ToString(exclude = "session") // exclude session from toString to avoid verbose output or sensitive
|
||||
// data
|
||||
@AllArgsConstructor
|
||||
public class AnonymusSessionInfo implements SessionsModelInterface {
|
||||
private static final String principalName = "anonymousUser";
|
||||
private HttpSession session;
|
||||
|
||||
@Setter(AccessLevel.NONE)
|
||||
private final Date createdAt;
|
||||
|
||||
private Date lastRequest;
|
||||
private Boolean expired;
|
||||
|
||||
public HttpSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
public Date getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getLastRequest() {
|
||||
return lastRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return expired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSessionId() {
|
||||
return session.getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrincipalName() {
|
||||
return principalName;
|
||||
}
|
||||
}
|
@ -0,0 +1,217 @@
|
||||
package stirling.software.SPDF.config.anonymus.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.servlet.http.HttpSessionEvent;
|
||||
import jakarta.servlet.http.HttpSessionListener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsInterface;
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class AnonymusSessionListener implements HttpSessionListener, SessionsInterface {
|
||||
|
||||
@Value("${server.servlet.session.timeout:30m}")
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
// Map for storing sessions including timestamp
|
||||
private static final Map<String, SessionsModelInterface> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent event) {
|
||||
HttpSession session = event.getSession();
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sessions.containsKey(session.getId())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Save creation timestamp
|
||||
Date creationTime = new Date();
|
||||
|
||||
int allNonExpiredSessions = getAllNonExpiredSessions().size();
|
||||
|
||||
if (allNonExpiredSessions >= getMaxUserSessions()) {
|
||||
sessions.put(
|
||||
session.getId(),
|
||||
new AnonymusSessionInfo(session, creationTime, creationTime, true));
|
||||
} else {
|
||||
sessions.put(
|
||||
session.getId(),
|
||||
new AnonymusSessionInfo(session, creationTime, creationTime, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionDestroyed(HttpSessionEvent event) {
|
||||
HttpSession session = event.getSession();
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
AnonymusSessionInfo sessionsInfo = (AnonymusSessionInfo) sessions.get(session.getId());
|
||||
if (sessionsInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Date lastRequest = sessionsInfo.getLastRequest();
|
||||
int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds();
|
||||
Instant now = Instant.now();
|
||||
Instant expirationTime =
|
||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionsInfo.setExpired(true);
|
||||
session.invalidate();
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark a single session as expired
|
||||
public void expireSession(String sessionId) {
|
||||
if (sessions.containsKey(sessionId)) {
|
||||
AnonymusSessionInfo sessionInfo = (AnonymusSessionInfo) sessions.get(sessionId);
|
||||
sessionInfo.setExpired(true);
|
||||
try {
|
||||
sessionInfo.getSession().invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.debug("Session {} already invalidated", sessionInfo.getSession().getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expire first session sorted by last request time aufsteigend
|
||||
public void expireFirstSession(String sessionId) {
|
||||
sessions.values().stream()
|
||||
.filter(info -> !info.isExpired())
|
||||
.filter(info -> !info.getSessionId().equals(sessionId))
|
||||
.sorted((s1, s2) -> s1.getLastRequest().compareTo(s2.getLastRequest()))
|
||||
.findFirst()
|
||||
.ifPresent(
|
||||
session -> {
|
||||
AnonymusSessionInfo sessionInfo = (AnonymusSessionInfo) session;
|
||||
sessionInfo.setExpired(true);
|
||||
try {
|
||||
log.info(
|
||||
"Session {} expired by first Session",
|
||||
sessionInfo.getSession().getId());
|
||||
} catch (IllegalStateException e) {
|
||||
log.info(
|
||||
"Session {} already invalidated",
|
||||
sessionInfo.getSession().getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mark all sessions as expired
|
||||
public void expireAllSessions() {
|
||||
sessions.values()
|
||||
.forEach(
|
||||
sessionInfo -> {
|
||||
AnonymusSessionInfo info = (AnonymusSessionInfo) sessionInfo;
|
||||
info.setExpired(true);
|
||||
HttpSession session = info.getSession();
|
||||
try {
|
||||
session.invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.debug("Session {} already invalidated", session.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Expire all sessions by username
|
||||
public void expireAllSessionsByUsername(String username) {
|
||||
sessions.values().stream()
|
||||
.filter(
|
||||
sessionInfo -> {
|
||||
AnonymusSessionInfo info = (AnonymusSessionInfo) sessionInfo;
|
||||
return info.getPrincipalName().equals(username);
|
||||
})
|
||||
.forEach(
|
||||
sessionInfo -> {
|
||||
AnonymusSessionInfo info = (AnonymusSessionInfo) sessionInfo;
|
||||
info.setExpired(true);
|
||||
HttpSession session = info.getSession();
|
||||
try {
|
||||
session.invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.debug("Session {} already invalidated", session.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionLastRequest(String sessionId) {
|
||||
if (sessions.containsKey(sessionId)) {
|
||||
AnonymusSessionInfo sessionInfo = (AnonymusSessionInfo) sessions.get(sessionId);
|
||||
sessionInfo.setLastRequest(new Date());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllSessions() {
|
||||
return sessions.values().stream().toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllNonExpiredSessions() {
|
||||
return sessions.values().stream().filter(info -> !info.isExpired()).toList();
|
||||
}
|
||||
|
||||
public Collection<SessionsModelInterface> getAllIsExpiredSessions() {
|
||||
return sessions.values().stream().filter(SessionsModelInterface::isExpired).toList();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
sessions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSession(HttpSession session) {
|
||||
if (!sessions.containsKey(session.getId())) {
|
||||
AnonymusSessionInfo sessionInfo =
|
||||
new AnonymusSessionInfo(session, new Date(), new Date(), false);
|
||||
sessions.put(session.getId(), sessionInfo);
|
||||
log.debug("Session {} registered", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSession(HttpSession session) {
|
||||
AnonymusSessionInfo sessionsInfo = (AnonymusSessionInfo) sessions.get(session.getId());
|
||||
if (sessionsInfo != null) {
|
||||
sessionsInfo.setExpired(true);
|
||||
}
|
||||
try {
|
||||
session.invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.debug("Session {} already invalidated", session.getId());
|
||||
}
|
||||
sessions.remove(session.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxApplicationSessions() {
|
||||
// return getMaxUserSessions();
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxUsers() {
|
||||
return 1;
|
||||
}
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
package stirling.software.SPDF.config.anonymus.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class AnonymusSessionService {
|
||||
|
||||
@Autowired private AnonymusSessionListener sessionRegistry;
|
||||
|
||||
@Value("${server.servlet.session.timeout:30m}")
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
// Runs every minute to expire inactive sessions
|
||||
@Scheduled(cron = "0 0/5 * * * ?")
|
||||
public void expireSessions() {
|
||||
Instant now = Instant.now();
|
||||
sessionRegistry.getAllSessions().stream()
|
||||
.filter(session -> !session.isExpired())
|
||||
.forEach(
|
||||
session -> {
|
||||
Date lastRequest = session.getLastRequest();
|
||||
int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds();
|
||||
Instant expirationTime =
|
||||
lastRequest
|
||||
.toInstant()
|
||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
|
||||
if (now.isAfter(expirationTime)) {
|
||||
log.debug("Session expiration triggered");
|
||||
sessionRegistry.expireSession(session.getSessionId());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
package stirling.software.SPDF.config.anonymus.session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
|
||||
@Controller
|
||||
@Slf4j
|
||||
public class AnonymusSessionStatusController {
|
||||
|
||||
@Autowired private AnonymusSessionListener sessionRegistry;
|
||||
|
||||
@GetMapping("/userSession")
|
||||
public String getUserSessions(HttpServletRequest request, Model model) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
|
||||
boolean isSessionValid =
|
||||
sessionRegistry.getAllNonExpiredSessions().stream()
|
||||
.allMatch(
|
||||
sessionEntity ->
|
||||
sessionEntity.getSessionId().equals(session.getId()));
|
||||
|
||||
// Get all sessions for the user
|
||||
List<SessionsModelInterface> sessionList =
|
||||
sessionRegistry.getAllNonExpiredSessions().stream()
|
||||
.filter(
|
||||
sessionEntity ->
|
||||
!sessionEntity.getSessionId().equals(session.getId()))
|
||||
.toList();
|
||||
|
||||
model.addAttribute("sessionList", sessionList);
|
||||
return "userSession";
|
||||
}
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@GetMapping("/userSession/invalidate/{sessionId}")
|
||||
public String invalidateUserSession(
|
||||
HttpServletRequest request, @PathVariable String sessionId) {
|
||||
sessionRegistry.expireSession(sessionId);
|
||||
sessionRegistry.registerSession(request.getSession(false));
|
||||
return "redirect:/userSession";
|
||||
}
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package stirling.software.SPDF.config.interfaces;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
public interface SessionsInterface {
|
||||
|
||||
void updateSessionLastRequest(String sessionId);
|
||||
|
||||
Collection<SessionsModelInterface> getAllSessions();
|
||||
|
||||
Collection<SessionsModelInterface> getAllNonExpiredSessions();
|
||||
|
||||
void registerSession(HttpSession session);
|
||||
|
||||
void removeSession(HttpSession session);
|
||||
|
||||
default int getMaxUserSessions() {
|
||||
return 3;
|
||||
}
|
||||
|
||||
default int getMaxApplicationSessions() {
|
||||
return getMaxUserSessions() * 3;
|
||||
}
|
||||
|
||||
default int getMaxUsers() {
|
||||
return 10;
|
||||
}
|
||||
}
|
@ -0,0 +1,14 @@
|
||||
package stirling.software.SPDF.config.interfaces;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public interface SessionsModelInterface {
|
||||
|
||||
String getSessionId();
|
||||
|
||||
String getPrincipalName();
|
||||
|
||||
Date getLastRequest();
|
||||
|
||||
boolean isExpired();
|
||||
}
|
@ -35,7 +35,7 @@ class AppUpdateAuthService implements ShowAdminInterface {
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return !showUpdateOnlyAdmin;
|
||||
}
|
||||
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
|
||||
if ("anonymousUser".equalsIgnoreCase(authentication.getName())) {
|
||||
return !showUpdateOnlyAdmin;
|
||||
}
|
||||
Optional<User> user = userRepository.findByUsername(authentication.getName());
|
||||
|
@ -36,6 +36,7 @@ import stirling.software.SPDF.config.security.oauth2.CustomOAuth2UserService;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationFailureHandler;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
||||
import stirling.software.SPDF.config.security.session.PreLogoutDataCaptureHandler;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.User;
|
||||
@ -149,7 +150,7 @@ public class SecurityConfiguration {
|
||||
sessionManagement ->
|
||||
sessionManagement
|
||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||
.maximumSessions(10)
|
||||
.maximumSessions(sessionRegistry.getMaxUserSessions())
|
||||
.maxSessionsPreventsLogin(false)
|
||||
.sessionRegistry(sessionRegistry)
|
||||
.expiredUrl("/login?logout=true"));
|
||||
@ -158,6 +159,8 @@ public class SecurityConfiguration {
|
||||
http.logout(
|
||||
logout ->
|
||||
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
||||
.addLogoutHandler(
|
||||
new PreLogoutDataCaptureHandler(sessionRegistry))
|
||||
.logoutSuccessHandler(
|
||||
new CustomLogoutSuccessHandler(applicationProperties))
|
||||
.clearAuthentication(true)
|
||||
|
@ -11,7 +11,6 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@ -397,23 +396,10 @@ public class UserService implements UserServiceInterface {
|
||||
}
|
||||
|
||||
public void invalidateUserSessions(String username) {
|
||||
String usernameP = "";
|
||||
|
||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||
for (SessionInformation sessionsInformation :
|
||||
sessionRegistry.getAllSessions(principal, false)) {
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
usernameP = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
usernameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
usernameP = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
usernameP = stringUser;
|
||||
}
|
||||
String usernameP = UserUtils.getUsernameFromPrincipal(principal);
|
||||
if (usernameP.equalsIgnoreCase(username)) {
|
||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
||||
}
|
||||
sessionRegistry.expireAllSessionsByPrincipalName(usernameP);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,22 @@
|
||||
package stirling.software.SPDF.config.security;
|
||||
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
|
||||
public class UserUtils {
|
||||
public static String getUsernameFromPrincipal(Object principal) {
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
return detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
return oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
return saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
return stringUser;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -5,7 +5,6 @@ import java.sql.SQLException;
|
||||
|
||||
import org.springframework.security.authentication.LockedException;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
||||
@ -17,6 +16,7 @@ import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import stirling.software.SPDF.config.security.LoginAttemptService;
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
@ -45,13 +45,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
||||
throws ServletException, IOException {
|
||||
|
||||
Object principal = authentication.getPrincipal();
|
||||
String username = "";
|
||||
|
||||
if (principal instanceof OAuth2User oAuth2User) {
|
||||
username = oAuth2User.getName();
|
||||
} else if (principal instanceof UserDetails detailsUser) {
|
||||
username = detailsUser.getUsername();
|
||||
}
|
||||
String username = UserUtils.getUsernameFromPrincipal(principal);
|
||||
|
||||
// Get the saved request
|
||||
HttpSession session = request.getSession(false);
|
||||
|
@ -1,30 +1,276 @@
|
||||
package stirling.software.SPDF.config.security.session;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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.Component;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import jakarta.servlet.http.HttpSessionEvent;
|
||||
import jakarta.servlet.http.HttpSessionListener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsInterface;
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class CustomHttpSessionListener implements HttpSessionListener {
|
||||
public class CustomHttpSessionListener implements HttpSessionListener, SessionsInterface {
|
||||
|
||||
private SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final boolean loginEnabled;
|
||||
private final boolean runningEE;
|
||||
|
||||
@Autowired
|
||||
public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) {
|
||||
@Value("${server.servlet.session.timeout:30m}")
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
public CustomHttpSessionListener(
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
@Qualifier("loginEnabled") boolean loginEnabled,
|
||||
@Qualifier("runningEE") boolean runningEE,
|
||||
ApplicationProperties applicationProperties) {
|
||||
super();
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.loginEnabled = loginEnabled;
|
||||
this.runningEE = runningEE;
|
||||
this.applicationProperties = applicationProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent se) {}
|
||||
public Collection<SessionsModelInterface> getAllNonExpiredSessions() {
|
||||
return sessionPersistentRegistry.getAllSessionsNotExpired().stream()
|
||||
.map(session -> (SessionsModelInterface) session)
|
||||
.toList();
|
||||
}
|
||||
|
||||
public List<SessionsModelInterface> getAllSessions(Object principalName, boolean expired) {
|
||||
return sessionPersistentRegistry.getAllSessions().stream()
|
||||
.filter(s -> s.getPrincipalName().equals(principalName))
|
||||
.filter(s -> expired == s.isExpired())
|
||||
.sorted(Comparator.comparing(SessionsModelInterface::getLastRequest))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllSessions() {
|
||||
return new ArrayList<>(sessionPersistentRegistry.getAllSessions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionLastRequest(String sessionId) {
|
||||
sessionPersistentRegistry.refreshLastRequest(sessionId);
|
||||
}
|
||||
|
||||
public Optional<SessionsModelInterface> findLatestSession(String principalName) {
|
||||
return getAllSessions(principalName, false).stream()
|
||||
.filter(s -> s.getPrincipalName().equals(principalName))
|
||||
.max(Comparator.comparing(SessionsModelInterface::getLastRequest));
|
||||
}
|
||||
|
||||
public void expireSession(String sessionId) {
|
||||
sessionPersistentRegistry.expireSession(sessionId);
|
||||
}
|
||||
|
||||
public void expireSession(String sessionId, boolean expiredByAdmin) {
|
||||
sessionPersistentRegistry.expireSession(sessionId, expiredByAdmin);
|
||||
}
|
||||
|
||||
public int getMaxInactiveInterval() {
|
||||
return (int) defaultMaxInactiveInterval.getSeconds();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionCreated(HttpSessionEvent se) {
|
||||
HttpSession session = se.getSession();
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
SecurityContext securityContext = SecurityContextHolder.getContext();
|
||||
if (securityContext == null) {
|
||||
return;
|
||||
}
|
||||
Authentication authentication = securityContext.getAuthentication();
|
||||
if (authentication == null) {
|
||||
return;
|
||||
}
|
||||
Object principal = authentication.getPrincipal();
|
||||
if (principal == null) {
|
||||
return;
|
||||
}
|
||||
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||
if (principalName == null) {
|
||||
return;
|
||||
}
|
||||
if ("anonymousUser".equals(principalName) && loginEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
int allNonExpiredSessions;
|
||||
|
||||
if ("anonymousUser".equals(principalName) && !loginEnabled) {
|
||||
allNonExpiredSessions =
|
||||
(int) getAllSessions().stream().filter(s -> !s.isExpired()).count();
|
||||
} else {
|
||||
allNonExpiredSessions =
|
||||
(int)
|
||||
getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
.filter(s -> s.getPrincipalName().equals(principalName))
|
||||
.count();
|
||||
}
|
||||
|
||||
int all =
|
||||
getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired() && s.getPrincipalName().equals(principalName))
|
||||
.toList()
|
||||
.size();
|
||||
boolean isAnonymousUserWithLogin = "anonymousUser".equals(principalName) && loginEnabled;
|
||||
log.info(
|
||||
"all {} allNonExpiredSessions {} {} isAnonymousUserWithLogin {}",
|
||||
all,
|
||||
allNonExpiredSessions,
|
||||
getMaxUserSessions(),
|
||||
isAnonymousUserWithLogin);
|
||||
|
||||
if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithLogin) {
|
||||
log.info("Session {} Expired=TRUE", session.getId());
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
// if (allNonExpiredSessions > getMaxUserSessions()) {
|
||||
// enforceMaxSessionsForPrincipal(principalName);
|
||||
// }
|
||||
} else if (all >= getMaxUserSessions() && !isAnonymousUserWithLogin) {
|
||||
enforceMaxSessionsForPrincipal(principalName);
|
||||
log.info("Session {} Expired=TRUE", session.getId());
|
||||
} else if (isAnonymousUserWithLogin) {
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
} else {
|
||||
log.info("Session created: {}", session.getId());
|
||||
sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName);
|
||||
}
|
||||
}
|
||||
|
||||
private void enforceMaxSessionsForPrincipal(String principalName) {
|
||||
// Alle aktiven Sessions des Benutzers über das gemeinsame Interface abrufen
|
||||
List<SessionsModelInterface> userSessions =
|
||||
getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired() && principalName.equals(s.getPrincipalName()))
|
||||
.sorted(Comparator.comparing(SessionsModelInterface::getLastRequest))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
int maxAllowed = getMaxUserSessions();
|
||||
if (userSessions.size() > maxAllowed) {
|
||||
int sessionsToRemove = userSessions.size() - maxAllowed;
|
||||
log.debug(
|
||||
"User {} has {} active sessions, removing {} oldest session(s).",
|
||||
principalName,
|
||||
userSessions.size(),
|
||||
sessionsToRemove);
|
||||
for (int i = 0; i < sessionsToRemove; i++) {
|
||||
SessionsModelInterface sessionModel = userSessions.get(i);
|
||||
// Statt auf die HttpSession zuzugreifen, rufen wir die Registry-Methoden auf,
|
||||
// die die Session anhand der Session-ID invalidieren und entfernen.
|
||||
sessionPersistentRegistry.expireSession(sessionModel.getSessionId());
|
||||
sessionPersistentRegistry.removeSessionInformation(sessionModel.getSessionId());
|
||||
log.debug(
|
||||
"Removed session {} for principal {}",
|
||||
sessionModel.getSessionId(),
|
||||
principalName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sessionDestroyed(HttpSessionEvent se) {
|
||||
sessionPersistentRegistry.expireSession(se.getSession().getId());
|
||||
HttpSession session = se.getSession();
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
SessionInformation sessionsInfo =
|
||||
sessionPersistentRegistry.getSessionInformation(session.getId());
|
||||
if (sessionsInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Date lastRequest = sessionsInfo.getLastRequest();
|
||||
int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds();
|
||||
Instant now = Instant.now();
|
||||
Instant expirationTime =
|
||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
session.invalidate();
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerSession(HttpSession session) {
|
||||
sessionCreated(new HttpSessionEvent(session));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeSession(HttpSession session) {
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
session.invalidate();
|
||||
sessionPersistentRegistry.removeSessionInformation(session.getId());
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
|
||||
// Get the maximum number of application sessions
|
||||
@Override
|
||||
public int getMaxApplicationSessions() {
|
||||
if (runningEE) {
|
||||
return getMaxUsers() * getMaxUserSessions();
|
||||
}
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
|
||||
// Get the maximum number of user sessions
|
||||
@Override
|
||||
public int getMaxUserSessions() {
|
||||
if (loginEnabled) {
|
||||
if (runningEE) {
|
||||
return 3;
|
||||
}
|
||||
return Integer.MAX_VALUE; // (3)
|
||||
}
|
||||
return Integer.MAX_VALUE; // (10)
|
||||
}
|
||||
|
||||
// Get the maximum number of user sessions
|
||||
@Override
|
||||
public int getMaxUsers() {
|
||||
if (loginEnabled) {
|
||||
if (runningEE) {
|
||||
int maxUsers = applicationProperties.getPremium().getMaxUsers();
|
||||
if (maxUsers > 0) {
|
||||
return maxUsers;
|
||||
}
|
||||
}
|
||||
return Integer.MAX_VALUE; // (50)
|
||||
}
|
||||
return Integer.MAX_VALUE; // (1)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,51 @@
|
||||
package stirling.software.SPDF.config.security.session;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.web.authentication.logout.LogoutHandler;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@AllArgsConstructor
|
||||
public class PreLogoutDataCaptureHandler implements LogoutHandler {
|
||||
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
|
||||
@Override
|
||||
public void logout(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
Authentication authentication) {
|
||||
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String sessionId = session.getId();
|
||||
if (sessionId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
String path = request.getServletPath();
|
||||
if (path == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only handle explicit logout requests
|
||||
if (!"/logout".equals(path)) {
|
||||
return;
|
||||
}
|
||||
|
||||
log.debug("Session ID: {} Principal: {}", sessionId, authentication.getPrincipal());
|
||||
|
||||
// Mark the session as expired and remove its record
|
||||
sessionPersistentRegistry.expireSession(sessionId);
|
||||
sessionPersistentRegistry.removeSessionInformation(sessionId);
|
||||
}
|
||||
}
|
@ -1,30 +1,45 @@
|
||||
package stirling.software.SPDF.config.security.session;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.session.SessionRegistry;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.transaction.Transactional;
|
||||
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.model.SessionEntity;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SessionPersistentRegistry implements SessionRegistry {
|
||||
|
||||
private final SessionRepository sessionRepository;
|
||||
private final boolean runningEE;
|
||||
private final boolean loginEnabled;
|
||||
|
||||
@Value("${server.servlet.session.timeout:30m}")
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
public SessionPersistentRegistry(SessionRepository sessionRepository) {
|
||||
public SessionPersistentRegistry(
|
||||
SessionRepository sessionRepository,
|
||||
@Qualifier("runningEE") boolean runningEE,
|
||||
@Qualifier("loginEnabled") boolean loginEnabled) {
|
||||
this.runningEE = runningEE;
|
||||
this.sessionRepository = sessionRepository;
|
||||
this.loginEnabled = loginEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -41,17 +56,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
public List<SessionInformation> getAllSessions(
|
||||
Object principal, boolean includeExpiredSessions) {
|
||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||
String principalName = null;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
principalName = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
principalName = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
principalName = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
principalName = stringUser;
|
||||
}
|
||||
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||
|
||||
if (principalName != null) {
|
||||
List<SessionEntity> sessionEntities =
|
||||
@ -72,33 +77,28 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
@Override
|
||||
@Transactional
|
||||
public void registerNewSession(String sessionId, Object principal) {
|
||||
String principalName = null;
|
||||
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
principalName = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
principalName = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
principalName = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
principalName = stringUser;
|
||||
}
|
||||
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||
|
||||
if (principalName != null) {
|
||||
// Clear old sessions for the principal (unsure if needed)
|
||||
// List<SessionEntity> existingSessions =
|
||||
// sessionRepository.findByPrincipalName(principalName);
|
||||
// for (SessionEntity session : existingSessions) {
|
||||
// session.setExpired(true);
|
||||
// sessionRepository.save(session);
|
||||
// }
|
||||
|
||||
SessionEntity sessionEntity = new SessionEntity();
|
||||
int sessionUserCount = getAllSessions(principalName, false).size();
|
||||
|
||||
SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId);
|
||||
if (sessionEntity == null) {
|
||||
sessionEntity = new SessionEntity();
|
||||
sessionEntity.setSessionId(sessionId);
|
||||
log.info("Registering new session for principal: {}", principalName);
|
||||
}
|
||||
sessionEntity.setPrincipalName(principalName);
|
||||
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
||||
|
||||
if (sessionUserCount >= getMaxUserSessions()) {
|
||||
sessionEntity.setExpired(true);
|
||||
} else {
|
||||
sessionEntity.setExpired(false);
|
||||
}
|
||||
sessionRepository.save(sessionEntity);
|
||||
sessionRepository.flush();
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,16 +106,57 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
@Transactional
|
||||
public void removeSessionInformation(String sessionId) {
|
||||
sessionRepository.deleteById(sessionId);
|
||||
sessionRepository.flush();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void removeSessionInformationByPrincipalName(String principalName) {
|
||||
sessionRepository.deleteByPrincipalName(principalName);
|
||||
sessionRepository.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void refreshLastRequest(String sessionId) {
|
||||
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
||||
if (sessionEntityOpt.isPresent()) {
|
||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||
SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId);
|
||||
if (sessionEntity != null) {
|
||||
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
||||
sessionRepository.save(sessionEntity);
|
||||
} else {
|
||||
log.error("Session not found for session ID: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void expireOldestSessionForPrincipal(String principalName) {
|
||||
// Alle Sessions des principalName abrufen
|
||||
List<SessionEntity> sessionsForPrincipal =
|
||||
sessionRepository.findByPrincipalName(principalName);
|
||||
|
||||
// Nur die nicht abgelaufenen Sessions filtern
|
||||
List<SessionEntity> nonExpiredSessions =
|
||||
sessionsForPrincipal.stream()
|
||||
.filter(session -> !session.isExpired())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (nonExpiredSessions.isEmpty()) {
|
||||
log.debug("No active sessions found for principal {}", principalName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Die Session mit dem ältesten lastRequest ermitteln
|
||||
Optional<SessionEntity> oldestSessionOpt =
|
||||
nonExpiredSessions.stream()
|
||||
.min(Comparator.comparing(SessionEntity::getLastRequest));
|
||||
|
||||
if (oldestSessionOpt.isPresent()) {
|
||||
SessionEntity oldestSession = oldestSessionOpt.get();
|
||||
expireSession(oldestSession.getSessionId());
|
||||
removeSessionInformation(oldestSession.getSessionId());
|
||||
log.debug(
|
||||
"Oldest session {} for principal {} has been marked as expired",
|
||||
oldestSession.getSessionId(),
|
||||
principalName);
|
||||
}
|
||||
}
|
||||
|
||||
@ -149,6 +190,61 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
sessionRepository.save(sessionEntity);
|
||||
log.debug("Session expired: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
public void expireAllSessions() {
|
||||
List<SessionEntity> sessionEntities = sessionRepository.findAll();
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
sessionRepository.save(sessionEntity);
|
||||
log.debug("Session expired: {}", sessionEntity.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all sessions as expired by username
|
||||
public void expireAllSessionsByUsername(String username) {
|
||||
List<SessionEntity> sessionEntities = sessionRepository.findByPrincipalName(username);
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
sessionRepository.save(sessionEntity);
|
||||
log.debug("Session expired: {}", sessionEntity.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
// Mark all sessions as expired for a given principal name
|
||||
public void expireAllSessionsByPrincipalName(String principalName) {
|
||||
List<SessionEntity> sessionEntities = sessionRepository.findByPrincipalName(principalName);
|
||||
log.debug("Session entities: {}", sessionEntities.size());
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
log.debug(
|
||||
"Session expired: {} {} {}",
|
||||
sessionEntity.getPrincipalName(),
|
||||
sessionEntity.isExpired(),
|
||||
sessionEntity.getSessionId());
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
removeSessionInformation(sessionEntity.getSessionId());
|
||||
}
|
||||
|
||||
sessionEntities = sessionRepository.findByPrincipalName(principalName);
|
||||
log.debug("Session entities: {}", sessionEntities.size());
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
if (sessionEntity.getPrincipalName().equals(principalName)) {
|
||||
log.debug("Session expired: {}", sessionEntity.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -163,10 +259,19 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
}
|
||||
|
||||
// Update session details by principal name
|
||||
public void updateSessionByPrincipalName(
|
||||
String principalName, boolean expired, Date lastRequest) {
|
||||
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||
}
|
||||
// public void updateSessionByPrincipalName(
|
||||
// String principalName, boolean expired, Date lastRequest) {
|
||||
// sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||
// }
|
||||
|
||||
// Update session details by session ID
|
||||
// public void updateSessionBySessionId(String sessionId) {
|
||||
// SessionEntity sessionEntity = getSessionEntity(sessionId);
|
||||
// if (sessionEntity != null) {
|
||||
// sessionEntity.setLastRequest(new Date());
|
||||
// sessionRepository.save(sessionEntity);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Find the latest session for a given principal name
|
||||
public Optional<SessionEntity> findLatestSession(String principalName) {
|
||||
@ -178,15 +283,29 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
// Sort sessions by lastRequest in descending order
|
||||
Collections.sort(
|
||||
allSessions,
|
||||
new Comparator<SessionEntity>() {
|
||||
@Override
|
||||
public int compare(SessionEntity s1, SessionEntity s2) {
|
||||
// Sort by lastRequest in descending order
|
||||
return s2.getLastRequest().compareTo(s1.getLastRequest());
|
||||
}
|
||||
});
|
||||
(SessionEntity s1, SessionEntity s2) ->
|
||||
s2.getLastRequest().compareTo(s1.getLastRequest()));
|
||||
|
||||
// The first session in the list is the latest session for the given principal name
|
||||
return Optional.of(allSessions.get(0));
|
||||
}
|
||||
|
||||
// Get the maximum number of sessions
|
||||
public int getMaxSessions() {
|
||||
if (runningEE) {
|
||||
return Integer.MAX_VALUE;
|
||||
}
|
||||
return getMaxUserSessions() * 10;
|
||||
}
|
||||
|
||||
// Get the maximum number of user sessions
|
||||
public int getMaxUserSessions() {
|
||||
if (loginEnabled) {
|
||||
if (runningEE) {
|
||||
return 3;
|
||||
}
|
||||
return Integer.MAX_VALUE; // (3)
|
||||
}
|
||||
return Integer.MAX_VALUE; // (10)
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package stirling.software.SPDF.config.security.session;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||
@ -14,7 +15,9 @@ public class SessionRegistryConfig {
|
||||
|
||||
@Bean
|
||||
public SessionPersistentRegistry sessionPersistentRegistry(
|
||||
SessionRepository sessionRepository) {
|
||||
return new SessionPersistentRegistry(sessionRepository);
|
||||
SessionRepository sessionRepository,
|
||||
@Qualifier("runningEE") boolean runningEE,
|
||||
@Qualifier("loginEnabled") boolean loginEnabled) {
|
||||
return new SessionPersistentRegistry(sessionRepository, runningEE, loginEnabled);
|
||||
}
|
||||
}
|
||||
|
@ -21,10 +21,13 @@ public interface SessionRepository extends JpaRepository<SessionEntity, String>
|
||||
|
||||
SessionEntity findBySessionId(String sessionId);
|
||||
|
||||
void deleteByPrincipalName(String principalName);
|
||||
|
||||
@Modifying
|
||||
@Transactional
|
||||
@Query(
|
||||
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE s.principalName = :principalName")
|
||||
"UPDATE SessionEntity s SET s.expired = :expired, s.lastRequest = :lastRequest WHERE"
|
||||
+ " s.principalName = :principalName")
|
||||
void saveByPrincipalName(
|
||||
@Param("expired") boolean expired,
|
||||
@Param("lastRequest") Date lastRequest,
|
||||
|
@ -5,23 +5,43 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class SessionScheduled {
|
||||
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final boolean loginEnabledValue;
|
||||
|
||||
public SessionScheduled(SessionPersistentRegistry sessionPersistentRegistry) {
|
||||
public SessionScheduled(
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 0/5 * * * ?")
|
||||
public void expireSessions() {
|
||||
Instant now = Instant.now();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
||||
if (principal == null) {
|
||||
continue;
|
||||
} else if (principal instanceof String stringPrincipal) {
|
||||
// Expire anonymousUser sessions if login is enabled
|
||||
if ("anonymousUser".equals(stringPrincipal) && loginEnabledValue) {
|
||||
sessionPersistentRegistry.expireAllSessionsByPrincipalName(stringPrincipal);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
List<SessionInformation> sessionInformations =
|
||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||
for (SessionInformation sessionInformation : sessionInformations) {
|
||||
@ -31,6 +51,19 @@ public class SessionScheduled {
|
||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
||||
sessionInformation.expireNow();
|
||||
|
||||
// Invalidate current authentication if expired session belongs to current user
|
||||
if (authentication != null && principal.equals(authentication.getPrincipal())) {
|
||||
authentication.setAuthenticated(false);
|
||||
}
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
log.debug(
|
||||
"Session expired for principal: {} SessionID: {}",
|
||||
principal,
|
||||
sessionInformation.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,136 @@
|
||||
package stirling.software.SPDF.config.anonymus.session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsInterface;
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.config.security.session.CustomHttpSessionListener;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
|
||||
@Controller
|
||||
@Slf4j
|
||||
public class SessionStatusController {
|
||||
|
||||
@Qualifier("loginEnabled")
|
||||
private boolean loginEnabled;
|
||||
|
||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||
@Autowired private SessionsInterface sessionInterface;
|
||||
|
||||
@Autowired private CustomHttpSessionListener customHttpSessionListener;
|
||||
|
||||
// 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()) && loginEnabled) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
String principalName = null;
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
Object principal = authentication.getPrincipal();
|
||||
principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||
if (principalName == null) {
|
||||
return "redirect:/login";
|
||||
}
|
||||
} else {
|
||||
principalName = "anonymousUser";
|
||||
}
|
||||
|
||||
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,
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
@ -13,8 +13,6 @@ import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.session.SessionInformation;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
@ -30,7 +28,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.security.UserService;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.AuthenticationType;
|
||||
@ -304,19 +302,11 @@ public class UserController {
|
||||
if (!enabled) {
|
||||
// Invalidate all sessions if the user is being disabled
|
||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||
String userNameP = "";
|
||||
for (Object principal : principals) {
|
||||
List<SessionInformation> sessionsInformation =
|
||||
sessionRegistry.getAllSessions(principal, false);
|
||||
if (principal instanceof UserDetails detailsUser) {
|
||||
userNameP = detailsUser.getUsername();
|
||||
} else if (principal instanceof OAuth2User oAuth2User) {
|
||||
userNameP = oAuth2User.getName();
|
||||
} else if (principal instanceof CustomSaml2AuthenticatedPrincipal saml2User) {
|
||||
userNameP = saml2User.name();
|
||||
} else if (principal instanceof String stringUser) {
|
||||
userNameP = stringUser;
|
||||
}
|
||||
String userNameP = UserUtils.getUsernameFromPrincipal(principal);
|
||||
|
||||
if (userNameP.equalsIgnoreCase(username)) {
|
||||
for (SessionInformation sessionInfo : sessionsInformation) {
|
||||
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
||||
|
@ -29,8 +29,9 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticatedPrincipal;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
import stirling.software.SPDF.config.security.session.CustomHttpSessionListener;
|
||||
import stirling.software.SPDF.model.ApplicationProperties;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||
@ -53,26 +54,29 @@ public class AccountWebController {
|
||||
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
|
||||
|
||||
private final ApplicationProperties applicationProperties;
|
||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||
private final CustomHttpSessionListener customHttpSessionListener;
|
||||
// Assuming you have a repository for user operations
|
||||
private final UserRepository userRepository;
|
||||
private final boolean loginEnabledValue;
|
||||
private final boolean runningEE;
|
||||
|
||||
public AccountWebController(
|
||||
ApplicationProperties applicationProperties,
|
||||
SessionPersistentRegistry sessionPersistentRegistry,
|
||||
UserRepository userRepository,
|
||||
@Qualifier("runningEE") boolean runningEE) {
|
||||
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||
@Qualifier("runningEE") boolean runningEE,
|
||||
CustomHttpSessionListener customHttpSessionListener) {
|
||||
this.applicationProperties = applicationProperties;
|
||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||
this.userRepository = userRepository;
|
||||
this.loginEnabledValue = loginEnabledValue;
|
||||
this.runningEE = runningEE;
|
||||
this.customHttpSessionListener = customHttpSessionListener;
|
||||
}
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||
// If the user is already authenticated, redirect them to the home page.
|
||||
if (authentication != null && authentication.isAuthenticated()) {
|
||||
if ((authentication != null && authentication.isAuthenticated()) || !loginEnabledValue) {
|
||||
return "redirect:/";
|
||||
}
|
||||
|
||||
@ -150,6 +154,7 @@ public class AccountWebController {
|
||||
case "badCredentials" -> error = "login.invalid";
|
||||
case "locked" -> error = "login.locked";
|
||||
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
|
||||
case "expiredSession" -> error = "expiredSessionMessage";
|
||||
}
|
||||
|
||||
model.addAttribute("error", error);
|
||||
@ -210,14 +215,20 @@ public class AccountWebController {
|
||||
@GetMapping("/adminSettings")
|
||||
public String showAddUserForm(
|
||||
HttpServletRequest request, Model model, Authentication authentication) {
|
||||
String currentSessionId = request.getSession().getId();
|
||||
|
||||
List<User> allUsers = userRepository.findAll();
|
||||
Iterator<User> iterator = allUsers.iterator();
|
||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||
// Map to store session information and user activity status
|
||||
Map<String, Boolean> userSessions = new HashMap<>();
|
||||
Map<String, Date> userLastRequest = new HashMap<>();
|
||||
Map<String, List<SessionsModelInterface>> userActiveSessions = new HashMap<>();
|
||||
int activeUsers = 0;
|
||||
int disabledUsers = 0;
|
||||
int maxSessions = customHttpSessionListener.getMaxApplicationSessions();
|
||||
int maxUserSessions = customHttpSessionListener.getMaxUserSessions();
|
||||
int sessionCount = customHttpSessionListener.getAllNonExpiredSessions().size();
|
||||
while (iterator.hasNext()) {
|
||||
User user = iterator.next();
|
||||
if (user != null) {
|
||||
@ -230,13 +241,13 @@ public class AccountWebController {
|
||||
}
|
||||
}
|
||||
// Determine the user's session status and last request time
|
||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
||||
int maxInactiveInterval = customHttpSessionListener.getMaxInactiveInterval();
|
||||
boolean hasActiveSession = false;
|
||||
Date lastRequest = null;
|
||||
Optional<SessionEntity> latestSession =
|
||||
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
||||
Date lastRequest;
|
||||
Optional<SessionsModelInterface> latestSession =
|
||||
customHttpSessionListener.findLatestSession(user.getUsername());
|
||||
if (latestSession.isPresent()) {
|
||||
SessionEntity sessionEntity = latestSession.get();
|
||||
SessionEntity sessionEntity = (SessionEntity) latestSession.get();
|
||||
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||
Instant now = Instant.now();
|
||||
// Calculate session expiration and update session status accordingly
|
||||
@ -245,7 +256,7 @@ public class AccountWebController {
|
||||
.toInstant()
|
||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
||||
customHttpSessionListener.expireSession(sessionEntity.getSessionId());
|
||||
} else {
|
||||
hasActiveSession = !sessionEntity.isExpired();
|
||||
}
|
||||
@ -262,6 +273,9 @@ public class AccountWebController {
|
||||
if (!user.isEnabled()) {
|
||||
disabledUsers++;
|
||||
}
|
||||
List<SessionsModelInterface> sessionInformations =
|
||||
customHttpSessionListener.getAllSessions(user.getUsername(), false);
|
||||
userActiveSessions.put(user.getUsername(), sessionInformations);
|
||||
}
|
||||
}
|
||||
// Sort users by active status and last request date
|
||||
@ -323,15 +337,21 @@ public class AccountWebController {
|
||||
}
|
||||
|
||||
model.addAttribute("users", sortedUsers);
|
||||
model.addAttribute("currentSessionId", currentSessionId);
|
||||
if (authentication != null) {
|
||||
model.addAttribute("currentUsername", authentication.getName());
|
||||
}
|
||||
model.addAttribute("roleDetails", roleDetails);
|
||||
model.addAttribute("userSessions", userSessions);
|
||||
model.addAttribute("userLastRequest", userLastRequest);
|
||||
model.addAttribute("userActiveSessions", userActiveSessions);
|
||||
model.addAttribute("totalUsers", allUsers.size());
|
||||
model.addAttribute("activeUsers", activeUsers);
|
||||
model.addAttribute("disabledUsers", disabledUsers);
|
||||
|
||||
model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers());
|
||||
model.addAttribute("maxSessions", maxSessions);
|
||||
model.addAttribute("maxUserSessions", maxUserSessions);
|
||||
model.addAttribute("sessionCount", sessionCount);
|
||||
model.addAttribute("maxPaidUsers", customHttpSessionListener.getMaxUsers());
|
||||
return "adminSettings";
|
||||
}
|
||||
|
||||
|
@ -9,15 +9,36 @@ import jakarta.persistence.Table;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||
|
||||
@Entity
|
||||
@Data
|
||||
@Table(name = "sessions")
|
||||
public class SessionEntity implements Serializable {
|
||||
public class SessionEntity implements Serializable, SessionsModelInterface {
|
||||
@Id private String sessionId;
|
||||
|
||||
private String principalName;
|
||||
|
||||
private Date lastRequest;
|
||||
|
||||
private boolean expired;
|
||||
|
||||
private Boolean adminExpired = false;
|
||||
|
||||
@Override
|
||||
public String getSessionId() {
|
||||
return sessionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getPrincipalName() {
|
||||
return principalName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getLastRequest() {
|
||||
return lastRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return expired;
|
||||
}
|
||||
}
|
||||
|
@ -176,6 +176,7 @@ settings.accountSettings=Kontoeinstellungen
|
||||
settings.bored.help=Aktiviert das Easter-Egg-Spiel
|
||||
settings.cacheInputs.name=Formulareingaben speichern
|
||||
settings.cacheInputs.help=Aktivieren, um zuvor verwendete Eingaben für zukünftige Durchläufe zu speichern
|
||||
settings.userSessions=Benutzersitzungen
|
||||
|
||||
changeCreds.title=Anmeldeinformationen ändern
|
||||
changeCreds.header=Aktualisieren Sie Ihre Kontodaten
|
||||
@ -237,6 +238,8 @@ adminUserSettings.activeUsers=Aktive Benutzer:
|
||||
adminUserSettings.disabledUsers=Deaktivierte Benutzer:
|
||||
adminUserSettings.totalUsers=Gesamtzahl der Benutzer:
|
||||
adminUserSettings.lastRequest=Letzte Anfrage
|
||||
adminUserSettings.userSessions=Benutzersitzungen
|
||||
adminUserSettings.totalSessions=Gesamtzahl der Sitzungen:
|
||||
adminUserSettings.usage=View Usage
|
||||
|
||||
endpointStatistics.title=Endpoint Statistics
|
||||
@ -285,6 +288,14 @@ database.notSupported=Diese Funktion ist für deine Datenbankverbindung nicht ve
|
||||
session.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
||||
session.refreshPage=Seite aktualisieren
|
||||
|
||||
#################
|
||||
# USER SESSION #
|
||||
#################
|
||||
userSession.title=Benutzersitzungen
|
||||
userSession.header=Benutzersitzungen
|
||||
userSession.maxUserSession=Wenn die maximale Anzahl Sitzungen für diesen Benutzer erreicht ist, können Sie hier andere Anmeldungen beenden, um auf diesem Gerät fortzufahren.
|
||||
userSession.lastRequest=Letzte Aufrufe
|
||||
|
||||
#############
|
||||
# HOME-PAGE #
|
||||
#############
|
||||
|
@ -176,6 +176,7 @@ settings.accountSettings=Account Settings
|
||||
settings.bored.help=Enables easter egg game
|
||||
settings.cacheInputs.name=Save form inputs
|
||||
settings.cacheInputs.help=Enable to store previously used inputs for future runs
|
||||
settings.userSessions=User Sessions
|
||||
|
||||
changeCreds.title=Change Credentials
|
||||
changeCreds.header=Update Your Account Details
|
||||
@ -237,6 +238,8 @@ adminUserSettings.activeUsers=Active Users:
|
||||
adminUserSettings.disabledUsers=Disabled Users:
|
||||
adminUserSettings.totalUsers=Total Users:
|
||||
adminUserSettings.lastRequest=Last Request
|
||||
adminUserSettings.userSessions=User sessions
|
||||
adminUserSettings.totalSessions=Total Sessions:
|
||||
adminUserSettings.usage=View Usage
|
||||
|
||||
endpointStatistics.title=Endpoint Statistics
|
||||
@ -285,6 +288,14 @@ database.notSupported=This function is not available for your database connectio
|
||||
session.expired=Your session has expired. Please refresh the page and try again.
|
||||
session.refreshPage=Refresh Page
|
||||
|
||||
#################
|
||||
# USER SESSION #
|
||||
#################
|
||||
userSession.title=User Sessions
|
||||
userSession.header=User Sessions
|
||||
userSession.maxUserSession=If the maximum number of sessions for this user is reached, you can end other logins here to continue on this device.
|
||||
userSession.lastRequest=last Request
|
||||
|
||||
#############
|
||||
# HOME-PAGE #
|
||||
#############
|
||||
|
@ -1,7 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html th:lang="${#locale.language}" th:dir="#{language.direction}" th:data-language="${#locale.toString()}" xmlns:th="https://www.thymeleaf.org">
|
||||
<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=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
|
||||
</th:block>
|
||||
<style>
|
||||
.active-user {
|
||||
color: green;
|
||||
@ -25,59 +28,68 @@
|
||||
<br><br>
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-9 bg-card">
|
||||
<div class="col-md-12 bg-card">
|
||||
<div class="tool-header">
|
||||
<span class="material-symbols-rounded tool-header-icon organize">manage_accounts</span>
|
||||
<span class="tool-header-text" th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
|
||||
</div>
|
||||
|
||||
<!-- User Settings Title -->
|
||||
<div style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
|
||||
<a href="#"
|
||||
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
|
||||
<div
|
||||
style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
|
||||
<a href="#" th:data-bs-toggle="${totalUsers >= maxPaidUsers} ? null : 'modal'"
|
||||
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
|
||||
th:class="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? 'btn btn-danger' : 'btn btn-outline-success'"
|
||||
th:title="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
|
||||
th:class="${totalUsers >= maxPaidUsers} ? 'btn btn-danger' : 'btn btn-outline-success'"
|
||||
th:title="${totalUsers >= maxPaidUsers} ? #{adminUserSettings.maxUsersReached} : #{adminUserSettings.addUser}">
|
||||
<span class="material-symbols-rounded">person_add</span>
|
||||
<span th:text="#{adminUserSettings.addUser}">Add New User</span>
|
||||
</a>
|
||||
|
||||
<a href="#"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#changeUserRoleModal"
|
||||
class="btn btn-outline-success"
|
||||
<a href="#" data-bs-toggle="modal" data-bs-target="#changeUserRoleModal" class="btn btn-outline-success"
|
||||
th:title="#{adminUserSettings.changeUserRole}">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</a>
|
||||
|
||||
<a href="/usage" th:if="${@runningEE}"
|
||||
class="btn btn-outline-success"
|
||||
th:title="#{adminUserSettings.usage}">
|
||||
<a href="/usage" th:if="${@runningEE}" class="btn btn-outline-success" th:title="#{adminUserSettings.usage}">
|
||||
<span class="material-symbols-rounded">analytics</span>
|
||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
||||
</a>
|
||||
|
||||
<div class="my-4">
|
||||
<strong th:if="${@runningEE}" style="margin-left: 20px;"
|
||||
text="#{adminUserSettings.totalUsers}">runningEE</strong>
|
||||
<strong th:if="${!@runningEE}" style="margin-left: 20px;"
|
||||
text="#{adminUserSettings.totalUsers}">Non-Paid</strong>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
|
||||
<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>
|
||||
<span th:text="${activeUsers}"></span>
|
||||
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
|
||||
<span th:text="${disabledUsers}"></span>
|
||||
<th:block>
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
|
||||
<span th:if="${@runningProOrHigher}" th:text="${sessionCount}"></span>
|
||||
<span th:text="' | ' + ${maxSessions}"></span>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div th:if="${addMessage}" class="p-3" style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||
<div th:if="${addMessage}" class="p-3"
|
||||
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||
<div class="alert alert-danger mb-auto">
|
||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
</div>
|
||||
<div th:if="${changeMessage}" class="p-3" style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||
<div th:if="${changeMessage}" class="p-3"
|
||||
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||
<div class="alert alert-danger mb-auto">
|
||||
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
||||
</div>
|
||||
@ -92,37 +104,105 @@
|
||||
<th scope="col">#</th>
|
||||
<th scope="col" th:title="#{username}" th:text="#{username}">Username</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2">Actions</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow"
|
||||
th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow"
|
||||
th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||
<th scope="col" class="text-center" th:title="#{adminUserSettings.userSessions}"
|
||||
th:text="#{adminUserSettings.userSessions}">User Sessions</th>
|
||||
<th scope="col" class="text-center" th:title="#{adminUserSettings.actions}"
|
||||
th:text="#{adminUserSettings.actions}" colspan="2">Actions</th>
|
||||
<!-- <th scope="col"></th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr th:each="user : ${users}">
|
||||
<th scope="row" style="align-content: center;" th:text="${user.id}"></th>
|
||||
<td style="align-content: center;" th:text="${user.username}" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
|
||||
<td style="align-content: center;" th:text="#{${user.roleName}}"></td>
|
||||
<td style="align-content: center;" th:text="${user.authenticationType}"></td>
|
||||
<td style="align-content: center;" th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}"></td>
|
||||
<td style="align-content: center;">
|
||||
<form th:if="${user.username != currentUsername}" th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post" onsubmit="return confirmDeleteUser()">
|
||||
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm"><span class="material-symbols-rounded">person_remove</span></button>
|
||||
</form>
|
||||
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}" th:href="@{'/account'}" class="btn btn-outline-success btn-sm"><span class="material-symbols-rounded">edit</span></a>
|
||||
<th:block th:each="user, iterStat : ${users}">
|
||||
<tr>
|
||||
<th scope="row" th:text="${user.id}"></th>
|
||||
<td th:text="${user.username}" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
|
||||
<td th:text="#{${user.roleName}}"></td>
|
||||
<td th:text="${user.authenticationType}"></td>
|
||||
<td
|
||||
th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}">
|
||||
</td>
|
||||
<td style="align-content: center;">
|
||||
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()">
|
||||
<th:block th:if="${@runningProOrHigher}">
|
||||
<td class="text-center">
|
||||
<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>
|
||||
</th:block>
|
||||
<th:block th:if="${!@runningProOrHigher}">
|
||||
<td class="text-center" th:text="${#lists.size(userActiveSessions[user.username])}">0</td>
|
||||
</th:block>
|
||||
<td class="text-center">
|
||||
<form th:if="${user.username != currentUsername}"
|
||||
th:action="@{'/api/v1/user/admin/deleteUser/' + ${user.username}}" method="post"
|
||||
onsubmit="return confirmDeleteUser()">
|
||||
<button type="submit" th:title="#{adminUserSettings.deleteUser}" class="btn btn-info btn-sm">
|
||||
<span class="material-symbols-rounded">person_remove</span>
|
||||
</button>
|
||||
</form>
|
||||
<a th:if="${user.username == currentUsername}" th:title="#{adminUserSettings.editOwnProfil}"
|
||||
th:href="@{'/account'}" class="btn btn-outline-success btn-sm">
|
||||
<span class="material-symbols-rounded">edit</span>
|
||||
</a>
|
||||
</td>
|
||||
<td>
|
||||
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post"
|
||||
onsubmit="return confirmChangeUserStatus()">
|
||||
<input type="hidden" name="enabled" th:value="!${user.enabled}" />
|
||||
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}" class="btn btn-success btn-sm">
|
||||
<button type="submit" th:if="${user.enabled}" th:title="#{adminUserSettings.enabledUser}"
|
||||
class="btn btn-success btn-sm">
|
||||
<span class="material-symbols-rounded">person</span>
|
||||
</button>
|
||||
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}" class="btn btn-danger btn-sm">
|
||||
<button type="submit" th:unless="${user.enabled}" th:title="#{adminUserSettings.disabledUser}"
|
||||
class="btn btn-danger btn-sm">
|
||||
<span class="material-symbols-rounded">person_off</span>
|
||||
</button>
|
||||
</form>
|
||||
</td>
|
||||
</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>
|
||||
</table>
|
||||
</div>
|
||||
@ -144,7 +224,8 @@
|
||||
</div>
|
||||
|
||||
<!-- change User role Modal start -->
|
||||
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel" aria-hidden="true">
|
||||
<div class="modal fade" id="changeUserRoleModal" tabindex="-1" aria-labelledby="changeUserRoleModalLabel"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
@ -154,20 +235,23 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto" th:title="#{downgradeCurrentUserLongMessage}" th:text="#{help}">Help</button>
|
||||
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto"
|
||||
th:title="#{downgradeCurrentUserLongMessage}" th:text="#{help}">Help</button>
|
||||
<form th:action="@{'/api/v1/user/admin/changeRole'}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
<select name="username" class="form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}" th:text="${user.username}">Username</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}"
|
||||
th:text="${user.username}">Username</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
|
||||
Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -192,12 +276,15 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto" th:title="#{adminUserSettings.usernameInfo}" th:text="#{help}">Help</button>
|
||||
<button class="btn btn-outline-info" data-toggle="tooltip" data-placement="auto"
|
||||
th:title="#{adminUserSettings.usernameInfo}" th:text="#{help}">Help</button>
|
||||
<form id="formsaveuser" th:action="@{'/api/v1/user/admin/saveUser'}" method="post">
|
||||
<div class="mb-3">
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
<input type="text" class="form-control" name="username" id="username" th:title="#{adminUserSettings.usernameInfo}" required>
|
||||
<span id="usernameError" style="display: none;" th:text="#{invalidUsernameMessage}">Invalid username!</span>
|
||||
<input type="text" class="form-control" name="username" id="username"
|
||||
th:title="#{adminUserSettings.usernameInfo}" required>
|
||||
<span id="usernameError" style="display: none;" th:text="#{invalidUsernameMessage}">Invalid
|
||||
username!</span>
|
||||
</div>
|
||||
<div class="mb-3" id="passwordContainer">
|
||||
<label for="password" th:text="#{password}">Password</label>
|
||||
@ -207,7 +294,8 @@
|
||||
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="form-control" id="role" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">Role</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
|
||||
Role</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
@ -219,7 +307,8 @@
|
||||
</div>
|
||||
<div class="form-check mb-3" id="checkboxContainer">
|
||||
<input type="checkbox" class="form-check-input" id="forceChange" name="forceChange">
|
||||
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user to change username/password on login</label>
|
||||
<label class="form-check-label" for="forceChange" th:text="#{adminUserSettings.forceChange}">Force user
|
||||
to change username/password on login</label>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
||||
</form>
|
||||
@ -316,4 +405,5 @@
|
||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -14,6 +14,9 @@
|
||||
<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="#{userSession.maxUserSession}">Max sessions reached for this user.</a>
|
||||
</p>
|
||||
<br>
|
||||
<h2 th:text="#{error.needHelp}"></h2>
|
||||
<p th:text="#{error.contactTip}"></p>
|
||||
|
@ -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>
|
||||
|
52
src/main/resources/templates/userSession.html
Normal file
52
src/main/resources/templates/userSession.html
Normal 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=#{userSession.title}, header=#{userSession.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="#{userSession.title}">User Session</span>
|
||||
</div>
|
||||
<div class="bg-card mt-3 mb-3">
|
||||
<span th:text="#{userSession.maxUserSession}">Max sessions reached for this user.</span>
|
||||
<table th:unless="${#lists.isEmpty(sessionList)}" 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="#{userSession.lastRequest}">last Request</th>
|
||||
<th scope="col"></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>
|
@ -9,7 +9,7 @@ check_webpage() {
|
||||
local result_file="$3"
|
||||
|
||||
# Use curl to fetch the page with timeout
|
||||
response=$(curl -s -w "\n%{http_code}" --max-time $timeout "$full_url")
|
||||
response=$(curl -b cookies.txt -s -w "\n%{http_code}" --max-time $timeout "$full_url")
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "FAILED - Connection error or timeout $full_url" >> "$result_file"
|
||||
return 1
|
||||
@ -105,6 +105,7 @@ test_all_urls() {
|
||||
|
||||
# Clean up
|
||||
rm -rf "$tmp_dir"
|
||||
rm -f cookies.txt
|
||||
|
||||
local end_time=$(date +%s)
|
||||
local duration=$((end_time - start_time))
|
||||
@ -158,6 +159,8 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
|
||||
curl -s -c cookies.txt -o /dev/null $base_url/
|
||||
|
||||
# Run tests using the URL list
|
||||
if test_all_urls "$url_file" "$base_url" "$max_parallel"; then
|
||||
echo "All webpage tests passed!"
|
||||
|
Loading…
x
Reference in New Issue
Block a user