## Résumé
Implémentation complète du système RBAC (Role-Based Access Control) pour Coltura.
### Backend
- Entités Permission et Role avec API Platform CRUD
- PermissionVoter : vérification des permissions effectives (rôles + directes), admin bypass
- Endpoints `PATCH /users/{id}/rbac` pour assigner rôles, permissions directes et isAdmin
- AdminHeadcountGuard : protection contre la suppression du dernier admin
- Commande `app:sync-permissions` pour synchroniser les permissions déclarées par les modules
- Filtrage sidebar par permission RBAC (`permission` key optionnelle dans sidebar.php)
- 115 tests PHPUnit (fonctionnels + unitaires)
### Frontend
- Composable `usePermissions()` avec `can()`, `canAny()`, `canAll()` et admin bypass
- Page `/admin/roles` : DataTable, création/édition via drawer, suppression avec confirmation
- Page `/admin/users` : DataTable, drawer RBAC avec rôles, permissions directes, résumé effectif
- PermissionGroup : checkboxes groupées par module avec "tout sélectionner"
- EffectivePermissions : résumé lecture seule avec badges source ("via Rôle X" / "Direct")
- Warning auto-édition, toggle isAdmin
- Tests Vitest pour usePermissions
### Permissions déclarées
- `core.users.view` — Voir les utilisateurs
- `core.users.manage` — Gérer les utilisateurs
- `core.roles.view` — Voir les rôles RBAC
- `core.roles.manage` — Gérer les rôles et permissions
- `GET /api/permissions` accessible à tout utilisateur authentifié (catalogue read-only)
## Tickets Lesstime
- ERP-23 (#343) — Entités Permission et Role
- ERP-24 (#344) — API CRUD Roles & Permissions
- ERP-25 (#345) — Voter Symfony + usePermissions
- ERP-26 (#346) — Interface Admin : Gestion des Rôles
- ERP-27 (#347) — Interface Admin : Permissions Utilisateur
## Test plan
- [ ] `make db-reset` puis vérifier les fixtures (admin/alice/bob, rôles système)
- [ ] Login admin : sidebar affiche Gestion des rôles + Utilisateurs
- [ ] Login alice : sidebar masque ces onglets (pas de permission)
- [ ] Page /admin/roles : CRUD rôles, permissions groupées, protection rôles système
- [ ] Page /admin/users : assignation rôles + permissions directes, résumé effectif
- [ ] Warning auto-édition quand admin modifie ses propres droits
- [ ] `make test` : 115 tests PHPUnit passent
- [ ] `cd frontend && npm run test` : tests Vitest passent
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-authored-by: Matthieu <mtholot19@gmail.com>
Co-authored-by: tristan <tristan@yuno.malio.fr>
Reviewed-on: #7
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
14 KiB
Coltura
CRM/ERP. Monorepo Symfony 8 (API Platform 4) + Nuxt 4. Architecture Modular Monolith DDD.
Architecture Modulaire
Le projet suit une architecture modular monolith pilotee par le backend : chaque module metier est un bounded context autonome, activable/desactivable par tenant. Le module Core est obligatoire.
Principe fondamental : le backend est la source de verite unique.
- Le backend dicte quels modules sont actifs (
config/modules.php). - Le backend dicte l'organisation de la sidebar (
config/sidebar.php), decouplee des modules eux-memes. - Le frontend ne connait rien : il scanne automatiquement les modules comme layers Nuxt et demande la sidebar au backend.
Backend — Organisation par module
src/
Kernel.php
Shared/ # Noyau technique partage
Domain/
ValueObject/ # VO de base (Email...)
Event/ # DomainEventInterface
Contract/ # Interfaces inter-modules (UserResolverInterface, TenantAwareInterface)
Application/
Bus/ # CommandBusInterface, QueryBusInterface (interfaces seules)
Infrastructure/
ApiPlatform/
Resource/ # AppVersion, ModulesResource, SidebarResource
State/ # AppVersionProvider, ModulesProvider, SidebarProvider
Module/
Core/ # Module obligatoire (auth, users)
CoreModule.php # Declaration (ID, LABEL, REQUIRED)
Domain/
Entity/ # Entites Doctrine + API Platform (User)
Repository/ # Interfaces repositories (UserRepositoryInterface)
Event/ # Domain events (UserCreated)
Application/
DTO/ # UserOutput
Infrastructure/
Doctrine/ # DoctrineUserRepository, Migrations/
ApiPlatform/
State/
Provider/ # MeProvider
Processor/ # UserPasswordHasherProcessor
Console/ # CreateUserCommand
DataFixtures/ # AppFixtures
Commercial/ # Autre module (exemple)
CommercialModule.php
config/
modules.php # Liste des modules actifs (source de verite activation)
sidebar.php # Structure de la sidebar (source de verite navigation)
version.yaml
jwt/ # Cles JWT
packages/ # Config Symfony
migrations/ # Anciennes migrations Doctrine
infra/dev/ # Docker dev
infra/prod/ # Docker prod (multi-stage)
Frontend — Organisation modulaire (auto-detectee)
frontend/
app/ # Shell applicatif
layouts/ # default.vue, auth.vue
middleware/ # auth.global.ts, modules.global.ts
shared/ # Code partage (hors modules)
composables/ # useApi, useAppVersion, useSidebar
components/ui/ # AppTopNav, ...
stores/ # auth, ui
services/ # auth
types/ # SidebarSection, SidebarItem, UserData
utils/ # api (Hydra)
modules/ # Modules auto-detectes comme layers Nuxt
core/
nuxt.config.ts # Marqueur layer (vide)
pages/ # index.vue, login.vue
commercial/
nuxt.config.ts
pages/ # commercial.vue
app.vue # Composant racine
nuxt.config.ts # Scanne modules/*/ automatiquement
i18n/locales/ # Traductions (cles sidebar.*, etc.)
assets/ # CSS, images
public/ # Fichiers statiques
Endpoints API cles
GET /api/version(public) — version de l'appGET /api/modules(public) — liste des IDs de modules actifsGET /api/sidebar(public) — sections de la sidebar +disabledRoutes- Filtre automatiquement les items dont le
moduleowner n'est pas actif - Les sections vides apres filtrage sont supprimees
disabledRoutes=todes items filtres (utilise par le middleware front)
- Filtre automatiquement les items dont le
GET /api/me(auth) — user courant
Flux d'activation/desactivation d'un module
Pour activer/desactiver un module, tu touches uniquement config/modules.php :
return [
\App\Module\Core\CoreModule::class,
// \App\Module\Commercial\CommercialModule::class, // commente = desactive
];
Cascade automatique :
GET /api/modulesne retourne pluscommercialGET /api/sidebarfiltre les itemsmodule: 'commercial'→ section "Commercial" disparait, ses routes passent dansdisabledRoutes- Frontend : sidebar se met a jour, middleware
modules.global.tsredirige toute navigation vers/commercialou/commercial/* - Le code du module reste dans le bundle Nuxt (layer auto-detecte) → reactivation instantanee sans rebuild
Reorganiser la sidebar sans toucher aux modules
Pour deplacer un item (ex: "Commandes fournisseurs") d'une section a une autre, tu edites juste config/sidebar.php :
// Avant : sous Commercial
['label' => 'sidebar.commercial.suppliers', 'to' => '/commercial/suppliers', 'module' => 'commercial'],
// Apres : sous Production (l'item reste "owned" par Commercial, seule sa place change)
[
'label' => 'sidebar.production.section',
'items' => [
['label' => 'sidebar.commercial.suppliers', 'to' => '/commercial/suppliers', 'module' => 'commercial'],
],
],
Le code du module Commercial n'est pas touche.
Regles d'architecture
Backend :
- Le domaine (
Domain/) peut garder les attributs ORM (approche pragmatique) mais les repositories sont des interfaces - Communication inter-modules par
Shared/Domain/Contract/ou domain events — jamais d'import direct entre modules - Chaque module declare un
*Module.phpavecID,LABEL,REQUIRED config/modules.php= seule source de verite pour l'activationconfig/sidebar.php= seule source de verite pour l'organisation de la sidebar (chaque item reference son module owner via la clemodule)- Migrations par module dans
src/Module/{Module}/Infrastructure/Doctrine/Migrations/ - Exception connue : avec plusieurs
migrations_pathsconfigures, Doctrine Migrations 3.x trie les migrations par FQCN alphabetique et non par version timestamp → ordre d'execution incorrect entre namespaces sur une base vide. Tant que ce n'est pas resolu (via unMigrationsComparatorcustom ou un upgrade), les migrations d'initialisation critiques (setup user, RBAC, etc.) vivent au namespace racineDoctrineMigrationsdansmigrations/. Le namespace modulaire reste configure pour les futures migrations applicatives (qui dependent d'un schema deja cree).
Frontend :
- Chaque module est un layer Nuxt auto-detecte (
modules/*/nuxt.config.tsminimal) - Un module front ne doit pas importer depuis un autre module — utiliser
shared/ useSidebar()fetch/api/sidebaret exposesections,disabledRoutes,isRouteDisabled()- Le layout
default.vueitere sur les sections retournees par l'API, appliquet()sur les labels - Middleware
auth.global.tscharge la sidebar apres authentification - Middleware
modules.global.tsredirige si la route demandee est dansdisabledRoutes - Les composables avec state singleton (refs module-level) doivent exposer une fonction
reset*()et etre reinitialises au logout (ex:useSidebar().resetSidebar()) - Interdit :
.module.ts,modules-loader.ts, hardcode de la sidebar, edition manuelle deextendsdansnuxt.config.ts
Stack
- Backend : PHP 8.4, Symfony 8.0, API Platform 4, Doctrine ORM, PostgreSQL 16
- Frontend : Nuxt 4 (SSR off / SPA), Vue 3, Pinia, Tailwind CSS, @malio/layer-ui, nuxt-toast, @nuxtjs/i18n, @nuxt/icon
- Auth : JWT HTTP-only cookie (lexik/jwt-authentication-bundle), login a
/login_check, cookieBEARER - Docker : PHP-FPM + Node 24, Nginx (port 8083), PostgreSQL (port 5437)
Commandes
make start # Demarrer les containers
make stop # Arreter les containers
make restart # Redemarrer les containers
make install # Install complet (composer, migrations, fixtures, build Nuxt)
make reset # Tout supprimer et reinstaller (supprime la BDD)
make dev-nuxt # Dev server Nuxt (hot reload, port 3004)
make shell # Shell dans le container PHP
make shell-root # Shell root dans le container PHP
make cache-clear # Vider le cache Symfony
make migration-migrate # Lancer les migrations
make fixtures # Charger les fixtures
make db-reset # Reset BDD + migrations + fixtures
make test # PHPUnit
make php-cs-fixer-allow-risky # Fix code style PHP
make logs-dev # Tail logs Symfony
Si make cache-clear echoue pour cause de permissions sur var/ :
docker exec -t -u root php-coltura-fpm chown -R www-data:www-data /var/www/html/var
docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear
Conventions
Commits
Format : <type>(<scope optionnel>) : <message> (espace avant et apres :)
Types autorises (minuscules) : build, chore, ci, docs, feat, fix, perf, refactor, revert, style, test
Exemples : feat : add login page, fix(auth) : prevent null token crash
Tags & Versioning
- La version de l'app est dans
config/version.yaml(parametreapp.version) - A chaque creation de tag, toujours mettre a jour
config/version.yamlavec la meme version - Faire un commit separe de bump :
chore : bump version to v<X.Y.Z> - Puis creer le tag et pusher :
git tag v<X.Y.Z> && git push origin develop --tags
Nommage
| Element | Convention | Exemple |
|---|---|---|
| Module back | PascalCase | Module/Commercial/ |
| Module front | kebab-case | modules/commercial/ |
| Module ID | snake_case | commercial, gestion_rh |
| Entity | PascalCase singulier | User.php |
| Repository interface | *RepositoryInterface |
UserRepositoryInterface.php |
| Repository impl | Doctrine*Repository |
DoctrineUserRepository.php |
| DTO | *Output / *Input |
UserOutput.php |
| API Resource | classe dans Infrastructure/ApiPlatform/Resource/ |
UserResource.php |
| Provider | *Provider |
MeProvider.php |
| Processor | *Processor |
UserPasswordHasherProcessor.php |
| Module declaration back | *Module.php |
CommercialModule.php |
| Composable front | use* |
useSidebar.ts |
| Cles i18n sidebar | sidebar.<module>.* |
sidebar.commercial.overview |
Backend
- Toujours
declare(strict_types=1)en haut des fichiers PHP - Commentaires en francais : tout commentaire PHP (docblock, inline, bloc) doit etre redige en francais. Le code (noms de classes, methodes, variables) reste en anglais. Objectif : faciliter la relecture par l'equipe FR sans polluer l'API publique du code.
- API Platform : utiliser ApiResource, Providers, Processors — pas de controllers
- Routes API prefixees
/api(viaconfig/routes/api_platform.yaml) - Le login (
/login_check) est hors prefix/api, nginx reecritREQUEST_URIvers/login_check - PHP CS Fixer : regles Symfony + PSR-12 + strict types
- Roles :
ROLE_ADMIN,ROLE_USER— hierarchie danssecurity.yaml - Permissions RBAC : format obligatoire
module.resource[.subresource].actionen snake_case, ex :core.users.view,commercial.clients.contacts.edit. Declarees via la methode statiquepermissions()des*Module.php, synchronisees par la commandeapp:sync-permissions. Verification viais_granted('module.resource.action')cote API Platform etusePermissions()cote front. - PostgreSQL : noms de colonnes toujours en minuscules dans le SQL brut
- Controllers custom sous
/api/: ajouterpriority: 1sur#[Route]pour eviter le conflit avec API Platform{id} - Serialization : pour embarquer une relation (pas IRI), ajouter le groupe du parent aux proprietes de l'entite cible
- Upload fichiers : utiliser
$file->getMimeType()(pasgetClientMimeType()) pour valider cote serveur
Frontend
- TypeScript strict
- Commentaires en francais : tout commentaire TS/Vue (JSDoc, inline, bloc) doit etre redige en francais. Le code reste en anglais. Meme regle que cote backend.
- Composable
useApi()pour tous les appels API (gere cookies, erreurs, toasts, i18n) - Stores Pinia :
useAuthStore(auth),useUiStore(ui) - Middleware global
auth.global.tsprotege les routes + charge la sidebar apres login - Middleware global
modules.global.tsredirige les routes des modules desactives - Traductions dans
frontend/i18n/locales/avec le namespacesidebar.*pour la nav - 4 espaces d'indentation
- Les labels de sidebar sont des cles i18n, jamais du texte brut (le layout applique
t()dessus)
Nginx
/api/*-> Symfony (via try_files + index.php)/api/login_check-> location exact match, fastcgi direct avec REQUEST_URI reecrit en/login_check/-> SPA frontend (frontend/dist/)
Docker
- Container PHP :
php-coltura-fpm - Container Nginx :
nginx-coltura - Container DB : PostgreSQL sur port 5437 (interne et externe)
- Config Docker dev :
infra/dev/.env.docker(override local :infra/dev/.env.docker.local) - Config Docker prod :
infra/prod/(Dockerfile multi-stage, docker-compose.prod.yml) - Apres modif nginx :
docker restart nginx-coltura
Fixtures
- User admin :
admin/admin(ROLE_ADMIN) - Users internes :
alice/alice,bob/bob(ROLE_USER)
Delegation Codex
Pour les taches mecaniques (tests, boilerplate, renommages, refacto repetitif), delegue a Codex via le plugin codex. Garde Claude pour la reflexion, l'architecture et la verification.
- Codex = junior dev rapide et pas cher (executions mecaniques)
- Claude = senior dev qui verifie et reflechit (design, review, decisions)
C'est le meilleur ratio qualite/credits.