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:
Serreau Jovann
2026-03-26 11:59:34 +01:00
parent 809a1055ec
commit 2ae28089d5
10 changed files with 576 additions and 3 deletions

View File

@@ -2,6 +2,8 @@
namespace App\Controller;
use App\Entity\AnalyticsEvent;
use App\Entity\AnalyticsUniqId;
use App\Entity\AuditLog;
use App\Entity\BilletBuyer;
use App\Entity\Event;
@@ -666,6 +668,101 @@ class AdminController extends AbstractController
return $this->render('admin/infra.html.twig', $data);
}
#[Route('/analytics', name: 'app_admin_analytics', methods: ['GET'])]
public function analytics(Request $request, EntityManagerInterface $em): Response
{
$period = $request->query->getString('period', '7d');
$since = match ($period) {
'today' => new \DateTimeImmutable('today'),
'30d' => new \DateTimeImmutable('-30 days'),
'all' => new \DateTimeImmutable('2020-01-01'),
default => new \DateTimeImmutable('-7 days'),
};
$visitors = (int) $em->createQueryBuilder()
->select('COUNT(v.id)')
->from(AnalyticsUniqId::class, 'v')
->where('v.createdAt >= :since')
->setParameter('since', $since)
->getQuery()
->getSingleScalarResult();
$pageviews = (int) $em->createQueryBuilder()
->select('COUNT(e.id)')
->from(AnalyticsEvent::class, 'e')
->where('e.createdAt >= :since')
->setParameter('since', $since)
->getQuery()
->getSingleScalarResult();
$topPages = $em->createQueryBuilder()
->select('e.url, COUNT(e.id) AS hits')
->from(AnalyticsEvent::class, 'e')
->where('e.createdAt >= :since')
->setParameter('since', $since)
->groupBy('e.url')
->orderBy('hits', 'DESC')
->setMaxResults(20)
->getQuery()
->getArrayResult();
$topReferrers = $em->createQueryBuilder()
->select('e.referrer, COUNT(e.id) AS hits')
->from(AnalyticsEvent::class, 'e')
->where('e.createdAt >= :since')
->andWhere('e.referrer IS NOT NULL')
->andWhere("e.referrer != ''")
->setParameter('since', $since)
->groupBy('e.referrer')
->orderBy('hits', 'DESC')
->setMaxResults(10)
->getQuery()
->getArrayResult();
$devices = $em->createQueryBuilder()
->select('v.deviceType, COUNT(v.id) AS cnt')
->from(AnalyticsUniqId::class, 'v')
->where('v.createdAt >= :since')
->setParameter('since', $since)
->groupBy('v.deviceType')
->getQuery()
->getArrayResult();
$browsers = $em->createQueryBuilder()
->select('v.browser, COUNT(v.id) AS cnt')
->from(AnalyticsUniqId::class, 'v')
->where('v.createdAt >= :since')
->andWhere('v.browser IS NOT NULL')
->setParameter('since', $since)
->groupBy('v.browser')
->orderBy('cnt', 'DESC')
->setMaxResults(10)
->getQuery()
->getArrayResult();
$osList = $em->createQueryBuilder()
->select('v.os, COUNT(v.id) AS cnt')
->from(AnalyticsUniqId::class, 'v')
->where('v.createdAt >= :since')
->andWhere('v.os IS NOT NULL')
->setParameter('since', $since)
->groupBy('v.os')
->orderBy('cnt', 'DESC')
->getQuery()
->getArrayResult();
return $this->render('admin/analytics.html.twig', [
'period' => $period,
'visitors' => $visitors,
'pageviews' => $pageviews,
'top_pages' => $topPages,
'top_referrers' => $topReferrers,
'devices' => $devices,
'browsers' => $browsers,
'os_list' => $osList,
]);
}
#[Route('/organisateurs/inviter', name: 'app_admin_invite_organizer', methods: ['GET', 'POST'])]
public function inviteOrganizer(Request $request, EntityManagerInterface $em, MailerService $mailerService): Response
{

View File

@@ -2,7 +2,9 @@
namespace App\Controller;
use App\Service\RgpdService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
@@ -63,10 +65,11 @@ class LegalController extends AbstractController
]);
}
#[Route('/rgpd', name: 'app_rgpd')]
public function rgpd(): Response
#[Route('/rgpd', name: 'app_rgpd', methods: ['GET'])]
public function rgpd(Request $request): Response
{
return $this->render('legal/rgpd.html.twig', [
'client_ip' => $request->getClientIp(),
'breadcrumbs' => [
['name' => 'Accueil', 'url' => '/'],
['name' => 'Politique RGPD', 'url' => '/rgpd'],
@@ -74,6 +77,52 @@ class LegalController extends AbstractController
]);
}
#[Route('/rgpd/acces', name: 'app_rgpd_access', methods: ['POST'])]
public function rgpdAccess(Request $request, RgpdService $rgpd): Response
{
$ip = trim($request->request->getString('ip'));
$email = trim($request->request->getString('email'));
if ('' === $ip || '' === $email) {
$this->addFlash('error', 'Veuillez remplir tous les champs.');
return $this->redirectToRoute('app_rgpd');
}
$result = $rgpd->handleAccessRequest($ip, $email);
if ($result['found']) {
$this->addFlash('success', 'Vos donnees ont ete envoyees par email.');
} else {
$this->addFlash('success', 'Aucune donnee trouvee pour cette adresse IP. Un email de confirmation a ete envoye.');
}
return $this->redirectToRoute('app_rgpd');
}
#[Route('/rgpd/suppression', name: 'app_rgpd_deletion', methods: ['POST'])]
public function rgpdDeletion(Request $request, RgpdService $rgpd): Response
{
$ip = trim($request->request->getString('ip'));
$email = trim($request->request->getString('email'));
if ('' === $ip || '' === $email) {
$this->addFlash('error', 'Veuillez remplir tous les champs.');
return $this->redirectToRoute('app_rgpd');
}
$result = $rgpd->handleDeletionRequest($ip, $email);
if ($result['found']) {
$this->addFlash('success', 'Vos donnees ont ete supprimees. Une attestation a ete envoyee par email.');
} else {
$this->addFlash('success', 'Aucune donnee trouvee pour cette adresse IP.');
}
return $this->redirectToRoute('app_rgpd');
}
#[Route('/conformite', name: 'app_conformite')]
public function conformite(): Response
{

147
src/Service/RgpdService.php Normal file
View File

@@ -0,0 +1,147 @@
<?php
namespace App\Service;
use App\Entity\AnalyticsEvent;
use App\Entity\AnalyticsUniqId;
use Doctrine\ORM\EntityManagerInterface;
use Dompdf\Dompdf;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Twig\Environment;
/**
* @codeCoverageIgnore RGPD data access/deletion with PDF generation
*/
class RgpdService
{
public function __construct(
private EntityManagerInterface $em,
private Environment $twig,
private MailerService $mailer,
#[Autowire('%kernel.project_dir%')] private string $projectDir,
) {
}
/**
* @return array{found: bool, count: int}
*/
public function handleAccessRequest(string $ip, string $email): array
{
$ipHash = hash('sha256', $ip);
$visitors = $this->em->getRepository(AnalyticsUniqId::class)->findBy(['ipHash' => $ipHash]);
if (0 === \count($visitors)) {
return ['found' => false, 'count' => 0];
}
$data = [];
foreach ($visitors as $visitor) {
$events = $this->em->getRepository(AnalyticsEvent::class)->findBy(
['visitor' => $visitor],
['createdAt' => 'DESC'],
);
$data[] = [
'visitor' => $visitor,
'events' => $events,
];
}
$pdfPath = $this->generateAccessPdf($data, $ip);
$this->mailer->sendEmail(
$email,
'E-Ticket - Vos donnees personnelles (RGPD)',
$this->twig->render('emails/rgpd_access.html.twig'),
null,
null,
false,
[['path' => $pdfPath, 'name' => 'donnees-personnelles.pdf']],
);
@unlink($pdfPath);
return ['found' => true, 'count' => \count($visitors)];
}
/**
* @return array{found: bool, deleted: int}
*/
public function handleDeletionRequest(string $ip, string $email): array
{
$ipHash = hash('sha256', $ip);
$visitors = $this->em->getRepository(AnalyticsUniqId::class)->findBy(['ipHash' => $ipHash]);
if (0 === \count($visitors)) {
return ['found' => false, 'deleted' => 0];
}
$count = \count($visitors);
foreach ($visitors as $visitor) {
$this->em->remove($visitor);
}
$this->em->flush();
$pdfPath = $this->generateDeletionPdf($ip);
$this->mailer->sendEmail(
$email,
'E-Ticket - Attestation de suppression (RGPD)',
$this->twig->render('emails/rgpd_deletion.html.twig'),
null,
null,
false,
[['path' => $pdfPath, 'name' => 'attestation-suppression.pdf']],
);
@unlink($pdfPath);
return ['found' => true, 'deleted' => $count];
}
/**
* @param list<array{visitor: AnalyticsUniqId, events: list<AnalyticsEvent>}> $data
*/
private function generateAccessPdf(array $data, string $ip): string
{
$logoPath = $this->projectDir.'/public/logo.png';
$logoBase64 = file_exists($logoPath) ? 'data:image/png;base64,'.base64_encode((string) file_get_contents($logoPath)) : '';
$html = $this->twig->render('pdf/rgpd_access.html.twig', [
'data' => $data,
'ip' => $ip,
'logo' => $logoBase64,
'date' => new \DateTimeImmutable(),
]);
return $this->renderPdf($html);
}
private function generateDeletionPdf(string $ip): string
{
$logoPath = $this->projectDir.'/public/logo.png';
$logoBase64 = file_exists($logoPath) ? 'data:image/png;base64,'.base64_encode((string) file_get_contents($logoPath)) : '';
$html = $this->twig->render('pdf/rgpd_deletion.html.twig', [
'ip' => $ip,
'logo' => $logoBase64,
'date' => new \DateTimeImmutable(),
]);
return $this->renderPdf($html);
}
private function renderPdf(string $html): string
{
$dompdf = new Dompdf();
$dompdf->loadHtml($html);
$dompdf->setPaper('A4');
$dompdf->render();
$path = tempnam(sys_get_temp_dir(), 'rgpd_').'.pdf';
file_put_contents($path, $dompdf->output());
return $path;
}
}

View 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 %}

View File

@@ -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>

View 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 %}

View 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 %}

View File

@@ -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 %}

View 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>

View 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>