Compare commits

...

2 Commits

Author SHA1 Message Date
ucef 19cee92437 feat: add active user session tracking and visualization in admin dashboard
AS Talange CI/CD Pipeline / Build & Run Unit Tests (push) Successful in 4m1s
AS Talange CI/CD Pipeline / Deploy to Test Environment (push) Successful in 5m34s
2026-07-03 17:18:14 +02:00
ucef 214ec08229 fix: resolve HTMX pagination bug on adherents list 2026-07-03 17:07:42 +02:00
5 changed files with 123 additions and 3 deletions
@@ -10,6 +10,9 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder; import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;
@Configuration @Configuration
@EnableWebSecurity @EnableWebSecurity
@@ -41,6 +44,10 @@ public class SecurityConfig {
.logout(logout -> logout .logout(logout -> logout
.logoutSuccessUrl("/login?logout") .logoutSuccessUrl("/login?logout")
.permitAll() .permitAll()
)
.sessionManagement(session -> session
.maximumSessions(100)
.sessionRegistry(sessionRegistry())
); );
return http.build(); return http.build();
} }
@@ -57,4 +64,14 @@ public class SecurityConfig {
authProvider.setPasswordEncoder(passwordEncoder()); authProvider.setPasswordEncoder(passwordEncoder());
return authProvider; return authProvider;
} }
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
} }
@@ -0,0 +1,37 @@
package com.astalange.web.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.List;
import java.util.stream.Collectors;
@Controller
@RequestMapping("/admin/sessions")
public class SessionController {
private final SessionRegistry sessionRegistry;
public SessionController(SessionRegistry sessionRegistry) {
this.sessionRegistry = sessionRegistry;
}
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
public String viewSessions(Model model) {
List<Object> principals = sessionRegistry.getAllPrincipals();
List<String> activeUsers = principals.stream()
.filter(principal -> principal instanceof UserDetails)
.map(principal -> ((UserDetails) principal).getUsername())
.collect(Collectors.toList());
model.addAttribute("activeUsers", activeUsers);
model.addAttribute("activeCount", activeUsers.size());
return "admin/sessions";
}
}
@@ -175,7 +175,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage - 1}}|" th:attr="hx-vals=|{&quot;page&quot;: ${currentPage - 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Précédent Précédent
</button> </button>
@@ -190,7 +190,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${pageNum}}|" th:attr="hx-vals=|{&quot;page&quot;: ${pageNum}}|"
th:text="${pageNum + 1}" th:text="${pageNum + 1}"
class="px-3 py-1 rounded transition-colors" class="px-3 py-1 rounded transition-colors"
th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}"> th:classappend="${currentPage == pageNum ? 'bg-blue-600 text-white' : 'border border-gray-300 hover:bg-gray-100'}">
@@ -205,7 +205,7 @@
hx-target="#adherents-table-container" hx-target="#adherents-table-container"
hx-select="#adherents-table-container" hx-select="#adherents-table-container"
hx-include="#filter-row input, #filter-row select, input[name='query']" hx-include="#filter-row input, #filter-row select, input[name='query']"
th:attr="hx-vals=|{'page': ${currentPage + 1}}|" th:attr="hx-vals=|{&quot;page&quot;: ${currentPage + 1}}|"
class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors"> class="px-3 py-1 border border-gray-300 rounded hover:bg-gray-100 transition-colors">
Suivant Suivant
</button> </button>
@@ -0,0 +1,65 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Utilisateurs Connectés - AS Talange</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/htmx.org@1.9.11"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
body { font-family: 'Inter', sans-serif; }
</style>
</head>
<body class="bg-gray-50 text-gray-900 flex h-screen overflow-hidden">
<!-- Sidebar -->
<div th:replace="~{fragments/sidebar :: sidebar}"></div>
<!-- Main Content -->
<main class="flex-1 flex flex-col h-screen overflow-hidden">
<!-- Header -->
<header class="h-16 bg-white border-b border-gray-200 flex items-center justify-between px-6">
<h2 class="text-lg font-semibold text-gray-800">Utilisateurs Connectés</h2>
</header>
<!-- Main section -->
<div class="flex-1 overflow-auto p-6">
<div class="mb-6 flex justify-between items-center">
<h3 class="text-xl font-bold text-gray-900">
Sessions Actives (<span th:text="${activeCount}">0</span>)
</h3>
<button hx-get="/admin/sessions" hx-target="body" hx-swap="outerHTML" class="bg-blue-600 text-white px-4 py-2 rounded-lg text-sm font-medium hover:bg-blue-700 transition-colors">
Rafraîchir
</button>
</div>
<div class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<table class="w-full text-left border-collapse">
<thead>
<tr class="bg-gray-50 text-gray-500 text-sm uppercase tracking-wider border-b border-gray-200">
<th class="py-3 px-6 font-medium text-left">Nom d'utilisateur</th>
<th class="py-3 px-6 font-medium text-left">Statut</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 text-sm">
<tr th:if="${#lists.isEmpty(activeUsers)}">
<td colspan="2" class="py-8 text-center text-gray-500">Aucun utilisateur connecté pour le moment.</td>
</tr>
<tr th:each="username : ${activeUsers}" class="hover:bg-gray-50 transition-colors">
<td class="py-4 px-6 font-medium text-gray-900 flex items-center">
<div class="w-8 h-8 rounded-full bg-blue-100 text-blue-600 flex items-center justify-center font-bold mr-3">
<span th:text="${#strings.substring(username, 0, 1).toUpperCase()}">U</span>
</div>
<span th:text="${username}">username</span>
</td>
<td class="py-4 px-6">
<span class="px-2 py-1 bg-green-100 text-green-800 text-xs font-semibold rounded-full">En ligne</span>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
</body>
</html>
@@ -44,6 +44,7 @@
<div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div> <div class="pt-4 pb-2 px-3 text-xs font-semibold text-gray-400 uppercase tracking-wider">Administration</div>
<a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a> <a href="/utilisateurs" th:classappend="${requestURI != null and requestURI.startsWith('/utilisateurs') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs</a>
<a href="/admin/logs/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/admin/logs/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Logs Paiements</a> <a href="/admin/logs/paiements" th:classappend="${requestURI != null and requestURI.startsWith('/admin/logs/paiements') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Logs Paiements</a>
<a href="/admin/sessions" th:classappend="${requestURI != null and requestURI.startsWith('/admin/sessions') ? 'bg-blue-50 text-blue-700 font-medium' : 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}" class="block px-3 py-2 rounded-lg transition-colors">Utilisateurs Connectés</a>
</th:block> </th:block>
</nav> </nav>
<div class="p-4 border-t border-gray-200"> <div class="p-4 border-t border-gray-200">