Add RGPD data access/deletion forms and admin Analytics dashboard
RGPD (/rgpd): - Access form: search by IP, generate PDF with all visitor data, email it - Deletion form: delete all visitor data by IP, generate attestation PDF - Both forms pre-fill client IP, require email for response - PDF templates with E-Cosplay branding, RGPD article references Admin Analytics (/admin/analytics): - KPIs: unique visitors, pageviews, pages/visitor - Top pages and referrers tables - Device type, browser, OS breakdowns - Period filter: today, 7d, 30d, all Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
124
templates/admin/analytics.html.twig
Normal file
124
templates/admin/analytics.html.twig
Normal file
@@ -0,0 +1,124 @@
|
||||
{% extends 'admin/base.html.twig' %}
|
||||
|
||||
{% block title %}Analytics{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-black uppercase tracking-tighter italic heading-page">Analytics</h1>
|
||||
<div class="flex gap-2">
|
||||
{% for key, label in {today: "Aujourd'hui", '7d': '7 jours', '30d': '30 jours', all: 'Tout'} %}
|
||||
<a href="{{ path('app_admin_analytics', {period: key}) }}"
|
||||
class="px-3 py-1.5 text-xs font-black uppercase tracking-widest border-2 transition-all {{ period == key ? 'border-gray-900 bg-gray-900 text-white' : 'border-gray-300 bg-white hover:bg-gray-100' }}">
|
||||
{{ label }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# KPIs #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div class="admin-card text-center">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Visiteurs uniques</p>
|
||||
<p class="text-3xl font-black">{{ visitors|number_format(0, '.', ' ') }}</p>
|
||||
</div>
|
||||
<div class="admin-card text-center">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Pages vues</p>
|
||||
<p class="text-3xl font-black">{{ pageviews|number_format(0, '.', ' ') }}</p>
|
||||
</div>
|
||||
<div class="admin-card text-center">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Pages / visiteur</p>
|
||||
<p class="text-3xl font-black">{{ visitors > 0 ? (pageviews / visitors)|number_format(1) : '0' }}</p>
|
||||
</div>
|
||||
<div class="admin-card text-center">
|
||||
<p class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-1">Periode</p>
|
||||
<p class="text-xl font-black">{{ {today: "Aujourd'hui", '7d': '7 derniers jours', '30d': '30 derniers jours', all: 'Depuis le debut'}[period] }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
|
||||
{# Top pages #}
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Pages les plus visitees</h2>
|
||||
{% if top_pages|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="admin-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">URL</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Vues</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for page in top_pages %}
|
||||
<tr>
|
||||
<td class="text-sm font-bold truncate max-w-[300px]">{{ page.url }}</td>
|
||||
<td class="text-right font-black text-sm">{{ page.hits }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-400">Aucune donnee.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# Top referrers #}
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Referrers</h2>
|
||||
{% if top_referrers|length > 0 %}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="admin-table w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300">Source</th>
|
||||
<th class="text-[10px] font-black uppercase tracking-widest text-gray-300 text-right">Vues</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for ref in top_referrers %}
|
||||
<tr>
|
||||
<td class="text-sm font-bold truncate max-w-[300px]">{{ ref.referrer }}</td>
|
||||
<td class="text-right font-black text-sm">{{ ref.hits }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-400">Aucun referrer.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Devices, Browsers, OS #}
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-6">
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Appareils</h2>
|
||||
{% for d in devices %}
|
||||
<div class="flex justify-between py-1 text-sm">
|
||||
<span class="font-bold text-gray-500">{{ d.deviceType|capitalize }}</span>
|
||||
<span class="font-black">{{ d.cnt }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Navigateurs</h2>
|
||||
{% for b in browsers %}
|
||||
<div class="flex justify-between py-1 text-sm">
|
||||
<span class="font-bold text-gray-500">{{ b.browser }}</span>
|
||||
<span class="font-black">{{ b.cnt }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="admin-card">
|
||||
<h2 class="text-sm font-black uppercase tracking-widest mb-4">Systemes</h2>
|
||||
{% for o in os_list %}
|
||||
<div class="flex justify-between py-1 text-sm">
|
||||
<span class="font-bold text-gray-500">{{ o.os }}</span>
|
||||
<span class="font-black">{{ o.cnt }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -24,6 +24,7 @@
|
||||
<a href="{{ path('app_admin_events') }}" class="px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all {{ current_route starts with 'app_admin_event' ? 'admin-nav-active' : 'hover:bg-gray-100' }}">Evenements</a>
|
||||
<a href="{{ path('app_admin_orders') }}" class="px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_admin_orders' ? 'admin-nav-active' : 'hover:bg-gray-100' }}">Commandes</a>
|
||||
<a href="{{ path('app_admin_logs') }}" class="px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_admin_logs' ? 'admin-nav-active' : 'hover:bg-gray-100' }}">Logs</a>
|
||||
<a href="{{ path('app_admin_analytics') }}" class="px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_admin_analytics' ? 'admin-nav-active' : 'hover:bg-gray-100' }}">Analytics</a>
|
||||
<a href="{{ path('app_admin_infra') }}" class="px-3 py-1.5 text-xs font-black uppercase tracking-widest transition-all {{ current_route == 'app_admin_infra' ? 'admin-nav-active' : 'hover:bg-gray-100' }}">Infra</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
7
templates/emails/rgpd_access.html.twig
Normal file
7
templates/emails/rgpd_access.html.twig
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'emails/base.html.twig' %}
|
||||
{% block content %}
|
||||
<p>Bonjour,</p>
|
||||
<p>Suite a votre demande d'acces a vos donnees personnelles (article 15 du RGPD), vous trouverez en piece jointe un document PDF contenant l'ensemble des donnees que nous detenons vous concernant.</p>
|
||||
<p>Si vous souhaitez exercer votre droit a l'effacement, vous pouvez le faire depuis notre page RGPD.</p>
|
||||
<p>Cordialement,<br>L'equipe E-Ticket</p>
|
||||
{% endblock %}
|
||||
7
templates/emails/rgpd_deletion.html.twig
Normal file
7
templates/emails/rgpd_deletion.html.twig
Normal file
@@ -0,0 +1,7 @@
|
||||
{% extends 'emails/base.html.twig' %}
|
||||
{% block content %}
|
||||
<p>Bonjour,</p>
|
||||
<p>Suite a votre demande de suppression de vos donnees personnelles (article 17 du RGPD), nous vous confirmons que vos donnees ont ete supprimees de nos systemes.</p>
|
||||
<p>Vous trouverez en piece jointe une attestation de suppression.</p>
|
||||
<p>Cordialement,<br>L'equipe E-Ticket</p>
|
||||
{% endblock %}
|
||||
@@ -151,7 +151,56 @@
|
||||
<p>Tout litige en relation avec le traitement des donnees personnelles est soumis au droit francais. Il est fait attribution exclusive de juridiction aux tribunaux competents de Laon.</p>
|
||||
</section>
|
||||
|
||||
<p class="text-sm opacity-70 italic">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
|
||||
<p class="text-sm opacity-70 italic mb-8">Derniere mise a jour : {{ "now"|date("d/m/Y") }}</p>
|
||||
|
||||
{% for message in app.flashes('success') %}
|
||||
<div class="flash-success mb-4"><p class="font-black text-sm">{{ message }}</p></div>
|
||||
{% endfor %}
|
||||
{% for message in app.flashes('error') %}
|
||||
<div class="flash-error mb-4"><p class="font-black text-sm">{{ message }}</p></div>
|
||||
{% endfor %}
|
||||
|
||||
<section id="exercer-droits">
|
||||
<h2 class="text-xl font-black uppercase mb-4">Exercer vos droits</h2>
|
||||
|
||||
<div class="flex flex-col lg:flex-row gap-6">
|
||||
<div class="flex-1 border-2 border-gray-900 p-6">
|
||||
<h3 class="text-lg font-black uppercase mb-2">Droit d'acces</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Recevez par email un PDF contenant toutes les donnees de navigation que nous detenons vous concernant (article 15 du RGPD).</p>
|
||||
<form method="post" action="{{ path('app_rgpd_access') }}" class="flex flex-col gap-3">
|
||||
<div>
|
||||
<label for="access_email" class="text-xs font-black uppercase block mb-1">Email</label>
|
||||
<input type="email" id="access_email" name="email" required placeholder="votre@email.fr" class="w-full px-3 py-2 border-2 border-gray-900 font-bold text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="access_ip" class="text-xs font-black uppercase block mb-1">Adresse IP</label>
|
||||
<input type="text" id="access_ip" name="ip" required value="{{ client_ip }}" class="w-full px-3 py-2 border-2 border-gray-900 font-bold text-sm">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 border-2 border-gray-900 bg-[#fabf04] font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-yellow-500 transition-all">
|
||||
Demander mes donnees
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 border-2 border-red-600 p-6">
|
||||
<h3 class="text-lg font-black uppercase mb-2">Droit a l'effacement</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">Supprimez toutes vos donnees de navigation de nos systemes et recevez une attestation par email (article 17 du RGPD).</p>
|
||||
<form method="post" action="{{ path('app_rgpd_deletion') }}" data-confirm="Cette action est irreversible. Toutes vos donnees de navigation seront definitivement supprimees. Continuer ?" class="flex flex-col gap-3">
|
||||
<div>
|
||||
<label for="delete_email" class="text-xs font-black uppercase block mb-1">Email</label>
|
||||
<input type="email" id="delete_email" name="email" required placeholder="votre@email.fr" class="w-full px-3 py-2 border-2 border-gray-900 font-bold text-sm">
|
||||
</div>
|
||||
<div>
|
||||
<label for="delete_ip" class="text-xs font-black uppercase block mb-1">Adresse IP</label>
|
||||
<input type="text" id="delete_ip" name="ip" required value="{{ client_ip }}" class="w-full px-3 py-2 border-2 border-gray-900 font-bold text-sm">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 border-2 border-red-600 bg-red-600 text-white font-black uppercase text-xs tracking-widest cursor-pointer hover:bg-red-800 transition-all">
|
||||
Supprimer mes donnees
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
52
templates/pdf/rgpd_access.html.twig
Normal file
52
templates/pdf/rgpd_access.html.twig
Normal file
@@ -0,0 +1,52 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><style>
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size: 11px; color: #111; }
|
||||
h1 { font-size: 18px; margin-bottom: 5px; }
|
||||
h2 { font-size: 14px; margin-top: 20px; border-bottom: 1px solid #ccc; padding-bottom: 3px; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 8px; font-size: 10px; }
|
||||
th, td { border: 1px solid #ddd; padding: 4px 6px; text-align: left; }
|
||||
th { background: #f5f5f5; font-weight: bold; }
|
||||
.header { display: flex; align-items: center; margin-bottom: 15px; }
|
||||
.logo { width: 60px; margin-right: 15px; }
|
||||
.meta { color: #666; font-size: 10px; margin-bottom: 15px; }
|
||||
</style></head>
|
||||
<body>
|
||||
{% if logo %}<img src="{{ logo }}" class="logo" alt="Logo">{% endif %}
|
||||
<h1>Droit d'acces aux donnees personnelles</h1>
|
||||
<p class="meta">Article 15 du RGPD — Genere le {{ date|date('d/m/Y a H:i') }}</p>
|
||||
<p>Adresse IP concernee : <strong>{{ ip }}</strong></p>
|
||||
|
||||
{% for entry in data %}
|
||||
<h2>Session #{{ loop.index }} — {{ entry.visitor.createdAt|date('d/m/Y H:i') }}</h2>
|
||||
<table>
|
||||
<tr><th>Appareil</th><td>{{ entry.visitor.deviceType }}</td><th>OS</th><td>{{ entry.visitor.os ?? 'Inconnu' }}</td></tr>
|
||||
<tr><th>Navigateur</th><td>{{ entry.visitor.browser ?? 'Inconnu' }}</td><th>Langue</th><td>{{ entry.visitor.language ?? 'Inconnu' }}</td></tr>
|
||||
<tr><th>Ecran</th><td>{{ entry.visitor.screenWidth ?? '?' }}x{{ entry.visitor.screenHeight ?? '?' }}</td><th>Compte lie</th><td>{{ entry.visitor.user ? entry.visitor.user.email : 'Non' }}</td></tr>
|
||||
</table>
|
||||
|
||||
{% if entry.events|length > 0 %}
|
||||
<table>
|
||||
<thead><tr><th>Date</th><th>Page</th><th>Titre</th><th>Referrer</th></tr></thead>
|
||||
<tbody>
|
||||
{% for event in entry.events %}
|
||||
<tr>
|
||||
<td>{{ event.createdAt|date('d/m/Y H:i') }}</td>
|
||||
<td>{{ event.url }}</td>
|
||||
<td>{{ event.title ?? '-' }}</td>
|
||||
<td>{{ event.referrer ?? '-' }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p><em>Aucune page visitee enregistree.</em></p>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
<p style="margin-top: 20px; font-size: 10px; color: #666;">
|
||||
Document genere automatiquement par E-Ticket (Association E-Cosplay) conformement au Reglement General sur la Protection des Donnees (RGPD).
|
||||
Pour toute question : contact@e-cosplay.fr
|
||||
</p>
|
||||
</body>
|
||||
</html>
|
||||
40
templates/pdf/rgpd_deletion.html.twig
Normal file
40
templates/pdf/rgpd_deletion.html.twig
Normal file
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head><meta charset="UTF-8"><style>
|
||||
body { font-family: DejaVu Sans, sans-serif; font-size: 12px; color: #111; }
|
||||
h1 { font-size: 20px; text-align: center; margin-bottom: 30px; }
|
||||
.content { max-width: 500px; margin: 0 auto; }
|
||||
.logo { width: 80px; display: block; margin: 0 auto 20px; }
|
||||
.box { border: 2px solid #111; padding: 20px; margin: 20px 0; }
|
||||
.signature { margin-top: 40px; font-size: 11px; color: #666; }
|
||||
</style></head>
|
||||
<body>
|
||||
<div class="content">
|
||||
{% if logo %}<img src="{{ logo }}" class="logo" alt="Logo">{% endif %}
|
||||
|
||||
<h1>Attestation de suppression de donnees</h1>
|
||||
|
||||
<div class="box">
|
||||
<p>Conformement a votre demande et en application de l'<strong>article 17 du Reglement General sur la Protection des Donnees (RGPD)</strong>, nous attestons que :</p>
|
||||
|
||||
<p style="margin: 15px 0;">Les donnees personnelles associees a l'adresse IP <strong>{{ ip }}</strong> ont ete <strong>supprimees</strong> de nos systemes le <strong>{{ date|date('d/m/Y a H:i') }}</strong>.</p>
|
||||
|
||||
<p>Cette suppression concerne l'ensemble des donnees de navigation collectees par notre systeme d'analyse d'audience, incluant :</p>
|
||||
<ul style="margin: 10px 0; padding-left: 20px;">
|
||||
<li>Identifiants de session</li>
|
||||
<li>Pages visitees</li>
|
||||
<li>Donnees techniques (appareil, navigateur, systeme d'exploitation)</li>
|
||||
<li>Liens eventuels avec un compte utilisateur</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="signature">
|
||||
<p><strong>Association E-Cosplay</strong></p>
|
||||
<p>SIREN : 943121517 / RNA : W022006988</p>
|
||||
<p>42 rue de Saint-Quentin, 02800 Beautor, France</p>
|
||||
<p>contact@e-cosplay.fr</p>
|
||||
<p style="margin-top: 10px;">Document genere automatiquement — Reference : RGPD-DEL-{{ date|date('YmdHis') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user