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/SessionEntity.java"
|
||||||
exclude "stirling/software/SPDF/model/User.java"
|
exclude "stirling/software/SPDF/model/User.java"
|
||||||
exclude "stirling/software/SPDF/repository/**"
|
exclude "stirling/software/SPDF/repository/**"
|
||||||
|
} else {
|
||||||
|
exclude "stirling/software/SPDF/config/anonymus/**"
|
||||||
}
|
}
|
||||||
|
|
||||||
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
if (System.getenv("STIRLING_PDF_DESKTOP_UI") == "false") {
|
||||||
|
@ -1,28 +1,225 @@
|
|||||||
package stirling.software.SPDF.config;
|
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.stereotype.Component;
|
||||||
import org.springframework.web.servlet.HandlerInterceptor;
|
import org.springframework.web.servlet.HandlerInterceptor;
|
||||||
|
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.interfaces.SessionsInterface;
|
||||||
|
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class EndpointInterceptor implements HandlerInterceptor {
|
public class EndpointInterceptor implements HandlerInterceptor {
|
||||||
|
|
||||||
private final EndpointConfiguration endpointConfiguration;
|
private final EndpointConfiguration endpointConfiguration;
|
||||||
|
private final SessionsInterface sessionsInterface;
|
||||||
|
|
||||||
public EndpointInterceptor(EndpointConfiguration endpointConfiguration) {
|
public EndpointInterceptor(
|
||||||
|
EndpointConfiguration endpointConfiguration, SessionsInterface sessionsInterface) {
|
||||||
this.endpointConfiguration = endpointConfiguration;
|
this.endpointConfiguration = endpointConfiguration;
|
||||||
|
this.sessionsInterface = sessionsInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean preHandle(
|
public boolean preHandle(
|
||||||
HttpServletRequest request, HttpServletResponse response, Object handler)
|
HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||||
throws Exception {
|
throws Exception {
|
||||||
|
HttpSession session = request.getSession(false);
|
||||||
|
if (session == null) {
|
||||||
|
session = request.getSession(true);
|
||||||
|
}
|
||||||
String requestURI = request.getRequestURI();
|
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;
|
boolean isEnabled;
|
||||||
|
|
||||||
// Extract the specific endpoint name (e.g: /api/v1/general/remove-pages -> remove-pages)
|
// 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()) {
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
return !showUpdateOnlyAdmin;
|
return !showUpdateOnlyAdmin;
|
||||||
}
|
}
|
||||||
if (authentication.getName().equalsIgnoreCase("anonymousUser")) {
|
if ("anonymousUser".equalsIgnoreCase(authentication.getName())) {
|
||||||
return !showUpdateOnlyAdmin;
|
return !showUpdateOnlyAdmin;
|
||||||
}
|
}
|
||||||
Optional<User> user = userRepository.findByUsername(authentication.getName());
|
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.CustomSaml2AuthenticationFailureHandler;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
import stirling.software.SPDF.config.security.saml2.CustomSaml2AuthenticationSuccessHandler;
|
||||||
import stirling.software.SPDF.config.security.saml2.CustomSaml2ResponseAuthenticationConverter;
|
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.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.User;
|
import stirling.software.SPDF.model.User;
|
||||||
@ -149,7 +150,7 @@ public class SecurityConfiguration {
|
|||||||
sessionManagement ->
|
sessionManagement ->
|
||||||
sessionManagement
|
sessionManagement
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
|
||||||
.maximumSessions(10)
|
.maximumSessions(sessionRegistry.getMaxUserSessions())
|
||||||
.maxSessionsPreventsLogin(false)
|
.maxSessionsPreventsLogin(false)
|
||||||
.sessionRegistry(sessionRegistry)
|
.sessionRegistry(sessionRegistry)
|
||||||
.expiredUrl("/login?logout=true"));
|
.expiredUrl("/login?logout=true"));
|
||||||
@ -158,6 +159,8 @@ public class SecurityConfiguration {
|
|||||||
http.logout(
|
http.logout(
|
||||||
logout ->
|
logout ->
|
||||||
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
logout.logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
|
||||||
|
.addLogoutHandler(
|
||||||
|
new PreLogoutDataCaptureHandler(sessionRegistry))
|
||||||
.logoutSuccessHandler(
|
.logoutSuccessHandler(
|
||||||
new CustomLogoutSuccessHandler(applicationProperties))
|
new CustomLogoutSuccessHandler(applicationProperties))
|
||||||
.clearAuthentication(true)
|
.clearAuthentication(true)
|
||||||
|
@ -11,7 +11,6 @@ import org.springframework.security.core.Authentication;
|
|||||||
import org.springframework.security.core.GrantedAuthority;
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@ -397,23 +396,10 @@ public class UserService implements UserServiceInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void invalidateUserSessions(String username) {
|
public void invalidateUserSessions(String username) {
|
||||||
String usernameP = "";
|
|
||||||
|
|
||||||
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
for (Object principal : sessionRegistry.getAllPrincipals()) {
|
||||||
for (SessionInformation sessionsInformation :
|
String usernameP = UserUtils.getUsernameFromPrincipal(principal);
|
||||||
sessionRegistry.getAllSessions(principal, false)) {
|
if (usernameP.equalsIgnoreCase(username)) {
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
sessionRegistry.expireAllSessionsByPrincipalName(usernameP);
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (usernameP.equalsIgnoreCase(username)) {
|
|
||||||
sessionRegistry.expireSession(sessionsInformation.getSessionId());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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.authentication.LockedException;
|
||||||
import org.springframework.security.core.Authentication;
|
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.oauth2.core.user.OAuth2User;
|
||||||
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
|
||||||
import org.springframework.security.web.savedrequest.SavedRequest;
|
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.LoginAttemptService;
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
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;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
@ -45,13 +45,7 @@ public class CustomOAuth2AuthenticationSuccessHandler
|
|||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
Object principal = authentication.getPrincipal();
|
Object principal = authentication.getPrincipal();
|
||||||
String username = "";
|
String username = UserUtils.getUsernameFromPrincipal(principal);
|
||||||
|
|
||||||
if (principal instanceof OAuth2User oAuth2User) {
|
|
||||||
username = oAuth2User.getName();
|
|
||||||
} else if (principal instanceof UserDetails detailsUser) {
|
|
||||||
username = detailsUser.getUsername();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the saved request
|
// Get the saved request
|
||||||
HttpSession session = request.getSession(false);
|
HttpSession session = request.getSession(false);
|
||||||
|
@ -1,30 +1,276 @@
|
|||||||
package stirling.software.SPDF.config.security.session;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpSession;
|
||||||
import jakarta.servlet.http.HttpSessionEvent;
|
import jakarta.servlet.http.HttpSessionEvent;
|
||||||
import jakarta.servlet.http.HttpSessionListener;
|
import jakarta.servlet.http.HttpSessionListener;
|
||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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
|
@Component
|
||||||
@Slf4j
|
@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
|
@Value("${server.servlet.session.timeout:30m}")
|
||||||
public CustomHttpSessionListener(SessionPersistentRegistry sessionPersistentRegistry) {
|
private Duration defaultMaxInactiveInterval;
|
||||||
|
|
||||||
|
public CustomHttpSessionListener(
|
||||||
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
|
@Qualifier("loginEnabled") boolean loginEnabled,
|
||||||
|
@Qualifier("runningEE") boolean runningEE,
|
||||||
|
ApplicationProperties applicationProperties) {
|
||||||
super();
|
super();
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
this.loginEnabled = loginEnabled;
|
||||||
|
this.runningEE = runningEE;
|
||||||
|
this.applicationProperties = applicationProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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
|
@Override
|
||||||
public void sessionDestroyed(HttpSessionEvent se) {
|
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;
|
package stirling.software.SPDF.config.security.session;
|
||||||
|
|
||||||
import java.time.Duration;
|
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.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
import org.springframework.security.core.session.SessionInformation;
|
||||||
import org.springframework.security.core.session.SessionRegistry;
|
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 org.springframework.stereotype.Component;
|
||||||
|
|
||||||
import jakarta.transaction.Transactional;
|
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;
|
import stirling.software.SPDF.model.SessionEntity;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@Slf4j
|
||||||
public class SessionPersistentRegistry implements SessionRegistry {
|
public class SessionPersistentRegistry implements SessionRegistry {
|
||||||
|
|
||||||
private final SessionRepository sessionRepository;
|
private final SessionRepository sessionRepository;
|
||||||
|
private final boolean runningEE;
|
||||||
|
private final boolean loginEnabled;
|
||||||
|
|
||||||
@Value("${server.servlet.session.timeout:30m}")
|
@Value("${server.servlet.session.timeout:30m}")
|
||||||
private Duration defaultMaxInactiveInterval;
|
private Duration defaultMaxInactiveInterval;
|
||||||
|
|
||||||
public SessionPersistentRegistry(SessionRepository sessionRepository) {
|
public SessionPersistentRegistry(
|
||||||
|
SessionRepository sessionRepository,
|
||||||
|
@Qualifier("runningEE") boolean runningEE,
|
||||||
|
@Qualifier("loginEnabled") boolean loginEnabled) {
|
||||||
|
this.runningEE = runningEE;
|
||||||
this.sessionRepository = sessionRepository;
|
this.sessionRepository = sessionRepository;
|
||||||
|
this.loginEnabled = loginEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -41,17 +56,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
public List<SessionInformation> getAllSessions(
|
public List<SessionInformation> getAllSessions(
|
||||||
Object principal, boolean includeExpiredSessions) {
|
Object principal, boolean includeExpiredSessions) {
|
||||||
List<SessionInformation> sessionInformations = new ArrayList<>();
|
List<SessionInformation> sessionInformations = new ArrayList<>();
|
||||||
String principalName = null;
|
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (principalName != null) {
|
if (principalName != null) {
|
||||||
List<SessionEntity> sessionEntities =
|
List<SessionEntity> sessionEntities =
|
||||||
@ -72,33 +77,28 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void registerNewSession(String sessionId, Object principal) {
|
public void registerNewSession(String sessionId, Object principal) {
|
||||||
String principalName = null;
|
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (principalName != null) {
|
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.setSessionId(sessionId);
|
|
||||||
|
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.setPrincipalName(principalName);
|
||||||
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
||||||
sessionEntity.setExpired(false);
|
|
||||||
|
if (sessionUserCount >= getMaxUserSessions()) {
|
||||||
|
sessionEntity.setExpired(true);
|
||||||
|
} else {
|
||||||
|
sessionEntity.setExpired(false);
|
||||||
|
}
|
||||||
sessionRepository.save(sessionEntity);
|
sessionRepository.save(sessionEntity);
|
||||||
|
sessionRepository.flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,16 +106,57 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
@Transactional
|
@Transactional
|
||||||
public void removeSessionInformation(String sessionId) {
|
public void removeSessionInformation(String sessionId) {
|
||||||
sessionRepository.deleteById(sessionId);
|
sessionRepository.deleteById(sessionId);
|
||||||
|
sessionRepository.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void removeSessionInformationByPrincipalName(String principalName) {
|
||||||
|
sessionRepository.deleteByPrincipalName(principalName);
|
||||||
|
sessionRepository.flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional
|
@Transactional
|
||||||
public void refreshLastRequest(String sessionId) {
|
public void refreshLastRequest(String sessionId) {
|
||||||
Optional<SessionEntity> sessionEntityOpt = sessionRepository.findById(sessionId);
|
SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId);
|
||||||
if (sessionEntityOpt.isPresent()) {
|
if (sessionEntity != null) {
|
||||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
|
||||||
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
sessionEntity.setLastRequest(new Date()); // Update lastRequest to the current date
|
||||||
sessionRepository.save(sessionEntity);
|
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 sessionEntity = sessionEntityOpt.get();
|
||||||
sessionEntity.setExpired(true); // Set expired to true
|
sessionEntity.setExpired(true); // Set expired to true
|
||||||
sessionRepository.save(sessionEntity);
|
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
|
// Update session details by principal name
|
||||||
public void updateSessionByPrincipalName(
|
// public void updateSessionByPrincipalName(
|
||||||
String principalName, boolean expired, Date lastRequest) {
|
// String principalName, boolean expired, Date lastRequest) {
|
||||||
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
// 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
|
// Find the latest session for a given principal name
|
||||||
public Optional<SessionEntity> findLatestSession(String principalName) {
|
public Optional<SessionEntity> findLatestSession(String principalName) {
|
||||||
@ -178,15 +283,29 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
|||||||
// Sort sessions by lastRequest in descending order
|
// Sort sessions by lastRequest in descending order
|
||||||
Collections.sort(
|
Collections.sort(
|
||||||
allSessions,
|
allSessions,
|
||||||
new Comparator<SessionEntity>() {
|
(SessionEntity s1, SessionEntity s2) ->
|
||||||
@Override
|
s2.getLastRequest().compareTo(s1.getLastRequest()));
|
||||||
public int compare(SessionEntity s1, SessionEntity s2) {
|
|
||||||
// Sort by lastRequest in descending order
|
|
||||||
return s2.getLastRequest().compareTo(s1.getLastRequest());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The first session in the list is the latest session for the given principal name
|
// The first session in the list is the latest session for the given principal name
|
||||||
return Optional.of(allSessions.get(0));
|
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;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.security.core.session.SessionRegistryImpl;
|
import org.springframework.security.core.session.SessionRegistryImpl;
|
||||||
@ -14,7 +15,9 @@ public class SessionRegistryConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SessionPersistentRegistry sessionPersistentRegistry(
|
public SessionPersistentRegistry sessionPersistentRegistry(
|
||||||
SessionRepository sessionRepository) {
|
SessionRepository sessionRepository,
|
||||||
return new SessionPersistentRegistry(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);
|
SessionEntity findBySessionId(String sessionId);
|
||||||
|
|
||||||
|
void deleteByPrincipalName(String principalName);
|
||||||
|
|
||||||
@Modifying
|
@Modifying
|
||||||
@Transactional
|
@Transactional
|
||||||
@Query(
|
@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(
|
void saveByPrincipalName(
|
||||||
@Param("expired") boolean expired,
|
@Param("expired") boolean expired,
|
||||||
@Param("lastRequest") Date lastRequest,
|
@Param("lastRequest") Date lastRequest,
|
||||||
|
@ -5,23 +5,43 @@ import java.time.temporal.ChronoUnit;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
import org.springframework.scheduling.annotation.Scheduled;
|
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.security.core.session.SessionInformation;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
@Slf4j
|
||||||
public class SessionScheduled {
|
public class SessionScheduled {
|
||||||
|
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final SessionPersistentRegistry sessionPersistentRegistry;
|
||||||
|
private final boolean loginEnabledValue;
|
||||||
|
|
||||||
public SessionScheduled(SessionPersistentRegistry sessionPersistentRegistry) {
|
public SessionScheduled(
|
||||||
|
SessionPersistentRegistry sessionPersistentRegistry,
|
||||||
|
@Qualifier("loginEnabled") boolean loginEnabledValue) {
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
||||||
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(cron = "0 0/5 * * * ?")
|
@Scheduled(cron = "0 0/5 * * * ?")
|
||||||
public void expireSessions() {
|
public void expireSessions() {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
for (Object principal : sessionPersistentRegistry.getAllPrincipals()) {
|
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 =
|
List<SessionInformation> sessionInformations =
|
||||||
sessionPersistentRegistry.getAllSessions(principal, false);
|
sessionPersistentRegistry.getAllSessions(principal, false);
|
||||||
for (SessionInformation sessionInformation : sessionInformations) {
|
for (SessionInformation sessionInformation : sessionInformations) {
|
||||||
@ -31,6 +51,19 @@ public class SessionScheduled {
|
|||||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
if (now.isAfter(expirationTime)) {
|
if (now.isAfter(expirationTime)) {
|
||||||
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
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.access.prepost.PreAuthorize;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.security.core.session.SessionInformation;
|
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.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
@ -30,7 +28,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
|||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import stirling.software.SPDF.config.security.UserService;
|
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.config.security.session.SessionPersistentRegistry;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties;
|
import stirling.software.SPDF.model.ApplicationProperties;
|
||||||
import stirling.software.SPDF.model.AuthenticationType;
|
import stirling.software.SPDF.model.AuthenticationType;
|
||||||
@ -304,19 +302,11 @@ public class UserController {
|
|||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
// Invalidate all sessions if the user is being disabled
|
// Invalidate all sessions if the user is being disabled
|
||||||
List<Object> principals = sessionRegistry.getAllPrincipals();
|
List<Object> principals = sessionRegistry.getAllPrincipals();
|
||||||
String userNameP = "";
|
|
||||||
for (Object principal : principals) {
|
for (Object principal : principals) {
|
||||||
List<SessionInformation> sessionsInformation =
|
List<SessionInformation> sessionsInformation =
|
||||||
sessionRegistry.getAllSessions(principal, false);
|
sessionRegistry.getAllSessions(principal, false);
|
||||||
if (principal instanceof UserDetails detailsUser) {
|
String userNameP = UserUtils.getUsernameFromPrincipal(principal);
|
||||||
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;
|
|
||||||
}
|
|
||||||
if (userNameP.equalsIgnoreCase(username)) {
|
if (userNameP.equalsIgnoreCase(username)) {
|
||||||
for (SessionInformation sessionInfo : sessionsInformation) {
|
for (SessionInformation sessionInfo : sessionsInformation) {
|
||||||
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
sessionRegistry.expireSession(sessionInfo.getSessionId());
|
||||||
|
@ -29,8 +29,9 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
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.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;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
import stirling.software.SPDF.model.ApplicationProperties.Security;
|
||||||
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
import stirling.software.SPDF.model.ApplicationProperties.Security.OAUTH2;
|
||||||
@ -53,26 +54,29 @@ public class AccountWebController {
|
|||||||
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
|
public static final String OAUTH_2_AUTHORIZATION = "/oauth2/authorization/";
|
||||||
|
|
||||||
private final ApplicationProperties applicationProperties;
|
private final ApplicationProperties applicationProperties;
|
||||||
private final SessionPersistentRegistry sessionPersistentRegistry;
|
private final CustomHttpSessionListener customHttpSessionListener;
|
||||||
// Assuming you have a repository for user operations
|
// Assuming you have a repository for user operations
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private final boolean loginEnabledValue;
|
||||||
private final boolean runningEE;
|
private final boolean runningEE;
|
||||||
|
|
||||||
public AccountWebController(
|
public AccountWebController(
|
||||||
ApplicationProperties applicationProperties,
|
ApplicationProperties applicationProperties,
|
||||||
SessionPersistentRegistry sessionPersistentRegistry,
|
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
@Qualifier("runningEE") boolean runningEE) {
|
@Qualifier("loginEnabled") boolean loginEnabledValue,
|
||||||
|
@Qualifier("runningEE") boolean runningEE,
|
||||||
|
CustomHttpSessionListener customHttpSessionListener) {
|
||||||
this.applicationProperties = applicationProperties;
|
this.applicationProperties = applicationProperties;
|
||||||
this.sessionPersistentRegistry = sessionPersistentRegistry;
|
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.loginEnabledValue = loginEnabledValue;
|
||||||
this.runningEE = runningEE;
|
this.runningEE = runningEE;
|
||||||
|
this.customHttpSessionListener = customHttpSessionListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/login")
|
@GetMapping("/login")
|
||||||
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
public String login(HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
// If the user is already authenticated, redirect them to the home page.
|
// 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:/";
|
return "redirect:/";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,6 +154,7 @@ public class AccountWebController {
|
|||||||
case "badCredentials" -> error = "login.invalid";
|
case "badCredentials" -> error = "login.invalid";
|
||||||
case "locked" -> error = "login.locked";
|
case "locked" -> error = "login.locked";
|
||||||
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
|
case "oauth2AuthenticationError" -> error = "userAlreadyExistsOAuthMessage";
|
||||||
|
case "expiredSession" -> error = "expiredSessionMessage";
|
||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("error", error);
|
model.addAttribute("error", error);
|
||||||
@ -210,14 +215,20 @@ public class AccountWebController {
|
|||||||
@GetMapping("/adminSettings")
|
@GetMapping("/adminSettings")
|
||||||
public String showAddUserForm(
|
public String showAddUserForm(
|
||||||
HttpServletRequest request, Model model, Authentication authentication) {
|
HttpServletRequest request, Model model, Authentication authentication) {
|
||||||
|
String currentSessionId = request.getSession().getId();
|
||||||
|
|
||||||
List<User> allUsers = userRepository.findAll();
|
List<User> allUsers = userRepository.findAll();
|
||||||
Iterator<User> iterator = allUsers.iterator();
|
Iterator<User> iterator = allUsers.iterator();
|
||||||
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
Map<String, String> roleDetails = Role.getAllRoleDetails();
|
||||||
// Map to store session information and user activity status
|
// Map to store session information and user activity status
|
||||||
Map<String, Boolean> userSessions = new HashMap<>();
|
Map<String, Boolean> userSessions = new HashMap<>();
|
||||||
Map<String, Date> userLastRequest = new HashMap<>();
|
Map<String, Date> userLastRequest = new HashMap<>();
|
||||||
|
Map<String, List<SessionsModelInterface>> userActiveSessions = new HashMap<>();
|
||||||
int activeUsers = 0;
|
int activeUsers = 0;
|
||||||
int disabledUsers = 0;
|
int disabledUsers = 0;
|
||||||
|
int maxSessions = customHttpSessionListener.getMaxApplicationSessions();
|
||||||
|
int maxUserSessions = customHttpSessionListener.getMaxUserSessions();
|
||||||
|
int sessionCount = customHttpSessionListener.getAllNonExpiredSessions().size();
|
||||||
while (iterator.hasNext()) {
|
while (iterator.hasNext()) {
|
||||||
User user = iterator.next();
|
User user = iterator.next();
|
||||||
if (user != null) {
|
if (user != null) {
|
||||||
@ -230,13 +241,13 @@ public class AccountWebController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Determine the user's session status and last request time
|
// Determine the user's session status and last request time
|
||||||
int maxInactiveInterval = sessionPersistentRegistry.getMaxInactiveInterval();
|
int maxInactiveInterval = customHttpSessionListener.getMaxInactiveInterval();
|
||||||
boolean hasActiveSession = false;
|
boolean hasActiveSession = false;
|
||||||
Date lastRequest = null;
|
Date lastRequest;
|
||||||
Optional<SessionEntity> latestSession =
|
Optional<SessionsModelInterface> latestSession =
|
||||||
sessionPersistentRegistry.findLatestSession(user.getUsername());
|
customHttpSessionListener.findLatestSession(user.getUsername());
|
||||||
if (latestSession.isPresent()) {
|
if (latestSession.isPresent()) {
|
||||||
SessionEntity sessionEntity = latestSession.get();
|
SessionEntity sessionEntity = (SessionEntity) latestSession.get();
|
||||||
Date lastAccessedTime = sessionEntity.getLastRequest();
|
Date lastAccessedTime = sessionEntity.getLastRequest();
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
// Calculate session expiration and update session status accordingly
|
// Calculate session expiration and update session status accordingly
|
||||||
@ -245,7 +256,7 @@ public class AccountWebController {
|
|||||||
.toInstant()
|
.toInstant()
|
||||||
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
.plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||||
if (now.isAfter(expirationTime)) {
|
if (now.isAfter(expirationTime)) {
|
||||||
sessionPersistentRegistry.expireSession(sessionEntity.getSessionId());
|
customHttpSessionListener.expireSession(sessionEntity.getSessionId());
|
||||||
} else {
|
} else {
|
||||||
hasActiveSession = !sessionEntity.isExpired();
|
hasActiveSession = !sessionEntity.isExpired();
|
||||||
}
|
}
|
||||||
@ -262,6 +273,9 @@ public class AccountWebController {
|
|||||||
if (!user.isEnabled()) {
|
if (!user.isEnabled()) {
|
||||||
disabledUsers++;
|
disabledUsers++;
|
||||||
}
|
}
|
||||||
|
List<SessionsModelInterface> sessionInformations =
|
||||||
|
customHttpSessionListener.getAllSessions(user.getUsername(), false);
|
||||||
|
userActiveSessions.put(user.getUsername(), sessionInformations);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sort users by active status and last request date
|
// Sort users by active status and last request date
|
||||||
@ -323,15 +337,21 @@ public class AccountWebController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model.addAttribute("users", sortedUsers);
|
model.addAttribute("users", sortedUsers);
|
||||||
model.addAttribute("currentUsername", authentication.getName());
|
model.addAttribute("currentSessionId", currentSessionId);
|
||||||
|
if (authentication != null) {
|
||||||
|
model.addAttribute("currentUsername", authentication.getName());
|
||||||
|
}
|
||||||
model.addAttribute("roleDetails", roleDetails);
|
model.addAttribute("roleDetails", roleDetails);
|
||||||
model.addAttribute("userSessions", userSessions);
|
model.addAttribute("userSessions", userSessions);
|
||||||
model.addAttribute("userLastRequest", userLastRequest);
|
model.addAttribute("userLastRequest", userLastRequest);
|
||||||
|
model.addAttribute("userActiveSessions", userActiveSessions);
|
||||||
model.addAttribute("totalUsers", allUsers.size());
|
model.addAttribute("totalUsers", allUsers.size());
|
||||||
model.addAttribute("activeUsers", activeUsers);
|
model.addAttribute("activeUsers", activeUsers);
|
||||||
model.addAttribute("disabledUsers", disabledUsers);
|
model.addAttribute("disabledUsers", disabledUsers);
|
||||||
|
model.addAttribute("maxSessions", maxSessions);
|
||||||
model.addAttribute("maxPaidUsers", applicationProperties.getPremium().getMaxUsers());
|
model.addAttribute("maxUserSessions", maxUserSessions);
|
||||||
|
model.addAttribute("sessionCount", sessionCount);
|
||||||
|
model.addAttribute("maxPaidUsers", customHttpSessionListener.getMaxUsers());
|
||||||
return "adminSettings";
|
return "adminSettings";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,15 +9,36 @@ import jakarta.persistence.Table;
|
|||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import stirling.software.SPDF.config.interfaces.SessionsModelInterface;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Data
|
@Data
|
||||||
@Table(name = "sessions")
|
@Table(name = "sessions")
|
||||||
public class SessionEntity implements Serializable {
|
public class SessionEntity implements Serializable, SessionsModelInterface {
|
||||||
@Id private String sessionId;
|
@Id private String sessionId;
|
||||||
|
|
||||||
private String principalName;
|
private String principalName;
|
||||||
|
|
||||||
private Date lastRequest;
|
private Date lastRequest;
|
||||||
|
|
||||||
private boolean expired;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,4 +40,4 @@ springdoc.api-docs.path=/v1/api-docs
|
|||||||
# Set the URL of the OpenAPI JSON for the Swagger UI
|
# Set the URL of the OpenAPI JSON for the Swagger UI
|
||||||
springdoc.swagger-ui.url=/v1/api-docs
|
springdoc.swagger-ui.url=/v1/api-docs
|
||||||
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
posthog.api.key=phc_fiR65u5j6qmXTYL56MNrLZSWqLaDW74OrZH0Insd2xq
|
||||||
posthog.host=https://eu.i.posthog.com
|
posthog.host=https://eu.i.posthog.com
|
||||||
|
@ -176,6 +176,7 @@ settings.accountSettings=Kontoeinstellungen
|
|||||||
settings.bored.help=Aktiviert das Easter-Egg-Spiel
|
settings.bored.help=Aktiviert das Easter-Egg-Spiel
|
||||||
settings.cacheInputs.name=Formulareingaben speichern
|
settings.cacheInputs.name=Formulareingaben speichern
|
||||||
settings.cacheInputs.help=Aktivieren, um zuvor verwendete Eingaben für zukünftige Durchläufe zu 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.title=Anmeldeinformationen ändern
|
||||||
changeCreds.header=Aktualisieren Sie Ihre Kontodaten
|
changeCreds.header=Aktualisieren Sie Ihre Kontodaten
|
||||||
@ -237,6 +238,8 @@ adminUserSettings.activeUsers=Aktive Benutzer:
|
|||||||
adminUserSettings.disabledUsers=Deaktivierte Benutzer:
|
adminUserSettings.disabledUsers=Deaktivierte Benutzer:
|
||||||
adminUserSettings.totalUsers=Gesamtzahl der Benutzer:
|
adminUserSettings.totalUsers=Gesamtzahl der Benutzer:
|
||||||
adminUserSettings.lastRequest=Letzte Anfrage
|
adminUserSettings.lastRequest=Letzte Anfrage
|
||||||
|
adminUserSettings.userSessions=Benutzersitzungen
|
||||||
|
adminUserSettings.totalSessions=Gesamtzahl der Sitzungen:
|
||||||
adminUserSettings.usage=View Usage
|
adminUserSettings.usage=View Usage
|
||||||
|
|
||||||
endpointStatistics.title=Endpoint Statistics
|
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.expired=Ihre Sitzung ist abgelaufen. Bitte laden Sie die Seite neu und versuchen Sie es erneut.
|
||||||
session.refreshPage=Seite aktualisieren
|
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 #
|
# HOME-PAGE #
|
||||||
#############
|
#############
|
||||||
|
@ -176,6 +176,7 @@ settings.accountSettings=Account Settings
|
|||||||
settings.bored.help=Enables easter egg game
|
settings.bored.help=Enables easter egg game
|
||||||
settings.cacheInputs.name=Save form inputs
|
settings.cacheInputs.name=Save form inputs
|
||||||
settings.cacheInputs.help=Enable to store previously used inputs for future runs
|
settings.cacheInputs.help=Enable to store previously used inputs for future runs
|
||||||
|
settings.userSessions=User Sessions
|
||||||
|
|
||||||
changeCreds.title=Change Credentials
|
changeCreds.title=Change Credentials
|
||||||
changeCreds.header=Update Your Account Details
|
changeCreds.header=Update Your Account Details
|
||||||
@ -237,6 +238,8 @@ adminUserSettings.activeUsers=Active Users:
|
|||||||
adminUserSettings.disabledUsers=Disabled Users:
|
adminUserSettings.disabledUsers=Disabled Users:
|
||||||
adminUserSettings.totalUsers=Total Users:
|
adminUserSettings.totalUsers=Total Users:
|
||||||
adminUserSettings.lastRequest=Last Request
|
adminUserSettings.lastRequest=Last Request
|
||||||
|
adminUserSettings.userSessions=User sessions
|
||||||
|
adminUserSettings.totalSessions=Total Sessions:
|
||||||
adminUserSettings.usage=View Usage
|
adminUserSettings.usage=View Usage
|
||||||
|
|
||||||
endpointStatistics.title=Endpoint Statistics
|
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.expired=Your session has expired. Please refresh the page and try again.
|
||||||
session.refreshPage=Refresh Page
|
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 #
|
# HOME-PAGE #
|
||||||
#############
|
#############
|
||||||
@ -1426,7 +1437,7 @@ cookieBanner.preferencesModal.description.2=Stirling PDF cannot—and will never
|
|||||||
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
|
cookieBanner.preferencesModal.description.3=Your privacy and trust are at the core of what we do.
|
||||||
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
|
cookieBanner.preferencesModal.necessary.title.1=Strictly Necessary Cookies
|
||||||
cookieBanner.preferencesModal.necessary.title.2=Always Enabled
|
cookieBanner.preferencesModal.necessary.title.2=Always Enabled
|
||||||
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off.
|
cookieBanner.preferencesModal.necessary.description=These cookies are essential for the website to function properly. They enable core features like setting your privacy preferences, logging in, and filling out forms—which is why they can’t be turned off.
|
||||||
cookieBanner.preferencesModal.analytics.title=Analytics
|
cookieBanner.preferencesModal.analytics.title=Analytics
|
||||||
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
cookieBanner.preferencesModal.analytics.description=These cookies help us understand how our tools are being used, so we can focus on building the features our community values most. Rest assured—Stirling PDF cannot and will never track the content of the documents you work with.
|
||||||
|
|
||||||
|
@ -1,319 +1,409 @@
|
|||||||
<!DOCTYPE html>
|
<!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()}"
|
||||||
<head>
|
xmlns:th="https://www.thymeleaf.org">
|
||||||
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}"></th:block>
|
|
||||||
<style>
|
|
||||||
.active-user {
|
|
||||||
color: green;
|
|
||||||
text-shadow: 0 0 5px green;
|
|
||||||
}
|
|
||||||
|
|
||||||
.text-overflow {
|
<head>
|
||||||
max-width: 100px;
|
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
|
||||||
white-space: nowrap;
|
</th:block>
|
||||||
overflow: hidden;
|
<style>
|
||||||
text-overflow:ellipsis;
|
.active-user {
|
||||||
}
|
color: green;
|
||||||
</style>
|
text-shadow: 0 0 5px green;
|
||||||
</head>
|
}
|
||||||
|
|
||||||
<body>
|
.text-overflow {
|
||||||
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
max-width: 100px;
|
||||||
<div id="page-container">
|
white-space: nowrap;
|
||||||
<div id="content-wrap">
|
overflow: hidden;
|
||||||
<th:block th:insert="~{fragments/navbar.html :: navbar}"></th:block>
|
text-overflow: ellipsis;
|
||||||
<br><br>
|
}
|
||||||
<div class="container">
|
</style>
|
||||||
<div class="row justify-content-center">
|
</head>
|
||||||
<div class="col-md-9 bg-card">
|
|
||||||
<div class="tool-header">
|
<body>
|
||||||
<span class="material-symbols-rounded tool-header-icon organize">manage_accounts</span>
|
<th:block th:insert="~{fragments/common :: game}"></th:block>
|
||||||
<span class="tool-header-text" th:text="#{adminUserSettings.header}">Admin User Control Settings</span>
|
<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-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="${totalUsers >= maxPaidUsers} ? null : 'modal'"
|
||||||
|
th:data-bs-target="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : '#addUserModal'"
|
||||||
|
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"
|
||||||
|
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}">
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- User Settings Title -->
|
<div class="my-4">
|
||||||
<div style="background: var(--md-sys-color-outline-variant);padding: .8rem; margin: 10px 0; border-radius: 2rem; text-align: center;">
|
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
|
||||||
<a href="#"
|
<span th:text="${totalUsers}"></span>
|
||||||
th:data-bs-toggle="${@runningProOrHigher && totalUsers >= maxPaidUsers} ? null : 'modal'"
|
<span th:if="${@runningProOrHigher}" th:text="' | ' + ${maxPaidUsers}"></span>
|
||||||
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}">
|
|
||||||
<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"
|
|
||||||
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}">
|
|
||||||
<span class="material-symbols-rounded">analytics</span>
|
|
||||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="my-4">
|
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
|
||||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
|
<span th:text="${activeUsers}"></span>
|
||||||
<span th:text="${totalUsers}"></span>
|
|
||||||
<span th:if="${@runningProOrHigher}" th:text="'/'+${maxPaidUsers}"></span>
|
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
|
||||||
|
<span th:text="${disabledUsers}"></span>
|
||||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
|
<th:block>
|
||||||
<span th:text="${activeUsers}"></span>
|
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
|
||||||
|
<span th:if="${@runningProOrHigher}" th:text="${sessionCount}"></span>
|
||||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
|
<span th:text="' | ' + ${maxSessions}"></span>
|
||||||
<span th:text="${disabledUsers}"></span>
|
</th:block>
|
||||||
</div>
|
</div>
|
||||||
</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"
|
||||||
<div class="alert alert-danger mb-auto">
|
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||||
<span th:text="#{${addMessage}}">Default message if not found</span>
|
<div class="alert alert-danger mb-auto">
|
||||||
</div>
|
<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>
|
||||||
<div class="alert alert-danger mb-auto">
|
<div th:if="${changeMessage}" class="p-3"
|
||||||
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
style="background: var(--md-sys-color-outline-variant);border-radius: 2rem; text-align: center;">
|
||||||
</div>
|
<div class="alert alert-danger mb-auto">
|
||||||
|
<span th:text="#{${changeMessage}}">Default message if not found</span>
|
||||||
</div>
|
</div>
|
||||||
<div th:if="${deleteMessage}" class="alert alert-danger">
|
</div>
|
||||||
<span th:text="#{${deleteMessage}}">Default message if not found</span>
|
<div th:if="${deleteMessage}" class="alert alert-danger">
|
||||||
</div>
|
<span th:text="#{${deleteMessage}}">Default message if not found</span>
|
||||||
<div class="bg-card mt-3 mb-3 table-responsive">
|
</div>
|
||||||
<table class="table table-striped table-hover">
|
<div class="bg-card mt-3 mb-3 table-responsive">
|
||||||
<thead>
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<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" 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>
|
||||||
|
<th:block th:each="user, iterStat : ${users}">
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">#</th>
|
<th scope="row" th:text="${user.id}"></th>
|
||||||
<th scope="col" th:title="#{username}" th:text="#{username}">Username</th>
|
<td th:text="${user.username}" th:classappend="${userSessions[user.username] ? 'active-user' : ''}"></td>
|
||||||
<th scope="col" th:title="#{adminUserSettings.roles}" th:text="#{adminUserSettings.roles}">Roles</th>
|
<td th:text="#{${user.roleName}}"></td>
|
||||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow" th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
<td th:text="${user.authenticationType}"></td>
|
||||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow" th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
<td
|
||||||
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2">Actions</th>
|
th:text="${userLastRequest[user.username] != null ? #dates.format(userLastRequest[user.username], 'yyyy-MM-dd HH:mm:ss') : 'N/A'}">
|
||||||
<!-- <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>
|
|
||||||
</td>
|
</td>
|
||||||
<td style="align-content: center;">
|
<th:block th:if="${@runningProOrHigher}">
|
||||||
<form th:action="@{'/api/v1/user/admin/changeUserEnabled/' + ${user.username}}" method="post" onsubmit="return confirmChangeUserStatus()">
|
<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}" />
|
<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>
|
<span class="material-symbols-rounded">person</span>
|
||||||
</button>
|
</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>
|
<span class="material-symbols-rounded">person_off</span>
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
<tr th:if="${@enableAlphaFunctionality}">
|
||||||
</table>
|
<td colspan="8" class="p-0 border-0">
|
||||||
</div>
|
<div th:id="'sessions__' + ${iterStat.index}" class="collapse">
|
||||||
<p th:if="${!@runningProOrHigher}" th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
<table class="table table-striped table-hover table-sm mb-0">
|
||||||
|
<tbody>
|
||||||
<script th:inline="javascript">
|
<tr th:each="s : ${userActiveSessions[user.username]}">
|
||||||
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
<td scope="row" colspan="4">
|
||||||
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
<span th:text="${s.sessionId}"></span>
|
||||||
function confirmDeleteUser() {
|
<span th:if="${s.sessionId == currentSessionId}" class="text-warning ms-2"
|
||||||
return confirm(delete_confirm_text);
|
title="Aktuelle Sitzung">
|
||||||
}
|
⚠️
|
||||||
function confirmChangeUserStatus() {
|
</span>
|
||||||
return confirm(change_confirm_text);
|
</td>
|
||||||
}
|
<td colspan="2" th:text="${#dates.format(s.lastRequest, 'yyyy-MM-dd HH:mm:ss')}"></td>
|
||||||
</script>
|
<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>
|
</div>
|
||||||
|
<p th:if="${!@runningProOrHigher}" th:text="#{enterpriseEdition.ssoAdvert}"></p>
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
const delete_confirm_text = /*[[#{adminUserSettings.confirmDeleteUser}]]*/ 'Should the user be deleted?';
|
||||||
|
const change_confirm_text = /*[[#{adminUserSettings.confirmChangeUserStatus}]]*/ 'Should the user be disabled/enabled?';
|
||||||
|
function confirmDeleteUser() {
|
||||||
|
return confirm(delete_confirm_text);
|
||||||
|
}
|
||||||
|
function confirmChangeUserStatus() {
|
||||||
|
return confirm(change_confirm_text);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- change User role Modal start -->
|
|
||||||
<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">
|
|
||||||
<h2 th:text="#{adminUserSettings.changeUserRole}">Change User's Role</h2>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
|
||||||
<span class="material-symbols-rounded">close</span>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</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>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add other fields as required -->
|
|
||||||
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- change User role Modal end -->
|
|
||||||
|
|
||||||
<!-- Add User Modal start -->
|
|
||||||
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered" role="document">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h5 class="modal-title" id="addUserModalLabel" th:text="#{adminUserSettings.addUser}">Add New User</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
|
||||||
<span class="material-symbols-rounded">close</span>
|
|
||||||
</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>
|
|
||||||
<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>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3" id="passwordContainer">
|
|
||||||
<label for="password" th:text="#{password}">Password</label>
|
|
||||||
<input type="password" class="form-control" name="password" id="password" required>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<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>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="authType">Authentication Type</label>
|
|
||||||
<select id="authType" name="authType" class="form-control" required>
|
|
||||||
<option value="web" selected>WEB</option>
|
|
||||||
<option value="sso">SSO</option>
|
|
||||||
</select>
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Add User Modal end -->
|
|
||||||
|
|
||||||
<script th:inline="javascript">
|
|
||||||
jQuery.validator.addMethod("usernamePattern", function(value, element) {
|
|
||||||
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
|
||||||
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
|
||||||
|
|
||||||
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
|
||||||
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
|
||||||
|
|
||||||
// Check if the field is optional or meets the requirements
|
|
||||||
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
|
||||||
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
|
||||||
$(document).ready(function() {
|
|
||||||
$('[data-toggle="tooltip"]').tooltip();
|
|
||||||
|
|
||||||
$('#formsaveuser').validate({
|
|
||||||
rules: {
|
|
||||||
username: {
|
|
||||||
required: true,
|
|
||||||
usernamePattern: true
|
|
||||||
},
|
|
||||||
password: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
authType: {
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
messages: {
|
|
||||||
username: {
|
|
||||||
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
errorPlacement: function(error, element) {
|
|
||||||
if (element.attr("name") === "username") {
|
|
||||||
$("#usernameError").text(error.text()).show();
|
|
||||||
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
|
|
||||||
error.insertAfter(element);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
success: function(label, element) {
|
|
||||||
if ($(element).attr("name") === "username") {
|
|
||||||
$("#usernameError").hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#username').on('input', function() {
|
|
||||||
var usernameInput = $(this);
|
|
||||||
var isValid = usernameInput[0].checkValidity();
|
|
||||||
var errorSpan = $('#usernameError');
|
|
||||||
|
|
||||||
if (isValid) {
|
|
||||||
usernameInput.removeClass('invalid').addClass('valid');
|
|
||||||
errorSpan.hide();
|
|
||||||
} else {
|
|
||||||
usernameInput.removeClass('valid').addClass('invalid');
|
|
||||||
errorSpan.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('#authType').on('change', function() {
|
|
||||||
var authType = $(this).val();
|
|
||||||
var passwordField = $('#password');
|
|
||||||
var passwordFieldContainer = $('#passwordContainer');
|
|
||||||
var checkboxContainer = $('#checkboxContainer');
|
|
||||||
|
|
||||||
if (authType === 'sso') {
|
|
||||||
passwordField.removeAttr('required');
|
|
||||||
passwordField.prop('disabled', true).val('');
|
|
||||||
passwordFieldContainer.slideUp('fast');
|
|
||||||
checkboxContainer.slideUp('fast');
|
|
||||||
} else {
|
|
||||||
passwordField.prop('disabled', false);
|
|
||||||
passwordField.attr('required', 'required');
|
|
||||||
passwordFieldContainer.slideDown('fast');
|
|
||||||
checkboxContainer.slideDown('fast');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
|
||||||
</div>
|
</div>
|
||||||
</body>
|
|
||||||
|
<!-- change User role Modal start -->
|
||||||
|
<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">
|
||||||
|
<h2 th:text="#{adminUserSettings.changeUserRole}">Change User's Role</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
<span class="material-symbols-rounded">close</span>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Add other fields as required -->
|
||||||
|
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- change User role Modal end -->
|
||||||
|
|
||||||
|
<!-- Add User Modal start -->
|
||||||
|
<div class="modal fade" id="addUserModal" tabindex="-1" aria-labelledby="addUserModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="addUserModalLabel" th:text="#{adminUserSettings.addUser}">Add New User</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||||
|
<span class="material-symbols-rounded">close</span>
|
||||||
|
</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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3" id="passwordContainer">
|
||||||
|
<label for="password" th:text="#{password}">Password</label>
|
||||||
|
<input type="password" class="form-control" name="password" id="password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<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>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="authType">Authentication Type</label>
|
||||||
|
<select id="authType" name="authType" class="form-control" required>
|
||||||
|
<option value="web" selected>WEB</option>
|
||||||
|
<option value="sso">SSO</option>
|
||||||
|
</select>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary" th:text="#{adminUserSettings.submit}">Save User</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Add User Modal end -->
|
||||||
|
|
||||||
|
<script th:inline="javascript">
|
||||||
|
jQuery.validator.addMethod("usernamePattern", function (value, element) {
|
||||||
|
// Regular expression for user name: Min. 3 characters, max. 50 characters
|
||||||
|
const regexUsername = /^[a-zA-Z0-9](?!.*[-@._+]{2,})([a-zA-Z0-9@._+-]{1,48})[a-zA-Z0-9]$/;
|
||||||
|
|
||||||
|
// Regular expression for email addresses: Max. 320 characters, with RFC-like validation
|
||||||
|
const regexEmail = /^(?=.{1,320}$)(?=.{1,64}@)[A-Za-z0-9](?:[A-Za-z0-9_.+-]*[A-Za-z0-9])?@[^-][A-Za-z0-9-]+(?:\.[A-Za-z0-9-]+)*(?:\.[A-Za-z]{2,})$/;
|
||||||
|
|
||||||
|
// Check if the field is optional or meets the requirements
|
||||||
|
return this.optional(element) || regexUsername.test(value) || regexEmail.test(value);
|
||||||
|
}, /*[[#{invalidUsernameMessage}]]*/ "Invalid username format");
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('[data-toggle="tooltip"]').tooltip();
|
||||||
|
|
||||||
|
$('#formsaveuser').validate({
|
||||||
|
rules: {
|
||||||
|
username: {
|
||||||
|
required: true,
|
||||||
|
usernamePattern: true
|
||||||
|
},
|
||||||
|
password: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
role: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
authType: {
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
messages: {
|
||||||
|
username: {
|
||||||
|
usernamePattern: /*[[#{invalidUsernameMessage}]]*/ "Invalid username format"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
errorPlacement: function (error, element) {
|
||||||
|
if (element.attr("name") === "username") {
|
||||||
|
$("#usernameError").text(error.text()).show();
|
||||||
|
} else if (element.attr("name") !== "role" && element.attr("name") !== "authType") {
|
||||||
|
error.insertAfter(element);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
success: function (label, element) {
|
||||||
|
if ($(element).attr("name") === "username") {
|
||||||
|
$("#usernameError").hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#username').on('input', function () {
|
||||||
|
var usernameInput = $(this);
|
||||||
|
var isValid = usernameInput[0].checkValidity();
|
||||||
|
var errorSpan = $('#usernameError');
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
usernameInput.removeClass('invalid').addClass('valid');
|
||||||
|
errorSpan.hide();
|
||||||
|
} else {
|
||||||
|
usernameInput.removeClass('valid').addClass('invalid');
|
||||||
|
errorSpan.show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#authType').on('change', function () {
|
||||||
|
var authType = $(this).val();
|
||||||
|
var passwordField = $('#password');
|
||||||
|
var passwordFieldContainer = $('#passwordContainer');
|
||||||
|
var checkboxContainer = $('#checkboxContainer');
|
||||||
|
|
||||||
|
if (authType === 'sso') {
|
||||||
|
passwordField.removeAttr('required');
|
||||||
|
passwordField.prop('disabled', true).val('');
|
||||||
|
passwordFieldContainer.slideUp('fast');
|
||||||
|
checkboxContainer.slideUp('fast');
|
||||||
|
} else {
|
||||||
|
passwordField.prop('disabled', false);
|
||||||
|
passwordField.attr('required', 'required');
|
||||||
|
passwordFieldContainer.slideDown('fast');
|
||||||
|
checkboxContainer.slideDown('fast');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<th:block th:insert="~{fragments/footer.html :: footer}"></th:block>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
@ -14,6 +14,9 @@
|
|||||||
<h1 class="display-2" th:text="#{oops}"></h1>
|
<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: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: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>
|
<br>
|
||||||
<h2 th:text="#{error.needHelp}"></h2>
|
<h2 th:text="#{error.needHelp}"></h2>
|
||||||
<p th:text="#{error.contactTip}"></p>
|
<p th:text="#{error.contactTip}"></p>
|
||||||
@ -21,7 +24,7 @@
|
|||||||
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
|
<a href="https://github.com/Stirling-Tools/Stirling-PDF/issues" id="github-button" class="btn btn-primary" target="_blank" th:text="#{error.github}"></a>
|
||||||
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
|
<a href="https://discord.gg/HYmhKj45pU" id="discord-button" class="btn btn-primary" target="_blank" th:text="#{joinDiscord}"></a>
|
||||||
</div>
|
</div>
|
||||||
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
|
<a th:href="@{'/'}" id="home-button" class="home-button btn btn-primary" th:text="#{goHomepage}"></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -259,6 +259,8 @@
|
|||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
|
<a th:if="${@loginEnabled and @activeSecurity}" class="btn btn-danger" role="button"
|
||||||
th:text="#{settings.signOut}" th:href="@{'/logout'}">Sign Out</a>
|
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>
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
@ -43,7 +43,7 @@ check_health() {
|
|||||||
capture_file_list() {
|
capture_file_list() {
|
||||||
local container_name=$1
|
local container_name=$1
|
||||||
local output_file=$2
|
local output_file=$2
|
||||||
|
|
||||||
echo "Capturing file list from $container_name..."
|
echo "Capturing file list from $container_name..."
|
||||||
# Get all files in one command, output directly from Docker to avoid path issues
|
# Get all files in one command, output directly from Docker to avoid path issues
|
||||||
# Skip proc, sys, dev, and the specified LibreOffice config directory
|
# Skip proc, sys, dev, and the specified LibreOffice config directory
|
||||||
@ -60,12 +60,12 @@ capture_file_list() {
|
|||||||
-not -path '*/tmp/lu*' \
|
-not -path '*/tmp/lu*' \
|
||||||
-not -path '*/tmp/tmp*' \
|
-not -path '*/tmp/tmp*' \
|
||||||
2>/dev/null | xargs -I{} sh -c 'stat -c \"%n %s %Y\" \"{}\" 2>/dev/null || true' | sort" > "$output_file"
|
2>/dev/null | xargs -I{} sh -c 'stat -c \"%n %s %Y\" \"{}\" 2>/dev/null || true' | sort" > "$output_file"
|
||||||
|
|
||||||
# Check if the output file has content
|
# Check if the output file has content
|
||||||
if [ ! -s "$output_file" ]; then
|
if [ ! -s "$output_file" ]; then
|
||||||
echo "WARNING: Failed to capture file list or container returned empty list"
|
echo "WARNING: Failed to capture file list or container returned empty list"
|
||||||
echo "Trying alternative approach..."
|
echo "Trying alternative approach..."
|
||||||
|
|
||||||
# Alternative simpler approach - just get paths as a fallback
|
# Alternative simpler approach - just get paths as a fallback
|
||||||
docker exec $container_name sh -c "find / -type f \
|
docker exec $container_name sh -c "find / -type f \
|
||||||
-not -path '*/proc/*' \
|
-not -path '*/proc/*' \
|
||||||
@ -79,14 +79,14 @@ capture_file_list() {
|
|||||||
-not -path '*/tmp/lu*' \
|
-not -path '*/tmp/lu*' \
|
||||||
-not -path '*/tmp/tmp*' \
|
-not -path '*/tmp/tmp*' \
|
||||||
2>/dev/null | sort" > "$output_file"
|
2>/dev/null | sort" > "$output_file"
|
||||||
|
|
||||||
if [ ! -s "$output_file" ]; then
|
if [ ! -s "$output_file" ]; then
|
||||||
echo "ERROR: All attempts to capture file list failed"
|
echo "ERROR: All attempts to capture file list failed"
|
||||||
# Create a dummy entry to prevent diff errors
|
# Create a dummy entry to prevent diff errors
|
||||||
echo "NO_FILES_FOUND 0 0" > "$output_file"
|
echo "NO_FILES_FOUND 0 0" > "$output_file"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "File list captured to $output_file"
|
echo "File list captured to $output_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,24 +96,24 @@ compare_file_lists() {
|
|||||||
local after_file=$2
|
local after_file=$2
|
||||||
local diff_file=$3
|
local diff_file=$3
|
||||||
local container_name=$4 # Added container_name parameter
|
local container_name=$4 # Added container_name parameter
|
||||||
|
|
||||||
echo "Comparing file lists..."
|
echo "Comparing file lists..."
|
||||||
|
|
||||||
# Check if files exist and have content
|
# Check if files exist and have content
|
||||||
if [ ! -s "$before_file" ] || [ ! -s "$after_file" ]; then
|
if [ ! -s "$before_file" ] || [ ! -s "$after_file" ]; then
|
||||||
echo "WARNING: One or both file lists are empty."
|
echo "WARNING: One or both file lists are empty."
|
||||||
|
|
||||||
if [ ! -s "$before_file" ]; then
|
if [ ! -s "$before_file" ]; then
|
||||||
echo "Before file is empty: $before_file"
|
echo "Before file is empty: $before_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -s "$after_file" ]; then
|
if [ ! -s "$after_file" ]; then
|
||||||
echo "After file is empty: $after_file"
|
echo "After file is empty: $after_file"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Create empty diff file
|
# Create empty diff file
|
||||||
> "$diff_file"
|
> "$diff_file"
|
||||||
|
|
||||||
# Check if we at least have the after file to look for temp files
|
# Check if we at least have the after file to look for temp files
|
||||||
if [ -s "$after_file" ]; then
|
if [ -s "$after_file" ]; then
|
||||||
echo "Checking for temp files in the after snapshot..."
|
echo "Checking for temp files in the after snapshot..."
|
||||||
@ -128,23 +128,23 @@ compare_file_lists() {
|
|||||||
echo "No temporary files found in the after snapshot."
|
echo "No temporary files found in the after snapshot."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Both files exist and have content, proceed with diff
|
# Both files exist and have content, proceed with diff
|
||||||
diff "$before_file" "$after_file" > "$diff_file"
|
diff "$before_file" "$after_file" > "$diff_file"
|
||||||
|
|
||||||
if [ -s "$diff_file" ]; then
|
if [ -s "$diff_file" ]; then
|
||||||
echo "Detected changes in files:"
|
echo "Detected changes in files:"
|
||||||
cat "$diff_file"
|
cat "$diff_file"
|
||||||
|
|
||||||
# Extract only added files (lines starting with ">")
|
# Extract only added files (lines starting with ">")
|
||||||
grep "^>" "$diff_file" > "${diff_file}.added" || true
|
grep "^>" "$diff_file" > "${diff_file}.added" || true
|
||||||
if [ -s "${diff_file}.added" ]; then
|
if [ -s "${diff_file}.added" ]; then
|
||||||
echo "New files created during test:"
|
echo "New files created during test:"
|
||||||
cat "${diff_file}.added" | sed 's/^> //'
|
cat "${diff_file}.added" | sed 's/^> //'
|
||||||
|
|
||||||
# Check for tmp files
|
# Check for tmp files
|
||||||
grep -i "tmp\|temp" "${diff_file}.added" > "${diff_file}.tmp" || true
|
grep -i "tmp\|temp" "${diff_file}.added" > "${diff_file}.tmp" || true
|
||||||
if [ -s "${diff_file}.tmp" ]; then
|
if [ -s "${diff_file}.tmp" ]; then
|
||||||
@ -155,7 +155,7 @@ compare_file_lists() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Extract only removed files (lines starting with "<")
|
# Extract only removed files (lines starting with "<")
|
||||||
grep "^<" "$diff_file" > "${diff_file}.removed" || true
|
grep "^<" "$diff_file" > "${diff_file}.removed" || true
|
||||||
if [ -s "${diff_file}.removed" ]; then
|
if [ -s "${diff_file}.removed" ]; then
|
||||||
@ -165,7 +165,7 @@ compare_file_lists() {
|
|||||||
else
|
else
|
||||||
echo "No file changes detected during test."
|
echo "No file changes detected during test."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -212,8 +212,8 @@ main() {
|
|||||||
|
|
||||||
cd "$PROJECT_ROOT"
|
cd "$PROJECT_ROOT"
|
||||||
|
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||||
export COMPOSE_DOCKER_CLI_BUILD=0
|
export COMPOSE_DOCKER_CLI_BUILD=0
|
||||||
export DOCKER_ENABLE_SECURITY=false
|
export DOCKER_ENABLE_SECURITY=false
|
||||||
# Run the gradlew build command and check if it fails
|
# Run the gradlew build command and check if it fails
|
||||||
if ! ./gradlew clean build; then
|
if ! ./gradlew clean build; then
|
||||||
@ -282,27 +282,27 @@ main() {
|
|||||||
# Create directory for file snapshots if it doesn't exist
|
# Create directory for file snapshots if it doesn't exist
|
||||||
SNAPSHOT_DIR="$PROJECT_ROOT/testing/file_snapshots"
|
SNAPSHOT_DIR="$PROJECT_ROOT/testing/file_snapshots"
|
||||||
mkdir -p "$SNAPSHOT_DIR"
|
mkdir -p "$SNAPSHOT_DIR"
|
||||||
|
|
||||||
# Capture file list before running behave tests
|
# Capture file list before running behave tests
|
||||||
BEFORE_FILE="$SNAPSHOT_DIR/files_before_behave.txt"
|
BEFORE_FILE="$SNAPSHOT_DIR/files_before_behave.txt"
|
||||||
AFTER_FILE="$SNAPSHOT_DIR/files_after_behave.txt"
|
AFTER_FILE="$SNAPSHOT_DIR/files_after_behave.txt"
|
||||||
DIFF_FILE="$SNAPSHOT_DIR/files_diff.txt"
|
DIFF_FILE="$SNAPSHOT_DIR/files_diff.txt"
|
||||||
|
|
||||||
# Define container name variable for consistency
|
# Define container name variable for consistency
|
||||||
CONTAINER_NAME="Stirling-PDF-Security-Fat-with-login"
|
CONTAINER_NAME="Stirling-PDF-Security-Fat-with-login"
|
||||||
|
|
||||||
capture_file_list "$CONTAINER_NAME" "$BEFORE_FILE"
|
capture_file_list "$CONTAINER_NAME" "$BEFORE_FILE"
|
||||||
|
|
||||||
cd "testing/cucumber"
|
cd "testing/cucumber"
|
||||||
if python -m behave; then
|
if python -m behave; then
|
||||||
# Wait 10 seconds before capturing the file list after tests
|
# Wait 10 seconds before capturing the file list after tests
|
||||||
echo "Waiting 5 seconds for any file operations to complete..."
|
echo "Waiting 5 seconds for any file operations to complete..."
|
||||||
sleep 5
|
sleep 5
|
||||||
|
|
||||||
# Capture file list after running behave tests
|
# Capture file list after running behave tests
|
||||||
cd "$PROJECT_ROOT"
|
cd "$PROJECT_ROOT"
|
||||||
capture_file_list "$CONTAINER_NAME" "$AFTER_FILE"
|
capture_file_list "$CONTAINER_NAME" "$AFTER_FILE"
|
||||||
|
|
||||||
# Compare file lists
|
# Compare file lists
|
||||||
if compare_file_lists "$BEFORE_FILE" "$AFTER_FILE" "$DIFF_FILE" "$CONTAINER_NAME"; then
|
if compare_file_lists "$BEFORE_FILE" "$AFTER_FILE" "$DIFF_FILE" "$CONTAINER_NAME"; then
|
||||||
echo "No unexpected temporary files found."
|
echo "No unexpected temporary files found."
|
||||||
@ -311,19 +311,19 @@ main() {
|
|||||||
echo "WARNING: Unexpected temporary files detected after behave tests!"
|
echo "WARNING: Unexpected temporary files detected after behave tests!"
|
||||||
failed_tests+=("Stirling-PDF-Regression-Temp-Files")
|
failed_tests+=("Stirling-PDF-Regression-Temp-Files")
|
||||||
fi
|
fi
|
||||||
|
|
||||||
passed_tests+=("Stirling-PDF-Regression")
|
passed_tests+=("Stirling-PDF-Regression")
|
||||||
else
|
else
|
||||||
failed_tests+=("Stirling-PDF-Regression")
|
failed_tests+=("Stirling-PDF-Regression")
|
||||||
echo "Printing docker logs of failed regression"
|
echo "Printing docker logs of failed regression"
|
||||||
docker logs "$CONTAINER_NAME"
|
docker logs "$CONTAINER_NAME"
|
||||||
echo "Printed docker logs of failed regression"
|
echo "Printed docker logs of failed regression"
|
||||||
|
|
||||||
# Still capture file list after failure for analysis
|
# Still capture file list after failure for analysis
|
||||||
# Wait 10 seconds before capturing the file list
|
# Wait 10 seconds before capturing the file list
|
||||||
echo "Waiting 5 seconds before capturing file list..."
|
echo "Waiting 5 seconds before capturing file list..."
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|
||||||
cd "$PROJECT_ROOT"
|
cd "$PROJECT_ROOT"
|
||||||
capture_file_list "$CONTAINER_NAME" "$AFTER_FILE"
|
capture_file_list "$CONTAINER_NAME" "$AFTER_FILE"
|
||||||
compare_file_lists "$BEFORE_FILE" "$AFTER_FILE" "$DIFF_FILE" "$CONTAINER_NAME"
|
compare_file_lists "$BEFORE_FILE" "$AFTER_FILE" "$DIFF_FILE" "$CONTAINER_NAME"
|
||||||
@ -372,4 +372,4 @@ main() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
main
|
main
|
||||||
|
@ -9,7 +9,7 @@ check_webpage() {
|
|||||||
local result_file="$3"
|
local result_file="$3"
|
||||||
|
|
||||||
# Use curl to fetch the page with timeout
|
# 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
|
if [ $? -ne 0 ]; then
|
||||||
echo "FAILED - Connection error or timeout $full_url" >> "$result_file"
|
echo "FAILED - Connection error or timeout $full_url" >> "$result_file"
|
||||||
return 1
|
return 1
|
||||||
@ -75,13 +75,13 @@ test_all_urls() {
|
|||||||
|
|
||||||
((total_count++))
|
((total_count++))
|
||||||
((url_index++))
|
((url_index++))
|
||||||
|
|
||||||
# Run the check in background
|
# Run the check in background
|
||||||
test_url "$url" "$base_url" "$tmp_dir" "$url_index" &
|
test_url "$url" "$base_url" "$tmp_dir" "$url_index" &
|
||||||
|
|
||||||
# Track the job
|
# Track the job
|
||||||
((active_jobs++))
|
((active_jobs++))
|
||||||
|
|
||||||
# If we've reached max_parallel, wait for a job to finish
|
# If we've reached max_parallel, wait for a job to finish
|
||||||
if [ $active_jobs -ge $max_parallel ]; then
|
if [ $active_jobs -ge $max_parallel ]; then
|
||||||
wait -n # Wait for any child process to exit
|
wait -n # Wait for any child process to exit
|
||||||
@ -97,7 +97,7 @@ test_all_urls() {
|
|||||||
if [ -f "${tmp_dir}/result_${i}.txt" ]; then
|
if [ -f "${tmp_dir}/result_${i}.txt" ]; then
|
||||||
cat "${tmp_dir}/result_${i}.txt"
|
cat "${tmp_dir}/result_${i}.txt"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -f "${tmp_dir}/failed_${i}" ]; then
|
if [ -f "${tmp_dir}/failed_${i}" ]; then
|
||||||
failed_count=$((failed_count + $(cat "${tmp_dir}/failed_${i}")))
|
failed_count=$((failed_count + $(cat "${tmp_dir}/failed_${i}")))
|
||||||
fi
|
fi
|
||||||
@ -105,6 +105,7 @@ test_all_urls() {
|
|||||||
|
|
||||||
# Clean up
|
# Clean up
|
||||||
rm -rf "$tmp_dir"
|
rm -rf "$tmp_dir"
|
||||||
|
rm -f cookies.txt
|
||||||
|
|
||||||
local end_time=$(date +%s)
|
local end_time=$(date +%s)
|
||||||
local duration=$((end_time - start_time))
|
local duration=$((end_time - start_time))
|
||||||
@ -158,6 +159,8 @@ main() {
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
curl -s -c cookies.txt -o /dev/null $base_url/
|
||||||
|
|
||||||
# Run tests using the URL list
|
# Run tests using the URL list
|
||||||
if test_all_urls "$url_file" "$base_url" "$max_parallel"; then
|
if test_all_urls "$url_file" "$base_url" "$max_parallel"; then
|
||||||
echo "All webpage tests passed!"
|
echo "All webpage tests passed!"
|
||||||
@ -171,4 +174,4 @@ main() {
|
|||||||
# Run main if script is executed directly
|
# Run main if script is executed directly
|
||||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||||
main "$@"
|
main "$@"
|
||||||
fi
|
fi
|
||||||
|
Loading…
x
Reference in New Issue
Block a user