Aller au contenu

Le back-office dans Forge (forge-mvc-admin)

Ce document explique ce que fait l'opt-in forge-mvc-admin, ce qu'il expose, et comment on s'en sert.

forge-mvc-admin fournit un back-office applicatif : on déclare des ressources administrables, on les enregistre, et l'opt-in branche un tableau de bord avec liste, fiche et CRUD, sécurisé par défaut (auth + CSRF), RBAC optionnel.

Le cœur ne fournit pas de back-office : ce paquet en est un châssis explicite, piloté par un registre de ressources.

1. Rôle du module

Administrer ses données demande des écrans répétitifs : lister, voir, créer, modifier, supprimer.

L'opt-in les génère à partir d'une déclaration : un AdminResource décrit quelle entité administrer et avec quels champs ; le AdminRegistry collecte ces ressources ; register_admin_routes branche le back-office.

Le câblage reste explicite : on enregistre les ressources et on branche les routes soi-même (couche optins/), sans découverte magique.

2. Installation

Prérequis : activez le venv du projet

Quelle que soit la source, installez dans le venv du projet :

source .venv/bin/activate

Lancé hors d'un venv, pip vise le Python système (Debian 12+, Ubuntu 23.04+),
protégé par PEP 668. Il refuse alors d'installer, pour ne pas écraser les paquets
gérés par apt, et affiche externally-managed-environment.
Le venv de projet créé par forge new n'a pas ce verrou.

Installer le paquet

A. Depuis PyPI (stable)

La dernière version publiée :

pip install --pre forge-mvc-admin

B. Depuis Git (avant-garde)

