update
This commit is contained in:
78
CLAUDE.md
78
CLAUDE.md
@@ -53,7 +53,9 @@ Le script tient un registre `schema_migration` (fichier + empreinte MD5) : un fi
|
||||
et inchangé est ignoré, un fichier modifié est rejoué. **Toute migration doit donc rester
|
||||
idempotente** (`CREATE TABLE IF NOT EXISTS`, `CREATE OR REPLACE VIEW`,
|
||||
`INSERT … ON DUPLICATE KEY UPDATE`). Les fichiers sont pris dans l'ordre alphabétique : nommer une
|
||||
migration `migration_v7_10_…` la ferait passer **avant** `migration_v7_9_…`.
|
||||
migration `migration_v7_10_…` la ferait passer **avant** `migration_v7_9_…`. C'est pourquoi la
|
||||
migration de la V7.11 s'appelle `migration_v7_9e_dsi_comptes_journal.sql` : le préfixe suit la
|
||||
dernière migration en date, pas le numéro de version fonctionnelle.
|
||||
|
||||
`PTA_DB_PORT`, `DB_PORT` et `PMA_PORT` du compose sont les ports joints **à l'intérieur** du réseau
|
||||
Docker, où MariaDB écoute sur 3306. Ce ne sont pas les ports publiés sur l'hôte (`APP_PORT`,
|
||||
@@ -66,7 +68,8 @@ Docker, où MariaDB écoute sur 3306. Ce ne sont pas les ports publiés sur l'h
|
||||
Il n'y a pas de routeur. Chaque URL correspond à un fichier PHP réel de 5 à 10 lignes.
|
||||
|
||||
**Pages HTML** — `dashboard.php`, `planning.php`, `agents.php`, `annual.php`, `pta.php`, `teams.php`,
|
||||
`coverage.php`, `structures.php`, `pending.php`, `vacations.php`, `administration.php` :
|
||||
`coverage.php`, `structures.php`, `pending.php`, `vacations.php`, `manage_agents.php`,
|
||||
`manage_structures.php`, `administration.php`, `dsi.php` :
|
||||
|
||||
```php
|
||||
require __DIR__ . '/page_bootstrap.php';
|
||||
@@ -119,19 +122,35 @@ l'application entière refuse de démarrer.
|
||||
|
||||
### Rôles et périmètres (`app/Core/Access.php`)
|
||||
|
||||
Trois rôles stockés en session : `AGENT` (lecture seule sur soi), `RESPONSABLE_STRUCTURE` (édition
|
||||
des brouillons de son seul lieu), `SERVICE_ENFANCE` (tout, seul habilité à valider).
|
||||
Quatre rôles stockés en session : `AGENT` (lecture seule sur soi), `RESPONSABLE_STRUCTURE` (édition
|
||||
des brouillons de son seul lieu), `SERVICE_ENFANCE` (tout le métier, seul habilité à valider) et
|
||||
`DSI` (administration technique : comptes d'accès et journal, aucun droit métier).
|
||||
|
||||
`role.php` est un **sélecteur de rôle, pas une authentification** — l'utilisateur choisit librement
|
||||
son identité. C'est la faille ouverte la plus importante du projet ; toute évolution vers la
|
||||
production doit remplacer cette sélection par un compte authentifié, sans toucher au reste de la
|
||||
mécanique de droits.
|
||||
`role.php` n'est plus un sélecteur depuis la V7.11 : c'est une **authentification par jeton** (voir
|
||||
`README_V7_11.md`). Une session ne peut naître que de `Access::openSession()`, appelée après
|
||||
vérification d'un jeton par `CompteModel::authentifier()`. Le rôle, l'agent et le lieu sont **lus sur
|
||||
le compte**, jamais déclarés par l'utilisateur. `Access::select()` a disparu — ne pas le
|
||||
réintroduire.
|
||||
|
||||
Les comptes `SERVICE_ENFANCE` et `DSI` exigent un **second facteur** depuis la V7.12 (voir
|
||||
`README_V7_12.md`) : après le jeton, un mot de passe choisi par le titulaire à sa première connexion
|
||||
et haché par `password_hash()`. La règle « qui doit avoir un mot de passe » est portée par la vue
|
||||
(`v_acces_compte.mot_de_passe_requis`), pas par du PHP. Entre les deux étapes, la session ne porte
|
||||
qu'une attente horodatée de cinq minutes : **un jeton validé sans mot de passe n'ouvre rien**. La DSI
|
||||
peut effacer un mot de passe, jamais en poser un — sans quoi elle connaîtrait celui de tout le monde.
|
||||
|
||||
`Access` relit le compte à **chaque requête** (`v_acces_compte`, filtrée sur `actif` et
|
||||
`identite_active`) : c'est ce qui fait qu'un jeton révoqué, un agent désactivé ou un lieu fermé
|
||||
referment la session en cours. Ne pas mettre ce résultat en cache entre deux requêtes.
|
||||
|
||||
**`Access.php` ne contient aucune règle de droit** : tout est lu dans les vues SQL créées par
|
||||
`database/migration_v7_8_securite_acces.sql` (voir `README_V7_8.md`). Modifier un droit se fait en
|
||||
SQL dans `acces_role_page` / `acces_role_permission`, sans redéploiement. Une permission absente de
|
||||
la base vaut « refusé ». `SchemaModel` vérifie la présence des sept vues au démarrage et fait
|
||||
échouer l'application si la migration manque — c'est volontairement fail-closed.
|
||||
la base vaut « refusé » (les lignes du rôle DSI viennent, elles, de
|
||||
`migration_v7_9e_dsi_comptes_journal.sql`). `SchemaModel` vérifie au démarrage la présence des sept
|
||||
vues de droits et des trois vues de comptes/journal, et fait échouer l'application si une migration
|
||||
manque — c'est volontairement fail-closed, et `docker/db/apply-migrations.sh` refait le même contrôle
|
||||
au déploiement.
|
||||
|
||||
`dashboard.php` (Service Enfance uniquement, voir `README_V7_9.md`) illustre la règle : le droit
|
||||
d'accès vient de `database/migration_v7_9_tableau_de_bord.sql`. Tant que la migration n'est pas
|
||||
@@ -139,14 +158,37 @@ jouée, l'onglet n'apparaît pas et `Access::homeUrl()` se replie sur `planning.
|
||||
indispensable, sans lui `requirePage()` renverrait indéfiniment vers une page interdite.
|
||||
|
||||
Les contrôles sont doublés : l'UI masque, mais le serveur tranche. Toute méthode de contrôleur
|
||||
commence par un garde `requireService()`, `requireDraftEditor()`, `requireStructureEdit($id)`,
|
||||
`requireAgentView($id)`, `requireEntryEdit($id)`, `requirePlanningEdit($id)`… Ces gardes émettent
|
||||
commence par un garde `requireService()`, `requireDsi()`, `requireDraftEditor()`,
|
||||
`requireStructureEdit($id)`, `requireAgentView($id)`, `requireEntryEdit($id)`,
|
||||
`requirePlanningEdit($id)`… Ces gardes émettent
|
||||
directement un 401/403 JSON et coupent l'exécution. **Ne jamais ajouter un endpoint mutant sans
|
||||
garde ni sans `Csrf::assertPayload($payload)`.**
|
||||
|
||||
CSRF : le jeton voyage **dans le corps JSON** (`csrf_token`), pas dans un en-tête. Côté JS :
|
||||
`csrf_token: window.PTA_CONFIG.csrfToken`.
|
||||
|
||||
### Journalisation (V7.11)
|
||||
|
||||
`App\Core\Journal` écrit dans `journal_activite`. Sa connexion PDO est posée une fois par
|
||||
`app/bootstrap.php`, les appelants n'ont donc rien à transporter :
|
||||
|
||||
```php
|
||||
Journal::ecriture('PLANNING_VALIDE', 'planning', $planningId, 'Planning validé pour…', [contexte]);
|
||||
```
|
||||
|
||||
Trois règles :
|
||||
|
||||
- **tout endpoint mutant doit journaliser son succès**, après l'écriture en base et avant le
|
||||
`JsonResponse::send()` — au même titre qu'il porte un garde et un `Csrf::assertPayload()` ;
|
||||
- **les lectures ne sont jamais journalisées** (choix de volume) ; les refus le sont, et `Access`
|
||||
comme `Csrf` s'en chargent déjà — inutile de les doubler dans les contrôleurs ;
|
||||
- **le journal ne doit jamais faire échouer l'action** : `Journal` avale ses propres erreurs. Ne pas
|
||||
entourer un appel de logique qui pourrait lever.
|
||||
|
||||
Le journal sert aussi de compteur au frein anti-force-brute de la connexion
|
||||
(`JournalModel::echecsRecents()`) : renommer les actions `JETON_REFUSE` ou `MOT_DE_PASSE_REFUSE`
|
||||
désarmerait ce frein silencieusement.
|
||||
|
||||
### Contraintes de sécurité à respecter
|
||||
|
||||
- **CSP à nonce** (`app/Core/Security.php`) : `script-src 'self' 'nonce-…'`, sans `unsafe-inline`.
|
||||
@@ -175,7 +217,7 @@ PTA.modules.agentOverview = { init, load };
|
||||
applique les filtres d'URL (`structure_id`, `agent_id`, `week`, `month`, `focus`). Les modules se
|
||||
rafraîchissent entre eux via l'appel optionnel `PTA.modules.autre?.refresh?.({ silent: true })`.
|
||||
|
||||
Les assets sont versionnés à la main : `assets/style.css?v=7.9` et `assets/js/*.js?v=7.9` dans
|
||||
Les assets sont versionnés à la main : `assets/style.css?v=7.23` et `assets/js/*.js?v=7.23` dans
|
||||
`layouts/header.php` / `layouts/footer.php` **et dans `Views/pages/role.php`**, qui porte sa propre
|
||||
balise `<head>`. **Bumper ce numéro aux trois endroits après toute modification de JS/CSS**, sinon
|
||||
les navigateurs et le `expires 7d` de nginx servent l'ancienne version.
|
||||
@@ -190,6 +232,13 @@ les navigateurs et le `expires 7d` de nginx servent l'ancienne version.
|
||||
imputer des heures de quota les décompterait une seconde fois. Une tentative de décompter un
|
||||
forfait de 7 h par jour de CP a été introduite puis annulée pour cette raison — ne pas la
|
||||
réintroduire.
|
||||
- **Un agent n'est planifiable qu'entre ses dates de contrat** (V7.15,
|
||||
`date_debut_contrat` / `date_fin_contrat`). La règle vaut pour TOUS les motifs, congés compris :
|
||||
hors contrat, il n'y a rien à décompter. Une borne absente ouvre — pas de date de fin signifie
|
||||
contrat à durée indéterminée. Elle est appliquée à l'écriture (`assertDatesWithinContract()` dans
|
||||
`PlanningController` et `WeekTemplateController`), au contrôle de validation (`HORS_CONTRAT`) et à
|
||||
l'inverse, sur la fiche agent : raccourcir un contrat sous des affectations existantes est refusé.
|
||||
Toute nouvelle voie d'écriture de créneaux doit poser la même garde.
|
||||
- Un `planning` = un agent × une semaine ISO × un lieu, statut `BROUILLON` ou `VALIDE`. Il contient
|
||||
des `creneau_horaire` porteurs d'un `motif_planning`. Le flag `compte_dans_quota` du motif décide
|
||||
de l'imputation (CONGE, AUTRE_ABSENCE et JNT ne comptent pas).
|
||||
@@ -231,5 +280,8 @@ message}` : une `ERREUR` bloque (422), une `ALERTE` demande une confirmation exp
|
||||
`PMA_USER` / `PMA_PASSWORD` dans le compose : cela rouvre une session sans mot de passe.
|
||||
- Le mot de passe MariaDB du `.env` est encore la valeur d'exemple : à changer avant tout
|
||||
déploiement réel.
|
||||
- Le **jeton DSI d'installation** (`PTA-INSTALLATION-DSI`) est écrit en clair dans
|
||||
`database/migration_v7_9e_dsi_comptes_journal.sql`, donc public. La page DSI alerte tant qu'il n'a
|
||||
pas été régénéré ; c'est la première action à faire après une installation.
|
||||
- Les `README_V7_*.md` documentent chacun une version successive et servent d'historique métier —
|
||||
utiles pour comprendre une décision, mais seul le code fait foi.
|
||||
|
||||
Reference in New Issue
Block a user