alpinux-portail/web/history.php
Alpinux fcdd094e26 Portail membres complet : profil, adhésion, historique, OTP
- Inscription sans redirection HelloAsso (acte volontaire séparé)
- Génération automatique d'identifiant AlpID (prenom.code mnémotechnique)
- Profil en tuiles : identité, compte, mot de passe, OTP, adhésion, adresse, connexions
- Double authentification : activation/suppression OTP via Keycloak
- Page d'accueil contextuelle (bienvenue si connecté, CTA adhésion si non adhérent)
- Historique des connexions avec statistiques et graphiques Chart.js
- Géocodage Nominatim + lien OpenStreetMap pour l'adresse
- HelloAsso : checkout intent, validation paiement, mise à jour Dolibarr

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 00:43:38 +02:00

202 lines
7 KiB
PHP

<?php
require_once __DIR__ . '/inc/config.php';
require_once __DIR__ . '/inc/auth.php';
require_once __DIR__ . '/inc/keycloak.php';
session_start_safe();
require_login();
$user = current_user();
// Récupérer les events LOGIN depuis Keycloak Admin API
function kc_events(string $user_id, string $type = 'LOGIN', int $max = 500): array {
$resp = _kc_request('GET', '/events?user=' . urlencode($user_id) . '&type=' . $type . '&max=' . $max);
return $resp['status'] === 200 ? (json_decode($resp['body'], true) ?? []) : [];
}
$logins = kc_events($user['sub'], 'LOGIN', 500);
$errors = kc_events($user['sub'], 'LOGIN_ERROR', 200);
$logouts = kc_events($user['sub'], 'LOGOUT', 200);
// ── Calculs stats ─────────────────────────────────────────────────────
$now = time() * 1000;
$ms_30d = 30 * 86400 * 1000;
$logins_30d = array_filter($logins, fn($e) => $e['time'] >= ($now - $ms_30d));
$errors_30d = array_filter($errors, fn($e) => $e['time'] >= ($now - $ms_30d));
// Logins par jour (30 derniers jours)
$by_day = [];
for ($i = 29; $i >= 0; $i--) {
$d = date('d/m', strtotime("-$i days"));
$by_day[$d] = 0;
}
foreach ($logins_30d as $e) {
$d = date('d/m', (int)($e['time'] / 1000));
if (isset($by_day[$d])) $by_day[$d]++;
}
// Logins par heure (0-23)
$by_hour = array_fill(0, 24, 0);
foreach ($logins as $e) {
$h = (int)date('G', (int)($e['time'] / 1000));
$by_hour[$h]++;
}
// Logins par jour de semaine (lundi = 0)
$days_fr = ['Lun','Mar','Mer','Jeu','Ven','Sam','Dim'];
$by_dow = array_fill(0, 7, 0);
foreach ($logins as $e) {
$by_dow[((int)date('w', (int)($e['time'] / 1000)) + 6) % 7]++;
}
// IPs uniques
$ips = array_count_values(array_column($logins, 'ipAddress'));
arsort($ips);
// Dernière connexion
$last_login = $logins ? max(array_column($logins, 'time')) : null;
$title = 'Historique des connexions';
require __DIR__ . '/views/layout.php';
?>
<div class="history-page">
<div class="page-header">
<h1>Historique des connexions</h1>
<a href="/profile.php" class="btn-outline btn-sm">Retour au profil</a>
</div>
<!-- Chiffres clés -->
<div class="stats-grid">
<div class="stat-card">
<div class="stat-value"><?= count($logins) ?></div>
<div class="stat-label">Connexions totales</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($logins_30d) ?></div>
<div class="stat-label">30 derniers jours</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($ips) ?></div>
<div class="stat-label">IP distinctes</div>
</div>
<div class="stat-card <?= count($errors_30d) > 0 ? 'stat-warning' : '' ?>">
<div class="stat-value"><?= count($errors_30d) ?></div>
<div class="stat-label">Échecs (30j)</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= count($logouts) ?></div>
<div class="stat-label">Déconnexions</div>
</div>
<div class="stat-card">
<div class="stat-value"><?= $last_login ? date('d/m/Y', (int)($last_login/1000)) : '—' ?></div>
<div class="stat-label">Dernière connexion</div>
</div>
</div>
<!-- Graphiques -->
<div class="charts-grid">
<div class="card chart-card">
<h2>Connexions par jour (30 derniers jours)</h2>
<canvas id="chartDay"></canvas>
</div>
<div class="card chart-card">
<h2>Par heure de la journée</h2>
<canvas id="chartHour"></canvas>
</div>
<div class="card chart-card">
<h2>Par jour de la semaine</h2>
<canvas id="chartDow"></canvas>
</div>
<div class="card chart-card">
<h2>IPs les plus fréquentes</h2>
<?php if ($ips): ?>
<div class="ip-bars">
<?php $max_ip = max(array_values($ips)); foreach (array_slice($ips, 0, 8, true) as $ip => $count): ?>
<div class="ip-bar-row">
<span class="ip-label"><?= htmlspecialchars($ip) ?></span>
<div class="ip-bar-wrap">
<div class="ip-bar" style="width:<?= round($count/$max_ip*100) ?>%"></div>
</div>
<span class="ip-count"><?= $count ?></span>
</div>
<?php endforeach; ?>
</div>
<?php else: ?>
<p class="text-muted">Aucune donnée.</p>
<?php endif; ?>
</div>
</div>
<!-- Historique récent -->
<div class="card" style="max-width:none;margin-top:1.5rem">
<h2>Dernières connexions</h2>
<?php $recent = array_slice($logins, 0, 30); ?>
<?php if ($recent): ?>
<table class="members-table">
<thead>
<tr>
<th>Date</th>
<th>Heure</th>
<th>Adresse IP</th>
</tr>
</thead>
<tbody>
<?php foreach ($recent as $e): $ts = (int)($e['time']/1000); ?>
<tr>
<td><?= date('d/m/Y', $ts) ?></td>
<td><?= date('H:i:s', $ts) ?></td>
<td><code><?= htmlspecialchars($e['ipAddress'] ?? '—') ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php else: ?>
<p class="text-muted">Aucune connexion enregistrée.</p>
<?php endif; ?>
</div>
<?php if ($errors): ?>
<div class="card" style="max-width:none;margin-top:1rem">
<h2 class="text-warning">Tentatives de connexion échouées</h2>
<table class="members-table">
<thead><tr><th>Date</th><th>Heure</th><th>IP</th></tr></thead>
<tbody>
<?php foreach (array_slice($errors, 0, 20) as $e): $ts=(int)($e['time']/1000); ?>
<tr>
<td><?= date('d/m/Y', $ts) ?></td>
<td><?= date('H:i:s', $ts) ?></td>
<td><code><?= htmlspecialchars($e['ipAddress'] ?? '—') ?></code></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
<script>
const primary = '#3b5bdb', light = '#dde4ff', muted = '#6b7289';
const opts = { responsive: true, plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } };
new Chart(document.getElementById('chartDay'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_keys($by_day)) ?>,
datasets: [{ data: <?= json_encode(array_values($by_day)) ?>, backgroundColor: primary }]
}});
new Chart(document.getElementById('chartHour'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_map(fn($h) => str_pad($h,2,'0',STR_PAD_LEFT).'h', range(0,23))) ?>,
datasets: [{ data: <?= json_encode($by_hour) ?>, backgroundColor: primary }]
}});
new Chart(document.getElementById('chartDow'), { type: 'bar', options: opts, data: {
labels: <?= json_encode(array_values($days_fr)) ?>,
datasets: [{ data: <?= json_encode(array_values($by_dow)) ?>, backgroundColor: primary }]
}});
</script>
<?php require __DIR__ . '/views/layout_end.php'; ?>