diff --git a/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java index aeee454..a926f37 100644 --- a/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java +++ b/as-talange-core/src/main/java/com/astalange/core/entity/AppUser.java @@ -36,4 +36,10 @@ public class AppUser { inverseJoinColumns = @JoinColumn(name = "role_id") ) private Set roles = new HashSet<>(); + + @Column(name = "last_login_at") + private java.time.LocalDateTime lastLoginAt; + + @Column(name = "last_logout_at") + private java.time.LocalDateTime lastLogoutAt; } diff --git a/as-talange-core/src/main/java/com/astalange/core/security/AuthenticationEventListener.java b/as-talange-core/src/main/java/com/astalange/core/security/AuthenticationEventListener.java new file mode 100644 index 0000000..169003b --- /dev/null +++ b/as-talange-core/src/main/java/com/astalange/core/security/AuthenticationEventListener.java @@ -0,0 +1,79 @@ +package com.astalange.core.security; + +import com.astalange.core.entity.AppUser; +import com.astalange.core.repository.AppUserRepository; +import org.springframework.context.event.EventListener; +import org.springframework.security.authentication.event.AuthenticationSuccessEvent; +import org.springframework.security.authentication.event.LogoutSuccessEvent; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.session.HttpSessionDestroyedEvent; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Component +public class AuthenticationEventListener { + + private final AppUserRepository userRepository; + + public AuthenticationEventListener(AppUserRepository userRepository) { + this.userRepository = userRepository; + } + + @EventListener + @Transactional + public void onAuthenticationSuccess(AuthenticationSuccessEvent event) { + String username = extractUsername(event.getAuthentication().getPrincipal()); + if (username != null) { + userRepository.findByUsername(username).ifPresent(user -> { + user.setLastLoginAt(LocalDateTime.now()); + userRepository.save(user); + }); + } + } + + @EventListener + @Transactional + public void onLogoutSuccess(LogoutSuccessEvent event) { + if (event.getAuthentication() != null) { + String username = extractUsername(event.getAuthentication().getPrincipal()); + if (username != null) { + userRepository.findByUsername(username).ifPresent(user -> { + user.setLastLogoutAt(LocalDateTime.now()); + userRepository.save(user); + }); + } + } + } + + @EventListener + @Transactional + public void onSessionDestroyed(HttpSessionDestroyedEvent event) { + List contexts = event.getSecurityContexts(); + for (SecurityContext context : contexts) { + if (context != null && context.getAuthentication() != null) { + String username = extractUsername(context.getAuthentication().getPrincipal()); + if (username != null) { + userRepository.findByUsername(username).ifPresent(user -> { + if (user.getLastLogoutAt() == null || (user.getLastLoginAt() != null && user.getLastLogoutAt().isBefore(user.getLastLoginAt()))) { + user.setLastLogoutAt(LocalDateTime.now()); + userRepository.save(user); + } + }); + } + } + } + } + + private String extractUsername(Object principal) { + if (principal instanceof UserDetails) { + return ((UserDetails) principal).getUsername(); + } else if (principal instanceof String) { + return (String) principal; + } + return null; + } +} diff --git a/as-talange-core/src/main/resources/db/migration/V39__add_user_last_login_logout.sql b/as-talange-core/src/main/resources/db/migration/V39__add_user_last_login_logout.sql new file mode 100644 index 0000000..4dcf6c5 --- /dev/null +++ b/as-talange-core/src/main/resources/db/migration/V39__add_user_last_login_logout.sql @@ -0,0 +1,2 @@ +ALTER TABLE app_user ADD COLUMN last_login_at TIMESTAMP; +ALTER TABLE app_user ADD COLUMN last_logout_at TIMESTAMP; diff --git a/as-talange-web/src/main/java/com/astalange/web/controller/SessionController.java b/as-talange-web/src/main/java/com/astalange/web/controller/SessionController.java index dc478a2..7e42fbe 100644 --- a/as-talange-web/src/main/java/com/astalange/web/controller/SessionController.java +++ b/as-talange-web/src/main/java/com/astalange/web/controller/SessionController.java @@ -1,5 +1,8 @@ package com.astalange.web.controller; +import com.astalange.core.entity.AppUser; +import com.astalange.core.entity.Role; +import com.astalange.core.repository.AppUserRepository; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.userdetails.UserDetails; @@ -8,7 +11,9 @@ import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import java.time.LocalDateTime; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; @Controller @@ -16,22 +21,73 @@ import java.util.stream.Collectors; public class SessionController { private final SessionRegistry sessionRegistry; + private final AppUserRepository userRepository; - public SessionController(SessionRegistry sessionRegistry) { + public SessionController(SessionRegistry sessionRegistry, AppUserRepository userRepository) { this.sessionRegistry = sessionRegistry; + this.userRepository = userRepository; + } + + public static class UserSessionDTO { + private final Long id; + private final String username; + private final boolean online; + private final LocalDateTime lastLoginAt; + private final LocalDateTime lastLogoutAt; + private final String roles; + + public UserSessionDTO(Long id, String username, boolean online, LocalDateTime lastLoginAt, LocalDateTime lastLogoutAt, String roles) { + this.id = id; + this.username = username; + this.online = online; + this.lastLoginAt = lastLoginAt; + this.lastLogoutAt = lastLogoutAt; + this.roles = roles; + } + + public Long getId() { return id; } + public String getUsername() { return username; } + public boolean isOnline() { return online; } + public LocalDateTime getLastLoginAt() { return lastLoginAt; } + public LocalDateTime getLastLogoutAt() { return lastLogoutAt; } + public String getRoles() { return roles; } } @GetMapping @PreAuthorize("hasRole('ADMIN')") public String viewSessions(Model model) { List principals = sessionRegistry.getAllPrincipals(); - List activeUsers = principals.stream() + Set activeUsernames = principals.stream() .filter(principal -> principal instanceof UserDetails) .map(principal -> ((UserDetails) principal).getUsername()) - .collect(Collectors.toList()); - - model.addAttribute("activeUsers", activeUsers); - model.addAttribute("activeCount", activeUsers.size()); + .collect(Collectors.toSet()); + + List allUsers = userRepository.findAll(); + + List userSessions = allUsers.stream().map(user -> { + boolean isOnline = activeUsernames.contains(user.getUsername()); + String rolesStr = user.getRoles().stream() + .map(Role::getName) + .map(r -> r.replace("ROLE_", "")) + .collect(Collectors.joining(", ")); + + return new UserSessionDTO( + user.getId(), + user.getUsername(), + isOnline, + user.getLastLoginAt(), + user.getLastLogoutAt(), + rolesStr + ); + }).collect(Collectors.toList()); + + long activeCount = userSessions.stream().filter(UserSessionDTO::isOnline).count(); + long offlineCount = userSessions.size() - activeCount; + + model.addAttribute("userSessions", userSessions); + model.addAttribute("activeCount", activeCount); + model.addAttribute("offlineCount", offlineCount); + model.addAttribute("totalCount", userSessions.size()); return "admin/sessions"; } } diff --git a/as-talange-web/src/main/resources/templates/admin/sessions.html b/as-talange-web/src/main/resources/templates/admin/sessions.html index dd5e52c..1ffe72f 100644 --- a/as-talange-web/src/main/resources/templates/admin/sessions.html +++ b/as-talange-web/src/main/resources/templates/admin/sessions.html @@ -2,7 +2,7 @@ - Utilisateurs Connectés - AS Talange + Utilisateurs & Sessions - AS Talange @@ -19,41 +19,99 @@
-

Utilisateurs Connectés

+

Suivi des Connectivité & Sessions

+ + +
+
+
+

Total Utilisateurs

+

0

+
+
+ 👥 +
+
+ +
+
+

Sessions Actives (En ligne)

+

0

+
+
+ 🟢 +
+
+ +
+
+

Hors Ligne

+

0

+
+
+ ⚪ +
+
+
+ +

- Sessions Actives (0) + Liste des Utilisateurs

-
+
- - + + + + + - - + + - + + + +
Nom d'utilisateur
UtilisateurRôle(s) StatutDernière ConnexionDernière Déconnexion
Aucun utilisateur connecté pour le moment.
Aucun utilisateur enregistré.
-
- U +
+ U
- username + username
- En ligne + ADMIN + + + + En ligne + + + + Hors ligne + + + 01/01/2026 10:00:00 + + 01/01/2026 12:00:00