Cœur puis opt-in depuis git, dans le venv du projet (l'opt-in trouve le cœur git déjà en place, sans version publiée sur PyPI) :

pip install "git+https://github.com/caucrogeGit/Forge.git@main"
pip install "git+https://github.com/caucrogeGit/Forge.git@main#subdirectory=packages/forge-mvc-admin"
3. Mise en service

Installer le paquet ne suffit pas à le rendre opérationnel.
Voici les gestes propres à forge-mvc-admin, dans l'ordre.

Ils déclinent la procédure canonique, Rendre un opt-in opérationnel : les cinq
points
.

1. L'épingler

forge-mvc-admin==<version de forge-mvc>

Dans requirements.txt, à la même version ou au même commit que forge-mvc.
Sans cette ligne, l'opt-in n'existe que sur votre machine.

2. L'inscrire

forge opt-in:enable admin --apply

L'opt-in est inscrit dans optins/registry.py (ADR-061), ce qui le rend visible du
projet.
--apply est obligatoire : sans lui, la commande simule et n'écrit rien.

3. Poser ce dont il a besoin

Cet opt-in n'apporte aucune table, mais il a tout de même une initialisation :

forge admin:init

Elle génère mvc/admin/, où l'application déclare ses ressources administrables.
Ne pas avoir de tables ne veut pas dire n'avoir rien à faire.

4. Le brancher là où il agit

Il se branche dans app.py, là où l'application compose ses middlewares et ses
fournisseurs de contexte. Ce câblage vous appartient : Forge ne l'écrit jamais à
votre place (principe 9).

5. Le prouver

make check
forge doctor

Puis un premier usage réel.
Un opt-in installé, inscrit et provisionné qu'aucun code n'appelle n'est pas
opérationnel : il est seulement présent.

4. Désinstallation
forge opt-in:disable admin
pip uninstall forge-mvc-admin

opt-in:disable est l'inverse d'enable : il dé-inscrit du registre, sans toucher au paquet.
forge opt-in:remove admin affiche la commande pip uninstall sans l'exécuter.

5. Commandes

forge-mvc-admin ajoute ces commandes :

Commande Rôle Exemple
admin:init Prépare la structure mvc/admin/ (write-if-new). forge admin:init
admin:doctor Vérifie la cohérence des ressources avec les contrats d'entité (lecture seule). forge admin:doctor
6. Vue d'ensemble rapide
Élément Valeur
Paquet forge-mvc-admin
Module forge_mvc_admin
Catégorie Exploitation et outillage (ADR-055)
Couche opt-in (brique optionnelle)
Dépend de forge-mvc ; RBAC optionnel (forge-mvc-rbac)
API publique AdminResource, AdminRegistry, registry, AdminController, register_admin_routes
Sécurité auth + CSRF par défaut, permission RBAC optionnelle
Templates embarqués (templates/admin/…, ADR-046)
Commandes admin:init, admin:doctor
Exceptions AdminError, AdminResourceError, AdminRegistryError
Installation pip install --pre forge-mvc-admin
7. Schémas UML

Les deux schémas suivants montrent deux vues complémentaires de l'opt-in.

Le diagramme de classe montre la ressource, le registre et le branchement.

Le diagramme de séquence montre l'affichage d'une liste administrée.

5.1 Diagramme de classe

Le diagramme de classe montre que des AdminResource sont enregistrées dans un AdminRegistry, que register_admin_routes lit pour brancher le AdminController.

classDiagram
    direction LR

    class AdminResource {
        <<dataclass>>
        +str entity
        +str slug
        +str label
        +tuple list_fields
        +tuple form_fields
        +str table
        +str pk
    }

    class AdminRegistry {
        +register(resource) AdminResource
        +get(slug) AdminResource
        +all() tuple
    }

    class AdminController {
        +list / detail / create / edit / delete
    }

    class http {
        <<module>>
        +register_admin_routes(router, registry, permission)
    }

    AdminRegistry --> AdminResource : contient 0..*
    http --> AdminRegistry : lit
    http --> AdminController : branche
    AdminController --> AdminResource : pilote les écrans

À retenir :

  • un AdminResource décrit une entité administrable (champs liste/formulaire) ;
  • le AdminRegistry collecte les ressources déclarées ;
  • register_admin_routes branche le contrôleur sur le routeur ;
  • le contrôleur produit les écrans CRUD à partir des ressources.

5.2 Diagramme de séquence

Le diagramme de séquence montre l'affichage d'une liste, sécurisé.

sequenceDiagram
    actor Admin
    participant Routes as register_admin_routes
    participant Ctrl as AdminController
    participant Reg as AdminRegistry
    participant DB as Base

    Admin->>Routes: GET /admin/<slug>
    Routes->>Routes: vérifie auth (+ CSRF, + permission)
    Routes->>Ctrl: list(slug)
    Ctrl->>Reg: get(slug) -> AdminResource
    Ctrl->>DB: lit les lignes (list_fields, pagination)
    Ctrl-->>Admin: liste rendue (template embarqué)

À retenir :

  • l'accès est protégé avant tout traitement (auth + CSRF) ;
  • une permission RBAC peut être exigée (permission=) ;
  • le contrôleur s'appuie sur la ressource pour savoir quoi afficher ;
  • les templates du back-office sont embarqués (ADR-046).
8. API publique
Élément Signature Rôle
AdminResource dataclass entity, slug, label, plural_label, list_fields, form_fields, table, order_by, pk
AdminRegistry.register register(resource) -> AdminResource enregistre une ressource
AdminRegistry.get / .all lecture une ressource / toutes
registry instance globale registre par défaut
register_admin_routes register_admin_routes(router, *, registry=None, permission=None) -> None branche le back-office
AdminController classe contrôleur des écrans CRUD
AdminError, AdminResourceError, AdminRegistryError exceptions erreurs
9. Contextes d'utilisation
Besoin Élément
Préparer le back-office forge admin:init
Déclarer une entité administrable AdminResource(...) + registry.register(...)
Brancher les écrans register_admin_routes(router)
Exiger une permission register_admin_routes(router, permission="admin.access")
Vérifier la cohérence forge admin:doctor
10. Exemples d'utilisation

8.1 Déclarer une ressource et brancher le back-office

# optins/admin/routes.py (couche optins du projet)
from forge_mvc_admin import AdminResource, registry, register_admin_routes

registry.register(AdminResource(
    entity="Article",
    slug="articles",
    label="Article",
    plural_label="Articles",
    list_fields=("title", "status"),
    form_fields=("title", "body", "status"),
    table="article",

))


def register(router) -> None:
    register_admin_routes(router, permission="admin.access")

forge opt-in:enable admin --apply crée la couche ; le branchement reste explicite.

Aide-mémoire

Déclarer, enregistrer, brancher :

  • AdminResource décrit l'entité ;
  • registry.register la collecte ;
  • register_admin_routes branche les écrans sécurisés.
11. Sécurité, templates et cohérence

Les routes du back-office exigent une session authentifiée et protègent les écritures par CSRF ; une permission RBAC peut être requise via permission=.

Les templates du back-office sont embarqués dans le paquet et enregistrés auprès du cœur (ADR-046) : render("admin/…") les résout sans copie dans le projet.

Sécurisé par défaut

Le back-office n'est pas public : auth obligatoire, CSRF sur les écritures.

Combinez avec forge-mvc-rbac pour restreindre l'accès par permission.

Cohérence avec les entités

forge admin:doctor vérifie (en lecture seule) que les ressources déclarées correspondent aux contrats d'entité, pour éviter une ressource qui pointe un champ inexistant.

Indépendance du cœur

Le cœur de Forge ne dépend pas de forge-mvc-admin : la dépendance va de l'opt-in vers le cœur.

Construction du SQL (query.py)

Le module query.py construit le SQL des ressources du back-office à partir d'un AdminResource, en ne laissant entrer que des identifiants déclarés et revalidés (anti-injection).
Il expose les constructeurs build_list_sql, build_count_sql, build_get_sql, build_insert_sql, build_update_sql, build_delete_sql, et leurs exécuteurs list_rows, count_rows, get_row, insert_row, update_row, delete_row.

12. Actions groupées

Le back-office ne savait supprimer qu'une ligne à la fois (ADMIN-BULK-ACTIONS-001) : nettoyer deux cents inscriptions de test demandait deux cents allers-retours, et deux cents confirmations.

AdminResource(
    entity="Article", slug="articles", label="Article", plural_label="Articles",
    list_fields=("id", "titre", "statut"), form_fields=("titre",),
    table="articles",
    bulk_delete=True,
    status_field="statut",
    bulk_transitions=(("brouillon", "publie"), ("publie", "archive")),
)

Une colonne de cases apparaît alors en liste, et une barre d'actions sous le tableau.

Rien n'est offert par défaut

bulk_delete vaut False, et bulk_transitions est vide.

Une case à cocher offerte sans qu'on l'ait demandée invite à un geste irréversible sur une table que l'exploitant croyait en lecture.

Toute action passe par une confirmation

Comme la suppression unitaire.

Une action qui porte sur cinquante lignes n'a pas moins besoin de sa page de confirmation, et celle ci montre les lignes concernées ainsi que celles qui ont disparu entre l'affichage et la validation.

Les identifiants partent en paramètres liés

Les concaténer serait une injection, et le fait qu'ils viennent de cases cochées n'y change rien : une case cochée est une donnée de requête comme une autre.

Une sélection vide est refusée, une suppression groupée sans sélection effaçant la table entière si la clause était omise. Le plafond vaut 200 lignes, une sélection de cette taille venant plus souvent d'un « tout cocher » que d'une intention.

Transitions groupées, et le workflow

Une transition écrit la colonne status_field. La clause porte aussi sur le statut de départ.

UPDATE articles SET statut = ? WHERE id IN (?, ?) AND statut = ?

C'est ce qui rend l'opération sûre

Une ligne dont le statut a changé entre l'affichage et la validation n'est pas touchée.

Une mise à jour sur la seule clé primaire écraserait un état que quelqu'un d'autre vient de poser. L'écart entre demandé et effectué est dit dans le message de retour.

La transition groupée exige forge-mvc-workflow installé

Sans lui, elle est refusée, et ce refus diffère délibérément de celui de la suppression.

Appliquer un changement de statut à N lignes sans pouvoir vérifier que la transition est déclarée écrirait un état que le workflow de l'application interdit peut-être, sur cinquante lignes d'un coup. Une fonctionnalité absente vaut mieux qu'une fonctionnalité qui contourne la règle.

Les transitions sont déclarées, jamais déduites

forge-mvc-admin ne lit pas le workflow de l'application.

Deviner qu'un statut brouillon mène à publie appliquerait à N lignes une transition que personne n'a écrite. La déclaration ne demande d'ailleurs pas forge-mvc-workflow : elle nomme des chaînes, et c'est l'exécution qui l'exige.

Les conditions de transition du workflow sont consultées, avec un contexte portant bulk: True : une règle métier peut refuser en masse ce qu'elle permet à l'unité. Le motif du refus remonte à l'écran.

Voir aussi

Filtrer, chercher et trier une liste

La liste affichait la table entière, page par page, sans autre choix que de tourner les pages.
Passé quelques centaines de lignes, retrouver un enregistrement devenait impraticable, et le back-office avec lui (ADMIN-LIST-FILTERS-001).

Une ressource déclare ce qu'elle ouvre.

AdminResource(
    entity="Article", slug="articles", label="Article", plural_label="Articles",
    list_fields=("titre", "statut"), form_fields=("titre", "statut"),
    table="articles",
    filter_fields=("statut",),
    search_fields=("titre", "resume"),
)
Paramètre d'URL Effet
?statut=publie filtre d'égalité, si statut est dans filter_fields
?q=terme recherche LIKE sur tous les search_fields
?tri=titre tri sur une colonne de list_fields
?sens=desc inverse le tri

Rien n'est ouvert par défaut

filter_fields et search_fields sont vides tant qu'ils ne sont pas déclarés, et ils ne sont jamais déduits de list_fields.

Un filtre porte sur une colonne nommée dans l'URL.
Accepter n'importe laquelle exposerait des colonnes que la liste n'affiche pas, un mot de passe haché par exemple, et une recherche sur une telle colonne permettrait d'en deviner le contenu caractère par caractère.

Les noms sont vérifiés, les valeurs sont liées

Un nom de colonne venu de l'URL est comparé à la liste déclarée, jamais à la seule forme d'un identifiant SQL.
Une colonne inconnue rend 400, la demande étant fautive et non le serveur.

Les valeurs partent en paramètres liés, quelles qu'elles soient.
Le tri, lui, ne prend aucune chaîne de l'URL dans sa clause : le sens est un booléen.

La recherche ne prend pas les jokers au mot

Chercher 100% ne ramène pas tout ce qui commence par 100.

Les métacaractères % et _ d'une saisie sont neutralisés, avec un ESCAPE '!' déclaré.
Le backslash serait piégeux, son sens dépendant d'un réglage de serveur sur MariaDB.

Le tri secondaire n'est pas décoratif

La liste trie aussi par clé primaire, sans quoi deux lignes de même valeur sortiraient dans un ordre que rien ne garantit, et une page paginée en montrerait une deux fois pendant qu'une autre disparaîtrait.

Il est omis quand le tri porte déjà sur la clé primaire : SQL Server refuse une colonne répétée dans un ORDER BY, là où les trois autres l'acceptent.

Les critères suivent la pagination.
Sans cela, tourner une page les perdrait et la liste repartirait entière.

Sessions actives

forge deploy:init demande de planifier forge sessions:gc, et rien ne disait si ce minuteur tournait.

forge-mvc-sessions-db compte les sessions depuis SESSIONS-METRICS-001, réparties par nature, et sait dire si la purge suit.
Personne ne regardait ce nombre : il fallait ouvrir un client SQL (ADMIN-SESSIONS-VIEW-001).

Le panneau vit sur /admin/_sessions, et le tableau de bord y mène.

Ce qu'il montre Ce que cela dit
Actives combien de personnes sont connectées ou ont un panier en cours
Expirées, en attente de purge ce que sessions:gc n'a pas encore balayé
Lignes en table ce que la table coûte à chaque balayage
Répartition par nature anonyme, authentifié, souvenir

La question à laquelle cette page répond

Une table qui grossit pendant que le nombre d'actives stagne signale un minuteur sessions:gc arrêté.

Au delà de la moitié de lignes expirées, la page le dit et nomme la commande.
C'est le seuil que SessionMetrics.purge_backlog_ratio documente : la table coûte alors deux fois ce qu'elle devrait.

Aucun identifiant de session n'est affiché

Il n'y en a d'ailleurs pas à afficher, la mesure rendant des totaux, et c'est heureux.

Un identifiant de session lu sur un écran, une capture ou une épaule est une session volée.
Le contrat de la donnée l'interdit autant que le gabarit : SessionsPanel ne porte aucun champ d'identifiant, si bien qu'un gabarit modifié ne peut rien en faire fuir.

Le couplage est souple

forge-mvc-admin ne déclare pas forge-mvc-sessions-db en dépendance, comme il ne déclare ni forge-mvc-rbac ni forge-mvc-workflow.

Un projet dont les sessions vivent en mémoire n'a pas à l'installer pour ouvrir son back-office : le panneau dit alors pourquoi il est vide.
Une table absente ne fait pas tomber la page non plus : une page d'administration qui tombe parce qu'un panneau ne répond pas retire l'accès à tout le reste.

Le panneau ne révoque rien

Fermer une session depuis cet écran demanderait de désigner laquelle, donc de l'identifier, donc de l'exposer.

Fermer toutes celles d'un utilisateur est possible sans cela, delete_for_user existe, mais c'est un geste destructeur qui mérite sa page de confirmation et sa décision propre.