This commit is contained in:
PTA
2026-08-25 15:12:11 +02:00
parent 44730eefca
commit 1a67080268
25 changed files with 2646 additions and 231 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,129 +0,0 @@
# PTA - Docker / Nginx / PHP / MariaDB / phpMyAdmin
## Installation
Copier les fichiers de ce kit à la racine du projet PTA :
- `Dockerfile`
- `docker-compose.yml`
- `.env.example`
- `docker/nginx/default.conf`
- `docker/php/php.ini`
- `docker/db/apply-migrations.sh`
- `database/pta_prod_schema_mariadb.sql`
- `database/migration_*.sql`
Puis :
```bash
cp .env.example .env
nano .env
```
Changer impérativement `DB_PASSWORD` et `DB_ROOT_PASSWORD`.
Démarrer :
```bash
docker compose up -d --build
```
Application :
```text
http://IP_DU_SERVEUR:8080
```
phpMyAdmin est volontairement publié sur `127.0.0.1:8081` par défaut et **exige
un identifiant MariaDB** à chaque connexion. Ne pas définir `PMA_USER` /
`PMA_PASSWORD` dans le compose : ces variables font basculer phpMyAdmin en
`auth_type=config`, c'est-à-dire une session déjà ouverte pour quiconque
atteint le port.
Sur le serveur lui-même :
```text
http://127.0.0.1:8081
```
Pour y accéder depuis un poste distant, le plus sûr est un tunnel SSH :
```bash
ssh -L 8081:127.0.0.1:8081 utilisateur@serveur
```
Puis ouvrir localement :
```text
http://127.0.0.1:8081
```
## Base de données
Le schéma `database/pta_prod_schema_mariadb.sql` est exécuté automatiquement uniquement
lorsque le volume MariaDB est créé pour la première fois.
Il ne sera PAS rejoué à chaque démarrage du conteneur.
### Migrations
Les fichiers `database/migration_*.sql` sont appliqués **automatiquement à chaque
`docker compose up`**, par le service jetable `db-migrate` :
- il attend que MariaDB soit sain, puis joue les migrations dans l'ordre alphabétique ;
- la table `schema_migration` mémorise les fichiers déjà appliqués — rien n'est rejoué
inutilement ;
- le service `php` ne démarre qu'après sa sortie en succès : une migration en échec arrête
le déploiement au lieu de faire tourner l'application sur une base incomplète ;
- il vérifie pour finir la présence des sept vues de contrôle d'accès de la V7.8.
Il n'est donc plus nécessaire de supprimer le volume ni d'exécuter les migrations à la main.
```bash
# Compte rendu de la dernière application
docker compose logs db-migrate
# Rejouer les migrations sans toucher au reste de la pile
docker compose up -d --force-recreate db-migrate
```
Toute nouvelle migration doit être écrite idempotente (`CREATE TABLE IF NOT EXISTS`,
`INSERT ... ON DUPLICATE KEY UPDATE`, `CREATE OR REPLACE VIEW`) : un fichier dont le contenu
change après coup est rejoué.
## Commandes utiles
```bash
# Etat
docker compose ps
# Logs
docker compose logs -f nginx php db
# Rebuild après modification du PHP / JS / CSS
docker compose up -d --build php nginx
# Journal de l'application des migrations
docker compose logs db-migrate
# Arrêt
docker compose down
# Arrêt SANS supprimer les données
docker compose down
# ATTENTION : supprime la base MariaDB persistée
docker compose down -v
```
## Connexion de l'application
Le `config.php` PTA utilise déjà les variables :
- `PTA_DB_HOST`
- `PTA_DB_PORT`
- `PTA_DB_NAME`
- `PTA_DB_USER`
- `PTA_DB_PASS`
Le compose les fournit automatiquement au conteneur PHP.

121
docs/README_V7_27.md Normal file
View File

