Espaces utilisateur et admin¶
Référence des pages servies après connexion : l'accueil de l'espace utilisateur (/u), le dashboard d'administration (/admin) et la page Réglages admin (/admin/reglages). Pour le déroulé de connexion qui y mène, voir le how-to Se connecter à Kirexo ; pour la séparation en deux firewalls, voir Architecture des firewalls.
Vue d'ensemble¶
| Page | Route | Chemin | Méthode | Controller | Protection |
|---|---|---|---|---|---|
| Accueil utilisateur | app_user_home |
/u |
GET |
src/Controller/AppUserHomeController.php |
Voter VIEW_OWN + firewall user |
| Dashboard admin | app_admin_dashboard |
/admin |
GET |
src/Controller/AppAdminDashboardController.php |
ROLE_ADMIN + firewall admin |
| Réglages admin | app_admin_settings |
/admin/reglages |
GET, POST |
src/Controller/AppAdminSettingsController.php |
ROLE_ADMIN + firewall admin |
| Détail article admin | app_admin_article_show |
/admin/articles/{id} |
GET |
src/Controller/AppAdminArticleShowController.php |
ROLE_ADMIN + Voter ARTICLE_VIEW + firewall admin |
Le dashboard admin est le poste de modération : la liste des articles publiés y est intégrée, il n'existe pas de page liste séparée.
L'accueil utilisateur et le dashboard admin sont chacun la cible de redirection après connexion (default_target_path) de leur firewall respectif. Voir la Référence security.yaml.
Le dashboard admin est alimenté par des services applicatifs synchrones injectés dans le controller (chaîne controller → service → repository, sans bus) ; l'accueil utilisateur, plus fin, lit directement ses carnets via NotebookRepositoryInterface. Voir Architecture.
Accueil de l'espace utilisateur (app_user_home, /u)¶
Page d'accueil de l'espace connecté, servie par src/Controller/AppUserHomeController.php (controller invokable) et rendue avec templates/user/home.html.twig. Le template étend base.html.twig (et non base_user.html.twig) : il porte sa propre barre du haut, sans l'en-tête partagé. Elle est orientée carnets : barre du haut, tête, onglets Actifs/Archivés et états vides. Les sections carnet, la frise et la recherche sont ajoutées par les lots suivants.
Accès¶
- Réservé aux comptes authentifiés sur le firewall
user. La règleaccess_control{ path: ^/u, roles: ROLE_USER }exigeROLE_USER. - Le compte connecté est injecté dans le controller via
#[CurrentUser]. - L'accès est en outre soumis au Voter
UserDataVoteravec l'attributVIEW_OWN, déclaré par#[IsGranted(UserDataVoter::VIEW_OWN, subject: 'user')]sur le controller. Le sujet du vote est le compte courant lui-même : un utilisateur ne peut consulter que ses propres données. Voir VoterUserDataVoter.
Données passées au template¶
Le controller ne calcule rien : il transmet le compte courant et ses carnets, lus via NotebookRepositoryInterface (src/Repository/NotebookRepositoryInterface.php).
| Variable Twig | Source |
|---|---|
user |
#[CurrentUser] User |
activeNotebooks |
NotebookRepositoryInterface::findActiveByOwner($user) — carnets non archivés (archivedAt IS NULL) |
archivedNotebooks |
NotebookRepositoryInterface::findArchivedByOwner($user) — carnets archivés (archivedAt IS NOT NULL) |
Voir Carnets : modèle de données et migration mono → multi-carnets.
Barre du haut¶
Barre sticky (classe ud-topbar) portant, de gauche à droite :
- le logo Kirexo, lien vers
app_user_home; - un sélecteur de thème segmenté clair / sombre / système (conteneur
.theme-toggle, boutons[data-theme-mode]pilotés partheme.js) ; - l'e-mail du compte courant (
user.email, classeud-user), masqué sous 680 px ; - un bouton « Espace admin » (POST vers
app_switch_to_admin, jeton CSRFswitch_space), rendu uniquement pour les comptes mixtes sous la condition Twigis_granted('CAN_SWITCH_SPACE'). Voir Basculer entre les espaces ; - un bouton « Se déconnecter » (POST vers
app_logout).
Tête et onglets¶
- Une tête : eyebrow « Mon espace », titre « Bonjour {prénom}. » (
user.firstName) et un sous-titre. - Un bouton « Nouveau carnet » (fantôme), affiché seulement s'il existe au moins un carnet actif. Il est inerte à ce stade (la modale de création est câblée par un lot ultérieur).
- Une barre d'onglets Actifs / Archivés avec compteurs (
activeNotebooks|length,archivedNotebooks|length) et une légende à deux entrées : « Publié » et « Brouillon ». La bascule d'onglet est assurée côté client par le controller Stimulustabs(assets/controllers/tabs_controller.js), qui synchronisearia-selectedsur les onglets et l'attributhiddensur les panneauxrole="tabpanel", sans requête serveur (onglet Actifs sélectionné par défaut). Le parcours utilisateur correspondant est décrit dans le how-to Créer, renommer, archiver et supprimer un carnet.
États et contenu¶
- Aucun carnet actif : un état vide (
ud-blank) affiche « Aucun carnet pour l'instant. » et un CTA primaire « Créer mon premier carnet » (également inerte à ce stade). Aucun bouton « Importer ». - Au moins un carnet actif : une liste minimale (
ud-list) rend chaque carnet actif (pastille +notebook.name+notebook.taglinesi présent), avec un bouton « Écrire » par carnet. Ce bouton est un formulaire POST vers la routeapp_article_newparamétrée par le carnet —/u/carnets/{id}/articles/nouveau, où{id}est l'UUID v7 du carnet dont le bouton est cliqué (contrainteRequirement::UUID_V7) —, jeton CSRFnew_articleconservé. L'article ainsi créé est rattaché au carnet source (celui du bouton cliqué), et non plus systématiquement au premier carnet de l'utilisateur : ce comportement transitoire n'existe plus. L'accès est protégé au niveau du controllersrc/Controller/AppArticleNewController.php: le carnet est résolu depuis{id}par l'EntityValueResolver(404 si aucun carnet ne porte cet id), puis#[IsGranted(NotebookVoter::EDIT, subject: 'notebook')]vérifie l'appartenance (403 si le carnet n'appartient pas au compte courant). Ce bloc de liste est un stub remplacé par la section carnet complète dans un lot ultérieur. - Panneau Archivés (
#ud-view-archived, masqué par défaut) : s'il n'existe aucun carnet archivé, un état vide (ud-blank) affiche « Aucune archive. » ; sinon, une note d'en-tête (ud-archived-note) rappelle que ces carnets sont hors ligne, puis la liste des carnets archivés — pliés par défaut, en lecture seule, avec les actions Restaurer (POSTapp_notebook_restore) et Supprimer (POSTapp_notebook_delete). L'archivage d'un carnet actif se fait via le menu ⋯ (POSTapp_notebook_archive).
Navigation entre home application, espace utilisateur et éditeur¶
Trois accès de navigation relient la home application (/), la home utilisateur (/u) et l'éditeur d'article (/u/articles/<uid>/edition). Ils sont indépendants de la bascule entre espaces utilisateur et admin, qui relève du header partagé — voir Header et menu utilisateur.
Accès à /u depuis la home application¶
La home application (app_home, GET /, src/Controller/AppHomeController.php, template templates/home/index.html.twig) affiche un CTA « Aller à mon espace » menant à app_user_home.
- Rendu sous le titre de la plateforme (
.hm-masthead), dans un bloc.hm-sub, sous forme de bouton primaire (btn btn-primary btn-sm,data-testid="home-go-user"). - Conditionné à
app.user: il apparaît dès qu'un compte est connecté, quel que soit son rôle — y compris via un cookieremember_me. La condition Twig estapp.user(variable globale), pasis_granted('IS_AUTHENTICATED_FULLY'): la home/ureste par ailleurs gardée par son propre VoterVIEW_OWN. - Absent pour un visiteur anonyme.
Le controller ne porte aucune logique dédiée : la condition est un simple test Twig, la cible est résolue par path('app_user_home') (jamais d'URL en dur). Dans l'état vierge de la home (aucun article publié), le CTA « Écrire le premier article » pointe déjà vers app_user_home et remplit le même besoin.
Sortie explicite de l'éditeur vers /u¶
L'éditeur d'article (/u/articles/<uid>/edition, template templates/user/article/edit.html.twig) porte en haut à gauche de sa barre (.editor-top > .left) un lien de retour vers la home utilisateur :
- lien
<a>(navigationGETsimple) versapp_user_home, libellé « Mon espace » précédé d'une icône flèche-retour (aria-hidden), classeeditor-backsur un bouton discretbtn btn-ghost btn-sm; data-testid="editor-exit"etaria-label="Revenir à mon espace"pour le ciblage et les lecteurs d'écran.
C'est un pattern « retour vers le hub » (et non une croix « Fermer ») : l'autosave de l'éditeur (controller autosave) sécurise déjà le contenu, donc le retour ne déclenche aucune confirmation destructrice. Le lien est placé côté gauche, zone de navigation/contexte ; les actions primaires (Publier, Aperçu, menu ⋯) restent à droite dans .editor-tools.
Dashboard admin (/admin)¶
Page d'accueil de l'espace d'administration, servie par src/Controller/AppAdminDashboardController.php et rendue avec templates/admin/dashboard.html.twig (layout base_admin.html.twig). Classes préfixées bo-.
Accès¶
- Réservé aux comptes authentifiés sur le firewall
admin, porteurs deROLE_ADMIN— appliqué à la fois par la règleaccess_control{ path: ^/admin, roles: ROLE_ADMIN }et par l'attribut#[IsGranted('ROLE_ADMIN')]sur le controller. - Pour un visiteur connecté à l'espace utilisateur,
/adminrenvoie un 404 (l'admin se masque), viaAdminAreaAccessSubscriber. Détails dans Masquer l'admin : 404 plutôt que 403.
Contenu affiché : cockpit de conformité¶
Le dashboard est le poste de modération centré conformité. Il porte le titre « Modération & conformité » et n'affiche que le contenu public (articles published) : aucun brouillon ni corbeille d'utilisateur. Le controller (src/Controller/AppAdminDashboardController.php) lit ses données directement via ArticleRepositoryInterface, sans service dédié :
ArticleRepositoryInterface::findPublished()— les articles publiés, plus récents d'abord (liste + total) ;ArticleRepositoryInterface::findPublishedAnalyzedIds()— les identifiants des articles publiés déjà analysés, normalisés en chaînes RFC 4122 pour un test d'appartenance en O(1) par ligne dans le template (pas de N+1).
Trois indicateurs (bo-stats) sont affichés en tête, dérivés de ces deux lectures :
| Indicateur | Source |
|---|---|
| Articles publiés | publishedCount = count(findPublished()) |
| Analysés | analyzedCount = count(findPublishedAnalyzedIds()) |
| À analyser | toAnalyzeCount = publishedCount - analyzedCount |
L'indicateur « À analyser » reçoit la classe d'accent bo-stat--accent tant que toAnalyzeCount > 0 : il est mis en avant tant qu'il reste des articles publiés non analysés.
Indicateurs cliquables = filtres de la liste (?filter=)¶
Les trois indicateurs sont des liens (bo-stat bo-stat--link) qui filtrent la liste des articles affichée en dessous. Le filtre est piloté par le paramètre d'URL ?filter= lu dans le controller ($request->query->getString('filter')) : le filtrage est côté serveur, donc l'état est partageable par URL, compatible avec le bouton Précédent du navigateur, et fonctionne sans JavaScript.
| Indicateur | Cible | filter |
activeFilter |
Effet |
|---|---|---|---|---|
| Articles publiés | path('app_admin_dashboard') |
absent | all |
Réinitialise — affiche tous les articles publiés |
| Analysés | path('app_admin_dashboard', { filter: 'analyzed' }) |
analyzed |
analyzed |
Ne garde que les articles déjà analysés |
| À analyser | path('app_admin_dashboard', { filter: 'to-analyze' }) |
to-analyze |
to-analyze |
Ne garde que les articles restant à analyser |
- Toute valeur de
filterabsente ou inconnue retombe surdefault→ liste complète (activeFilter = 'all') : aucune URL forgée ne casse la page. - Le filtrage se fait en mémoire dans le controller (
array_filtersurfindPublished()selon l'appartenance à l'ensemble des IDs analysés), pas par une requête SQL distincte. - La carte active est mise en évidence par la classe
is-activeet l'attributaria-current="true". L'accentbo-stat--accentsur « À analyser » (tant quetoAnalyzeCount > 0) reste indépendant du filtre actif. - Le titre de la liste s'adapte au filtre (variable
listTitles) : « Articles publiés » (all), « Articles analysés » (analyzed) ou « Articles à analyser » (to-analyze), suivi du compteurarticles|length. - Les états vides sont contextualisés : « Aucun article publié pour l'instant. » si aucun publié, « Aucun article analysé pour l'instant. » sous le filtre
analyzed, « Tous les articles publiés ont été analysés. » sous le filtreto-analyze.
Le parcours de filtrage côté utilisateur est décrit dans le how-to Consulter et analyser la conformité d'un article publié.
Sous les indicateurs, un tableau (bo-table bo-articles) liste directement les articles publiés (filtrés selon ?filter=), avec pour chacun :
| Colonne | Source |
|---|---|
| Titre | article.title — lien vers le détail (app_admin_article_show), ligne cliquable |
| Auteur | article.author.firstName |
| Analyse | badge « Analysé » (.bo-tag--ok) / « Non analysé » (.bo-tag--muted) selon l'appartenance de article.id.toRfc4122() à analyzedIds |
| Publié le | article.publishedAt, formatée d/m/Y ; — si absent |
Si aucun article n'est publié, le tableau est remplacé par « Aucun article publié pour l'instant. ».
Le corps du dashboard n'expose ni lien « Réglages » ni bouton de bascule d'espace : ces actions vivent dans le menu utilisateur de l'en-tête partagé (<twig:Header space="admin">). Voir Header et menu utilisateur.
Pas de « Mon espace » dans le header admin
Le lien « Mon espace » (action principale .header__primary vers app_user_home) n'apparaît pas dans le header de l'espace admin : il ferait doublon avec l'entrée « Espace user » de la bascule d'espace du menu utilisateur, qui est le chemin d'accès à l'espace utilisateur depuis l'admin. « Mon espace » n'est rendu que sur l'accueil public (space == 'public'). Voir Header et menu utilisateur et Basculer entre les espaces.
L'admin gère les comptes et la plateforme, jamais le contenu
Le dashboard affiche les articles publiés (indicateurs + liste) pour donner à voir l'activité de modération, mais l'espace d'administration n'expose ni rédaction ni publication d'articles — cela relève de l'espace utilisateur. La consultation reste cantonnée au contenu public : uniquement les articles published, jamais un brouillon ni une corbeille d'utilisateur. La frontière est portée par l'ArticleVoter (voir Détail article admin). Voir Architecture.
Réglages admin (/admin/reglages)¶
Page de configuration de l'instance, servie par src/Controller/AppAdminSettingsController.php et rendue avec templates/admin/settings.html.twig (layout base_admin.html.twig, classes bo-). Elle porte deux formulaires distincts sur la même route GET/POST (chacun avec son traitement Post/Redirect/Get) : la couleur d'accent de la plateforme et la clé API Claude.
Accès¶
- Réservé à
ROLE_ADMIN(#[IsGranted('ROLE_ADMIN')]), sur le firewalladmin. Aucun Voter dédié : la décision « accéder aux réglages » ne dépasse pas le rôle (pas de ressource à scoper). - Comme tout
/admin, masquée en 404 pour un visiteur connecté à l'espace utilisateur.
Retour vers la home admin¶
La page Réglages est une destination (pas une modale) : elle porte donc deux affordances de retour vers le dashboard, toutes deux vers app_admin_dashboard.
- Un fil d'Ariane (
<nav class="bo-breadcrumb" aria-label="Fil d'Ariane">) à 2 maillons : Espace admin › Réglages (maillon courant enaria-current="page"), le maillon « Espace admin » ramenant au dashboard. - Un bouton retour explicite en complément : un lien
btn btn-secondary btn-sm bo-back(data-testid="settings-back"), précédé d'une icône flèche-retour (aria-hidden), libellé « Retour à l'espace admin », pointant vers le dashboard.
Ce n'est jamais une popin : une page de réglages est une destination à part entière, pas une interruption superposée — d'où le retour par navigation de page. Le pourquoi de ce choix (page vs modale) est discuté dans Architecture des firewalls.
Couleur d'accent¶
Le formulaire App\Form\AccentSettingType (src/Form/AccentSettingType.php) présente les 6 valeurs de l'enum App\Entity\Enum\AccentColor (src/Entity/Enum/AccentColor.php) sous forme de pastilles radio :
Libellé (AccentColor::label()) |
Valeur backed (data-accent) |
|---|---|
| Encre | encre |
| Bordeaux | bordeaux |
| Forêt | foret |
| Prune | prune |
| Rouille | rouille |
| Graphite | graphite |
La valeur par défaut d'une instance neuve est AccentColor::default() → Encre.
Persistance et effet¶
- La valeur est persistée dans l'entité singleton
App\Entity\Setting(src/Entity/Setting.php) — une seule ligne en base, lue viaSettingRepositoryInterface::getOrCreate(). - À la soumission, le service
App\Service\Settings\AccentUpdater(src/Service/Settings/AccentUpdater.php) écrit la nouvelle valeur de façon synchrone (flush dans la requête), puis le controller redirige (motif Post/Redirect/Get) versapp_admin_settings. - L'accent est rendu côté serveur sur la balise
<html data-accent="…">de chaque page (fonction Twigplatform_accent()), donc le changement est visible immédiatement après la redirection, pour tous les visiteurs.
La procédure pas à pas est décrite dans le how-to Définir la couleur d'accent de la plateforme. La mécanique data-accent et la fonction platform_accent() sont détaillées dans Design system : tokens, thèmes et accent.
Clé API Claude¶
Le second formulaire (App\Form\ClaudeApiKeySettingType, src/Form/ClaudeApiKeySettingType.php) permet de renseigner la clé API Claude servant à l'analyse IA des articles.
- Write-only masqué : champ unique
apiKeyenPasswordType, non mappé,always_empty— la clé n'est jamais pré-remplie ni réaffichée. La saisie transite par le DTO immuableApp\Dto\ClaudeApiKeySettingDto(src/Dto/ClaudeApiKeySettingDto.php). - Chiffrement : à la soumission, le service
App\Service\Settings\ClaudeApiKeyUpdater(src/Service/Settings/ClaudeApiKeyUpdater.php) chiffre la clé viaApp\Service\Security\ClaudeApiKeyCipher(libsodiumsodium_crypto_secretbox) puis persiste le ciphertext dans le champencryptedClaudeApiKeydu singletonSetting. La clé en clair n'est jamais stockée. - Clé de chiffrement : injectée depuis le vault Symfony Secrets sous
CLAUDE_API_KEY_CIPHER_KEY(jamais en base, jamais en clair). VoirCLAUDE_API_KEY_CIPHER_KEY. - État :
Setting::hasClaudeApiKey()renvoietruedès qu'un ciphertext non vide est présent. Cet état conditionne l'affichage (« Une clé est configurée » vs « Aucune clé configurée ») et l'activation du bouton « Analyser la conformité » sur le détail d'article. - Après enregistrement, le flash « Clé API Claude enregistrée. » s'affiche et le controller redirige (Post/Redirect/Get).
La procédure complète (génération du secret de vault + saisie) est décrite dans Configurer la clé API Claude pour l'analyse IA.
Détail admin des articles publiés¶
Le dashboard intègre la liste des articles publiés (voir Contenu affiché : cockpit de conformité) ; l'espace admin expose en plus le détail d'un article, lui aussi restreint au contenu publié. L'administration ne voit donc que les articles published, sans aucune capacité de rédaction ni de publication (« Admin ≠ publication »).
Le statut d'analyse affiché dans la liste (badges « Analysé » / « Non analysé ») provient de ArticleRepositoryInterface::findPublishedAnalyzedIds() : le libellé est toujours écrit en toutes lettres (jamais couleur seule — RGAA), la puce colorée étant décorative. La ligne est cliquable à la souris (overlay CSS .bo-row-link::after), le lien du titre restant le seul élément focusable/activable au clavier — pas de role="link" ni de handler JS sur le <tr>.
Détail (/admin/articles/{id})¶
Servi par src/Controller/AppAdminArticleShowController.php, rendu avec templates/admin/article/show.html.twig.
- Réservé à
ROLE_ADMIN, plus une décision du Voter :denyAccessUnlessGranted(ArticleVoter::VIEW, $article). L'admin n'étant pas l'auteur, l'accès n'est accordé que si l'article est publié — un brouillon ou un article en corbeille d'un autre auteur renvoie 403. - Affiche le titre et le contenu publié figé en mode aperçu : le controller rend le Markdown source (révision courante
article.currentRevision, sinonarticle.content) via le service partagéApp\Service\Article\MarkdownRenderer(GFM assaini parHtmlSanitizer) et le passe au template enpreviewHtml, injecté sous|rawdans un conteneur.bo-prose. C'est le même rendu qu'à la publication, pas du texte brut. Sans bouton d'édition ni de publication. - Porte le panneau « Analyse de conformité » (
templates/admin/article/_analysis.html.twig) : un texte de portée toujours visible (risques juridiques FR, indicatif, pas un avis juridique) et un bouton « Analyser la conformité » — intitulé « Relancer l'analyse » si un rapport existe déjà. Le bouton est désactivé tant queSetting::hasClaudeApiKey()estfalse(avec un lien vers les Réglages) ; actif une fois la clé API configurée. Voir le how-to Consulter et analyser la conformité d'un article publié. - Fil d'Ariane à 2 maillons : Espace admin › titre (maillon courant en
aria-current="page"), le maillon « Espace admin » ramenant au dashboard — donc à la liste des publiés, celle-ci étant le dashboard. Le retour dans l'espace admin passe toujours par ce fil d'Ariane, jamais par une popin.
La frontière de sécurité est le Voter, pas le filtre de liste
La règle « l'admin ne voit que published » est portée par l'ArticleVoter (attribut ARTICLE_VIEW, src/Security/Voter/ArticleVoter.php), jamais par un check inline isAdmin(). Le filtre findPublished() de la liste du dashboard est une optimisation de chargement (ne pas charger des brouillons inutiles), pas la frontière d'autorisation — celle-ci reste le Voter, vérifié au détail.
Voter UserDataVoter¶
Déclaré dans src/Security/Voter/UserDataVoter.php. Autorise un compte à consulter uniquement ses propres données.
| Élément | Valeur |
|---|---|
| Attribut | VIEW_OWN (constante UserDataVoter::VIEW_OWN) |
| Sujet supporté | une instance de App\Entity\User |
| Règle de vote | accordé si l'identifiant du sujet est égal à celui du compte authentifié |
Le vote est refusé si le token ne porte pas une instance de User. Ce Voter est aujourd'hui consommé par AppUserHomeController ; il a vocation à garder toute consultation de données utilisateur cantonnée au compte propriétaire.
Voir aussi¶
- Se connecter à Kirexo — le parcours d'authentification qui mène à ces pages.
- Consulter et analyser la conformité d'un article publié — le parcours admin dashboard cockpit → détail, pas à pas.
- Définir la couleur d'accent de la plateforme — la page Réglages pas à pas.
- Référence
security.yaml— firewalls,access_controletdefault_target_path. - Architecture des firewalls — pourquoi deux espaces et comment l'admin se masque.
- Design system : tokens, thèmes et accent — la mécanique
data-accentrendue côté serveur.