diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..624824f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +# Le Dockerfile fait COPY . /var/www/html : tout ce qui est listé ici reste +# hors de l'image, donc hors de la racine web et hors des couches publiées. +.env +.env.* +!.env.example +.git +.gitignore +.dockerignore + +# Historique et documentation de version : inutiles dans l'image. +README*.md +CLAUDE.md + +# Journaux, sauvegardes et artefacts locaux +*.log +*.sql.gz +*.dump +logs/ +tmp/ +.idea/ +.vscode/ +.DS_Store diff --git a/.env.example b/.env.example index 0f3a3a4..a6789ca 100644 --- a/.env.example +++ b/.env.example @@ -4,12 +4,17 @@ APP_PORT=8080 TZ=Europe/Paris # MariaDB +# Remplacer impérativement les deux mots de passe avant tout déploiement. DB_NAME=pta DB_USER=pta -DB_PASSWORD=pta -DB_ROOT_PASSWORD=pta +DB_PASSWORD=a_remplacer +DB_ROOT_PASSWORD=a_remplacer_aussi + +# Débogage : 1 renvoie le détail des exceptions au navigateur. Laisser à 0. +PTA_DEBUG=0 # phpMyAdmin # 127.0.0.1 = uniquement accessible depuis le serveur lui-même. +# phpMyAdmin demande désormais un identifiant MariaDB à chaque connexion. PMA_BIND=127.0.0.1 PMA_PORT=8081 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..00636d6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,28 @@ +# Configuration locale : contient les mots de passe MariaDB. +# Seul .env.example doit être versionné. +.env +.env.* +!.env.example + +# Dumps et sauvegardes de base +*.sql.gz +*.dump +database/backup/ +database/*_backup*.sql + +# Journaux et fichiers temporaires +*.log +logs/ +tmp/ + +# Dépendances éventuelles +/vendor/ +/node_modules/ + +# Artefacts d'éditeur et de système +.DS_Store +Thumbs.db +.idea/ +.vscode/ +*.swp +*~ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ca893a5 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,188 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Contexte + +PTA (Planning de Travail Annualisé) — application PHP 8.3 multipage (MVC maison) pour la +planification annualisée des agents d'accueils de loisirs périscolaires/extrascolaires. + +**Aucune dépendance** : pas de Composer, pas de npm, pas de framework, pas de bundler, pas de +suite de tests. Uniquement PHP + PDO/MySQL + JavaScript vanilla. Ne pas introduire de dépendance +sans raison forte : le PDF (`app/Services/SimplePdf.php`) et l'autoloader (`app/autoload.php`) sont +volontairement réécrits à la main. + +**Tout est en français** : identifiants SQL (`creneau_horaire`, `id_agent`, `statut`), messages +utilisateur, commentaires. Conserver cette langue pour tout nouveau code. + +## Commandes + +```bash +# Démarrage complet (nginx :8080, phpMyAdmin :8081, MariaDB, php-fpm) +cp .env.example .env # puis renseigner DB_PASSWORD / DB_ROOT_PASSWORD +docker compose up -d --build + +# IMPORTANT : le code source est COPIÉ dans l'image au build (Dockerfile), il n'est pas monté. +# Toute modification PHP/JS/CSS exige un rebuild des DEUX services : +# (nginx sert lui-même les assets statiques depuis sa propre copie de /var/www/html) +docker compose up -d --build php nginx + +docker compose logs -f php nginx db +docker compose ps +docker compose down # arrêt en conservant les données +docker compose down -v # DÉTRUIT le volume MariaDB + +# Accès SQL direct +docker compose exec db mariadb -upta -p pta + +# Vérification syntaxique (il n'existe ni linter ni tests automatisés) +find . -name '*.php' -not -path './.git/*' -exec php -l {} \; +``` + +Le schéma `database/pta_prod_schema_mariadb.sql` n'est joué **qu'à la création initiale du volume** +MariaDB (`docker-entrypoint-initdb.d`). Sur une base existante, toute évolution de schéma doit être +appliquée manuellement en SQL — il n'y a pas d'outil de migration. + +## Architecture + +### Deux types de points d'entrée, tous à la racine ou dans `api/` + +Il n'y a pas de routeur. Chaque URL correspond à un fichier PHP réel de 5 à 10 lignes. + +**Pages HTML** — `planning.php`, `agents.php`, `annual.php`, `pta.php`, `teams.php`, `coverage.php`, +`structures.php`, `pending.php`, `vacations.php`, `administration.php` : + +```php +require __DIR__ . '/page_bootstrap.php'; +runPage('planning'); // -> PageController::planning() +``` + +`page_bootstrap.php` attrape les `Throwable` et affiche une page d'erreur HTML. + +**Endpoints JSON/PDF** — `api/*.php` plus `save_planning.php`, `validate_planning.php`, +`reopen_planning.php` (ces trois-là sont restés à la racine pour raisons historiques) : + +```php +JsonResponse::bootstrap(); // handlers d'erreur -> JSON, jamais du HTML +$pdo = require dirname(__DIR__) . '/app/bootstrap.php'; +(new PlanningController($pdo))->show(); +``` + +`JsonResponse::bootstrap()` convertit warnings et exceptions en réponses JSON 500 — indispensable, +car le JS refuse toute réponse non-JSON. **Les endpoints PDF ne l'appellent pas** (ils émettent des +octets binaires via `PdfResponse::inline()`). + +Pour ajouter un endpoint : créer le fichier mince dans `api/`, ajouter la méthode au contrôleur. +Pour ajouter une page : fichier racine + méthode dans `PageController` + vue `app/Views/pages/` + +partiel `app/Views/partials/` + module JS + **inscription de la clé dans `Access::allowedPages()` +et dans le tableau `$navigation` de `app/Views/layouts/header.php`**. + +### Chaîne d'un appel + +``` +api/x.php → Controller (Access + Csrf + Request) → Model (SQL PDO) → JsonResponse::send() +page.php → PageController → Models → Views/pages/x.php + ├── layouts/header.php (nav filtrée par rôle) + ├── partials/x.php (markup) + └── layouts/footer.php (injecte window.PTA_CONFIG + scripts) +``` + +`app/bootstrap.php` démarre la session, ouvre le PDO (`db.php` ← `config.php` ← variables +d'environnement `PTA_DB_*`) et appelle `SchemaModel::assertCurrent()` **à chaque requête**. Ajouter +une table ou une colonne obligatoire impose de mettre à jour les listes de `SchemaModel`, sinon +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). + +`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. + +**`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. + +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 +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`. + +### Contraintes de sécurité à respecter + +- **CSP à nonce** (`app/Core/Security.php`) : `script-src 'self' 'nonce-…'`, sans `unsafe-inline`. + Tout ` - + - + - + diff --git a/app/Views/layouts/header.php b/app/Views/layouts/header.php index deb7fae..259fb64 100644 --- a/app/Views/layouts/header.php +++ b/app/Views/layouts/header.php @@ -3,6 +3,7 @@ /** @var string $activePage */ /** @var array $allowedPages */ /** @var array $accessProfile */ +/** @var string $csrfToken */ $navigation = [ 'planning' => ['planning.php', 'Saisie'], @@ -23,7 +24,7 @@ $navigation = [ <?= htmlspecialchars($pageTitle) ?> — PTA - +
@@ -44,7 +45,7 @@ $navigation = [ - Changer de rôle + Changer de rôle diff --git a/app/Views/pages/role.php b/app/Views/pages/role.php index 577e45a..7ab631c 100644 --- a/app/Views/pages/role.php +++ b/app/Views/pages/role.php @@ -4,6 +4,7 @@ /** @var array $structures */ /** @var string $error */ /** @var array|null $currentProfile */ +/** @var string $cspNonce */ ?> @@ -11,7 +12,7 @@ Choisir votre accès — PTA - +
@@ -97,7 +98,7 @@ Version de démonstration : ce sélecteur organise les droits dans l’application, mais ne remplace pas une authentification nominative. En production, le rôle devra être associé au compte connecté.
-