@@ -0,0 +1,121 @@
# PTA V7.27 — Le Service Enfance distribue les jetons des agents et des responsables
## Le problème traité
Depuis la V7.11, tout compte d’accès passait par la DSI : création, jeton perdu, départ d’un
remplaçant. Or c’est le **Service Enfance** qui sait qu’un agent est arrivé, qu’un responsable a
laissé son jeton chez lui ou qu’un contrat s’arrête. Le détour par la DSI ajoutait un délai — parfois
plusieurs jours — sans ajouter le moindre contrôle : la DSI ne pouvait rien vérifier de ce qu’on lui
demandait, elle exécutait.
## Ce qui change
L’onglet **Administration** du Service Enfance gagne une section « Jetons d’accès des agents et des
responsables », sous les horaires de couverture. On y fait, pour ces deux rôles, ce que la page DSI
permet :
- **créer** un compte pour un agent ou un lieu qui n’en a pas encore ;
- **régénérer** un jeton perdu ou compromis — l’ancien cesse aussitôt de fonctionner ;
- **désactiver** puis réactiver un compte (départ temporaire) ;
- **supprimer** un compte définitivement ;
- **générer en lot** les jetons de toutes les identités actives qui n’en ont pas.
Comme sur la page DSI, un jeton produit n’est lisible **qu’une fois**, dans le panneau « Jetons à
transmettre » : la base n’en garde qu’une empreinte SHA-256. Perdu, il ne se retrouve pas — il se
régénère.
## Ce qui ne change pas : la frontière de privilège
La délégation s’arrête à **deux rôles : AGENT et RESPONSABLE_STRUCTURE**. Les comptes
**SERVICE_ENFANCE** et **DSI** n’apparaissent pas dans cette section, ne sont pas créables, et toute
action qui en désigne un est refusée en **403**. Ce n’est pas une commodité d’affichage :
- un compte Service Enfance créé par le Service Enfance serait une **auto-promotion** — le rôle se
distribuerait lui-même, sans contrôle extérieur ;
- un compte DSI créé par le Service Enfance lui ouvrirait le **journal d’activité**, c’est-à-dire la
trace de ses propres actions.
Le **second facteur** de la V7.12 reste hors de portée : ces deux rôles n’ont pas de mot de passe, et
le Service Enfance n’a aucun moyen d’en effacer un. La réinitialisation de mot de passe reste une
action DSI, sur la page DSI.
La borne est posée **des deux côtés** :
| Où | Quoi |
| --- | --- |
| `AccessAccountController::ROLES_DELEGUES` | La liste blanche PHP, contrôlée avant toute écriture et à la relecture de tout compte désigné |
| `CompteModel::lister(['roles' => …])` | La liste SQL est bornée ; le filtre d’interface ne peut que **restreindre** à l’intérieur de cette borne |
| `migration_v7_9h_jetons_service_enfance.sql` | La permission `can_manage_field_access`, accordée au seul Service Enfance |
Une frontière de privilège ne doit pas dépendre d’un seul des deux côtés : une ligne mal saisie en
base ne doit pas pouvoir ouvrir la création de comptes privilégiés, et inversement.
## Traçabilité
Toutes les actions sont journalisées en catégorie `ADMINISTRATION`, avec le rôle de l’acteur et un
contexte `canal = DELEGATION_SERVICE_ENFANCE` qui les distingue des mêmes actions faites par la DSI.
Les **refus** le sont aussi, en `ACCES_REFUSE` : `ROLE_HORS_PERIMETRE` (création tentée sur un rôle
privilégié) et `COMPTE_HORS_PERIMETRE` (action tentée sur un compte privilégié). La DSI garde donc la
lecture complète de ce que le Service Enfance fait des accès.
## Fichiers
| Fichier | Rôle |
| --- | --- |
| `database/migration_v7_9h_jetons_service_enfance.sql` | Permission `can_manage_field_access`. Aucune page nouvelle : « Administration » est déjà ouverte au Service Enfance |
| `app/Controllers/AccessAccountController.php` | Les cinq endpoints, la liste blanche et les refus |
| `api/acces_comptes.php`, `acces_compte_creer.php`, `acces_compte_jeton.php`, `acces_compte_statut.php`, `acces_comptes_manquants.php` | Points d’entrée minces |
| `app/Models/CompteModel.php` | Filtre `roles` sur `lister()` |
| `app/Views/partials/administration.php` | La section et le panneau des jetons |
| `assets/js/access-tokens.js` | Module `accessTokens` |
## V7.28 — Couper l’accès depuis la fiche des agents
La page **Agents** gagne, à côté de « Modifier », un bouton qui **coupe ou rétablit l’accès** de
l’agent : « Couper l’accès » / « Rétablir l’accès ». C’est le geste courant — un départ, un arrêt
long, un retour — là où la section « Jetons d’accès » de l’onglet Administration porte la
distribution des jetons elle-même.
Ce bouton **ne touche pas au jeton** : désactiver puis réactiver rend le *même* jeton valable, et
l’agent n’a rien à recevoir. Il ne touche pas non plus à la fiche de l’agent, qui reste planifiable —
c’est l’accès à l’application qui est coupé, pas la personne au planning. Un agent qui n’a pas encore
de compte affiche un bouton inerte « Sans compte d’accès », qui renvoie vers Administration : seul
cet écran sait afficher un jeton fraîchement produit.
Aucun droit nouveau : le module `agentAccess` réutilise `api/acces_comptes.php` et
`api/acces_compte_statut.php`, donc les mêmes gardes, la même liste blanche de rôles et la même
journalisation. L’état des comptes n’est **pas** rendu par PHP mais chargé par le module — le HTML
servi ne porte aucune information sur les comptes, comme sur l’écran DSI.
## V7.29 — Un accès coupé retire l’agent des listes d’affectation
Couper l’accès d’un agent le retire désormais des listes **où l’on affecte** :
| Écran | Effet |
| --- | --- |
| Saisie (`api/agents.php` → `AgentModel::listForLocation()`) | L’agent n’est plus proposé dans le sélecteur |
| Équipes (`PageController::teams()` → `allSelectable()`) | L’agent n’est plus proposé à la composition |
Trois bornes, toutes portées par le même `NOT EXISTS` de `AgentModel::SANS_COMPTE_COUPE` :
- **un agent SANS compte reste proposé.** Il n’a pas encore reçu son jeton, ce qui est l’état normal
avant distribution : le filtrer viderait les listes d’une installation neuve ;
- le filtre porte sur `compte_acces.actif`, **pas** sur `agent.actif` : fermer une fiche d’agent et
couper un accès restent deux décisions distinctes ;
- les écrans de **consultation** (planning par agent, vue annuelle, PTA détaillé) et le
**référentiel Agents** continuent de montrer l’agent. Sans quoi on ne pourrait plus ni relire son
année passée, ni lui rétablir son accès — le bouton vit précisément sur cette page.
**Ce qui n’est pas défait :** les créneaux déjà saisis et les équipes déjà composées restent en
place. Couper un accès n’efface rien du passé. En revanche, un lien profond qui désigne un agent
coupé (file de validation, tableau de bord) ne peut plus le sélectionner : l’écran de saisie le dit
alors explicitement, plutôt que de rester vide — rétablir l’accès rend la saisie de nouveau possible.
## Correctif embarqué
`CompteModel::lister()` répétait le paramètre nommé `:q` quatre fois dans sa clause de recherche. PDO
tourne ici **sans émulation de requêtes préparées** (`ATTR_EMULATE_PREPARES = 0`) : MariaDB refuse
alors un même paramètre employé plusieurs fois. **Toute recherche par nom, matricule ou début de
jeton échouait en 500 sur la page DSI**, depuis la V7.11. Les marqueurs sont désormais numérotés
(`:q_0` … `:q_3`). Le même champ de recherche existant dans la nouvelle section, le défaut aurait été
reconduit tel quel.