feat(admin): page d'information RGPD sur le traitement des donnees (Cr 3.d.2)
Some checks failed
CI / secret-scan (push) Successful in 10s
CI / php-lint (push) Successful in 20s
CI / static-tests (push) Has been cancelled
CI / js-tests (push) Has been cancelled

This commit is contained in:
Imugiii 2026-06-22 06:20:27 +00:00
parent 7ab9a5a8cf
commit 6243304a29
7 changed files with 2005 additions and 0 deletions

1623
docs/SESSION_RESUME.md Executable file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,105 @@
# P1 Merise v0.2 (prod-like) reecrit + migration vers Forgejo auto-heberge
**Date** : 2026-06-04 (seconde session du jour, suite de `conception-prodlike-revision`)
**Branche** : `feat/p1-conception`
**PR** : commits directs sur la branche ; PR `feat/p1-conception -> dev` a ouvrir apres l'UML
**Duree estimee** : session longue (decisions + execution + infra)
---
## Ce qui a ete fait
Trois chantiers enchaines.
### 1. Decisions de conception restantes tranchees (D4-D8 + stock + Maxi)
- **D4 - Roles / RBAC / workflow** : seed de 5 roles `admin / manager / kitchen / counter / drive`. RBAC dynamique (roles + matrice role-permission editables en UI). Attributs sur `role` pour rendre les comportements dynamiques sans hardcode : `default_route`, `order_source`, et table `role_visible_source` (filtre du dashboard par canal). Machine a etats reduite de 6 a **4 etats** : `pending_payment -> paid -> delivered` (+ `cancelled`). La cuisine est en lecture seule ; counter/drive font la livraison en un geste.
- **D5 - Permissions** : catalogue fige de 23 codes `resource.action` + matrice par defaut. Correction appliquee : `admin` recoit `order.create`/`order.deliver` ; `manager` ne recoit pas `order.cancel`.
- **D6 - service_day** : coupure a 10h00, calcul applicatif (`CASE WHEN HOUR(created_at) < 10 ...`), la colonne generee buguee a 4h30 du MLD v0.1 est abandonnee.
- **D7 - Subnet Docker** : retour a l'auto-allocation (IP liberees sur le serveur), le subnet explicite n'est plus necessaire.
- **D8 - Numero de commande** : prefixe par canal `K`/`C`/`D`.
- **Stock numerique** par ingredient : colonnes `unit`, `stock_quantity`, `pack_size`, `pack_label`, `low_stock_threshold` sur `ingredient` ; decrement automatique a la transition `paid`, re-credit a l'annulation, reconciliation manuelle (inventaire), journal `stock_movement` append-only, permissions `stock.read`/`stock.count`/`stock.manage`.
- **Format Maxi** : multiplicateur de recette. `product_ingredient` porte `quantity_normal` et `quantity_maxi` ; le decrement suit `order_item.format`. Le Maxi agrandit l'accompagnement et la boisson uniquement (burger et sauce invariants).
Decisions consignees dans `docs/notes/revue-alignement-p1.md` section 7 (non versionne).
### 2. Reecriture des 5 docs Merise en prod-like v0.2 (19 entites, anglais)
Delegation a l'agent `expert-merise-agile`, en deux lots (donnees : dictionary/MCD/MLD ; traitements : MCT/MLT), chaque sortie verifiee contre le tableau de decision puis recalee a la main (matrice de permissions, multiplicateur Maxi). Resultat : `dictionary.md`, `mcd.md`, `mld.md`, `mct.md`, `mlt.md` reecrits, 19 entites, vocabulaire anglais `snake_case`, `commande_event` et `menu_produit` supprimes au profit du configurateur d'ingredients, des allergenes, des slots de menu et du journal de stock. Cinq commits (un par doc) sur `feat/p1-conception`.
### 3. Migration du depot vers Forgejo auto-heberge + gouvernance PR
- Nouveau depot `https://git.acadenice.com/AcadeNice/corentin_wakdo` (Forgejo sur le serveur).
- **Dual-push** configure : `git push origin` pousse desormais sur GitHub ET Forgejo, les deux en HTTPS via des tokens lus dans `.env` (gitignore). Aucun token persiste dans `.git/config` (credential helper qui lit `.env` au moment du push).
- **Reconciliation** d'un desalignement decouvert au passage : `dev` sur GitHub (`a3eae01`) etait le vrai tronc integre (PR #4 front, #5 conception, #6 admin shell mergees), tandis que `dev` local et Forgejo etaient restes a `68db2ee`. Forgejo a ete resynchronise sur l'etat GitHub complet, sans perte.
- **Template de PR** (`.gitea/PULL_REQUEST_TEMPLATE.md`, conventions BYAN) ajoute sur `dev`.
- **Protections de branche** sur `main` et `dev` (API Forgejo) : push direct interdit (PR obligatoire), force-push bloque, 0 approbation requise (adapte au travail solo).
---
## Pourquoi - decisions et alternatives
- **Machine a 4 etats au lieu de 6** : en fast-food, le KDS cuisine est un affichage visuel ; `preparing` et `ready` ajoutaient des transitions sans valeur metier proportionnelle. La livraison fusionne `ready`+`delivered` en un clic. Le KPI reste le temps total `delivered_at - paid_at`.
- **Stock Maxi via multiplicateur de recette** (alternative ecartee : produits de taille distincte Moyenne/Grande) : l'auteur a prefere un seul produit par choix avec `quantity_normal`/`quantity_maxi`, plus simple a saisir au seed tout en gardant un stock realiste.
- **Forgejo auto-heberge** (alternative : rester sur GitHub) : GitHub facture les depots prives, et l'auto-hebergement apporte Forgejo Actions et un controle fin des regles de PR. Cela ajoute un argument infra (Bloc 5) puisque la chaine forge + CI est maitrisee de bout en bout.
- **Tout en HTTPS + token plutot que SSH** : la cle SSH GitHub (`~/.ssh/git_wakdo`) est protegee par passphrase et inutilisable depuis le shell non-interactif de l'assistant. Les tokens HTTPS stockes dans `.env` permettent un push automatise des deux cotes.
- **0 approbation sur les protections** : en solo, exiger une approbation bloquerait le merge (on ne peut pas approuver sa propre PR). La protection conserve l'essentiel : PR obligatoire et pas de force-push.
---
## Comment - points techniques cles
- **Credential helper lisant `.env`** : `git config credential.https://<host>.helper` pointe vers un script qui extrait le token de `.env` au moment du push. Avantage : roter un token = editer `.env`, sans reconfiguration git, et aucun secret dans `.git/config`.
- **Reconciliation `dev`** : `git branch -f dev origin/dev` pour reprendre l'etat GitHub reel, template ajoute par-dessus dans un worktree dedie (working tree principal preserve), push fast-forward sur GitHub et push force sur Forgejo (lignee perimee remplacee, depot neuf donc sans risque).
- **Protections via API** : `POST /api/v1/repos/.../branch_protections` avec `enable_push=false` et `required_approvals=0` pour `main` et `dev`.
---
## Criteres RNCP couverts
- **Bloc 2 - Cr 3.a / 3.b** : modelisation des donnees prod-like (dictionnaire, MCD, MLD), polymorphisme, snapshots, configurateur d'ingredients, journal de stock.
- **Bloc 2 - Cr 3.d** : TVA portee par le produit et calculee ligne par ligne.
- **Bloc 5** : forge auto-hebergee, gouvernance de branches (protections + template PR), socle pour la CI Forgejo Actions a venir.
---
## Questions anticipees du jury
- **Q** : "Pourquoi heberger votre propre forge plutot qu'un service gere ?"
**R** : Maitrise de la chaine (depot, CI via Forgejo Actions, regles de PR), depots prives sans cout, et coherence avec l'infra deja en place sur le serveur. GitHub est conserve comme copie synchronisee.
- **Q** : "Comment gerez-vous le stock d'un menu Maxi ?"
**R** : Un multiplicateur de recette : `product_ingredient` porte une quantite Normale et une quantite Maxi, le decrement suit le format de la ligne. Le Maxi n'agrandit que l'accompagnement et la boisson.
- **Q** : "Vos secrets sont-ils exposes dans le depot ?"
**R** : Non. Les tokens vivent dans `.env` (gitignore) ; `.git/config` ne contient que des scripts qui les lisent. Le credential helper evite de stocker un token en clair dans la config git.
---
## Points d'amelioration conscients
- **UML pas encore reecrit** : `state-commande.md`, `use-cases.md`, `sequence-passer-commande.md` sont encore en v0.1 (6 etats, ancien vocabulaire). A reecrire pour 4 etats / 5 roles / slots+modifiers avant d'ouvrir la PR conception.
- **PROJECT_CONTEXT sections 7 et 11** : a mettre a jour (retirer "MVP", passer a ~19 entites, rechiffrer le planning).
- **Diagrammes drawio** (MCD/MLD) : a regenerer pour 19 entites ; les `.md` utilisent du Mermaid inline en attendant.
- **SESSION_RESUME.md** : perime (Session 7), a rafraichir avec le nouveau remote dual-push et l'etat v0.2.
- **Synchronisation des merges Forgejo vers GitHub** : un merge fait dans l'UI Forgejo ne remonte pas tout seul sur GitHub ; un push-mirror Forgejo -> GitHub serait la reponse propre, a decider.
- **Token Forgejo expose dans le chat** : il a transite en clair pendant la mise en place ; le roter quand pratique (il est dedie au push, donc impact limite).
---
## Etat a la reprise
- Conception v0.2 **committee** (dictionnaire + MCD/MLD/MCT/MLT) et **deja presente dans `dev`** (`971ce0c`) via la PR #5 mergee puis resynchronisee.
- `feat/p1-conception` est a `392ba9a` ; une PR vers `dev` serait vide tant que l'UML + PROJECT_CONTEXT ne sont pas produits.
- **Prochaine action** : reecrire l'UML + PROJECT_CONTEXT sur `feat/p1-conception`, committer (un commit par doc), pousser (dual), puis ouvrir la PR `feat/p1-conception -> dev` avec le template.
---
## Liens vers artefacts
- Commits Merise v0.2 : `6ceebf7` (dictionary), `6c1cede` (MCD), `36332b4` (MLD), `6057ef9` (MCT), `392ba9a` (MLT)
- Template PR + sync dev : `971ce0c`
- Depots : `https://git.acadenice.com/AcadeNice/corentin_wakdo` (Forgejo) + `https://github.com/AcadeNice/wakdo_corentin` (GitHub, miroir synchronise)
- Docs reecrits : `docs/merise/{dictionary,mcd,mld,mct,mlt}.md`
- Tableau de decision : `docs/notes/revue-alignement-p1.md` section 7 (non versionne)
- Journal precedent du jour : `docs/journal/2026-06-04--conception-prodlike-revision.md`

View 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);
}
}

View file

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

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

View file

@ -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']);

View 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);
}
}