mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-04-19 11:11:18 +00:00
clean up
This commit is contained in:
parent
762571f42b
commit
fe4d2823aa
@ -2,7 +2,6 @@ package stirling.software.SPDF.config;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
@ -20,15 +19,11 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
private final EndpointConfiguration endpointConfiguration;
|
||||
private final SessionsInterface sessionsInterface;
|
||||
private boolean loginEnabled = true;
|
||||
|
||||
public EndpointInterceptor(
|
||||
EndpointConfiguration endpointConfiguration,
|
||||
SessionsInterface sessionsInterface,
|
||||
@Qualifier("loginEnabled") boolean loginEnabled) {
|
||||
EndpointConfiguration endpointConfiguration, SessionsInterface sessionsInterface) {
|
||||
this.endpointConfiguration = endpointConfiguration;
|
||||
this.sessionsInterface = sessionsInterface;
|
||||
this.loginEnabled = loginEnabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -44,6 +39,7 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
Principal principal = request.getUserPrincipal();
|
||||
|
||||
// allowlist for public or static routes
|
||||
if ("/".equals(request.getRequestURI())
|
||||
|| "/login".equals(request.getRequestURI())
|
||||
|| "/home".equals(request.getRequestURI())
|
||||
@ -70,7 +66,6 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
|
||||
final String currentPrincipal = principal.getName();
|
||||
|
||||
// Zähle alle nicht abgelaufenen Sessions des aktuellen Benutzers.
|
||||
long userSessions =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(
|
||||
@ -80,25 +75,19 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
s.getPrincipalName()))
|
||||
.count();
|
||||
|
||||
// Zähle alle nicht abgelaufenen Sessions in der Anwendung.
|
||||
long totalSessions =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
.count();
|
||||
|
||||
log.info(
|
||||
"Aktive Sessions für {}: {} (max: {}) | Gesamt: {} (max: {})",
|
||||
log.debug(
|
||||
"Active sessions for {}: {} (max: {}) | Total: {} (max: {})",
|
||||
currentPrincipal,
|
||||
userSessions,
|
||||
sessionsInterface.getMaxUserSessions(),
|
||||
totalSessions,
|
||||
sessionsInterface.getMaxApplicationSessions());
|
||||
|
||||
// Prüfe die Grenzen:
|
||||
// Falls entweder die Benutzersessions oder die Anwendungssessions das Limit
|
||||
// erreicht haben
|
||||
// und die aktuelle Session noch NICHT registriert ist, dann wird ein Fehler
|
||||
// zurückgegeben.
|
||||
boolean isCurrentSessionRegistered =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
@ -114,8 +103,40 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Wenn die Session noch nicht registriert ist, registriere sie; andernfalls update
|
||||
// den Last-Request.
|
||||
// If session is not registered yet, register it; otherwise, update the last request
|
||||
// timestamp.
|
||||
if (!isCurrentSessionRegistered) {
|
||||
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();
|
||||
|
||||
long totalSessions =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
.count();
|
||||
boolean isCurrentSessionRegistered =
|
||||
sessionsInterface.getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
.anyMatch(s -> s.getSessionId().equals(sessionId));
|
||||
|
||||
if (totalSessions >= sessionsInterface.getMaxApplicationSessions()
|
||||
&& !isCurrentSessionRegistered) {
|
||||
response.sendError(
|
||||
HttpServletResponse.SC_UNAUTHORIZED,
|
||||
"Max sessions reached for this user. To continue on this device, please"
|
||||
+ " close your session in another browser.");
|
||||
return false;
|
||||
}
|
||||
if (!isCurrentSessionRegistered) {
|
||||
log.info("Register session: {}", sessionId);
|
||||
sessionsInterface.registerSession(finalSession);
|
||||
@ -128,6 +149,7 @@ public class EndpointInterceptor implements HandlerInterceptor {
|
||||
}
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
// Check if endpoint is enabled in config
|
||||
if (!endpointConfiguration.isEndpointEnabled(requestURI)) {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN, "This endpoint is disabled");
|
||||
return false;
|
||||
|
@ -5,13 +5,16 @@ 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")
|
||||
@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;
|
||||
@ -22,14 +25,6 @@ public class AnonymusSessionInfo implements SessionsModelInterface {
|
||||
private Date lastRequest;
|
||||
private Boolean expired;
|
||||
|
||||
public AnonymusSessionInfo(
|
||||
HttpSession session, Date createdAt, Date lastRequest, Boolean expired) {
|
||||
this.session = session;
|
||||
this.createdAt = createdAt;
|
||||
this.lastRequest = lastRequest;
|
||||
this.expired = expired;
|
||||
}
|
||||
|
||||
public HttpSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
@ -4,7 +4,6 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
@ -28,7 +27,7 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
@Value("${server.servlet.session.timeout:120s}") // TODO: Change to 30m
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
// Map zur Speicherung der Sessions inkl. Timestamp
|
||||
// Map for storing sessions including timestamp
|
||||
private static final Map<String, SessionsModelInterface> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
@ -42,7 +41,7 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
return;
|
||||
}
|
||||
|
||||
// Speichern des Erstellungszeitpunkts
|
||||
// Save creation timestamp
|
||||
Date creationTime = new Date();
|
||||
|
||||
int allNonExpiredSessions = getAllNonExpiredSessions().size();
|
||||
@ -78,11 +77,11 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionsInfo.setExpired(true);
|
||||
session.invalidate();
|
||||
log.info("Session {} wurde Expired=TRUE", session.getId());
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// Make a session as expired
|
||||
// Mark a single session as expired
|
||||
public void expireSession(String sessionId) {
|
||||
if (sessions.containsKey(sessionId)) {
|
||||
AnonymusSessionInfo sessionInfo = (AnonymusSessionInfo) sessions.get(sessionId);
|
||||
@ -90,12 +89,12 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
try {
|
||||
sessionInfo.getSession().invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.info("Session {} ist bereits invalidiert", sessionInfo.getSession().getId());
|
||||
log.debug("Session {} already invalidated", sessionInfo.getSession().getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make all sessions as expired
|
||||
// Mark all sessions as expired
|
||||
public void expireAllSessions() {
|
||||
sessions.values()
|
||||
.forEach(
|
||||
@ -106,12 +105,12 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
try {
|
||||
session.invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.info("Session {} ist bereits invalidiert", session.getId());
|
||||
log.debug("Session {} already invalidated", session.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Mark all sessions as expired by username
|
||||
// Expire all sessions by username
|
||||
public void expireAllSessionsByUsername(String username) {
|
||||
sessions.values().stream()
|
||||
.filter(
|
||||
@ -127,27 +126,11 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
try {
|
||||
session.invalidate();
|
||||
} catch (IllegalStateException e) {
|
||||
log.info("Session {} ist bereits invalidiert", session.getId());
|
||||
log.debug("Session {} already invalidated", session.getId());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSessionValid(String sessionId) {
|
||||
boolean exists = sessions.containsKey(sessionId);
|
||||
boolean expired = exists ? sessions.get(sessionId).isExpired() : false;
|
||||
return exists && !expired;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOldestNonExpiredSession(String sessionId) {
|
||||
Collection<SessionsModelInterface> nonExpiredSessions = getAllNonExpiredSessions();
|
||||
return nonExpiredSessions.stream()
|
||||
.min(Comparator.comparing(SessionsModelInterface::getLastRequest))
|
||||
.map(oldest -> oldest.getSessionId().equals(sessionId))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionLastRequest(String sessionId) {
|
||||
if (sessions.containsKey(sessionId)) {
|
||||
@ -174,21 +157,13 @@ public class AnonymusSessionRegistry implements HttpSessionListener, SessionsInt
|
||||
sessions.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllNonExpiredSessionsBySessionId(
|
||||
String sessionId) {
|
||||
return sessions.values().stream()
|
||||
.filter(info -> !info.isExpired() && info.getSessionId().equals(sessionId))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@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.info("Session {} wurde registriert", session.getId());
|
||||
log.debug("Session {} registered", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,16 +4,16 @@ import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
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 AnonymusSessionRegistry sessionRegistry;
|
||||
@ -21,27 +21,25 @@ public class AnonymusSessionService {
|
||||
@Value("${server.servlet.session.timeout:120s}") // TODO: Change to 30m
|
||||
private Duration defaultMaxInactiveInterval;
|
||||
|
||||
// Runs every minute to expire inactive sessions
|
||||
@Scheduled(cron = "0 0/1 * * * ?")
|
||||
public void expireSessions() {
|
||||
Instant now = Instant.now();
|
||||
List<AnonymusSessionInfo> allNonExpiredSessions =
|
||||
sessionRegistry.getAllNonExpiredSessions().stream()
|
||||
.map(s -> (AnonymusSessionInfo) s)
|
||||
.collect(Collectors.toList());
|
||||
for (AnonymusSessionInfo sessionInformation : allNonExpiredSessions) {
|
||||
Date lastRequest = sessionInformation.getLastRequest();
|
||||
int maxInactiveInterval = (int) defaultMaxInactiveInterval.getSeconds();
|
||||
Instant expirationTime =
|
||||
lastRequest.toInstant().plus(maxInactiveInterval, ChronoUnit.SECONDS);
|
||||
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.info(
|
||||
"SessionID: {} expiration time: {} Current time: {}",
|
||||
sessionInformation.getSession().getId(),
|
||||
expirationTime,
|
||||
now);
|
||||
sessionInformation.setExpired(true);
|
||||
}
|
||||
}
|
||||
if (now.isAfter(expirationTime)) {
|
||||
log.debug("Session expiration triggered");
|
||||
sessionRegistry.expireSession(session.getSessionId());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -19,10 +19,11 @@ public class AnonymusSessionStatusController {
|
||||
public ResponseEntity<String> getSessionStatus(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
// No session found
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found");
|
||||
}
|
||||
|
||||
boolean isActivSesssion =
|
||||
boolean isActiveSession =
|
||||
sessionRegistry.getAllSessions().stream()
|
||||
.filter(s -> s.getSessionId().equals(session.getId()))
|
||||
.anyMatch(s -> !s.isExpired());
|
||||
@ -33,12 +34,17 @@ public class AnonymusSessionStatusController {
|
||||
long userSessions = sessionCount;
|
||||
int maxUserSessions = sessionRegistry.getMaxUserSessions();
|
||||
|
||||
if (userSessions >= maxUserSessions && !isActivSesssion) {
|
||||
// Session invalid or expired
|
||||
if (userSessions >= maxUserSessions && !isActiveSession) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body("Session ungültig oder abgelaufen");
|
||||
} else if (session.getId() != null && isActivSesssion) {
|
||||
return ResponseEntity.ok("Session gültig: " + session.getId());
|
||||
} else {
|
||||
.body("Session invalid or expired");
|
||||
}
|
||||
// Valid session
|
||||
else if (session.getId() != null && isActiveSession) {
|
||||
return ResponseEntity.ok("Valid session: " + session.getId());
|
||||
}
|
||||
// Fallback message with session count
|
||||
else {
|
||||
return ResponseEntity.ok("User has " + userSessions + " sessions");
|
||||
}
|
||||
}
|
||||
@ -47,6 +53,7 @@ public class AnonymusSessionStatusController {
|
||||
public ResponseEntity<String> expireSession(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session != null) {
|
||||
// Invalidate current session
|
||||
sessionRegistry.expireSession(session.getId());
|
||||
return ResponseEntity.ok("Session invalidated");
|
||||
} else {
|
||||
@ -56,12 +63,14 @@ public class AnonymusSessionStatusController {
|
||||
|
||||
@GetMapping("/session/expire/all")
|
||||
public ResponseEntity<String> expireAllSessions() {
|
||||
// Invalidate all sessions
|
||||
sessionRegistry.expireAllSessions();
|
||||
return ResponseEntity.ok("All sessions invalidated");
|
||||
}
|
||||
|
||||
@GetMapping("/session/expire/{username}")
|
||||
public ResponseEntity<String> expireAllSessionsByUsername(@PathVariable String username) {
|
||||
// Invalidate all sessions for specific user
|
||||
sessionRegistry.expireAllSessionsByUsername(username);
|
||||
return ResponseEntity.ok("All sessions invalidated for user: " + username);
|
||||
}
|
||||
|
@ -6,18 +6,12 @@ import jakarta.servlet.http.HttpSession;
|
||||
|
||||
public interface SessionsInterface {
|
||||
|
||||
boolean isSessionValid(String sessionId);
|
||||
|
||||
boolean isOldestNonExpiredSession(String sessionId);
|
||||
|
||||
void updateSessionLastRequest(String sessionId);
|
||||
|
||||
Collection<SessionsModelInterface> getAllSessions();
|
||||
|
||||
Collection<SessionsModelInterface> getAllNonExpiredSessions();
|
||||
|
||||
Collection<SessionsModelInterface> getAllNonExpiredSessionsBySessionId(String sessionId);
|
||||
|
||||
void registerSession(HttpSession session);
|
||||
|
||||
void removeSession(HttpSession session);
|
||||
|
@ -58,13 +58,26 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (!loginEnabledValue) {
|
||||
// If login is not enabled, just pass all requests without authentication
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
String requestURI = request.getRequestURI();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
// Check for session expiration (unsure if needed)
|
||||
// if (authentication != null && authentication.isAuthenticated()) {
|
||||
// String sessionId = request.getSession().getId();
|
||||
// SessionInformation sessionInfo =
|
||||
// sessionPersistentRegistry.getSessionInformation(sessionId);
|
||||
//
|
||||
// if (sessionInfo != null && sessionInfo.isExpired()) {
|
||||
// SecurityContextHolder.clearContext();
|
||||
// response.sendRedirect(request.getContextPath() + "/login?expired=true");
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
|
||||
// Check for API key in the request headers if no authentication exists
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
@ -110,10 +123,10 @@ public class UserAuthenticationFilter extends OncePerRequestFilter {
|
||||
response.getWriter()
|
||||
.write(
|
||||
"Authentication required. Please provide a X-API-KEY in request"
|
||||
+ " header.\n"
|
||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
||||
+ "Alternatively you can disable authentication if this is"
|
||||
+ " unexpected");
|
||||
+ " header.\n"
|
||||
+ "This is found in Settings -> Account Settings -> API Key\n"
|
||||
+ "Alternatively you can disable authentication if this is"
|
||||
+ " unexpected");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ 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.SessionEntity;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ -57,40 +56,11 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllNonExpiredSessionsBySessionId(
|
||||
String sessionId) {
|
||||
return sessionPersistentRegistry.getAllNonExpiredSessionsBySessionId(sessionId).stream()
|
||||
.map(session -> (SessionsModelInterface) session)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<SessionsModelInterface> getAllSessions() {
|
||||
return new ArrayList<>(sessionPersistentRegistry.getAllSessions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSessionValid(String sessionId) {
|
||||
List<SessionEntity> allSessions = sessionPersistentRegistry.getAllSessions();
|
||||
// gib zurück ob ist expired
|
||||
return allSessions.stream()
|
||||
.anyMatch(
|
||||
session ->
|
||||
session.getSessionId().equals(sessionId) && !session.isExpired());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isOldestNonExpiredSession(String sessionId) {
|
||||
log.info("isOldestNonExpiredSession for sessionId: {}", sessionId);
|
||||
List<SessionEntity> nonExpiredSessions =
|
||||
sessionPersistentRegistry.getAllSessionsNotExpired();
|
||||
return nonExpiredSessions.stream()
|
||||
.min(Comparator.comparing(SessionEntity::getLastRequest))
|
||||
.map(oldest -> oldest.getSessionId().equals(sessionId))
|
||||
.orElse(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSessionLastRequest(String sessionId) {
|
||||
sessionPersistentRegistry.refreshLastRequest(sessionId);
|
||||
@ -121,16 +91,20 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
if ("anonymousUser".equals(principalName) && loginEnabled) {
|
||||
return;
|
||||
}
|
||||
int allNonExpiredSessions = getAllNonExpiredSessions().size();
|
||||
|
||||
allNonExpiredSessions =
|
||||
getAllSessions().stream()
|
||||
.filter(s -> !s.isExpired())
|
||||
.filter(s -> s.getPrincipalName().equals(principalName))
|
||||
.filter(s -> "anonymousUser".equals(principalName) && !loginEnabled)
|
||||
.peek(s -> log.info("Session {}", s.getPrincipalName()))
|
||||
.toList()
|
||||
.size();
|
||||
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()
|
||||
@ -138,7 +112,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
.toList()
|
||||
.size();
|
||||
boolean isAnonymousUserWithoutLogin = "anonymousUser".equals(principalName) && loginEnabled;
|
||||
log.info(
|
||||
log.debug(
|
||||
"all {} allNonExpiredSessions {} {} isAnonymousUserWithoutLogin {}",
|
||||
all,
|
||||
allNonExpiredSessions,
|
||||
@ -146,7 +120,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
isAnonymousUserWithoutLogin);
|
||||
|
||||
if (allNonExpiredSessions >= getMaxApplicationSessions() && !isAnonymousUserWithoutLogin) {
|
||||
log.info("Session {} Expired=TRUE", session.getId());
|
||||
log.debug("Session {} Expired=TRUE", session.getId());
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
// if (allNonExpiredSessions > getMaxUserSessions()) {
|
||||
@ -154,12 +128,12 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
// }
|
||||
} else if (all >= getMaxUserSessions() && !isAnonymousUserWithoutLogin) {
|
||||
enforceMaxSessionsForPrincipal(principalName);
|
||||
log.info("Session {} Expired=TRUE", principalName);
|
||||
log.debug("Session {} Expired=TRUE", session.getId());
|
||||
} else if (isAnonymousUserWithoutLogin) {
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
} else {
|
||||
log.info("Session created: {}", principalName);
|
||||
log.debug("Session created: {}", session.getId());
|
||||
sessionPersistentRegistry.registerNewSession(se.getSession().getId(), principalName);
|
||||
}
|
||||
}
|
||||
@ -175,7 +149,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
int maxAllowed = getMaxUserSessions();
|
||||
if (userSessions.size() > maxAllowed) {
|
||||
int sessionsToRemove = userSessions.size() - maxAllowed;
|
||||
log.info(
|
||||
log.debug(
|
||||
"User {} has {} active sessions, removing {} oldest session(s).",
|
||||
principalName,
|
||||
userSessions.size(),
|
||||
@ -186,7 +160,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
// die die Session anhand der Session-ID invalidieren und entfernen.
|
||||
sessionPersistentRegistry.expireSession(sessionModel.getSessionId());
|
||||
sessionPersistentRegistry.removeSessionInformation(sessionModel.getSessionId());
|
||||
log.info(
|
||||
log.debug(
|
||||
"Removed session {} for principal {}",
|
||||
sessionModel.getSessionId(),
|
||||
principalName);
|
||||
@ -216,7 +190,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
session.invalidate();
|
||||
sessionPersistentRegistry.removeSessionInformation(se.getSession().getId());
|
||||
log.info("Session {} wurde Expired=TRUE", session.getId());
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -230,7 +204,7 @@ public class CustomHttpSessionListener implements HttpSessionListener, SessionsI
|
||||
sessionPersistentRegistry.expireSession(session.getId());
|
||||
session.invalidate();
|
||||
sessionPersistentRegistry.removeSessionInformation(session.getId());
|
||||
log.info("Session {} wurde Expired=TRUE", session.getId());
|
||||
log.debug("Session {} expired=TRUE", session.getId());
|
||||
}
|
||||
|
||||
// Get the maximum number of sessions
|
||||
|
@ -31,14 +31,20 @@ public class PreLogoutDataCaptureHandler implements LogoutHandler {
|
||||
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);
|
||||
}
|
||||
|
@ -76,11 +76,17 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
String principalName = UserUtils.getUsernameFromPrincipal(principal);
|
||||
|
||||
if (principalName != null) {
|
||||
|
||||
int sessionUserCount = getAllSessions(principalName, false).size();
|
||||
|
||||
if (sessionUserCount >= getMaxUserSessions()) {
|
||||
return;
|
||||
}
|
||||
SessionEntity sessionEntity = sessionRepository.findBySessionId(sessionId);
|
||||
if (sessionEntity == null) {
|
||||
sessionEntity = new SessionEntity();
|
||||
sessionEntity.setSessionId(sessionId);
|
||||
log.info("Registering new session for principal: {}", principalName);
|
||||
log.debug("Registering new session for principal: {}", principalName);
|
||||
}
|
||||
sessionEntity.setPrincipalName(principalName);
|
||||
sessionEntity.setLastRequest(new Date()); // Set lastRequest to the current date
|
||||
@ -128,7 +134,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (nonExpiredSessions.isEmpty()) {
|
||||
log.info("Keine nicht abgelaufenen Sessions für principal {} gefunden", principalName);
|
||||
log.debug("No active sessions found for principal {}", principalName);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -141,8 +147,8 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
SessionEntity oldestSession = oldestSessionOpt.get();
|
||||
expireSession(oldestSession.getSessionId());
|
||||
removeSessionInformation(oldestSession.getSessionId());
|
||||
log.info(
|
||||
"Die älteste Session {} für principal {} wurde als expired markiert",
|
||||
log.debug(
|
||||
"Oldest session {} for principal {} has been marked as expired",
|
||||
oldestSession.getSessionId(),
|
||||
principalName);
|
||||
}
|
||||
@ -161,11 +167,6 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Retrieve all non-expired sessions
|
||||
public List<SessionEntity> getAllNonExpiredSessionsBySessionId(String sessionId) {
|
||||
return sessionRepository.findBySessionIdAndExpired(sessionId, false);
|
||||
}
|
||||
|
||||
// Retrieve all non-expired sessions
|
||||
public List<SessionEntity> getAllSessionsNotExpired() {
|
||||
return sessionRepository.findByExpired(false);
|
||||
@ -183,6 +184,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
SessionEntity sessionEntity = sessionEntityOpt.get();
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
sessionRepository.save(sessionEntity);
|
||||
log.debug("Session expired: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +194,7 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
sessionRepository.save(sessionEntity);
|
||||
log.debug("Session expired: {}", sessionEntity.getSessionId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -201,28 +204,29 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
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.info("Session entities: {}", sessionEntities.size());
|
||||
log.debug("Session entities: {}", sessionEntities.size());
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
log.info(
|
||||
log.debug(
|
||||
"Session expired: {} {} {}",
|
||||
sessionEntity.getPrincipalName(),
|
||||
sessionEntity.isExpired(),
|
||||
sessionEntity.getSessionId());
|
||||
sessionEntity.setExpired(true); // Set expired to true
|
||||
removeSessionInformation(sessionEntity.getSessionId());
|
||||
// sessionRepository.flush();
|
||||
}
|
||||
|
||||
sessionEntities = sessionRepository.findByPrincipalName(principalName);
|
||||
log.info("Session entities: {}", sessionEntities.size());
|
||||
log.debug("Session entities: {}", sessionEntities.size());
|
||||
for (SessionEntity sessionEntity : sessionEntities) {
|
||||
if (sessionEntity.getPrincipalName().equals(principalName)) {
|
||||
log.info("Session expired: {}", sessionEntity.getSessionId());
|
||||
log.debug("Session expired: {}", sessionEntity.getSessionId());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,19 +242,19 @@ public class SessionPersistentRegistry implements SessionRegistry {
|
||||
}
|
||||
|
||||
// Update session details by principal name
|
||||
public void updateSessionByPrincipalName(
|
||||
String principalName, boolean expired, Date lastRequest) {
|
||||
sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||
}
|
||||
// public void updateSessionByPrincipalName(
|
||||
// String principalName, boolean expired, Date lastRequest) {
|
||||
// sessionRepository.saveByPrincipalName(expired, lastRequest, principalName);
|
||||
// }
|
||||
|
||||
// Update session details by session ID
|
||||
public void updateSessionBySessionId(String sessionId) {
|
||||
SessionEntity sessionEntity = getSessionEntity(sessionId);
|
||||
if (sessionEntity != null) {
|
||||
sessionEntity.setLastRequest(new Date());
|
||||
sessionRepository.save(sessionEntity);
|
||||
}
|
||||
}
|
||||
// public void updateSessionBySessionId(String sessionId) {
|
||||
// SessionEntity sessionEntity = getSessionEntity(sessionId);
|
||||
// if (sessionEntity != null) {
|
||||
// sessionEntity.setLastRequest(new Date());
|
||||
// sessionRepository.save(sessionEntity);
|
||||
// }
|
||||
// }
|
||||
|
||||
// Find the latest session for a given principal name
|
||||
public Optional<SessionEntity> findLatestSession(String principalName) {
|
||||
|
@ -21,8 +21,6 @@ public interface SessionRepository extends JpaRepository<SessionEntity, String>
|
||||
|
||||
SessionEntity findBySessionId(String sessionId);
|
||||
|
||||
List<SessionEntity> findBySessionIdAndExpired(String sessionId, boolean expired);
|
||||
|
||||
void deleteByPrincipalName(String principalName);
|
||||
|
||||
@Modifying
|
||||
|
@ -36,7 +36,7 @@ public class SessionScheduled {
|
||||
if (principal == null) {
|
||||
continue;
|
||||
} else if (principal instanceof String stringPrincipal) {
|
||||
// Skip anonymousUser if login is enabled
|
||||
// Expire anonymousUser sessions if login is enabled
|
||||
if ("anonymousUser".equals(stringPrincipal) && loginEnabledValue) {
|
||||
sessionPersistentRegistry.expireAllSessionsByPrincipalName(stringPrincipal);
|
||||
continue;
|
||||
@ -52,10 +52,14 @@ public class SessionScheduled {
|
||||
if (now.isAfter(expirationTime)) {
|
||||
sessionPersistentRegistry.expireSession(sessionInformation.getSessionId());
|
||||
sessionInformation.expireNow();
|
||||
|
||||
// Invalidate current authentication if expired session belongs to current user
|
||||
if (authentication != null && principal.equals(authentication.getPrincipal())) {
|
||||
authentication.setAuthenticated(false);
|
||||
}
|
||||
|
||||
SecurityContextHolder.clearContext();
|
||||
|
||||
log.debug(
|
||||
"Session expired for principal: {} SessionID: {}",
|
||||
principal,
|
||||
|
@ -15,15 +15,30 @@ import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import stirling.software.SPDF.config.security.UserUtils;
|
||||
import stirling.software.SPDF.config.security.session.SessionPersistentRegistry;
|
||||
|
||||
@RestController
|
||||
@Slf4j
|
||||
public class SessionStatusController {
|
||||
|
||||
@Autowired private SessionPersistentRegistry sessionPersistentRegistry;
|
||||
|
||||
// Returns the current session ID or 401 if no session exists
|
||||
@GetMapping("/session")
|
||||
public ResponseEntity<String> getSession(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
if (session == null) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("No session found");
|
||||
} else {
|
||||
return ResponseEntity.ok(session.getId());
|
||||
}
|
||||
}
|
||||
|
||||
// Checks if the session is active and valid according to user session limits
|
||||
@GetMapping("/session/status")
|
||||
public ResponseEntity<String> getSessionStatus(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
@ -46,20 +61,23 @@ public class SessionStatusController {
|
||||
int userSessions = allSessions.size();
|
||||
int maxUserSessions = sessionPersistentRegistry.getMaxUserSessions();
|
||||
|
||||
// Check if the current session is valid or expired based on the session registry
|
||||
if (userSessions >= maxUserSessions && !isActivSession) {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body("Session ungültig oder abgelaufen");
|
||||
.body("Session invalid or expired");
|
||||
} else if (session.getId() != null && isActivSession) {
|
||||
return ResponseEntity.ok("Session gültig: " + session.getId());
|
||||
return ResponseEntity.ok("Valid session: " + session.getId());
|
||||
} else {
|
||||
return ResponseEntity.ok(
|
||||
"User: " + username + " has " + userSessions + " sessions");
|
||||
}
|
||||
} else {
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Session ungültig oder abgelaufen");
|
||||
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
|
||||
.body("Session invalid or expired");
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidates the current session
|
||||
@GetMapping("/session/expire")
|
||||
public ResponseEntity<String> expireSession(HttpServletRequest request) {
|
||||
HttpSession session = request.getSession(false);
|
||||
@ -71,12 +89,15 @@ public class SessionStatusController {
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidates all sessions
|
||||
@GetMapping("/session/expire/all")
|
||||
public ResponseEntity<String> expireAllSessions() {
|
||||
log.debug("Expire all sessions");
|
||||
sessionPersistentRegistry.expireAllSessions();
|
||||
return ResponseEntity.ok("All sessions invalidated");
|
||||
}
|
||||
|
||||
// Invalidates all sessions for a specific user, only if requested by the same user
|
||||
@GetMapping("/session/expire/{username}")
|
||||
public ResponseEntity<String> expireAllSessionsByUsername(@PathVariable String username) {
|
||||
SecurityContext cxt = SecurityContextHolder.getContext();
|
||||
|
@ -3,8 +3,7 @@
|
||||
xmlns:th="https://www.thymeleaf.org">
|
||||
|
||||
<head>
|
||||
<th:block
|
||||
th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
|
||||
<th:block th:insert="~{fragments/common :: head(title=#{adminUserSettings.title}, header=#{adminUserSettings.header})}">
|
||||
</th:block>
|
||||
<style>
|
||||
.active-user {
|
||||
@ -52,16 +51,22 @@
|
||||
<span th:text="#{adminUserSettings.changeUserRole}">Change User's Role</span>
|
||||
</a>
|
||||
|
||||
<a href="/usage" th:if="${@runningEE}" class="btn btn-outline-success"
|
||||
th:title="#{adminUserSettings.usage}">
|
||||
<a href="/usage" th:if="${@runningEE}" class="btn btn-outline-success" th:title="#{adminUserSettings.usage}">
|
||||
<span class="material-symbols-rounded">analytics</span>
|
||||
<span th:text="#{adminUserSettings.usage}">Usage Statistics</span>
|
||||
</a>
|
||||
|
||||
<div class="my-4">
|
||||
<strong th:if="${@runningEE}" style="margin-left: 20px;"
|
||||
text="#{adminUserSettings.totalUsers}">runningEE</strong>
|
||||
<strong th:if="${!@runningEE}" style="margin-left: 20px;"
|
||||
text="#{adminUserSettings.totalUsers}">Non-Paid</strong>
|
||||
</div>
|
||||
|
||||
<div class="my-4">
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalUsers}">Total Users:</strong>
|
||||
<span th:text="${totalUsers}"></span>
|
||||
<span th:if="${@runningProOrHigher}" th:text="'/'+${maxPaidUsers}"></span>
|
||||
<span th:if="${@runningProOrHigher}" th:text="'| ' + ${maxPaidUsers}"></span>
|
||||
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.activeUsers}">Active Users:</strong>
|
||||
<span th:text="${activeUsers}"></span>
|
||||
@ -69,14 +74,12 @@
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.disabledUsers}">Disabled Users:</strong>
|
||||
<span th:text="${disabledUsers}"></span>
|
||||
<th:block th:if="${@runningProOrHigher}">
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
|
||||
Sessions:</strong>
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
|
||||
<span th:text="${sessionCount}"></span>
|
||||
</th:block>
|
||||
<th:block th:if="${!@runningProOrHigher}">
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total
|
||||
Sessions:</strong>
|
||||
<span th:text="${sessionCount}"></span>/<span th:text="${maxSessions}"></span>
|
||||
<strong style="margin-left: 20px;" th:text="#{adminUserSettings.totalSessions}">Total Sessions:</strong>
|
||||
<span th:text="${sessionCount}"></span><span th:if="${!@runningEE}" th:text="' | ' + ${maxSessions}"></span>
|
||||
</th:block>
|
||||
</div>
|
||||
</div>
|
||||
@ -103,16 +106,15 @@
|
||||
<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.roles}" th:text="#{adminUserSettings.roles}">Roles</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.authenticated}" class="text-overflow"
|
||||
th:text="#{adminUserSettings.authenticated}">Authenticated</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.lastRequest}" class="text-overflow"
|
||||
th:text="#{adminUserSettings.lastRequest}">Last Request</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.userSessions}"
|
||||
th:text="#{adminUserSettings.userSessions}">User Sessions</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}"
|
||||
colspan="2">Actions</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.userSessions}" th:text="#{adminUserSettings.userSessions}">
|
||||
User Sessions</th>
|
||||
<th scope="col" th:title="#{adminUserSettings.actions}" th:text="#{adminUserSettings.actions}" colspan="2">
|
||||
Actions</th>
|
||||
<!-- <th scope="col"></th> -->
|
||||
</tr>
|
||||
</thead>
|
||||
@ -140,8 +142,7 @@
|
||||
<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
|
||||
<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}"
|
||||
@ -202,16 +203,16 @@
|
||||
<label for="username" th:text="#{username}">Username</label>
|
||||
<select name="username" class="form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}"
|
||||
th:value="${user.username}" th:text="${user.username}">Username</option>
|
||||
<option th:each="user : ${users}" th:if="${user.username != currentUsername}" th:value="${user.username}"
|
||||
th:text="${user.username}">Username</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="form-control" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
|
||||
th:text="#{${roleDetail.value}}">Role</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
|
||||
Role</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@ -254,8 +255,8 @@
|
||||
<label for="role" th:text="#{adminUserSettings.role}">Role</label>
|
||||
<select name="role" class="form-control" id="role" required>
|
||||
<option value="" disabled selected th:text="#{selectFillter}">-- Select --</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}"
|
||||
th:text="#{${roleDetail.value}}">Role</option>
|
||||
<option th:each="roleDetail : ${roleDetails}" th:value="${roleDetail.key}" th:text="#{${roleDetail.value}}">
|
||||
Role</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
|
Loading…
x
Reference in New Issue
Block a user