- 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>
202 lines
7 KiB
PHP
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'; ?>
|