feat(admin): page d'information RGPD sur le traitement des donnees (Cr 3.d.2)
This commit is contained in:
parent
7ab9a5a8cf
commit
319b97f944
5 changed files with 277 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>
|
</button>
|
||||||
<div class="dropdown-menu" id="userMenu">
|
<div class="dropdown-menu" id="userMenu">
|
||||||
<a href="/admin/profile/pin">Mon PIN d'action sensible</a>
|
<a href="/admin/profile/pin">Mon PIN d'action sensible</a>
|
||||||
|
<a href="/admin/privacy">Traitement de mes donnees</a>
|
||||||
<div class="divider"></div>
|
<div class="divider"></div>
|
||||||
<form method="post" action="/logout">
|
<form method="post" action="/logout">
|
||||||
<input type="hidden" name="_csrf" value="<?= $csrf ?>">
|
<input type="hidden" name="_csrf" value="<?= $csrf ?>">
|
||||||
|
|
|
||||||
88
src/app/Views/admin/privacy.php
Normal file
88
src/app/Views/admin/privacy.php
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
<?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-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>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>E-mail, prenom, nom</td>
|
||||||
|
<td>Identifier le compte et la personne qui se connecte</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Mot de passe et PIN (stockes hashes, argon2id)</td>
|
||||||
|
<td>Authentifier la connexion et valider les actions sensibles ; jamais stockes en clair, jamais affiches</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Role, statut actif, date de derniere connexion</td>
|
||||||
|
<td>Determiner les actions autorisees (RBAC) et l'etat du compte</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>
|
||||||
|
</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>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="card" aria-labelledby="privacy-share">
|
||||||
|
<h2 id="privacy-share">Conservation et partage</h2>
|
||||||
|
<p>
|
||||||
|
Les donnees sont hebergees sur l'infrastructure du restaurant et ne sont
|
||||||
|
<strong>partagees avec aucun tiers</strong>. Le journal d'audit est purge
|
||||||
|
periodiquement selon la duree de retention configuree. 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\OrderAdminController;
|
||||||
use App\Controllers\OrderController;
|
use App\Controllers\OrderController;
|
||||||
use App\Controllers\PasswordResetController;
|
use App\Controllers\PasswordResetController;
|
||||||
|
use App\Controllers\PrivacyController;
|
||||||
use App\Controllers\ProductController;
|
use App\Controllers\ProductController;
|
||||||
use App\Controllers\ProfileController;
|
use App\Controllers\ProfileController;
|
||||||
use App\Controllers\StatsController;
|
use App\Controllers\StatsController;
|
||||||
|
|
@ -147,6 +148,10 @@ try {
|
||||||
$router->add('GET', '/admin/profile/pin', [ProfileController::class, 'showPin']);
|
$router->add('GET', '/admin/profile/pin', [ProfileController::class, 'showPin']);
|
||||||
$router->add('POST', '/admin/profile/pin', [ProfileController::class, 'updatePin']);
|
$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
|
// CRUD Produits (product.read/create/update/delete). PIN equipier + audit sur
|
||||||
// changement prix/TVA (update) et suppression (delete).
|
// changement prix/TVA (update) et suppression (delete).
|
||||||
$router->add('GET', '/admin/products', [ProductController::class, 'index']);
|
$router->add('GET', '/admin/products', [ProductController::class, 'index']);
|
||||||
|
|
|
||||||
142
tests/Unit/Admin/PrivacyControllerTest.php
Normal file
142
tests/Unit/Admin/PrivacyControllerTest.php
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<?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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue