feat(admin): page d'information RGPD sur le traitement des donnees (Cr 3.d.2) (#72)
This commit is contained in:
parent
7ab9a5a8cf
commit
121877ea65
5 changed files with 303 additions and 0 deletions
41
src/app/Controllers/PrivacyController.php
Normal file
41
src/app/Controllers/PrivacyController.php
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Response;
|
||||
|
||||
/**
|
||||
* Mention d'information sur le traitement des donnees personnelles (RGPD, Cr 3.d.2 :
|
||||
* l'application informe l'utilisateur du stockage, de l'utilisation et du cadre de
|
||||
* partage de ses donnees personnelles). GET /admin/privacy, accessible a tout
|
||||
* utilisateur authentifie.
|
||||
*
|
||||
* La page concerne les donnees du STAFF : la borne client est anonyme (aucune PII
|
||||
* client collectee, customer_order.acting_user_id = NULL cote kiosk, cf.
|
||||
* PROJECT_CONTEXT section 19). Elle informe sur les donnees stockees, leur usage et
|
||||
* leur (non-)partage, et rappelle les droits d'acces / rectification / effacement
|
||||
* (effacement materialise par l'anonymisation, mlt 10.5 ERASE_USER_PII).
|
||||
*
|
||||
* Page statique (pas d'acces BDD) rendue dans le shell admin. Non `final` : les
|
||||
* tests sous-classent pour injecter des doubles.
|
||||
*/
|
||||
class PrivacyController extends AdminController
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
*/
|
||||
public function index(array $params = []): Response
|
||||
{
|
||||
$guard = $this->guard();
|
||||
if ($guard instanceof Response) {
|
||||
return $guard;
|
||||
}
|
||||
|
||||
return $this->adminView('admin/privacy', [
|
||||
'title' => 'Traitement des donnees personnelles - Wakdo Admin',
|
||||
'activeNav' => '',
|
||||
], $guard);
|
||||
}
|
||||
}
|
||||
|
|
@ -72,6 +72,7 @@ $navClass = static function (string $code, string $current): string {
|
|||
</button>
|
||||
<div class="dropdown-menu" id="userMenu">
|
||||
<a href="/admin/profile/pin">Mon PIN d'action sensible</a>
|
||||
<a href="/admin/privacy">Traitement de mes donnees</a>
|
||||
<div class="divider"></div>
|
||||
<form method="post" action="/logout">
|
||||
<input type="hidden" name="_csrf" value="<?= $csrf ?>">
|
||||
|
|
|
|||
108
src/app/Views/admin/privacy.php
Normal file
108
src/app/Views/admin/privacy.php
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Mention d'information RGPD (Cr 3.d.2), injectee dans admin/layout.php. Page
|
||||
* statique : informe le personnel des donnees traitees par l'application, de leur
|
||||
* usage, de leur conservation, de leur (non-)partage et des droits associes. Le
|
||||
* contenu est litteral (aucune donnee dynamique a echapper).
|
||||
*/
|
||||
?>
|
||||
<div class="page-header">
|
||||
<div>
|
||||
<h1 class="page-title">Traitement des donnees personnelles</h1>
|
||||
<p class="page-subtitle">Information sur les donnees que cette application stocke, utilise et conserve, et sur vos droits (RGPD).</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="card" aria-labelledby="privacy-scope">
|
||||
<h2 id="privacy-scope">Qui est concerne</h2>
|
||||
<p>
|
||||
Cette mention concerne les <strong>comptes du personnel</strong> (administration,
|
||||
manager, cuisine, comptoir, drive). La borne client est <strong>anonyme</strong> :
|
||||
une commande passee en borne ne collecte aucune donnee personnelle (pas de nom,
|
||||
ni e-mail, ni telephone) ; seul un numero de table facultatif y est saisi.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="privacy-controller">
|
||||
<h2 id="privacy-controller">Responsable du traitement</h2>
|
||||
<p>
|
||||
Le responsable du traitement est <strong>l'exploitant du restaurant Wakdo</strong>.
|
||||
Pour toute question ou pour exercer vos droits, le contact est
|
||||
l'administrateur du systeme : <strong>contact@wakdo.local</strong>, ou
|
||||
l'administration sur place.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="privacy-data">
|
||||
<h2 id="privacy-data">Donnees traitees</h2>
|
||||
<div class="table-container">
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">Donnee</th>
|
||||
<th scope="col">Finalite</th>
|
||||
<th scope="col">Base legale</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>E-mail, prenom, nom</td>
|
||||
<td>Identifier le compte et la personne qui se connecte</td>
|
||||
<td>Execution de la relation d'emploi</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Mot de passe et PIN (stockes uniquement haches, argon2id)</td>
|
||||
<td>Authentifier la connexion et valider les actions sensibles ; hors journaux et hors affichage</td>
|
||||
<td>Execution de la relation d'emploi (securite des acces)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Role, statut actif, date de derniere connexion</td>
|
||||
<td>Determiner les actions autorisees (RBAC) et l'etat du compte</td>
|
||||
<td>Interet legitime (gestion des acces)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Journal d'audit des actions sensibles (auteur, action, horodatage)</td>
|
||||
<td>Tracer qui a effectue une action sensible (annulation, changement de prix, gestion des comptes)</td>
|
||||
<td>Interet legitime (tracabilite, prevention de la fraude interne)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Compteurs de tentatives de connexion et adresse IP de connexion</td>
|
||||
<td>Limiter les attaques par force brute sur l'authentification</td>
|
||||
<td>Interet legitime (securite du systeme)</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="privacy-share">
|
||||
<h2 id="privacy-share">Conservation et partage</h2>
|
||||
<ul>
|
||||
<li><strong>Donnees de compte</strong> (identite, role, statut) : conservees tant que le compte est actif, puis anonymisees a l'effacement.</li>
|
||||
<li><strong>Journal d'audit</strong> : conserve environ <strong>12 mois</strong> (interet legitime, tracabilite fiscale), puis purge par une tache planifiee, independamment du cycle de vie du compte.</li>
|
||||
<li><strong>Compteurs de connexion</strong> : reinitialises a la connexion reussie ; non conserves au-dela de leur usage de securite.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Les donnees sont hebergees sur l'infrastructure du restaurant et ne sont
|
||||
<strong>partagees avec aucun tiers</strong>. Aucune donnee n'est utilisee a des
|
||||
fins publicitaires ni cedee a des fins commerciales.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section class="card" aria-labelledby="privacy-rights">
|
||||
<h2 id="privacy-rights">Vos droits</h2>
|
||||
<p>Vous disposez d'un droit d'acces, de rectification et d'effacement de vos donnees personnelles :</p>
|
||||
<ul>
|
||||
<li><strong>Acces et rectification</strong> : un administrateur peut consulter et corriger les informations de votre compte (rubrique Utilisateurs).</li>
|
||||
<li><strong>Effacement</strong> : a la demande, vos donnees personnelles sont anonymisees ; le compte est conserve sous une forme non identifiante pour preserver l'integrite des historiques, et vos identifiants sont invalides.</li>
|
||||
</ul>
|
||||
<p>
|
||||
Pour exercer ces droits, adressez-vous a l'administration du restaurant, qui
|
||||
traite la demande depuis le back-office.
|
||||
</p>
|
||||
</section>
|
||||
|
|
@ -23,6 +23,7 @@ use App\Controllers\MenuController;
|
|||
use App\Controllers\OrderAdminController;
|
||||
use App\Controllers\OrderController;
|
||||
use App\Controllers\PasswordResetController;
|
||||
use App\Controllers\PrivacyController;
|
||||
use App\Controllers\ProductController;
|
||||
use App\Controllers\ProfileController;
|
||||
use App\Controllers\StatsController;
|
||||
|
|
@ -147,6 +148,10 @@ try {
|
|||
$router->add('GET', '/admin/profile/pin', [ProfileController::class, 'showPin']);
|
||||
$router->add('POST', '/admin/profile/pin', [ProfileController::class, 'updatePin']);
|
||||
|
||||
// Mention d'information RGPD (Cr 3.d.2) : traitement des donnees personnelles du
|
||||
// personnel. Accessible a tout utilisateur authentifie (aucune permission requise).
|
||||
$router->add('GET', '/admin/privacy', [PrivacyController::class, 'index']);
|
||||
|
||||
// CRUD Produits (product.read/create/update/delete). PIN equipier + audit sur
|
||||
// changement prix/TVA (update) et suppression (delete).
|
||||
$router->add('GET', '/admin/products', [ProductController::class, 'index']);
|
||||
|
|
|
|||
148
tests/Unit/Admin/PrivacyControllerTest.php
Normal file
148
tests/Unit/Admin/PrivacyControllerTest.php
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Admin;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Auth\Authorizer;
|
||||
use App\Auth\SessionGuard;
|
||||
use App\Auth\SessionManager;
|
||||
use App\Auth\UserDirectory;
|
||||
use App\Controllers\PrivacyController;
|
||||
use App\Core\Config;
|
||||
use App\Core\Database;
|
||||
use App\Core\Request;
|
||||
use App\Tests\Support\FakeDatabase;
|
||||
|
||||
/**
|
||||
* Sous-classe de test : injecte session test + FakeDatabase dans la garde,
|
||||
* l'autorisation et l'annuaire, sans base reelle.
|
||||
*/
|
||||
final class TestPrivacyController extends PrivacyController
|
||||
{
|
||||
public function __construct(
|
||||
Request $request,
|
||||
Config $config,
|
||||
Database $database,
|
||||
private readonly SessionManager $testSession,
|
||||
private readonly FakeDatabase $fakeDb,
|
||||
) {
|
||||
parent::__construct($request, $config, $database);
|
||||
}
|
||||
|
||||
protected function sessionManager(): SessionManager
|
||||
{
|
||||
return $this->testSession;
|
||||
}
|
||||
|
||||
protected function sessionGuard(): SessionGuard
|
||||
{
|
||||
return new SessionGuard($this->testSession, $this->fakeDb, $this->config);
|
||||
}
|
||||
|
||||
protected function authorizer(): Authorizer
|
||||
{
|
||||
return new Authorizer($this->fakeDb);
|
||||
}
|
||||
|
||||
protected function userDirectory(): UserDirectory
|
||||
{
|
||||
return new UserDirectory($this->fakeDb);
|
||||
}
|
||||
}
|
||||
|
||||
final class PrivacyControllerTest extends TestCase
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $touchedKeys = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->setEnv('SESSION_LIFETIME_IDLE', '14400');
|
||||
$this->setEnv('SESSION_LIFETIME_ABSOLUTE', '36000');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ($this->touchedKeys as $key) {
|
||||
putenv($key);
|
||||
}
|
||||
$this->touchedKeys = [];
|
||||
}
|
||||
|
||||
private function setEnv(string $key, string $value): void
|
||||
{
|
||||
$this->touchedKeys[] = $key;
|
||||
putenv($key . '=' . $value);
|
||||
}
|
||||
|
||||
private function controller(SessionManager $session, FakeDatabase $db): TestPrivacyController
|
||||
{
|
||||
$request = new Request('GET', '/admin/privacy', [], [], '', '203.0.113.5');
|
||||
|
||||
return new TestPrivacyController($request, new Config(), new Database(new Config()), $session, $db);
|
||||
}
|
||||
|
||||
private function authedSession(): SessionManager
|
||||
{
|
||||
$session = new SessionManager(new Config(), true);
|
||||
$now = time();
|
||||
$session->set('user_id', 1);
|
||||
$session->set('role_id', 1);
|
||||
$session->set('logged_in_at', $now - 100);
|
||||
$session->set('last_activity', $now - 50);
|
||||
|
||||
return $session;
|
||||
}
|
||||
|
||||
public function testRedirectsToLoginWithoutSession(): void
|
||||
{
|
||||
$response = $this->controller(new SessionManager(new Config(), true), new FakeDatabase())->index();
|
||||
|
||||
self::assertSame(302, $response->status());
|
||||
self::assertSame('/login', $response->header('Location'));
|
||||
}
|
||||
|
||||
public function testInactiveUserRedirectsToLogin(): void
|
||||
{
|
||||
$db = new FakeDatabase();
|
||||
$db->guardUserRow = ['is_active' => 0];
|
||||
|
||||
$response = $this->controller($this->authedSession(), $db)->index();
|
||||
|
||||
self::assertSame(302, $response->status());
|
||||
self::assertSame('/login', $response->header('Location'));
|
||||
}
|
||||
|
||||
public function testRendersRgpdNoticeForAnyAuthenticatedUser(): void
|
||||
{
|
||||
// Aucune permission specifique requise : tout utilisateur authentifie y accede,
|
||||
// meme un role sans permission de gestion (canResult = false).
|
||||
$db = new FakeDatabase();
|
||||
$db->guardUserRow = ['is_active' => 1];
|
||||
$db->userDisplayRow = ['first_name' => 'Sami', 'last_name' => 'K', 'role_label' => 'Equipier'];
|
||||
$db->permissionCodes = [];
|
||||
$db->canResult = false;
|
||||
|
||||
$response = $this->controller($this->authedSession(), $db)->index();
|
||||
|
||||
self::assertSame(200, $response->status());
|
||||
$body = $response->body();
|
||||
// Shell rendu + contenu RGPD attendu (Cr 3.d.2 : stockage / utilisation / partage / droits).
|
||||
self::assertStringContainsString('admin-layout', $body);
|
||||
self::assertStringContainsString('Traitement des donnees personnelles', $body);
|
||||
self::assertStringContainsString('Donnees traitees', $body);
|
||||
self::assertStringContainsString('partage', $body);
|
||||
self::assertStringContainsString('droit', $body);
|
||||
self::assertStringContainsString('effacement', $body);
|
||||
// La page rappelle l'anonymisation comme materialisation de l'effacement.
|
||||
self::assertStringContainsString('anonymis', $body);
|
||||
// Completude d'une notice RGPD (Cr 3.d.2) : base legale, responsable de
|
||||
// traitement + contact, et duree de conservation concrete (coherente MLD ~12 mois).
|
||||
self::assertStringContainsString('Base legale', $body);
|
||||
self::assertStringContainsString('Responsable du traitement', $body);
|
||||
self::assertStringContainsString('contact@wakdo.local', $body);
|
||||
self::assertStringContainsString('12 mois', $body);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue