GSB AppliFrais

Application web de gestion des frais en CakePHP, réalisée en BTS SIO. Les utilisateurs déclarent leurs frais, forfaitisés ou hors forfait, sur des fiches qu'un comptable contrôle puis valide.

Présentation

GSB est l'atelier de professionnalisation que j'ai réalisé en BTS SIO autour de l'entreprise fictive Galaxy Swiss Bourdin : une application web de gestion des frais. Chaque utilisateur ouvre des fiches, y déclare ses dépenses, et un comptable les contrôle. J'ai construit cette application, baptisée AppliFrais dans sa barre de navigation, entre novembre 2023 et avril 2024, avec CakePHP 4 et MySQL. L'authentification et les rôles reposent sur le plugin CakeDC/Users.

Une fiche, deux familles de frais

Le modèle tourne autour de la table sheets. Une fiche appartient à un utilisateur, porte un état et un booléen sheetvalidated. Les forfaits (packages) ont un prix unitaire fixé par l'administrateur, comme l'hôtel ou le taxi : sur la fiche, l'utilisateur ne saisit qu'une quantité, stockée dans la table de jointure sheets_packages. Les frais hors forfait (outpackages) ont un titre, un montant et une description libres, et sont reliés à la fiche par sheets_outpackages. Dans SheetsTable, les deux relations sont déclarées en belongsToMany.

Pour que le formulaire affiche toutes les lignes de forfait dès la création, j'ai choisi de préremplir la table de jointure : dans SheetsController::list(), une fois la fiche enregistrée, chaque forfait reçoit une ligne à quantité zéro.

$packages = $this->Sheets->Packages->find()->toArray();
 
foreach ($packages as $package) {
    $sheetPackage = $this->Sheets->SheetsPackages->newEntity([
        'sheet_id' => $sheet->id,
        'package_id' => $package->id,
        'quantity' => 0,
    ]);
 
    $this->Sheets->SheetsPackages->save($sheetPackage);
}

L'utilisateur modifie ensuite ses quantités dans clientview, qui les enregistre en un seul envoi, avec un updateAll par forfait sur la table de jointure. Les totaux (quantité fois prix pour les forfaits, somme des montants pour le hors forfait) sont calculés dans le template.

Fiche de test à l'état Créé, côté utilisateur : chaque forfait a sa ligne et son champ de quantité (les frais kilométriques sont restés à 0), un seul bouton Sauvegarder les enregistre, puis viennent les frais hors forfait et les totaux.

Trois rôles décrits dans un seul fichier

Les droits tiennent dans config/permissions.php, au format RBAC de CakeDC/Auth : les règles sont lues de haut en bas et la première qui correspond s'applique. Le rôle admin a accès à tout, dont les écrans CRUD générés avec Bake (fiches, états, deux types de frais), que j'ai regroupés avec la liste des utilisateurs dans un panneau d'administration. Le rôle user n'atteint que ses propres actions, notamment list, clientview, addoutpackage, clientdelete. Le rôle comptable a les siennes, notamment complist, compview, validate, unvalidate. Le menu de templates/layout/default.php n'affiche que les liens du rôle connecté.

Connecté avec le rôle comptable : le menu se réduit aux liens de ce rôle, les quantités s'affichent en simple texte et le bouton Validate Sheet envoie la fiche à l'action validate.

Une fiche suit cinq états : Créé, Fermé, Paiement, Remboursé, Fini. L'administrateur la fait avancer depuis l'écran d'édition. Tant qu'elle est à l'état Créé et non validée, l'utilisateur peut changer ses quantités, ajouter des frais hors forfait ou supprimer la fiche. Ensuite, elle s'affiche en lecture seule. La liste du comptable (complist) ne montre que les fiches des états Fermé à Remboursé (state_id de 2 à 4). Le comptable consulte le détail et les totaux, puis valide ou dévalide la fiche.

Les cinq états sur des fiches de test, dans la liste d'un utilisateur : seules les fiches Créé non validées s'ouvrent en modification (Edit), les autres s'ouvrent en lecture seule (Voir).

En avril 2024, j'ai commencé à rattacher un médecin à chaque fiche (table doctors, clé étrangère doctor_id). Cette partie est restée inachevée : editdoctor ne traite pas encore la saisie et listdoctor appelle un finder qui n'existe pas.

Une clé API pour lire sa propre fiche

En janvier 2024, j'ai ajouté une petite API dans ApiController. Depuis sa page de profil, un utilisateur génère une clé : generateApiToken() tire 32 octets avec random_bytes, les encode en hexadécimal et les enregistre dans la colonne api_token. getSheetInfo() reçoit ensuite la clé et l'identifiant de fiche, retrouve l'utilisateur par sa clé et renvoie la fiche en JSON. Cette action contourne la session (bypassAuth dans permissions.php), donc le contrôle d'accès se fait dans le code : si la fiche n'appartient pas au porteur de la clé, la réponse est un refus. C'est le seul endroit où l'accès à une fiche précise est vérifié contre son propriétaire.

Ce que je corrigerais aujourd'hui

La règle RBAC autorise le rôle user sur clientview, mais rien ne vérifie que la fiche demandée est la sienne : changer l'identifiant dans l'URL suffit pour lire celle d'un autre. Le verrouillage d'une fiche validée n'existe que dans le template, pas dans le contrôleur qui reçoit le POST. À l'inverse, le lien qui retire un frais hors forfait s'affiche, mais vise l'action delete, que la règle du rôle user refuse. À la création, user_id vient d'un champ caché du formulaire alors qu'il devrait être pris dans la session côté serveur. Côté API, la clé passe dans l'URL et reste en clair en base, et la génération compare un mot de passe API à une valeur écrite dans le code au lieu de le demander. Aujourd'hui, j'ajouterais une règle de propriété (le fichier de permissions accepte un callback allowed prévu pour ça), je hacherais la clé et je la ferais passer dans un en-tête.

Côté maintenance, j'ai traduit les écrans du plugin et ajouté le lien de génération de clé en modifiant directement les fichiers de vendor/cakedc/users, versionnés avec le projet. Une mise à jour des dépendances aurait écrasé ces changements. Il fallait surcharger les templates du plugin dans templates/plugin/ et passer par des fichiers de traduction. Les tests générés par Bake sont restés marqués markTestIncomplete. Enfin, un forfait créé après coup n'apparaît pas sur les fiches existantes, puisque leurs lignes ne sont créées qu'une fois : le dump gsb-cake.sql le montre pour le quatrième forfait. Compléter les lignes manquantes à l'affichage aurait évité ce piège.