Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3c9eaf5d69 | |||
| 33599db5a3 | |||
| 34e75a35fb | |||
| 1696602abb | |||
| cacd8718e5 | |||
| f7a50168d5 | |||
| 93cbd48bf5 |
@@ -41,8 +41,8 @@ Si une verification echoue ou ne peut pas etre lancee (ex : container pas demarr
|
||||
|
||||
## Time tracking Lesstime
|
||||
|
||||
Au demarrage de toute tache de dev sur Coltura, creer une time entry via l'API Lesstime (cf. `~/.claude/CLAUDE.md` pour la procedure complete).
|
||||
- Projet : `/api/projects/6` (COLTURA)
|
||||
Au demarrage de toute tache de dev sur Starseed, creer une time entry via l'API Lesstime (cf. `~/.claude/CLAUDE.md` pour la procedure complete).
|
||||
- Projet : `/api/projects/6` (STARSEED)
|
||||
- Tags : choisir selon le type (Backend `3`, Frontend `2`, Infra `5`, UI/UX `4`, Maintenance `6`, Gestion projet `9`, etc.)
|
||||
|
||||
## Fix `make cache-clear` (permissions `var/`)
|
||||
@@ -50,17 +50,17 @@ Au demarrage de toute tache de dev sur Coltura, creer une time entry via l'API L
|
||||
Si `make cache-clear` echoue sur les permissions de `var/` :
|
||||
|
||||
```bash
|
||||
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
|
||||
docker exec -t -u root php-starseed-fpm chown -R www-data:www-data /var/www/html/var
|
||||
docker exec -t -u www-data php-starseed-fpm php bin/console cache:clear
|
||||
```
|
||||
|
||||
A terme : integrer ce fix dans le `makefile` lui-meme.
|
||||
|
||||
## Docker — references utiles
|
||||
|
||||
- Container PHP : `php-coltura-fpm`
|
||||
- Container Nginx : `nginx-coltura` (port 8083)
|
||||
- Container PHP : `php-starseed-fpm`
|
||||
- Container Nginx : `nginx-starseed` (port 8083)
|
||||
- Container DB : PostgreSQL port **5437** (interne et externe)
|
||||
- Config dev : `infra/dev/.env.docker` (override local : `infra/dev/.env.docker.local`)
|
||||
- Config prod : `infra/prod/` (Dockerfile multi-stage, `docker-compose.prod.yml`)
|
||||
- Apres modif nginx : `docker restart nginx-coltura`
|
||||
- Apres modif nginx : `docker restart nginx-starseed`
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
---
|
||||
name: create-module
|
||||
description: Scaffold a new Coltura module (backend + frontend) and optionally wire its entries into the sidebar config. Use when the user asks to create, add, scaffold, or generate a new module — e.g., "crée un module Paie", "add a Pointage module", "ajoute un module RH". The backend is the source of truth for activation and sidebar layout; the frontend scans modules automatically.
|
||||
description: Scaffold a new Starseed module (backend + frontend) and optionally wire its entries into the sidebar config. Use when the user asks to create, add, scaffold, or generate a new module — e.g., "crée un module Paie", "add a Pointage module", "ajoute un module RH". The backend is the source of truth for activation and sidebar layout; the frontend scans modules automatically.
|
||||
---
|
||||
|
||||
# Create a new Coltura module
|
||||
# Create a new Starseed module
|
||||
|
||||
Scaffolds a new module across backend and frontend following Coltura's modular monolith DDD architecture.
|
||||
Scaffolds a new module across backend and frontend following Starseed's modular monolith DDD architecture.
|
||||
|
||||
## Architecture reminder — read before acting
|
||||
|
||||
@@ -178,8 +178,8 @@ Execute in this exact order:
|
||||
6. **Backend: sidebar** — if the user wants sidebar entries, edit `config/sidebar.php`.
|
||||
7. **Frontend: translations** — edit `frontend/i18n/locales/fr.json`.
|
||||
8. **Verify** — run:
|
||||
- `docker exec -t -u root php-coltura-fpm chown -R www-data:www-data /var/www/html/var` (avoid permission issues)
|
||||
- `docker exec -t -u www-data php-coltura-fpm php bin/console cache:clear` (validates backend)
|
||||
- `docker exec -t -u root php-starseed-fpm chown -R www-data:www-data /var/www/html/var` (avoid permission issues)
|
||||
- `docker exec -t -u www-data php-starseed-fpm php bin/console cache:clear` (validates backend)
|
||||
- `cd frontend && npx nuxi prepare` (validates Nuxt auto-detection of the new layer)
|
||||
9. **Report** — list files created, the route(s) to test, and the sidebar items added.
|
||||
|
||||
|
||||
@@ -20,11 +20,11 @@ jobs:
|
||||
run: |
|
||||
docker build \
|
||||
-f infra/prod/Dockerfile \
|
||||
-t gitea.malio.fr/malio-dev/coltura:${{ gitea.ref_name }} \
|
||||
-t gitea.malio.fr/malio-dev/coltura:latest \
|
||||
-t gitea.malio.fr/malio-dev/starseed:${{ gitea.ref_name }} \
|
||||
-t gitea.malio.fr/malio-dev/starseed:latest \
|
||||
.
|
||||
|
||||
- name: Push Docker image
|
||||
run: |
|
||||
docker push gitea.malio.fr/malio-dev/coltura:${{ gitea.ref_name }}
|
||||
docker push gitea.malio.fr/malio-dev/coltura:latest
|
||||
docker push gitea.malio.fr/malio-dev/starseed:${{ gitea.ref_name }}
|
||||
docker push gitea.malio.fr/malio-dev/starseed:latest
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
name: Pull Request — Quality gate
|
||||
|
||||
# Lance les tests + lint + build sur chaque PR ciblant develop.
|
||||
# Deux jobs en parallele (backend / frontend) pour reduire le temps de feedback.
|
||||
# E2E volontairement hors scope (cf. regle d'or testing.md).
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
|
||||
# Annule les runs obsoletes quand on repush sur la meme PR.
|
||||
concurrency:
|
||||
group: pr-${{ gitea.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
name: Backend (PHP CS + PHPUnit)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
env:
|
||||
# Doivent matcher la DATABASE_URL ci-dessous. Le suffixe `_test`
|
||||
# est applique automatiquement par Doctrine en APP_ENV=test.
|
||||
POSTGRES_USER: app
|
||||
POSTGRES_PASSWORD: '!ChangeMe!'
|
||||
POSTGRES_DB: app
|
||||
# Pas de `ports:` host mapping — le runner partage l'hote avec la
|
||||
# prod (Postgres deja sur 5432) et les jobs Gitea Actions tournent
|
||||
# en container sur un reseau Docker dedie : le service est joignable
|
||||
# via son nom (`postgres`), pas via 127.0.0.1.
|
||||
options: >-
|
||||
--health-cmd "pg_isready -U app"
|
||||
--health-interval 5s
|
||||
--health-timeout 5s
|
||||
--health-retries 10
|
||||
|
||||
env:
|
||||
APP_ENV: test
|
||||
APP_SECRET: ci-secret-not-used
|
||||
APP_DEBUG: 0
|
||||
DEFAULT_URI: http://localhost/
|
||||
DATABASE_URL: postgresql://app:!ChangeMe!@postgres:5432/app?serverVersion=16&charset=utf8
|
||||
JWT_SECRET_KEY: '%kernel.project_dir%/config/jwt/private.pem'
|
||||
JWT_PUBLIC_KEY: '%kernel.project_dir%/config/jwt/public.pem'
|
||||
JWT_PASSPHRASE: change_me_in_env_local
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup PHP 8.4
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: '8.4'
|
||||
extensions: pdo, pdo_pgsql, intl, opcache, zip, mbstring, sodium
|
||||
coverage: none
|
||||
tools: composer:v2
|
||||
|
||||
- name: Cache Composer
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.composer/cache
|
||||
key: composer-${{ hashFiles('composer.lock') }}
|
||||
restore-keys: |
|
||||
composer-
|
||||
|
||||
- name: Install PHP dependencies
|
||||
run: composer install --no-interaction --no-progress --prefer-dist
|
||||
|
||||
- name: Generate JWT keypair
|
||||
run: php bin/console lexik:jwt:generate-keypair --skip-if-exists --no-interaction
|
||||
|
||||
- name: PHP CS Fixer (dry-run)
|
||||
run: vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php --allow-risky=yes --dry-run --diff
|
||||
|
||||
- name: Bootstrap test database
|
||||
run: |
|
||||
php bin/console doctrine:database:create --env=test --if-not-exists --no-interaction
|
||||
php bin/console doctrine:migrations:migrate --env=test --no-interaction
|
||||
php bin/console doctrine:schema:update --env=test --force --no-interaction
|
||||
php bin/console doctrine:fixtures:load --env=test --no-interaction
|
||||
php bin/console app:sync-permissions --env=test --no-interaction
|
||||
|
||||
- name: Run PHPUnit
|
||||
run: php -d memory_limit=512M vendor/bin/phpunit
|
||||
|
||||
frontend:
|
||||
name: Frontend (lint + Vitest + build)
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: frontend
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22'
|
||||
cache: npm
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Install Node dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: ESLint
|
||||
run: npm run lint
|
||||
|
||||
- name: Unit tests (Vitest)
|
||||
run: npm run test
|
||||
|
||||
- name: Build production (nuxt build)
|
||||
run: npm run build:dist
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Changelog
|
||||
|
||||
Liste des évolutions du projet Coltura
|
||||
Liste des évolutions du projet Starseed
|
||||
|
||||
## [0.0.0]
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Coltura
|
||||
# Starseed
|
||||
|
||||
## Contexte
|
||||
CRM/ERP en architecture **modular monolith DDD**. Le backend est la source de verite unique (modules actifs, sidebar). Le frontend scanne `frontend/modules/*/` comme layers Nuxt et consomme l'API pour la navigation. Multi-tenant : chaque module est activable/desactivable.
|
||||
@@ -9,7 +9,7 @@ Doc humaine : @README.md — Spec audit : @doc/audit-log.md
|
||||
- Backend : PHP 8.4, Symfony 8, API Platform 4, Doctrine ORM, PostgreSQL 16 (port 5437)
|
||||
- Frontend : Nuxt 4 (SPA), Vue 3, Pinia, Tailwind, @malio/layer-ui, @nuxtjs/i18n
|
||||
- Auth : JWT HTTP-only cookie (Lexik), login a `/login_check`
|
||||
- Containers : `php-coltura-fpm`, `nginx-coltura` (port 8083), dev Nuxt port **3004**
|
||||
- Containers : `php-starseed-fpm`, `nginx-starseed` (port 8083), dev Nuxt port **3004**
|
||||
|
||||
## Regles ABSOLUES
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Coltura
|
||||
# Starseed
|
||||
|
||||
CRM/ERP — Symfony 8 (API Platform 4) + Nuxt 4
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ La branche est globalement solide : les trois miroirs RBAC sont synchronises, le
|
||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||
```
|
||||
|
||||
La documentation Swagger/OpenAPI d'API Platform est accessible sans authentification, quel que soit l'environnement — y compris en production sur `coltura.malio-dev.fr`. Elle expose :
|
||||
La documentation Swagger/OpenAPI d'API Platform est accessible sans authentification, quel que soit l'environnement — y compris en production sur `starseed.malio-dev.fr`. Elle expose :
|
||||
|
||||
- la liste complete des endpoints (`/api/audit-logs`, `/api/users/{id}/rbac`, `/api/sites`, etc.)
|
||||
- les schemas de securite (`is_granted('core.audit_log.view')`)
|
||||
@@ -94,7 +94,7 @@ Le reverse proxy ecoute uniquement sur le port 80 (HTTP), sans redirection 301 v
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name coltura.malio-dev.fr;
|
||||
server_name starseed.malio-dev.fr;
|
||||
|
||||
# Redirection HTTPS obligatoire (ajouter un server block HTTPS par ailleurs).
|
||||
# Tant que le TLS n'est pas en place, au minimum poser les en-tetes suivants.
|
||||
@@ -123,7 +123,7 @@ User-Agent: *
|
||||
Disallow:
|
||||
```
|
||||
|
||||
La valeur `Disallow:` (vide) signifie "rien n'est interdit" — tous les crawlers peuvent indexer la totalite du site. Pour un outil CRM interne accessible sur un DNS public (`coltura.malio-dev.fr`), c'est un leak inutile : la page de login, les URLs `/admin/*`, les URLs des fiches clients peuvent remonter dans Google.
|
||||
La valeur `Disallow:` (vide) signifie "rien n'est interdit" — tous les crawlers peuvent indexer la totalite du site. Pour un outil CRM interne accessible sur un DNS public (`starseed.malio-dev.fr`), c'est un leak inutile : la page de login, les URLs `/admin/*`, les URLs des fiches clients peuvent remonter dans Google.
|
||||
|
||||
**Correction** :
|
||||
|
||||
@@ -581,7 +581,7 @@ Et ajouter les cles manquantes dans `fr.json` :
|
||||
await loadSidebar() // apres chaque switch
|
||||
```
|
||||
|
||||
Commentaire : *"les filtres de modules peuvent dependre du site courant"*. En pratique, dans `config/sidebar.php` de Coltura aucun item ne depend du site. C'est un aller-retour reseau inutile a chaque switch, et la sidebar peut "flicker" pour l'utilisateur.
|
||||
Commentaire : *"les filtres de modules peuvent dependre du site courant"*. En pratique, dans `config/sidebar.php` de Starseed aucun item ne depend du site. C'est un aller-retour reseau inutile a chaque switch, et la sidebar peut "flicker" pour l'utilisateur.
|
||||
|
||||
**Correction** : rendre le rechargement opt-in ou documenter la raison actuelle (prevoir le futur).
|
||||
|
||||
|
||||
+9
-9
@@ -39,7 +39,7 @@ access_control:
|
||||
Comme `/api/docs` tombe desormais dans le dernier pattern (`^/api`), il faudra etre authentifie pour le voir. Les devs continueront de l'utiliser apres login — les attaquants non.
|
||||
|
||||
3. Recharger : `make cache-clear` puis `make restart`.
|
||||
4. Tester : `curl -i https://coltura.malio-dev.fr/api/docs` doit retourner `401 Unauthorized` (avant : `200`).
|
||||
4. Tester : `curl -i https://starseed.malio-dev.fr/api/docs` doit retourner `401 Unauthorized` (avant : `200`).
|
||||
|
||||
**Fichiers :** `config/packages/security.yaml`
|
||||
|
||||
@@ -47,12 +47,12 @@ Comme `/api/docs` tombe desormais dans le dernier pattern (`^/api`), il faudra e
|
||||
|
||||
### T-002 — Ajouter les en-tetes de securite HTTP de base en prod
|
||||
|
||||
**Pourquoi :** sans `X-Frame-Options`, quelqu'un peut integrer Coltura dans une iframe sur un site tiers et faire du clickjacking (faire croire a l'utilisateur qu'il clique sur un bouton anodin alors qu'il valide une action dans Coltura). Sans `X-Content-Type-Options: nosniff`, un navigateur peut deviner le type MIME et executer un fichier qui n'aurait pas du l'etre. Ce sont 3 lignes de config Nginx pour proteger l'application.
|
||||
**Pourquoi :** sans `X-Frame-Options`, quelqu'un peut integrer Starseed dans une iframe sur un site tiers et faire du clickjacking (faire croire a l'utilisateur qu'il clique sur un bouton anodin alors qu'il valide une action dans Starseed). Sans `X-Content-Type-Options: nosniff`, un navigateur peut deviner le type MIME et executer un fichier qui n'aurait pas du l'etre. Ce sont 3 lignes de config Nginx pour proteger l'application.
|
||||
|
||||
**A faire :**
|
||||
|
||||
1. Ouvrir `infra/prod/nginx-proxy.conf` (c'est le proxy expose au public).
|
||||
2. Ajouter juste apres `server_name coltura.malio-dev.fr;` :
|
||||
2. Ajouter juste apres `server_name starseed.malio-dev.fr;` :
|
||||
|
||||
```nginx
|
||||
# En-tetes de securite applicables a toutes les reponses
|
||||
@@ -62,13 +62,13 @@ add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||
```
|
||||
|
||||
Explication :
|
||||
- `X-Frame-Options: DENY` : personne ne peut mettre Coltura dans une iframe.
|
||||
- `X-Frame-Options: DENY` : personne ne peut mettre Starseed dans une iframe.
|
||||
- `X-Content-Type-Options: nosniff` : le navigateur ne devine pas les types MIME, il fait confiance a ce que le serveur annonce.
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin` : limite ce que Coltura envoie comme Referer a des sites externes (evite de leaker `/admin/users/42` a un site tiers).
|
||||
- `Referrer-Policy: strict-origin-when-cross-origin` : limite ce que Starseed envoie comme Referer a des sites externes (evite de leaker `/admin/users/42` a un site tiers).
|
||||
- `always` : envoyer ces en-tetes meme sur les reponses d'erreur (4xx/5xx).
|
||||
|
||||
3. Recharger Nginx : `docker restart nginx-coltura` (ou celui qui fait office de proxy public).
|
||||
4. Verifier : `curl -I https://coltura.malio-dev.fr/` doit afficher ces trois en-tetes.
|
||||
3. Recharger Nginx : `docker restart nginx-starseed` (ou celui qui fait office de proxy public).
|
||||
4. Verifier : `curl -I https://starseed.malio-dev.fr/` doit afficher ces trois en-tetes.
|
||||
|
||||
**Note :** si un reverse proxy externe (Traefik, Cloudflare) ajoute deja ces en-tetes, les poser ici ne fait que dupliquer, c'est sans risque (meme valeur).
|
||||
|
||||
@@ -813,7 +813,7 @@ if (!is_array($payload)) {
|
||||
```markdown
|
||||
# Changelog
|
||||
|
||||
Liste des evolutions du projet Coltura.
|
||||
Liste des evolutions du projet Starseed.
|
||||
|
||||
## [0.1.34] - 2026-04-XX
|
||||
|
||||
@@ -859,7 +859,7 @@ Liste des evolutions du projet Coltura.
|
||||
|
||||
### T-019 — Conditionner `loadSidebar()` apres switch de site
|
||||
|
||||
**Pourquoi :** apres chaque switch de site, `useCurrentSite` recharge la sidebar — mais la sidebar de Coltura ne depend d'aucun site. C'est un aller-retour reseau inutile par switch (~100ms + possible flicker visuel).
|
||||
**Pourquoi :** apres chaque switch de site, `useCurrentSite` recharge la sidebar — mais la sidebar de Starseed ne depend d'aucun site. C'est un aller-retour reseau inutile par switch (~100ms + possible flicker visuel).
|
||||
|
||||
**A faire :**
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
api_platform:
|
||||
title: Coltura API
|
||||
title: Starseed API
|
||||
version: 1.0.0
|
||||
# Scan du module Core pour decouvrir les classes ApiResource et ApiFilter.
|
||||
# Ajouter un chemin par module lors de l'ajout d'entites ApiResource dans d'autres modules.
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
parameters:
|
||||
app.version: '0.1.36'
|
||||
app.version: '0.1.39'
|
||||
|
||||
@@ -210,7 +210,7 @@ Le `requestId` est set en `kernel.request` mais jamais cleared. En deploiement F
|
||||
|
||||
**Fichiers** : `config/packages/framework.yaml`, `src/Module/Core/Infrastructure/Audit/AuditLogWriter.php:69`
|
||||
|
||||
Aucune entree `trusted_proxies` ni env `TRUSTED_PROXIES`. Coltura tourne derriere `nginx-coltura` → `php-coltura-fpm`. `Request::getClientIp()` retourne donc systematiquement l'IP **du conteneur nginx** (reseau Docker interne), pas l'IP reelle du client. Toute la valeur forensique de `ip_address` est nulle en prod.
|
||||
Aucune entree `trusted_proxies` ni env `TRUSTED_PROXIES`. Starseed tourne derriere `nginx-starseed` → `php-starseed-fpm`. `Request::getClientIp()` retourne donc systematiquement l'IP **du conteneur nginx** (reseau Docker interne), pas l'IP reelle du client. Toute la valeur forensique de `ip_address` est nulle en prod.
|
||||
|
||||
Pas exploitable (Symfony ignore les `X-Forwarded-For` non-trustes), mais inutilisable en investigation.
|
||||
|
||||
|
||||
+28
-28
@@ -1,4 +1,4 @@
|
||||
# Deploiement Docker — Coltura
|
||||
# Deploiement Docker — Starseed
|
||||
|
||||
## Pre-requis
|
||||
|
||||
@@ -29,9 +29,9 @@ sudo systemctl start nginx
|
||||
### PostgreSQL
|
||||
|
||||
PostgreSQL tourne dans un conteneur Docker separe (voir le repo `infra-postgres`).
|
||||
Il doit etre installe et accessible avant de deployer Coltura.
|
||||
Il doit etre installe et accessible avant de deployer Starseed.
|
||||
|
||||
Creer la base de donnees pour Coltura :
|
||||
Creer la base de donnees pour Starseed :
|
||||
|
||||
```bash
|
||||
cd /var/www/postgres
|
||||
@@ -43,7 +43,7 @@ docker compose exec postgres psql -U admin
|
||||
CREATE USER malio WITH PASSWORD 'motdepasse';
|
||||
|
||||
-- Creer la base
|
||||
CREATE DATABASE coltura_prod OWNER malio;
|
||||
CREATE DATABASE starseed_prod OWNER malio;
|
||||
\q
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ CREATE DATABASE coltura_prod OWNER malio;
|
||||
|
||||
## Premiere installation (nouvelle machine)
|
||||
|
||||
Guide complet pour mettre en ligne Coltura sur une machine vierge. Inclut les pre-requis, la BDD et l'app.
|
||||
Guide complet pour mettre en ligne Starseed sur une machine vierge. Inclut les pre-requis, la BDD et l'app.
|
||||
|
||||
### 1. Installer les pre-requis
|
||||
|
||||
@@ -60,9 +60,9 @@ Installer Docker, Nginx et PostgreSQL (voir section Pre-requis ci-dessus).
|
||||
### 2. Creer le dossier de deploiement
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/coltura
|
||||
sudo chown -R $(whoami):$(whoami) /var/www/coltura
|
||||
cd /var/www/coltura
|
||||
sudo mkdir -p /var/www/starseed
|
||||
sudo chown -R $(whoami):$(whoami) /var/www/starseed
|
||||
cd /var/www/starseed
|
||||
```
|
||||
|
||||
### 3. Se connecter au registry Docker de Gitea
|
||||
@@ -83,8 +83,8 @@ Creer `docker-compose.yml` :
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: gitea.malio.fr/malio-dev/coltura:${COLTURA_IMAGE_TAG:-latest}
|
||||
container_name: coltura-app
|
||||
image: gitea.malio.fr/malio-dev/starseed:${STARSEED_IMAGE_TAG:-latest}
|
||||
container_name: starseed-app
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8083:80"
|
||||
@@ -105,9 +105,9 @@ set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TAG="${1:-latest}"
|
||||
export COLTURA_IMAGE_TAG="$TAG"
|
||||
export STARSEED_IMAGE_TAG="$TAG"
|
||||
|
||||
echo "==> Deploying coltura:${TAG}..."
|
||||
echo "==> Deploying starseed:${TAG}..."
|
||||
|
||||
echo "==> Pulling image..."
|
||||
docker compose pull
|
||||
@@ -146,22 +146,22 @@ APP_DEBUG=0
|
||||
APP_SECRET=<generer avec: openssl rand -hex 32>
|
||||
|
||||
# Database (host.docker.internal = la machine hote, ou le PG tourne en Docker)
|
||||
DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/coltura_prod?serverVersion=16&charset=utf8"
|
||||
DATABASE_URL="postgresql://malio:password@host.docker.internal:5432/starseed_prod?serverVersion=16&charset=utf8"
|
||||
|
||||
# JWT
|
||||
JWT_SECRET_KEY=%kernel.project_dir%/config/jwt/private.pem
|
||||
JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
||||
JWT_PASSPHRASE=<generer avec: openssl rand -hex 32>
|
||||
JWT_COOKIE_SECURE=1
|
||||
JWT_COOKIE_SECURE=0
|
||||
JWT_COOKIE_SAMESITE=lax
|
||||
JWT_TOKEN_TTL=86400
|
||||
JWT_COOKIE_TTL=86400
|
||||
|
||||
# CORS
|
||||
CORS_ALLOW_ORIGIN='^https?://coltura\.malio-dev\.fr$'
|
||||
CORS_ALLOW_ORIGIN='^https?://starseed\.malio-dev\.fr$'
|
||||
|
||||
# App
|
||||
DEFAULT_URI=https://coltura.malio-dev.fr
|
||||
DEFAULT_URI=http://starseed.malio-dev.fr
|
||||
```
|
||||
|
||||
### 6. Generer les cles JWT
|
||||
@@ -190,17 +190,17 @@ mkdir -p uploads
|
||||
Copier la config reverse proxy depuis le repo :
|
||||
|
||||
```bash
|
||||
sudo cp infra/prod/nginx-proxy.conf /etc/nginx/sites-available/coltura.conf
|
||||
sudo cp infra/prod/nginx-proxy.conf /etc/nginx/sites-available/starseed.conf
|
||||
```
|
||||
|
||||
Ou creer `/etc/nginx/sites-available/coltura.conf` manuellement (voir `infra/prod/nginx-proxy.conf`).
|
||||
Ou creer `/etc/nginx/sites-available/starseed.conf` manuellement (voir `infra/prod/nginx-proxy.conf`).
|
||||
|
||||
La config inclut le **mode maintenance** : si le fichier `/var/www/coltura/maintenance.on` existe, Nginx renvoie une 503 avec `maintenance.html`.
|
||||
La config inclut le **mode maintenance** : si le fichier `/var/www/starseed/maintenance.on` existe, Nginx renvoie une 503 avec `maintenance.html`.
|
||||
|
||||
Activer le site :
|
||||
|
||||
```bash
|
||||
sudo ln -sf /etc/nginx/sites-available/coltura.conf /etc/nginx/sites-enabled/coltura.conf
|
||||
sudo ln -sf /etc/nginx/sites-available/starseed.conf /etc/nginx/sites-enabled/starseed.conf
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
@@ -208,13 +208,13 @@ sudo nginx -t && sudo systemctl reload nginx
|
||||
|
||||
```bash
|
||||
# Activer la maintenance
|
||||
touch /var/www/coltura/maintenance.on
|
||||
touch /var/www/starseed/maintenance.on
|
||||
|
||||
# Desactiver la maintenance
|
||||
rm /var/www/coltura/maintenance.on
|
||||
rm /var/www/starseed/maintenance.on
|
||||
```
|
||||
|
||||
Optionnel : creer une page `/var/www/coltura/public/maintenance.html` personnalisee.
|
||||
Optionnel : creer une page `/var/www/starseed/public/maintenance.html` personnalisee.
|
||||
|
||||
### 9. Deployer
|
||||
|
||||
@@ -232,7 +232,7 @@ Choisir `App\Entity\User`, taper le mdp, copier le hash. Puis :
|
||||
|
||||
```bash
|
||||
cd /var/www/postgres
|
||||
docker compose exec -T postgres psql -U malio coltura_prod -c "INSERT INTO \"user\" (username, roles, password, created_at) VALUES ('admin', '[\"ROLE_ADMIN\"]', '<le-hash>', NOW());"
|
||||
docker compose exec -T postgres psql -U malio starseed_prod -c "INSERT INTO \"user\" (username, roles, password, created_at) VALUES ('admin', '[\"ROLE_ADMIN\"]', '<le-hash>', NOW());"
|
||||
```
|
||||
|
||||
Ou charger les fixtures (dev uniquement) :
|
||||
@@ -244,7 +244,7 @@ docker compose exec -T -u www-data app php bin/console doctrine:fixtures:load --
|
||||
### Structure finale du dossier
|
||||
|
||||
```
|
||||
/var/www/coltura/
|
||||
/var/www/starseed/
|
||||
├── docker-compose.yml
|
||||
├── deploy.sh
|
||||
├── .env
|
||||
@@ -261,7 +261,7 @@ docker compose exec -T -u www-data app php bin/console doctrine:fixtures:load --
|
||||
Quand l'app est deja installee, deployer une mise a jour :
|
||||
|
||||
```bash
|
||||
cd /var/www/coltura
|
||||
cd /var/www/starseed
|
||||
./deploy.sh # deploie la derniere version (latest)
|
||||
./deploy.sh v0.2.0 # deploie une version specifique
|
||||
```
|
||||
@@ -293,7 +293,7 @@ docker compose exec -T -u www-data app php bin/console doctrine:migrations:migra
|
||||
|
||||
Le workflow `.gitea/workflows/build-docker.yml` se declenche automatiquement sur push de tag `v*` :
|
||||
1. Build l'image multi-stage
|
||||
2. Push vers `gitea.malio.fr/malio-dev/coltura:<tag>` et `:latest`
|
||||
2. Push vers `gitea.malio.fr/malio-dev/starseed:<tag>` et `:latest`
|
||||
|
||||
Combine avec `auto-tag-develop.yml`, chaque push sur `develop` cree automatiquement un tag → build → image disponible.
|
||||
|
||||
@@ -302,7 +302,7 @@ Combine avec `auto-tag-develop.yml`, chaque push sur `develop` cree automatiquem
|
||||
## Voir les logs
|
||||
|
||||
```bash
|
||||
cd /var/www/coltura
|
||||
cd /var/www/starseed
|
||||
docker compose logs -f # tous les logs
|
||||
docker compose logs -f --tail=100 # 100 dernieres lignes
|
||||
```
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
# Prompt — Migration prod Coltura -> Starseed
|
||||
|
||||
Copier-coller integralement dans une session Claude lancee **sur le serveur de prod** apres que :
|
||||
- le push develop + build CI ont publie l'image `gitea.malio.fr/malio-dev/starseed:latest`,
|
||||
- la resolution reseau local (DNS interne ou `/etc/hosts` des postes clients) pour `starseed.malio-dev.fr` est en place.
|
||||
|
||||
> Setup : HTTP en reseau local, pas de TLS. Pas de Let's Encrypt.
|
||||
|
||||
---
|
||||
|
||||
## Prompt a fournir au Claude prod
|
||||
|
||||
Tu es sur le serveur de production d'une app Symfony+Nuxt qui s'appelait **Coltura** et qui doit etre renommee en **Starseed**. Le rename cote code est deja fait et merge. Le repo Gitea s'appelle deja `starseed`. L'image `gitea.malio.fr/malio-dev/starseed:latest` est publiee.
|
||||
|
||||
L'app est servie en **HTTP sur reseau local** (pas de TLS, pas de Let's Encrypt). La resolution `starseed.malio-dev.fr` est faite via DNS interne ou `/etc/hosts` cote postes clients — pas de certificat a gerer.
|
||||
|
||||
Objectif : basculer la prod sur le nouveau nom (registry, container, DB, path FS, vhost) **sans perdre les donnees** et avec downtime minimal (mode maintenance pendant la migration).
|
||||
|
||||
**Etat actuel a verifier en premier** (donne-moi le retour de chaque commande avant de continuer) :
|
||||
|
||||
```bash
|
||||
# 1. Container actuel + image
|
||||
sudo docker ps --filter name=coltura-app --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
|
||||
|
||||
# 2. DB existante
|
||||
sudo -u postgres psql -c "\l" | grep -E "coltura|starseed"
|
||||
|
||||
# 3. Path FS app
|
||||
ls -la /var/www/coltura/ 2>/dev/null | head -5
|
||||
ls -la /var/www/starseed/ 2>/dev/null | head -5
|
||||
|
||||
# 4. Vhost nginx system
|
||||
sudo ls -la /etc/nginx/sites-enabled/ | grep -E "coltura|starseed"
|
||||
```
|
||||
|
||||
**Apres confirmation de l'etat, executer dans cet ordre, en demandant validation utilisateur AVANT chaque etape destructive (DB drop, rm -rf, certificat) :**
|
||||
|
||||
### Etape 1 — Mode maintenance
|
||||
|
||||
```bash
|
||||
cd /var/www/coltura
|
||||
touch maintenance.on
|
||||
# Verifier qu'une requete renvoie 503
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://coltura.malio-dev.fr/
|
||||
```
|
||||
|
||||
Doit renvoyer `503`.
|
||||
|
||||
### Etape 2 — Backup DB (CRITIQUE — ne pas skipper)
|
||||
|
||||
```bash
|
||||
BACKUP_FILE="/root/coltura_prod_backup_$(date +%Y%m%d_%H%M%S).sql"
|
||||
sudo -u postgres pg_dump -F c -f "$BACKUP_FILE" coltura_prod
|
||||
ls -lh "$BACKUP_FILE"
|
||||
```
|
||||
|
||||
**Stocker ce chemin** — il sera utilise pour le rollback.
|
||||
|
||||
### Etape 3 — Creer la DB cible et migrer
|
||||
|
||||
Recuperer l'owner et le user de connexion actuels :
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql -c "\l coltura_prod"
|
||||
grep DATABASE_URL /var/www/coltura/.env
|
||||
```
|
||||
|
||||
Puis (adapter l'owner si different de `malio`) :
|
||||
|
||||
```bash
|
||||
sudo -u postgres psql <<'SQL'
|
||||
CREATE DATABASE starseed_prod OWNER malio;
|
||||
SQL
|
||||
|
||||
sudo -u postgres pg_dump coltura_prod | sudo -u postgres psql starseed_prod
|
||||
sudo -u postgres psql starseed_prod -c "\dt" | head -20
|
||||
```
|
||||
|
||||
Verifier que les tables sont bien copiees. Si le user PG s'appelle `coltura`, le renommer ou en creer un `starseed` est OPTIONNEL — la connexion peut continuer avec `coltura` tant que `GRANT` est OK. **Confirmer avec l'utilisateur** s'il veut renommer le role PG :
|
||||
|
||||
```bash
|
||||
# Optionnel : renommer le role PG (si user de connexion s'appelle 'coltura')
|
||||
# sudo -u postgres psql -c "ALTER ROLE coltura RENAME TO starseed;"
|
||||
```
|
||||
|
||||
### Etape 4 — Renommer le path FS
|
||||
|
||||
```bash
|
||||
sudo mv /var/www/coltura /var/www/starseed
|
||||
# Verifier le contenu
|
||||
sudo ls -la /var/www/starseed/ | head -10
|
||||
# Verifier que .env existe encore
|
||||
sudo test -f /var/www/starseed/.env && echo ".env OK"
|
||||
```
|
||||
|
||||
### Etape 5 — Mettre a jour .env de prod
|
||||
|
||||
Editer `/var/www/starseed/.env` :
|
||||
- `DATABASE_URL` : remplacer `/coltura_prod` -> `/starseed_prod` (et user si renomme a etape 3)
|
||||
- `CORS_ALLOW_ORIGIN` : remplacer `coltura.malio-dev.fr` -> `starseed.malio-dev.fr`
|
||||
- `DEFAULT_URI` : `http://starseed.malio-dev.fr`
|
||||
- `JWT_COOKIE_SECURE` : doit etre `0` (HTTP, pas de TLS) — verifier qu'il l'est deja
|
||||
|
||||
Diff attendu :
|
||||
|
||||
```diff
|
||||
- DATABASE_URL="postgresql://malio:xxx@host.docker.internal:5432/coltura_prod?..."
|
||||
+ DATABASE_URL="postgresql://malio:xxx@host.docker.internal:5432/starseed_prod?..."
|
||||
- CORS_ALLOW_ORIGIN='^http://coltura\.malio-dev\.fr$'
|
||||
+ CORS_ALLOW_ORIGIN='^http://starseed\.malio-dev\.fr$'
|
||||
- DEFAULT_URI=http://coltura.malio-dev.fr
|
||||
+ DEFAULT_URI=http://starseed.malio-dev.fr
|
||||
```
|
||||
|
||||
### Etape 6 — Stopper et supprimer l'ancien container
|
||||
|
||||
```bash
|
||||
cd /var/www/starseed
|
||||
sudo docker compose down
|
||||
# Verifier qu'il n'y a plus de coltura-app
|
||||
sudo docker ps -a --filter name=coltura
|
||||
```
|
||||
|
||||
### Etape 7 — Pull la nouvelle image et demarrer
|
||||
|
||||
Le `docker-compose.prod.yml` du dossier deja a jour pointe sur `gitea.malio.fr/malio-dev/starseed:latest` et `container_name: starseed-app`.
|
||||
|
||||
```bash
|
||||
cd /var/www/starseed
|
||||
sudo docker compose pull
|
||||
sudo docker compose up -d
|
||||
sleep 5
|
||||
sudo docker ps --filter name=starseed-app
|
||||
sudo docker logs starseed-app --tail 30
|
||||
```
|
||||
|
||||
### Etape 8 — Migrations Doctrine + cache
|
||||
|
||||
```bash
|
||||
cd /var/www/starseed
|
||||
sudo docker compose exec -T -u www-data app php bin/console doctrine:migrations:migrate --no-interaction
|
||||
sudo docker compose exec -T -u www-data app php bin/console cache:clear --env=prod
|
||||
sudo docker compose exec -T -u www-data app php bin/console cache:warmup --env=prod
|
||||
```
|
||||
|
||||
### Etape 9 — Vhost nginx system (HTTP only)
|
||||
|
||||
Copier le nouveau vhost (a jour avec `server_name starseed.malio-dev.fr` et `root /var/www/starseed/public`, `listen 80` uniquement) :
|
||||
|
||||
```bash
|
||||
sudo cp /var/www/starseed/infra/prod/nginx-proxy.conf /etc/nginx/sites-available/starseed.conf
|
||||
sudo ln -sf /etc/nginx/sites-available/starseed.conf /etc/nginx/sites-enabled/starseed.conf
|
||||
sudo rm -f /etc/nginx/sites-enabled/coltura.conf
|
||||
sudo nginx -t
|
||||
```
|
||||
|
||||
Verifier la resolution reseau local avant reload :
|
||||
|
||||
```bash
|
||||
getent hosts starseed.malio-dev.fr || echo "ATTENTION : starseed.malio-dev.fr ne resout pas localement"
|
||||
```
|
||||
|
||||
Puis :
|
||||
|
||||
```bash
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
### Etape 10 — Desactiver le mode maintenance et tester
|
||||
|
||||
```bash
|
||||
rm -f /var/www/starseed/maintenance.on
|
||||
|
||||
# Tests externes (HTTP local)
|
||||
curl -s -o /dev/null -w "HTTP %{http_code}\n" http://starseed.malio-dev.fr/
|
||||
curl -s http://starseed.malio-dev.fr/api/version
|
||||
```
|
||||
|
||||
`/api/version` doit renvoyer du JSON avec la version courante.
|
||||
|
||||
### Etape 11 — Cleanup (apres 24-48h de stabilite)
|
||||
|
||||
A faire **plus tard**, seulement quand on est sur que tout marche :
|
||||
|
||||
```bash
|
||||
# Backup deja conserve en /root/coltura_prod_backup_*.sql.
|
||||
# Apres validation utilisateur :
|
||||
sudo -u postgres psql -c "DROP DATABASE coltura_prod;"
|
||||
sudo rm -f /etc/nginx/sites-available/coltura.conf
|
||||
sudo docker image prune # nettoie les vieilles images coltura
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rollback (si echec apres etape 5)
|
||||
|
||||
```bash
|
||||
# 1. Remettre maintenance
|
||||
touch /var/www/starseed/maintenance.on 2>/dev/null || touch /var/www/coltura/maintenance.on
|
||||
|
||||
# 2. Restaurer le path FS
|
||||
sudo mv /var/www/starseed /var/www/coltura 2>/dev/null || true
|
||||
|
||||
# 3. Restaurer le vhost coltura
|
||||
sudo rm -f /etc/nginx/sites-enabled/starseed.conf
|
||||
sudo ln -sf /etc/nginx/sites-available/coltura.conf /etc/nginx/sites-enabled/coltura.conf
|
||||
sudo systemctl reload nginx
|
||||
|
||||
# 4. Redemarrer l'ancien container (l'image coltura est encore dans le registry)
|
||||
cd /var/www/coltura
|
||||
# Editer docker-compose.prod.yml pour pointer sur coltura:latest si necessaire
|
||||
sudo docker compose up -d
|
||||
|
||||
# 5. Si la DB starseed_prod a ete modifiee, restaurer depuis le backup
|
||||
sudo -u postgres psql -c "DROP DATABASE IF EXISTS coltura_prod;"
|
||||
sudo -u postgres pg_restore -C -d postgres "$BACKUP_FILE"
|
||||
|
||||
# 6. Lever maintenance
|
||||
rm -f /var/www/coltura/maintenance.on
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Regles de comportement pour le Claude prod
|
||||
|
||||
- **Ne jamais skipper le backup** (etape 2).
|
||||
- **Demander confirmation utilisateur** avant : `DROP DATABASE`, `rm -rf`, et avant de lever le mode maintenance final.
|
||||
- **Une seule operation destructive a la fois**, attendre le retour utilisateur entre chaque.
|
||||
- **Logger systematiquement** la sortie des commandes critiques (pg_dump, docker compose up, nginx -t / reload).
|
||||
- **Si une etape echoue**, NE PAS continuer — declencher le rollback.
|
||||
- **Ne commit rien** sur le repo depuis le serveur prod.
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
## Résumé de la PR
|
||||
|
||||
Cette PR restructure Coltura (CRM/ERP) en **architecture modulaire DDD** (Domain-Driven Design) :
|
||||
Cette PR restructure Starseed (CRM/ERP) en **architecture modulaire DDD** (Domain-Driven Design) :
|
||||
|
||||
- **Backend** : introduction de bounded contexts (`Module/Core`, `Module/Commercial`) avec séparation Domain / Application / Infrastructure
|
||||
- **Shared** : couche partagée (events, value objects, contracts, bus interfaces)
|
||||
@@ -36,9 +36,9 @@ Cette PR restructure Coltura (CRM/ERP) en **architecture modulaire DDD** (Domain
|
||||
Liste des évolutions du projet Ferme
|
||||
```
|
||||
|
||||
Ce fichier appartient à **Coltura**, pas au projet Ferme. C'est une erreur de copier-coller lors du scaffolding initial.
|
||||
Ce fichier appartient à **Starseed**, pas au projet Ferme. C'est une erreur de copier-coller lors du scaffolding initial.
|
||||
|
||||
**Correction** : Remplacer "Ferme" par "Coltura".
|
||||
**Correction** : Remplacer "Ferme" par "Starseed".
|
||||
|
||||
---
|
||||
|
||||
@@ -76,10 +76,10 @@ Mais la seule page du module commercial est `frontend/modules/commercial/pages/c
|
||||
|---|---|
|
||||
| **Sévérité** | Majeure |
|
||||
| **Fichier** | `infra/dev/.env.docker` |
|
||||
| **Règle violée** | Workspace `CLAUDE.md` : "Coltura — 8083 / 3003 / **5436**" |
|
||||
| **Règle violée** | Workspace `CLAUDE.md` : "Starseed — 8083 / 3003 / **5436**" |
|
||||
| **Confiance** | 75/100 |
|
||||
|
||||
**Constat** : Le fichier `.env.docker` définit `POSTGRES_PORT=5437`, alors que le port documenté pour Coltura est `5436`.
|
||||
**Constat** : Le fichier `.env.docker` définit `POSTGRES_PORT=5437`, alors que le port documenté pour Starseed est `5436`.
|
||||
|
||||
**Impact** : Tout développeur qui suit les ports documentés (ou qui utilise des scripts basés sur ces ports) ne pourra pas se connecter à la base.
|
||||
|
||||
@@ -93,7 +93,7 @@ Mais la seule page du module commercial est `frontend/modules/commercial/pages/c
|
||||
|---|---|
|
||||
| **Sévérité** | Majeure |
|
||||
| **Fichiers** | `frontend/nuxt.config.ts` (ligne 40), `docker-compose.yml` (ligne 33) |
|
||||
| **Règle violée** | Workspace `CLAUDE.md` : "Coltura — 8083 / **3003** / 5436" et `CLAUDE.md` projet : "make dev-nuxt # port 3003" |
|
||||
| **Règle violée** | Workspace `CLAUDE.md` : "Starseed — 8083 / **3003** / 5436" et `CLAUDE.md` projet : "make dev-nuxt # port 3003" |
|
||||
| **Confiance** | 75/100 (confirmé par 3 agents indépendants) |
|
||||
|
||||
**Constat** :
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ services:
|
||||
- "8083:80"
|
||||
volumes:
|
||||
- ./:/var/www/html:ro
|
||||
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/coltura.conf:ro
|
||||
- ./infra/dev/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
restart: unless-stopped
|
||||
db:
|
||||
image: postgres:16-alpine
|
||||
|
||||
@@ -13,7 +13,7 @@ Ce ticket livre la base RBAC backend de l'epic en 5 tickets en remplacant le sto
|
||||
- Faire evoluer `User` avec une relation ManyToMany vers `Role`, une relation ManyToMany vers `Permission` pour les permissions directes et un booleen `is_admin`.
|
||||
- Faire evoluer `User::getRoles()` pour rester compatible Symfony en retournant toujours `ROLE_USER` et `ROLE_ADMIN` si `is_admin = true`.
|
||||
- Ajouter `User::getEffectivePermissions()` pour retourner l'union des codes de permissions provenant des roles et des permissions directes.
|
||||
- Ajouter une methode statique `permissions()` sur `/home/matthieu/dev_malio/Coltura/src/Module/Core/CoreModule.php` et definir le pattern a reproduire pour les autres modules.
|
||||
- Ajouter une methode statique `permissions()` sur `/home/matthieu/dev_malio/Starseed/src/Module/Core/CoreModule.php` et definir le pattern a reproduire pour les autres modules.
|
||||
- Ajouter une commande console `app:sync-permissions` transactionnelle, idempotente et non destructive avec gestion `orphan`.
|
||||
- Ajouter une migration Doctrine modulaire Core qui cree les tables RBAC, migre les donnees depuis `user.roles`, cree les roles systeme `admin` et `user`, puis supprime la colonne JSON `roles`.
|
||||
- Mettre a jour les fixtures Core pour creer les roles systeme et rattacher l'utilisateur admin au role `admin`.
|
||||
@@ -31,30 +31,30 @@ Ce ticket livre la base RBAC backend de l'epic en 5 tickets en remplacant le sto
|
||||
|
||||
### Domaine - Entités
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Permission.php` : entite Doctrine de permission RBAC, code unique, module source et etat `orphan`.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Role.php` : entite Doctrine de role RBAC avec relations vers permissions et garde de role systeme.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Permission.php` : entite Doctrine de permission RBAC, code unique, module source et etat `orphan`.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Role.php` : entite Doctrine de role RBAC avec relations vers permissions et garde de role systeme.
|
||||
|
||||
### Domaine - Repositories
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Repository/PermissionRepositoryInterface.php` : contrat de lecture/ecriture des permissions pour la commande de sync et les fixtures.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Repository/RoleRepositoryInterface.php` : contrat de lecture/ecriture des roles pour migration fonctionnelle, fixtures et usages futurs.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Repository/PermissionRepositoryInterface.php` : contrat de lecture/ecriture des permissions pour la commande de sync et les fixtures.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Repository/RoleRepositoryInterface.php` : contrat de lecture/ecriture des roles pour migration fonctionnelle, fixtures et usages futurs.
|
||||
|
||||
### Domaine - Exceptions
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Exception/SystemRoleDeletionException.php` : exception domaine levee si une suppression vise un role systeme.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Exception/SystemRoleDeletionException.php` : exception domaine levee si une suppression vise un role systeme.
|
||||
|
||||
### Infrastructure - Doctrine
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/DoctrinePermissionRepository.php` : implementation Doctrine de `PermissionRepositoryInterface`.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/DoctrineRoleRepository.php` : implementation Doctrine de `RoleRepositoryInterface`.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/DoctrinePermissionRepository.php` : implementation Doctrine de `PermissionRepositoryInterface`.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/DoctrineRoleRepository.php` : implementation Doctrine de `RoleRepositoryInterface`.
|
||||
|
||||
### Infrastructure - Doctrine Migrations
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` : migration modulaire RBAC Core avec schema + migration de donnees + rollback minimal.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` : migration modulaire RBAC Core avec schema + migration de donnees + rollback minimal.
|
||||
|
||||
### Infrastructure - Console
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php` : commande `app:sync-permissions` qui scanne les modules actifs et synchronise la table `permission`.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php` : commande `app:sync-permissions` qui scanne les modules actifs et synchronise la table `permission`.
|
||||
|
||||
### Infrastructure - DataFixtures
|
||||
|
||||
@@ -62,12 +62,12 @@ Ce ticket livre la base RBAC backend de l'epic en 5 tickets en remplacant le sto
|
||||
|
||||
### Constantes domaine
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Security/SystemRoles.php` : constantes partagees `ADMIN_CODE = 'admin'` et `USER_CODE = 'user'`, utilisees a la fois par les fixtures et par la migration SQL. Place dans `Domain/Security/` (pas `ValueObject/` : ce n'est pas un VO, c'est un conteneur de constantes metier laissant de la place pour d'autres constantes de securite plus tard).
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Security/SystemRoles.php` : constantes partagees `ADMIN_CODE = 'admin'` et `USER_CODE = 'user'`, utilisees a la fois par les fixtures et par la migration SQL. Place dans `Domain/Security/` (pas `ValueObject/` : ce n'est pas un VO, c'est un conteneur de constantes metier laissant de la place pour d'autres constantes de securite plus tard).
|
||||
|
||||
## 4. Fichiers à modifier
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/User.php` : supprimer le stockage JSON `roles`, ajouter `isAdmin`, `roles`, `directPermissions`, initialiser les collections, configurer les relations ManyToMany en `fetch=EAGER`, ajouter `getEffectivePermissions()` et adapter `getRoles()` / mutateurs.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/CoreModule.php` : ajouter une methode statique `public static function permissions(): array` qui declare les permissions natives du module Core et sert de reference pour les autres modules. Contenu initial exact :
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/User.php` : supprimer le stockage JSON `roles`, ajouter `isAdmin`, `roles`, `directPermissions`, initialiser les collections, configurer les relations ManyToMany en `fetch=EAGER`, ajouter `getEffectivePermissions()` et adapter `getRoles()` / mutateurs.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/CoreModule.php` : ajouter une methode statique `public static function permissions(): array` qui declare les permissions natives du module Core et sert de reference pour les autres modules. Contenu initial exact :
|
||||
```php
|
||||
public static function permissions(): array
|
||||
{
|
||||
@@ -80,17 +80,17 @@ Ce ticket livre la base RBAC backend de l'epic en 5 tickets en remplacant le sto
|
||||
}
|
||||
```
|
||||
La cle `module` n'est PAS presente dans le payload : elle est auto-injectee par la commande de sync a partir de `CoreModule::ID`. Le code de permission doit obligatoirement commencer par `self::ID . '.'` sous peine d'echec de la sync (garde anti-typo).
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/DoctrineUserRepository.php` : aucun changement attendu dans ce ticket. Les nouvelles relations `$roles`, `$directPermissions` sont chargees par Doctrine via leurs mappings `fetch=EAGER` declares sur l'entite. Si les tests d'integration revelent un lazy-load non voulu au refresh JWT ou a la desserialisation, ajouter une methode `findForSecurity(string $username): ?User` avec `leftJoin` + `addSelect` explicites sur `roles`, `roles.permissions`, `directPermissions`, et brancher le user provider dessus. A trancher par les tests, pas en prevention.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Repository/UserRepositoryInterface.php` : aucun changement dans ce ticket. Ajout eventuel de `findForSecurity()` uniquement si le cas ci-dessus se materialise.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` : remplacer l'usage de `setRoles(array)` par la creation des roles systeme, le rattachement des utilisateurs a ces roles et le positionnement de `is_admin`.
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Console/CreateUserCommand.php` : remplacer la gestion historique de `ROLE_ADMIN` par `setIsAdmin(true)` et rattachement au role systeme `admin` si l'option `--admin` est conservee.
|
||||
- `/home/matthieu/dev_malio/Coltura/config/services.yaml` : ajouter 2 alias repository, aligne sur le pattern existant pour `UserRepositoryInterface` :
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/DoctrineUserRepository.php` : aucun changement attendu dans ce ticket. Les nouvelles relations `$roles`, `$directPermissions` sont chargees par Doctrine via leurs mappings `fetch=EAGER` declares sur l'entite. Si les tests d'integration revelent un lazy-load non voulu au refresh JWT ou a la desserialisation, ajouter une methode `findForSecurity(string $username): ?User` avec `leftJoin` + `addSelect` explicites sur `roles`, `roles.permissions`, `directPermissions`, et brancher le user provider dessus. A trancher par les tests, pas en prevention.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Repository/UserRepositoryInterface.php` : aucun changement dans ce ticket. Ajout eventuel de `findForSecurity()` uniquement si le cas ci-dessus se materialise.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` : remplacer l'usage de `setRoles(array)` par la creation des roles systeme, le rattachement des utilisateurs a ces roles et le positionnement de `is_admin`.
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Console/CreateUserCommand.php` : remplacer la gestion historique de `ROLE_ADMIN` par `setIsAdmin(true)` et rattachement au role systeme `admin` si l'option `--admin` est conservee.
|
||||
- `/home/matthieu/dev_malio/Starseed/config/services.yaml` : ajouter 2 alias repository, aligne sur le pattern existant pour `UserRepositoryInterface` :
|
||||
```yaml
|
||||
App\Module\Core\Domain\Repository\RoleRepositoryInterface: '@App\Module\Core\Infrastructure\Doctrine\DoctrineRoleRepository'
|
||||
App\Module\Core\Domain\Repository\PermissionRepositoryInterface: '@App\Module\Core\Infrastructure\Doctrine\DoctrinePermissionRepository'
|
||||
```
|
||||
La commande `SyncPermissionsCommand` est auto-configuree via `autoconfigure: true`, aucun binding manuel necessaire.
|
||||
- `/home/matthieu/dev_malio/Coltura/config/modules.php` : aucun changement de contenu requis, mais la commande `app:sync-permissions` devra s'appuyer sur ce fichier comme source de verite des modules actifs.
|
||||
- `/home/matthieu/dev_malio/Starseed/config/modules.php` : aucun changement de contenu requis, mais la commande `app:sync-permissions` devra s'appuyer sur ce fichier comme source de verite des modules actifs.
|
||||
|
||||
## 5. Schéma cible — mappings Doctrine
|
||||
|
||||
@@ -209,7 +209,7 @@ Etat final attendu :
|
||||
|
||||
## 6. Plan de migration Doctrine
|
||||
|
||||
La migration doit etre implementée dans `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` et executer `up()` dans cet ordre.
|
||||
La migration doit etre implementée dans `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` et executer `up()` dans cet ordre.
|
||||
|
||||
**Workflow recommande** :
|
||||
1. Ecrire d'abord les entites `Permission`, `Role` et la mutation de `User` (section 5).
|
||||
@@ -287,7 +287,7 @@ Cas couverts explicitement :
|
||||
Le mapping Doctrine actuel (`array` PHP → default) peut avoir genere une colonne `JSON` OU `TEXT` selon la version de Symfony/Doctrine. Le cast `::jsonb` fonctionne directement sur `JSON`, mais pas sur `TEXT`. **Avant d'executer la migration en prod**, verifier avec :
|
||||
|
||||
```bash
|
||||
docker exec -it db-coltura psql -U malio -d coltura -c '\d "user"'
|
||||
docker exec -it db-starseed psql -U malio -d starseed -c '\d "user"'
|
||||
```
|
||||
|
||||
- Si `roles | json` : le SQL ci-dessus fonctionne tel quel.
|
||||
@@ -306,11 +306,11 @@ Le rollback ne restitue pas la granularite RBAC complete, ce qui est acceptable
|
||||
|
||||
## 7. Algorithme sync-permissions
|
||||
|
||||
La commande `app:sync-permissions` doit vivre dans `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php` et encapsuler toute l'operation dans une transaction Doctrine unique.
|
||||
La commande `app:sync-permissions` doit vivre dans `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php` et encapsuler toute l'operation dans une transaction Doctrine unique.
|
||||
|
||||
### Source de verite
|
||||
|
||||
- Le scan des modules actifs vient de `/home/matthieu/dev_malio/Coltura/config/modules.php`.
|
||||
- Le scan des modules actifs vient de `/home/matthieu/dev_malio/Starseed/config/modules.php`.
|
||||
- Chaque classe module active peut exposer `public static function permissions(): array`.
|
||||
- Par compatibilite montante, si une classe module n'expose pas encore `permissions()`, elle est traitee comme retournant `[]`.
|
||||
|
||||
@@ -330,7 +330,7 @@ Garde anti-typo : le sync command verifie que chaque `code` commence obligatoire
|
||||
```text
|
||||
begin transaction
|
||||
|
||||
load active module classes from /home/matthieu/dev_malio/Coltura/config/modules.php
|
||||
load active module classes from /home/matthieu/dev_malio/Starseed/config/modules.php
|
||||
desired_permissions = empty map keyed by code
|
||||
|
||||
for each module class:
|
||||
@@ -439,7 +439,7 @@ Repasse `orphan` a `false` et remet a jour les metadonnees issues de la declarat
|
||||
|
||||
## 9. Fixtures mises à jour
|
||||
|
||||
Le fichier cible reste `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php`.
|
||||
Le fichier cible reste `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php`.
|
||||
|
||||
### Principe cle : decouplage via `is_admin`
|
||||
|
||||
@@ -519,7 +519,7 @@ Les tests d'integration migration up/down exigent une base de test dediee avec u
|
||||
- Risque de perte de donnees pendant la suppression de la colonne `user.roles`.
|
||||
- Mitigation : creer les roles systeme et inserer les jointures `user_role` avant tout `DROP COLUMN`, avec tests de migration sur etats mixtes.
|
||||
- Risque de divergence entre migration SQL brute et fixtures sur les codes des roles systeme.
|
||||
- Mitigation : centraliser `admin` et `user` dans `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Security/SystemRoles.php` et documenter que la migration doit reprendre ces valeurs telles quelles.
|
||||
- Mitigation : centraliser `admin` et `user` dans `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Security/SystemRoles.php` et documenter que la migration doit reprendre ces valeurs telles quelles.
|
||||
- Risque d'accumulation de permissions orphelines sur des environnements de dev ou apres refactors de codes.
|
||||
- Mitigation : conserver `orphan = true` pour la non-destruction, mais ajouter un suivi explicite dans les tests et dans la documentation d'exploitation; une strategie de purge pourra etre traitee plus tard si necessaire.
|
||||
- Risque de sync incoherente entre dev et prod si un module actif ne declare pas encore `permissions()`.
|
||||
@@ -533,12 +533,12 @@ Les tests d'integration migration up/down exigent une base de test dediee avec u
|
||||
|
||||
1. Creer `Permission`, `Role`, `SystemRoleDeletionException` et `SystemRoles`.
|
||||
2. Creer `PermissionRepositoryInterface`, `RoleRepositoryInterface` et leurs implementations Doctrine.
|
||||
3. Faire evoluer `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/User.php` avec `is_admin`, `roles`, `directPermissions`, `getRoles()` et `getEffectivePermissions()`.
|
||||
3. Faire evoluer `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/User.php` avec `is_admin`, `roles`, `directPermissions`, `getRoles()` et `getEffectivePermissions()`.
|
||||
4. Ajouter `CoreModule::permissions()` et documenter le pattern de declaration statique pour les autres modules.
|
||||
5. Ajouter la commande `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php`.
|
||||
6. Ecrire la migration `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` avec schema + migration de donnees + down().
|
||||
7. Mettre a jour `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` et `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Console/CreateUserCommand.php`.
|
||||
8. Ajouter les alias repository dans `/home/matthieu/dev_malio/Coltura/config/services.yaml`.
|
||||
5. Ajouter la commande `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Console/SyncPermissionsCommand.php`.
|
||||
6. Ecrire la migration `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/Migrations/Version<timestamp>.php` avec schema + migration de donnees + down().
|
||||
7. Mettre a jour `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` et `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Console/CreateUserCommand.php`.
|
||||
8. Ajouter les alias repository dans `/home/matthieu/dev_malio/Starseed/config/services.yaml`.
|
||||
9. Ecrire les tests unitaires et d'integration couvrant domaine, sync, fixtures et migration.
|
||||
|
||||
## 13. Critères d'acceptation (DoD)
|
||||
@@ -553,4 +553,4 @@ Les tests d'integration migration up/down exigent une base de test dediee avec u
|
||||
- La suppression d'un role systeme leve `SystemRoleDeletionException` au niveau domaine.
|
||||
- Les associations `User::$roles`, `User::$directPermissions` et `Role::$permissions` sont explicitement configurees en `fetch=EAGER` et ce point est verifie par tests.
|
||||
- Les fixtures attribuent `is_admin = true` + role `admin` a l'utilisateur `admin`, et le role `user` aux utilisateurs standards.
|
||||
- Le spec est compatible avec l'architecture modulaire actuelle basee sur `/home/matthieu/dev_malio/Coltura/config/modules.php` et n'introduit aucune resource API Platform ni voter dans ce ticket.
|
||||
- Le spec est compatible avec l'architecture modulaire actuelle basee sur `/home/matthieu/dev_malio/Starseed/config/modules.php` et n'introduit aucune resource API Platform ni voter dans ce ticket.
|
||||
|
||||
@@ -38,28 +38,28 @@ Le ticket n'introduit **aucune logique d'autorisation metier** : toute la verifi
|
||||
|
||||
### Infrastructure - Processors
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/RoleProcessor.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/RoleProcessor.php`
|
||||
Decorator de `ApiPlatform\Doctrine\Common\State\PersistProcessor` et `RemoveProcessor`. Charge de la garde `ensureDeletable()` et de la protection des champs immuables sur un role systeme.
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php`
|
||||
Decorator de `PersistProcessor` specifique a l'operation `PATCH /api/users/{id}/rbac`. Persiste les mutations `isAdmin`, `roles`, `directPermissions` sans passer par `UserPasswordHasherProcessor`.
|
||||
|
||||
### Tests unitaires
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/RoleProcessorTest.php`
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessorTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/RoleProcessorTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessorTest.php`
|
||||
|
||||
### Tests fonctionnels
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Api/PermissionApiTest.php`
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Api/RoleApiTest.php`
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Api/UserRbacApiTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Api/PermissionApiTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Api/RoleApiTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Api/UserRbacApiTest.php`
|
||||
|
||||
## 4. Fichiers a modifier
|
||||
|
||||
### Entite `Permission`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Permission.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Permission.php`
|
||||
|
||||
- Ajouter l'attribut `#[ApiResource]` avec operations `GetCollection` + `Get` uniquement.
|
||||
- Normalization context : groupe `permission:read` uniquement.
|
||||
@@ -89,7 +89,7 @@ Extrait attendu :
|
||||
|
||||
### Entite `Role`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Role.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Role.php`
|
||||
|
||||
- Ajouter l'attribut `#[ApiResource]` avec operations `GetCollection`, `Get`, `Post`, `Patch`, `Delete`.
|
||||
- Normalization context : `role:read`. Denormalization context : `role:write`.
|
||||
@@ -107,7 +107,7 @@ Extrait attendu :
|
||||
|
||||
### Entite `User`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/User.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/User.php`
|
||||
|
||||
- Ajouter dans la liste des operations `ApiResource` existantes une operation dediee :
|
||||
|
||||
|
||||
@@ -39,50 +39,50 @@ A l'issue de ce ticket, l'application dispose d'un systeme d'autorisation applic
|
||||
|
||||
### Domaine - Securite
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Security/AdminHeadcountGuard.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Security/AdminHeadcountGuard.php`
|
||||
Service domaine encapsulant l'invariant "au moins un admin reste apres l'operation". Depend uniquement de `UserRepositoryInterface::countAdmins()`. Aucune dependance infrastructure, testable en isolation.
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Exception/LastAdminProtectionException.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Exception/LastAdminProtectionException.php`
|
||||
Exception metier levee par le guard. Traduite en `BadRequestHttpException` (400) dans les processors.
|
||||
|
||||
### Infrastructure - Security
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Security/PermissionVoter.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Security/PermissionVoter.php`
|
||||
Voter Symfony etendant `Symfony\Component\Security\Core\Authorization\Voter\Voter`. Decouvert automatiquement par `autoconfigure: true`.
|
||||
|
||||
### Infrastructure - Processors
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserProcessor.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserProcessor.php`
|
||||
Decorateur de `RemoveProcessor` cible sur `DELETE /api/users/{id}`. Appelle `AdminHeadcountGuard` avant de deleguer. Meme pattern qu'`UserRbacProcessor`/`RoleProcessor` : `final class`, `#[Autowire]` sur l'inner, `LogicException` fail-fast si le type entrant n'est pas `User`.
|
||||
|
||||
### Frontend - Composable
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/frontend/shared/composables/usePermissions.ts`
|
||||
- `/home/matthieu/dev_malio/Starseed/frontend/shared/composables/usePermissions.ts`
|
||||
Composable stateless qui lit `useAuthStore().user`. Pas de fetch propre, pas de reset (le cycle de vie est porte par l'auth store).
|
||||
|
||||
### Tests unitaires PHP
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Infrastructure/Security/PermissionVoterTest.php`
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Domain/Security/AdminHeadcountGuardTest.php`
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserProcessorTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Infrastructure/Security/PermissionVoterTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Domain/Security/AdminHeadcountGuardTest.php`
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserProcessorTest.php`
|
||||
|
||||
### Tests fonctionnels PHP
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Api/MeApiTest.php` (si absent — sinon extension)
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Api/MeApiTest.php` (si absent — sinon extension)
|
||||
Couvre l'enrichissement du payload `/api/me`.
|
||||
- `/home/matthieu/dev_malio/Coltura/tests/Module/Core/Api/UserApiTest.php` (si absent — sinon extension)
|
||||
- `/home/matthieu/dev_malio/Starseed/tests/Module/Core/Api/UserApiTest.php` (si absent — sinon extension)
|
||||
Couvre la garde "dernier admin global" sur `DELETE /api/users/{id}`.
|
||||
|
||||
### Tests frontend
|
||||
|
||||
- `/home/matthieu/dev_malio/Coltura/frontend/shared/composables/__tests__/usePermissions.test.ts`
|
||||
- `/home/matthieu/dev_malio/Starseed/frontend/shared/composables/__tests__/usePermissions.test.ts`
|
||||
Vitest. Emplacement a adapter si le projet Nuxt a une autre convention (colocalise avec un fichier `.spec.ts`, ou repertoire `tests/`). A verifier au debut de la task frontend.
|
||||
|
||||
## 4. Fichiers a modifier
|
||||
|
||||
### `CoreModule.php`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/CoreModule.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/CoreModule.php`
|
||||
|
||||
Ajouter une cinquieme entree au catalogue :
|
||||
|
||||
@@ -103,7 +103,7 @@ La commande `app:sync-permissions` creera automatiquement `core.roles.view` a la
|
||||
|
||||
### Entite `Permission`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Permission.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Permission.php`
|
||||
|
||||
Remplacer les 2 gardes placeholder :
|
||||
|
||||
@@ -122,7 +122,7 @@ Supprimer les commentaires `// TODO ticket #345`.
|
||||
|
||||
### Entite `Role`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/Role.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/Role.php`
|
||||
|
||||
Remplacer les 5 gardes placeholder :
|
||||
|
||||
@@ -136,7 +136,7 @@ Supprimer les commentaires `// TODO ticket #345`.
|
||||
|
||||
### Entite `User`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Entity/User.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Entity/User.php`
|
||||
|
||||
Remplacer les 6 gardes `ROLE_ADMIN` restantes :
|
||||
|
||||
@@ -172,7 +172,7 @@ Supprimer tous les commentaires `// TODO ticket #345` rencontres.
|
||||
|
||||
### `UserRepositoryInterface`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Domain/Repository/UserRepositoryInterface.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Domain/Repository/UserRepositoryInterface.php`
|
||||
|
||||
Ajouter la methode :
|
||||
|
||||
@@ -187,7 +187,7 @@ public function countAdmins(): int;
|
||||
|
||||
### `DoctrineUserRepository`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/Doctrine/DoctrineUserRepository.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/Doctrine/DoctrineUserRepository.php`
|
||||
|
||||
Implementer `countAdmins()` via un `QueryBuilder` simple :
|
||||
|
||||
@@ -204,7 +204,7 @@ public function countAdmins(): int
|
||||
|
||||
### `UserRbacProcessor`
|
||||
|
||||
`/home/matthieu/dev_malio/Coltura/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php`
|
||||
`/home/matthieu/dev_malio/Starseed/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php`
|
||||
|
||||
Ajouter la dependance `AdminHeadcountGuard` et l'invoquer **apres** la garde auto-suicide existante, **avant** de deleguer au persist processor. Supprimer le `TODO ticket #345` du docblock.
|
||||
|
||||
|
||||
@@ -10,14 +10,14 @@ Le resultat attendu est un socle de persistance activable par tenant via `config
|
||||
|
||||
### IN
|
||||
|
||||
- Creer le module `/home/m-tristan/workspace/Coltura/src/Module/Sites/SitesModule.php` avec `ID = 'sites'`, `LABEL = 'Sites'`, `REQUIRED = false`, et une methode statique `permissions()` declarant les deux codes RBAC `sites.view` et `sites.manage`.
|
||||
- Creer le module `/home/m-tristan/workspace/Starseed/src/Module/Sites/SitesModule.php` avec `ID = 'sites'`, `LABEL = 'Sites'`, `REQUIRED = false`, et une methode statique `permissions()` declarant les deux codes RBAC `sites.view` et `sites.manage`.
|
||||
- Creer l'entite Doctrine `Site` avec `id`, `name` (unique), `city`, `postalCode`, `color`, `fullAddress`, `createdAt`, `updatedAt` et les contraintes de validation applicatives associees (NotBlank, Length, Regex hex `#RRGGBB`, Regex CP FR `^\d{5}$`, UniqueEntity).
|
||||
- Creer l'interface `SiteRepositoryInterface` et son implementation Doctrine `DoctrineSiteRepository`, avec un contrat CRUD complet (`findById`, `findByName`, `findAllOrderedByName`, `save`, `remove`) en anticipation du ticket 2.
|
||||
- Creer une migration Doctrine creant la table `site` avec son index unique `uniq_site_name`. La migration est placee dans `/home/m-tristan/workspace/Coltura/migrations/` au namespace racine `DoctrineMigrations` conformement a l'exception documentee dans `CLAUDE.md` (bug de tri alphabetique des migrations multi-namespaces dans Doctrine Migrations 3.x).
|
||||
- Creer une migration Doctrine creant la table `site` avec son index unique `uniq_site_name`. La migration est placee dans `/home/m-tristan/workspace/Starseed/migrations/` au namespace racine `DoctrineMigrations` conformement a l'exception documentee dans `CLAUDE.md` (bug de tri alphabetique des migrations multi-namespaces dans Doctrine Migrations 3.x).
|
||||
- Creer `SitesFixtures` creant trois sites de demonstration : `Chatellerault` (`#056CF2`), `Saint-Jean` (`#10B981`), `Pommevic` (`#F59E0B`). Fixtures idempotentes via lookup par nom lorsque le purger Doctrine est desactive.
|
||||
- Enregistrer `SitesModule::class` dans `/home/m-tristan/workspace/Coltura/config/modules.php` pour l'activer par defaut.
|
||||
- Declarer le mapping Doctrine du module dans `/home/m-tristan/workspace/Coltura/config/packages/doctrine.yaml` (inconditionnel, le mapping reste charge meme si le module est retire de `modules.php`).
|
||||
- Enregistrer l'alias service `SiteRepositoryInterface → DoctrineSiteRepository` dans `/home/m-tristan/workspace/Coltura/config/services.yaml`.
|
||||
- Enregistrer `SitesModule::class` dans `/home/m-tristan/workspace/Starseed/config/modules.php` pour l'activer par defaut.
|
||||
- Declarer le mapping Doctrine du module dans `/home/m-tristan/workspace/Starseed/config/packages/doctrine.yaml` (inconditionnel, le mapping reste charge meme si le module est retire de `modules.php`).
|
||||
- Enregistrer l'alias service `SiteRepositoryInterface → DoctrineSiteRepository` dans `/home/m-tristan/workspace/Starseed/config/services.yaml`.
|
||||
- Ajouter deux suites de tests PHPUnit :
|
||||
- `SiteTest` (pure `TestCase`) pour le comportement de l'entite (constructeur, getters/setters, lifecycle `PreUpdate`).
|
||||
- `SiteValidationTest` (`KernelTestCase`) pour la validation complete : regex hex, regex CP FR, NotBlank, Length, UniqueEntity via Doctrine.
|
||||
@@ -25,7 +25,7 @@ Le resultat attendu est un socle de persistance activable par tenant via `config
|
||||
### OUT
|
||||
|
||||
- Ticket `#02` : relation `User ↔ Site` (FK ou ManyToMany selon decision UX), expose les sites de l'utilisateur courant via `/api/me` et propage l'autorisation au niveau des ressources decoupees par site.
|
||||
- Ticket `#03` : integration dans la navbar Coltura (selecteur de site actif, persistance du choix cote front, consommation du flux issu du ticket 2).
|
||||
- Ticket `#03` : integration dans la navbar Starseed (selecteur de site actif, persistance du choix cote front, consommation du flux issu du ticket 2).
|
||||
- Ticket `#04` : ecran d'administration CRUD des sites (page admin/sites, DataTable, drawer creation/edition, modale suppression, API Platform `Site` resource avec voters RBAC).
|
||||
- Gestion des soft-deletes sur `Site` : non introduite dans ce ticket.
|
||||
- Rattachement historique ou audit trail des modifications : hors scope.
|
||||
@@ -34,38 +34,38 @@ Le resultat attendu est un socle de persistance activable par tenant via `config
|
||||
|
||||
### Domaine — Entité
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Entity/Site.php` : entite Doctrine porteuse des attributs metier (nom unique, ville, code postal FR, couleur hex, adresse complete multi-ligne) et des timestamps auto-maintenus via lifecycle callbacks.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Domain/Entity/Site.php` : entite Doctrine porteuse des attributs metier (nom unique, ville, code postal FR, couleur hex, adresse complete multi-ligne) et des timestamps auto-maintenus via lifecycle callbacks.
|
||||
|
||||
### Domaine — Repository
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php` : contrat d'acces domaine a l'entite Site (CRUD applicatif ; l'acces API Platform du ticket 4 utilisera le provider Doctrine par defaut).
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Domain/Repository/SiteRepositoryInterface.php` : contrat d'acces domaine a l'entite Site (CRUD applicatif ; l'acces API Platform du ticket 4 utilisera le provider Doctrine par defaut).
|
||||
|
||||
### Infrastructure — Doctrine
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/Doctrine/DoctrineSiteRepository.php` : implementation Doctrine de `SiteRepositoryInterface` basee sur `ServiceEntityRepository`.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/Doctrine/DoctrineSiteRepository.php` : implementation Doctrine de `SiteRepositoryInterface` basee sur `ServiceEntityRepository`.
|
||||
|
||||
### Infrastructure — Migration
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/migrations/Version<timestamp>.php` : migration racine (namespace `DoctrineMigrations`) qui cree la table `site` et son index unique. Emplacement racine et non modulaire, cf. exception documentee dans `CLAUDE.md` (bug Doctrine 3.x sur le tri alphabetique des migrations multi-namespaces).
|
||||
- `/home/m-tristan/workspace/Starseed/migrations/Version<timestamp>.php` : migration racine (namespace `DoctrineMigrations`) qui cree la table `site` et son index unique. Emplacement racine et non modulaire, cf. exception documentee dans `CLAUDE.md` (bug Doctrine 3.x sur le tri alphabetique des migrations multi-namespaces).
|
||||
|
||||
### Infrastructure — DataFixtures
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/DataFixtures/SitesFixtures.php` : fixture Doctrine seedant les 3 sites de demonstration. Ne declare pas de `DependentFixtureInterface` (aucune dependance a AppFixtures dans ce ticket).
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/DataFixtures/SitesFixtures.php` : fixture Doctrine seedant les 3 sites de demonstration. Ne declare pas de `DependentFixtureInterface` (aucune dependance a AppFixtures dans ce ticket).
|
||||
|
||||
### Module — Declaration
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/SitesModule.php` : marker class du module avec `ID`, `LABEL`, `REQUIRED` et `permissions()`. Meme pattern que `CoreModule`.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/SitesModule.php` : marker class du module avec `ID`, `LABEL`, `REQUIRED` et `permissions()`. Meme pattern que `CoreModule`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Domain/Entity/SiteTest.php` : tests unitaires purs (`TestCase`) couvrant constructeur, getters, setters et lifecycle `PreUpdate`.
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Domain/Entity/SiteValidationTest.php` : tests de validation (`KernelTestCase`) couvrant regex hex, regex CP FR, NotBlank, Length sur tous les champs, et `UniqueEntity` via la DB de test.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Domain/Entity/SiteTest.php` : tests unitaires purs (`TestCase`) couvrant constructeur, getters, setters et lifecycle `PreUpdate`.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Domain/Entity/SiteValidationTest.php` : tests de validation (`KernelTestCase`) couvrant regex hex, regex CP FR, NotBlank, Length sur tous les champs, et `UniqueEntity` via la DB de test.
|
||||
|
||||
## 4. Fichiers à modifier
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/config/modules.php` : ajouter `App\Module\Sites\SitesModule::class` dans le tableau de retour. Le module est actif par defaut. Le commenter suffit a le desactiver sans autre intervention (les permissions deviendront orphelines a la prochaine sync mais la table reste).
|
||||
- `/home/m-tristan/workspace/Coltura/config/packages/doctrine.yaml` : ajouter une mapping `Sites:` alignee sur le pattern du module `Core:`. Le mapping est inconditionnel : il reste declare meme si `SitesModule::class` est retire de `modules.php`. Le commentaire doit etre explicite sur cette decoupe (activation fonctionnelle via `modules.php`, structure DB via la mapping Doctrine).
|
||||
- `/home/m-tristan/workspace/Coltura/config/services.yaml` : ajouter l'alias `App\Module\Sites\Domain\Repository\SiteRepositoryInterface` → `App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository`. Pattern aligne sur les trois aliases Core existants.
|
||||
- `/home/m-tristan/workspace/Starseed/config/modules.php` : ajouter `App\Module\Sites\SitesModule::class` dans le tableau de retour. Le module est actif par defaut. Le commenter suffit a le desactiver sans autre intervention (les permissions deviendront orphelines a la prochaine sync mais la table reste).
|
||||
- `/home/m-tristan/workspace/Starseed/config/packages/doctrine.yaml` : ajouter une mapping `Sites:` alignee sur le pattern du module `Core:`. Le mapping est inconditionnel : il reste declare meme si `SitesModule::class` est retire de `modules.php`. Le commentaire doit etre explicite sur cette decoupe (activation fonctionnelle via `modules.php`, structure DB via la mapping Doctrine).
|
||||
- `/home/m-tristan/workspace/Starseed/config/services.yaml` : ajouter l'alias `App\Module\Sites\Domain\Repository\SiteRepositoryInterface` → `App\Module\Sites\Infrastructure\Doctrine\DoctrineSiteRepository`. Pattern aligne sur les trois aliases Core existants.
|
||||
|
||||
## 5. Schéma cible — mapping Doctrine
|
||||
|
||||
@@ -152,7 +152,7 @@ Sites:
|
||||
|
||||
## 6. Plan de migration Doctrine
|
||||
|
||||
La migration est placee dans `/home/m-tristan/workspace/Coltura/migrations/Version<timestamp>.php` au namespace racine `DoctrineMigrations`, conformement a l'exception documentee dans `CLAUDE.md`. Tant que le bug de tri alphabetique des `MigrationsComparator` multi-namespaces n'est pas resolu (via un comparator custom ou un upgrade Doctrine), toute migration d'initialisation (creation de table sur base vide) reste au namespace racine.
|
||||
La migration est placee dans `/home/m-tristan/workspace/Starseed/migrations/Version<timestamp>.php` au namespace racine `DoctrineMigrations`, conformement a l'exception documentee dans `CLAUDE.md`. Tant que le bug de tri alphabetique des `MigrationsComparator` multi-namespaces n'est pas resolu (via un comparator custom ou un upgrade Doctrine), toute migration d'initialisation (creation de table sur base vide) reste au namespace racine.
|
||||
|
||||
### `up()` — ordre des instructions
|
||||
|
||||
@@ -289,7 +289,7 @@ Trois sites de demonstration, avec des couleurs distinctes suffisamment contrast
|
||||
|
||||
| Nom | Ville | CP | Couleur | Commentaire |
|
||||
|-----|-------|-----|---------|-------------|
|
||||
| Chatellerault | Chatellerault | 86100 | `#056CF2` | Couleur imposee par le ticket (bleu Coltura). |
|
||||
| Chatellerault | Chatellerault | 86100 | `#056CF2` | Couleur imposee par le ticket (bleu Starseed). |
|
||||
| Saint-Jean | Saint-Jean-de-Sauves | 86330 | `#10B981` | Vert emeraude (contraste avec le bleu). |
|
||||
| Pommevic | Pommevic | 82400 | `#F59E0B` | Ambre (troisieme teinte nettement distincte). |
|
||||
|
||||
|
||||
@@ -40,70 +40,70 @@ Le resultat attendu est un module Sites utilisable de bout en bout cote admin (c
|
||||
|
||||
### Backend — Module Sites
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Exception/SiteNotAuthorizedException.php` : exception domaine levee si un user tente de switcher vers un site qui ne fait pas partie de ses sites autorises. Porte un message i18n-able et le code du site cible.
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/ApiPlatform/Resource/CurrentSiteResource.php` : ressource API Platform **virtuelle** (pas de mapping Doctrine, pas de `#[ORM\Entity]`). Sert uniquement a porter l'operation `Patch` `/me/current-site`. Expose une propriete `site: Site` en denormalisation pour recevoir l'IRI du site cible, et re-expose l'user courant en normalisation via le groupe `me:read`.
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/ApiPlatform/State/Processor/CurrentSiteProcessor.php` : processor dedie a l'operation de switch. Valide l'appartenance du site aux `user.sites`, positionne `user.currentSite`, flush, retourne l'user.
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/ApiPlatform/EventListener/SiteNotAuthorizedExceptionListener.php` : listener Kernel qui convertit `SiteNotAuthorizedException` en `ForbiddenHttpException` (403) avec un code i18n stable (cf. pattern `SystemRoleDeletionException` du module Core dans les tickets RBAC precedents).
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Domain/Exception/SiteNotAuthorizedException.php` : exception domaine levee si un user tente de switcher vers un site qui ne fait pas partie de ses sites autorises. Porte un message i18n-able et le code du site cible.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/ApiPlatform/Resource/CurrentSiteResource.php` : ressource API Platform **virtuelle** (pas de mapping Doctrine, pas de `#[ORM\Entity]`). Sert uniquement a porter l'operation `Patch` `/me/current-site`. Expose une propriete `site: Site` en denormalisation pour recevoir l'IRI du site cible, et re-expose l'user courant en normalisation via le groupe `me:read`.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/ApiPlatform/State/Processor/CurrentSiteProcessor.php` : processor dedie a l'operation de switch. Valide l'appartenance du site aux `user.sites`, positionne `user.currentSite`, flush, retourne l'user.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/ApiPlatform/EventListener/SiteNotAuthorizedExceptionListener.php` : listener Kernel qui convertit `SiteNotAuthorizedException` en `ForbiddenHttpException` (403) avec un code i18n stable (cf. pattern `SystemRoleDeletionException` du module Core dans les tickets RBAC precedents).
|
||||
|
||||
### Backend — Migration
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/migrations/Version<timestamp2>.php` : migration au namespace racine `DoctrineMigrations` (cf. exception Doctrine documentee dans `CLAUDE.md`). Cree la table `user_site` et la colonne `user.current_site_id` avec les FKs et cascades appropriees.
|
||||
- `/home/m-tristan/workspace/Starseed/migrations/Version<timestamp2>.php` : migration au namespace racine `DoctrineMigrations` (cf. exception Doctrine documentee dans `CLAUDE.md`). Cree la table `user_site` et la colonne `user.current_site_id` avec les FKs et cascades appropriees.
|
||||
|
||||
### Backend — Tests API
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Api/SiteApiTest.php` : CRUD complet `/api/sites` avec matrices RBAC (admin, user avec `sites.view`, user avec `sites.manage`, user sans permission).
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Api/CurrentSiteSwitchApiTest.php` : PATCH `/me/current-site` (OK avec site autorise, 403 avec site non autorise, 400 avec IRI invalide).
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Api/MeEndpointSitesTest.php` : `/api/me` expose bien `sites` et `currentSite` en objets. User sans site : `sites: []`, `currentSite: null`.
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Api/SiteCascadeTest.php` : suppression d'un site `X` → toutes les lignes `user_site` referencant `X` sont supprimees, tous les users ayant `X` en `currentSite` voient leur `currentSite` repasser a `NULL`.
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Core/Api/UserRbacSitesApiTest.php` : extension du endpoint `/api/users/{id}/rbac` — ajout de `sites: []` dans le payload, retrait du `currentSite` quand le site retire etait le courant.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Api/SiteApiTest.php` : CRUD complet `/api/sites` avec matrices RBAC (admin, user avec `sites.view`, user avec `sites.manage`, user sans permission).
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Api/CurrentSiteSwitchApiTest.php` : PATCH `/me/current-site` (OK avec site autorise, 403 avec site non autorise, 400 avec IRI invalide).
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Api/MeEndpointSitesTest.php` : `/api/me` expose bien `sites` et `currentSite` en objets. User sans site : `sites: []`, `currentSite: null`.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Api/SiteCascadeTest.php` : suppression d'un site `X` → toutes les lignes `user_site` referencant `X` sont supprimees, tous les users ayant `X` en `currentSite` voient leur `currentSite` repasser a `NULL`.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Core/Api/UserRbacSitesApiTest.php` : extension du endpoint `/api/users/{id}/rbac` — ajout de `sites: []` dans le payload, retrait du `currentSite` quand le site retire etait le courant.
|
||||
|
||||
### Frontend — Module Sites (nouveau layer)
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/nuxt.config.ts` : marker de layer Nuxt (vide). Declenche l'auto-detection par `nuxt.config.ts` racine.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/pages/admin/sites.vue` : page `/admin/sites`. Reutilise les composants Malio UI (`MalioDataTable`, `MalioButton`, `MalioInputText`, `MalioInputTextArea`). Pattern identique a `frontend/modules/core/pages/admin/roles.vue`.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/components/SiteDrawer.vue` : drawer creation/edition. Formulaire 5 champs (nom, ville, CP, couleur avec preview puce, adresse). Valide cote front sur le submit avant d'envoyer.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/components/SiteDeleteModal.vue` : modale de confirmation suppression. Pattern aligne sur `RoleDeleteModal.vue`.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/nuxt.config.ts` : marker de layer Nuxt (vide). Declenche l'auto-detection par `nuxt.config.ts` racine.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/pages/admin/sites.vue` : page `/admin/sites`. Reutilise les composants Malio UI (`MalioDataTable`, `MalioButton`, `MalioInputText`, `MalioInputTextArea`). Pattern identique a `frontend/modules/core/pages/admin/roles.vue`.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/components/SiteDrawer.vue` : drawer creation/edition. Formulaire 5 champs (nom, ville, CP, couleur avec preview puce, adresse). Valide cote front sur le submit avant d'envoyer.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/components/SiteDeleteModal.vue` : modale de confirmation suppression. Pattern aligne sur `RoleDeleteModal.vue`.
|
||||
|
||||
### Frontend — Types partages
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/types/sites.ts` : types `Site`, `SiteInput`. Pattern identique a `frontend/shared/types/rbac.ts`.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/types/sites.ts` : types `Site`, `SiteInput`. Pattern identique a `frontend/shared/types/rbac.ts`.
|
||||
|
||||
### Tests frontend (optionnels mais recommandes)
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/pages/admin/sites.spec.ts` : smoke test Vitest (rendu + clic bouton "Nouveau site" ouvre le drawer).
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/pages/admin/sites.spec.ts` : smoke test Vitest (rendu + clic bouton "Nouveau site" ouvre le drawer).
|
||||
|
||||
## 4. Fichiers à modifier
|
||||
|
||||
### Backend — Module Core
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Core/Domain/Entity/User.php` :
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Core/Domain/Entity/User.php` :
|
||||
- Ajouter `private Collection $sites;` (M2M, `fetch: EAGER`, `JoinTable: user_site`), groupes `me:read`, `user:list`, `user:rbac:read`, `user:rbac:write`.
|
||||
- Ajouter `private ?Site $currentSite = null;` (M2O, `fetch: EAGER`, `onDelete: 'SET NULL'`), groupe `me:read`.
|
||||
- Initialiser `$this->sites = new ArrayCollection();` dans le constructeur.
|
||||
- Ajouter les accesseurs `getSites()`, `addSite(Site)`, `removeSite(Site)`, `hasSite(Site)`, `getCurrentSite()`, `setCurrentSite(?Site)`.
|
||||
- **Important** : `import` direct `App\Module\Sites\Domain\Entity\Site`. Ce ticket assume le couplage Core → Sites au niveau code PHP (cf. Risque 1).
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php` :
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Core/Infrastructure/ApiPlatform/State/Processor/UserRbacProcessor.php` :
|
||||
- Etendre le contrat d'entree pour accepter le champ `sites` (collection d'IRIs denormalisees en `Collection<Site>`).
|
||||
- Apres l'application des roles et permissions directes, detecter si `currentSite` du user cible n'est plus dans la nouvelle collection `sites` → basculer `currentSite` a `null`.
|
||||
- Conserver toutes les gardes existantes (auto-suicide admin, dernier admin global).
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` :
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Core/Infrastructure/DataFixtures/AppFixtures.php` :
|
||||
- Declarer l'implementation `DependentFixtureInterface` avec `getDependencies(): [SitesFixtures::class]` (inversion de l'ordre actuel : AppFixtures doit tourner **apres** SitesFixtures pour pouvoir reference les sites).
|
||||
- Rattacher chaque user a au moins un site : `admin` a tous les sites (`Chatellerault`, `Saint-Jean`, `Pommevic`), `alice` a `Chatellerault`, `bob` a `Saint-Jean`.
|
||||
- Positionner `currentSite` : `admin.currentSite = Chatellerault`, `alice.currentSite = Chatellerault`, `bob.currentSite = Saint-Jean`.
|
||||
|
||||
### Backend — Module Sites
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Domain/Entity/Site.php` :
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Domain/Entity/Site.php` :
|
||||
- Ajouter les attributs `#[ApiResource]` + operations (cf. section 5 Schema).
|
||||
- Ajouter les groupes de serialisation `site:read`, `site:write`, `me:read` sur les proprietes scalaires.
|
||||
- Ajouter la relation inverse `private Collection $users;` (M2M mappedBy=`sites`), **sans** groupe de serialisation (pas d'exposition API cote Site).
|
||||
- Initialiser `$this->users = new ArrayCollection();` dans le constructeur.
|
||||
- Ajouter les accesseurs `getUsers()` pour les besoins metier (count / cascade manuel si besoin).
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/DataFixtures/SitesFixtures.php` : aucun changement de contenu, mais verifier que la fixture n'est plus en bout de chaine de dependance (AppFixtures depend d'elle maintenant).
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/DataFixtures/SitesFixtures.php` : aucun changement de contenu, mais verifier que la fixture n'est plus en bout de chaine de dependance (AppFixtures depend d'elle maintenant).
|
||||
|
||||
### Backend — Configuration
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/config/sidebar.php` : inserer l'entree `Sites` dans la section `sidebar.general.section` entre `sidebar.core.users` et `sidebar.general.logout` :
|
||||
- `/home/m-tristan/workspace/Starseed/config/sidebar.php` : inserer l'entree `Sites` dans la section `sidebar.general.section` entre `sidebar.core.users` et `sidebar.general.logout` :
|
||||
```php
|
||||
[
|
||||
'label' => 'sidebar.core.sites',
|
||||
@@ -113,18 +113,18 @@ Le resultat attendu est un module Sites utilisable de bout en bout cote admin (c
|
||||
'permission' => 'sites.view',
|
||||
],
|
||||
```
|
||||
- `/home/m-tristan/workspace/Coltura/config/services.yaml` : aucun changement requis. `CurrentSiteProcessor`, `SiteNotAuthorizedExceptionListener` sont autoconfigures.
|
||||
- `/home/m-tristan/workspace/Starseed/config/services.yaml` : aucun changement requis. `CurrentSiteProcessor`, `SiteNotAuthorizedExceptionListener` sont autoconfigures.
|
||||
|
||||
### Frontend
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/core/components/UserRbacDrawer.vue` :
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/core/components/UserRbacDrawer.vue` :
|
||||
- Charger `GET /api/sites?itemsPerPage=999` a l'ouverture du drawer (parallelement aux roles et permissions deja charges).
|
||||
- Ajouter une section `sidebar.admin.usersDrawer.sitesSection` sous la section permissions directes, avec un groupe de `MalioCheckbox` par site (ou un `MalioMultiSelect` si le composant existe dans `@malio/layer-ui`).
|
||||
- Etendre le payload `PATCH /api/users/{id}/rbac` avec `sites: Array<string>` (IRIs).
|
||||
- Auto-refresh de l'auth store apres save si `isSelfEdit` (deja present, conserver).
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/types/rbac.ts` : ajouter le champ `sites: string[]` a `UserListItem` (IRIs de sites attaches).
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/stores/auth.ts` : le store auth expose deja `user` via `/api/me`. Aucune modification requise, les nouveaux champs `sites` et `currentSite` suivent automatiquement via la typologie — a condition de mettre a jour le type `UserData` dans `shared/types/` (ajouter `sites: Site[]` et `currentSite: Site | null`).
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/i18n/locales/fr.json` : cles
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/types/rbac.ts` : ajouter le champ `sites: string[]` a `UserListItem` (IRIs de sites attaches).
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/stores/auth.ts` : le store auth expose deja `user` via `/api/me`. Aucune modification requise, les nouveaux champs `sites` et `currentSite` suivent automatiquement via la typologie — a condition de mettre a jour le type `UserData` dans `shared/types/` (ajouter `sites: Site[]` et `currentSite: Site | null`).
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/i18n/locales/fr.json` : cles
|
||||
- `sidebar.core.sites` = "Sites".
|
||||
- `admin.sites.title`, `admin.sites.newSite`, `admin.sites.editSite`, `admin.sites.createSite`, `admin.sites.noSites`.
|
||||
- `admin.sites.table.{name, city, postalCode, color, fullAddress}`.
|
||||
@@ -228,7 +228,7 @@ final class CurrentSiteResource
|
||||
|
||||
## 6. Plan de migration Doctrine
|
||||
|
||||
La migration est placee dans `/home/m-tristan/workspace/Coltura/migrations/Version<timestamp2>.php` au namespace racine (cf. Risque 2 du ticket 1 et `CLAUDE.md`).
|
||||
La migration est placee dans `/home/m-tristan/workspace/Starseed/migrations/Version<timestamp2>.php` au namespace racine (cf. Risque 2 du ticket 1 et `CLAUDE.md`).
|
||||
|
||||
### `up()` — ordre des instructions
|
||||
|
||||
|
||||
@@ -77,42 +77,42 @@ Resultat attendu : apres merge, un user avec ≥ 1 site voit une barre sous la n
|
||||
|
||||
### Frontend — Module Sites (layer deja cree au ticket 2)
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/components/SiteSelector.vue` : wrapper Vue autour de `MalioSiteSelector`. Branche `useCurrentSite()`, gere l'optimistic update et les toasts.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/composables/useCurrentSite.ts` : composable global exposant l'etat `currentSite` / `availableSites`, les actions `switchSite`, `resetCurrentSite`, et un flag `switching: Ref<boolean>` pour desactiver le selecteur pendant une requete en vol.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/components/SiteSelector.vue` : wrapper Vue autour de `MalioSiteSelector`. Branche `useCurrentSite()`, gere l'optimistic update et les toasts.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/composables/useCurrentSite.ts` : composable global exposant l'etat `currentSite` / `availableSites`, les actions `switchSite`, `resetCurrentSite`, et un flag `switching: Ref<boolean>` pour desactiver le selecteur pendant une requete en vol.
|
||||
|
||||
### Frontend — Shared
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/composables/useModules.ts` : composable qui charge `/api/modules` et expose `isModuleActive(id: string): boolean`. Pattern aligne sur `useSidebar()` : ref singleton au niveau module, chargement idempotent, `resetModules()` expose pour le logout.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/utils/color.ts` : fonctions utilitaires de couleur, au minimum :
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/composables/useModules.ts` : composable qui charge `/api/modules` et expose `isModuleActive(id: string): boolean`. Pattern aligne sur `useSidebar()` : ref singleton au niveau module, chargement idempotent, `resetModules()` expose pour le logout.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/utils/color.ts` : fonctions utilitaires de couleur, au minimum :
|
||||
- `parseHex(hex: string): { r: number; g: number; b: number }` — tolere la casse, rejette les formats hors `#RRGGBB`.
|
||||
- `getRelativeLuminance({r, g, b}): number` — formule WCAG standard.
|
||||
- `getReadableTextColor(hex: string): 'black' | 'white'` — renvoie `'black'` si la luminance > 0.5, `'white'` sinon. Seuil simple, suffisant pour un CRM interne (pas WCAG AAA).
|
||||
|
||||
### Frontend — Tests
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/composables/__tests__/useCurrentSite.spec.ts` : Vitest. Tests :
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/composables/__tests__/useCurrentSite.spec.ts` : Vitest. Tests :
|
||||
- `switchSite` met a jour l'etat localement avant la requete (optimistic).
|
||||
- Si la requete reussit, l'etat reste aligne.
|
||||
- Si la requete echoue, l'etat rollback a l'ancien `currentSite`.
|
||||
- `resetCurrentSite` vide l'etat.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/composables/__tests__/useModules.spec.ts` : Vitest. Tests `isModuleActive` apres chargement, `resetModules` vide l'etat.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/utils/__tests__/color.spec.ts` : Vitest. Jeu de donnees sur `getReadableTextColor` : `#000000` → white, `#FFFFFF` → black, `#056CF2` (bleu Coltura) → white, `#F59E0B` (ambre) → black, `#10B981` (vert) → black ou white selon seuil (a verifier). Tester aussi le rejet de formats invalides.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/sites/components/__tests__/SiteSelector.spec.ts` : smoke test Vitest.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/composables/__tests__/useModules.spec.ts` : Vitest. Tests `isModuleActive` apres chargement, `resetModules` vide l'etat.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/utils/__tests__/color.spec.ts` : Vitest. Jeu de donnees sur `getReadableTextColor` : `#000000` → white, `#FFFFFF` → black, `#056CF2` (bleu Starseed) → white, `#F59E0B` (ambre) → black, `#10B981` (vert) → black ou white selon seuil (a verifier). Tester aussi le rejet de formats invalides.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/sites/components/__tests__/SiteSelector.spec.ts` : smoke test Vitest.
|
||||
|
||||
## 4. Fichiers à modifier
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/package.json` : upgrade `@malio/layer-ui` vers la version qui inclut `MalioSiteSelector`. Commit du `package-lock.json` dans le meme changeset.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/types/user-data.ts` : ajouter les champs
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/package.json` : upgrade `@malio/layer-ui` vers la version qui inclut `MalioSiteSelector`. Commit du `package-lock.json` dans le meme changeset.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/types/user-data.ts` : ajouter les champs
|
||||
```ts
|
||||
sites: Site[]
|
||||
currentSite: Site | null
|
||||
```
|
||||
Import du type `Site` depuis `./sites`. Note : si le type `Site` a deja ete introduit au ticket 2, reutiliser ; sinon, ce ticket le cree dans `frontend/shared/types/sites.ts`.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/shared/types/sites.ts` : si absent, creer avec l'interface `Site` (cf. section Schema ticket 2 pour la forme). Si present, aucune modification.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/app/layouts/default.vue` : integrer `SiteSelector` sous le header, avant `<main>`, dans le flex column. Rendu conditionnel via `v-if="showSiteSelector"` ou via un `defineAsyncComponent` chargement lazy si on veut eviter l'import statique quand le module est off.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/app/middleware/auth.global.ts` : ajouter le chargement de `useModules().loadModules()` apres `loadSidebar()`. Necessaire pour que `isModuleActive` soit resolu quand le layout se rend.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/modules/core/pages/logout.vue` : appeler `useCurrentSite().resetCurrentSite()` et `useModules().resetModules()` apres le `auth.logout()`, aligne sur le pattern `resetSidebar()` deja present.
|
||||
- `/home/m-tristan/workspace/Coltura/frontend/i18n/locales/fr.json` : ajouter les cles
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/shared/types/sites.ts` : si absent, creer avec l'interface `Site` (cf. section Schema ticket 2 pour la forme). Si present, aucune modification.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/app/layouts/default.vue` : integrer `SiteSelector` sous le header, avant `<main>`, dans le flex column. Rendu conditionnel via `v-if="showSiteSelector"` ou via un `defineAsyncComponent` chargement lazy si on veut eviter l'import statique quand le module est off.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/app/middleware/auth.global.ts` : ajouter le chargement de `useModules().loadModules()` apres `loadSidebar()`. Necessaire pour que `isModuleActive` soit resolu quand le layout se rend.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/modules/core/pages/logout.vue` : appeler `useCurrentSite().resetCurrentSite()` et `useModules().resetModules()` apres le `auth.logout()`, aligne sur le pattern `resetSidebar()` deja present.
|
||||
- `/home/m-tristan/workspace/Starseed/frontend/i18n/locales/fr.json` : ajouter les cles
|
||||
```json
|
||||
"sites": {
|
||||
"selector": {
|
||||
|
||||
@@ -34,50 +34,50 @@ Le ticket livre aussi une documentation developpeur (`docs/modules/site-aware.md
|
||||
|
||||
### Shared — Contrat
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Shared/Domain/Contract/SiteAwareInterface.php` : interface minimale. Depends uniquement du type `App\Module\Sites\Domain\Entity\Site`, qui est deja couple cote Core depuis le ticket 2 — le placement dans Shared n'introduit pas de nouvelle dependance transversale non souhaitee.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Shared/Domain/Contract/SiteAwareInterface.php` : interface minimale. Depends uniquement du type `App\Module\Sites\Domain\Entity\Site`, qui est deja couple cote Core depuis le ticket 2 — le placement dans Shared n'introduit pas de nouvelle dependance transversale non souhaitee.
|
||||
|
||||
### Module Sites — Application
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Application/Service/CurrentSiteProvider.php` : service injecte partout ou le site courant doit etre lu (extensions, processor, futurs voters). Gere les trois cas de retour `null` : pas d'user, `currentSite` null, module desactive.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Application/Service/CurrentSiteProvider.php` : service injecte partout ou le site courant doit etre lu (extensions, processor, futurs voters). Gere les trois cas de retour `null` : pas d'user, `currentSite` null, module desactive.
|
||||
|
||||
### Module Sites — Infrastructure
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/ApiPlatform/Extension/SiteScopedQueryExtension.php` : une seule classe, implementant a la fois `QueryCollectionExtensionInterface` et `QueryItemExtensionInterface`. Le comportement est identique pour les deux, modulo que l'item manque retourne 404 (API Platform converti un `getOneOrNullResult` null en 404).
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/Infrastructure/ApiPlatform/State/Processor/SiteAwareInjectionProcessor.php` : decorator sur le persist processor Doctrine. Injecte le site courant sur `$data` si applicable, puis delegue a `$persistProcessor`.
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/ApiPlatform/Extension/SiteScopedQueryExtension.php` : une seule classe, implementant a la fois `QueryCollectionExtensionInterface` et `QueryItemExtensionInterface`. Le comportement est identique pour les deux, modulo que l'item manque retourne 404 (API Platform converti un `getOneOrNullResult` null en 404).
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/Infrastructure/ApiPlatform/State/Processor/SiteAwareInjectionProcessor.php` : decorator sur le persist processor Doctrine. Injecte le site courant sur `$data` si applicable, puis delegue a `$persistProcessor`.
|
||||
|
||||
### Documentation
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/docs/modules/site-aware.md` : guide developpeur (cf. contenu section 10).
|
||||
- `/home/m-tristan/workspace/Starseed/docs/modules/site-aware.md` : guide developpeur (cf. contenu section 10).
|
||||
|
||||
### Tests
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Infrastructure/ApiPlatform/Extension/SiteScopedQueryExtensionTest.php` : tests d'integration (`KernelTestCase`) avec l'entite `FakeSiteAwareEntity` (declaree uniquement dans le dossier de tests). Verifie :
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Infrastructure/ApiPlatform/Extension/SiteScopedQueryExtensionTest.php` : tests d'integration (`KernelTestCase`) avec l'entite `FakeSiteAwareEntity` (declaree uniquement dans le dossier de tests). Verifie :
|
||||
- Le filtre s'applique sur une resource `SiteAware` quand le provider retourne un site.
|
||||
- Le filtre est no-op si `SiteAware` mais provider null.
|
||||
- Le filtre est no-op si resource non `SiteAware`.
|
||||
- Le filtre est no-op si user a `sites.bypass_scope`.
|
||||
- `totalItems` Hydra reflete bien le filtrage.
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Infrastructure/ApiPlatform/State/Processor/SiteAwareInjectionProcessorTest.php` : tests unitaires (`TestCase` pur) avec mocks. Verifie :
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Infrastructure/ApiPlatform/State/Processor/SiteAwareInjectionProcessorTest.php` : tests unitaires (`TestCase` pur) avec mocks. Verifie :
|
||||
- `$data` SiteAware sans site → injection du site courant.
|
||||
- `$data` SiteAware avec site deja positionne → pas d'overwrite.
|
||||
- `$data` non-SiteAware → delegation directe sans modification.
|
||||
- Provider retourne null (module off ou user sans site) ET `$data` SiteAware sans site → BadRequestHttpException (400) "aucun site selectionne".
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Module/Sites/Application/Service/CurrentSiteProviderTest.php` : tests unitaires `TestCase`. Couvre :
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Module/Sites/Application/Service/CurrentSiteProviderTest.php` : tests unitaires `TestCase`. Couvre :
|
||||
- User authentifie avec currentSite → retourne le Site.
|
||||
- User authentifie sans currentSite → null.
|
||||
- Pas d'user → null.
|
||||
- Module desactive dans config/modules.php de test → null meme si user.currentSite existe.
|
||||
- `/home/m-tristan/workspace/Coltura/tests/Fixtures/SiteAware/FakeSiteAwareEntity.php` : entite Doctrine minimale (`id`, `name`, `site`) utilisee **uniquement** en tests. Mapping Doctrine declare via un `#[ORM\Entity]` mais la table n'existe jamais en prod car la fixture n'est jamais chargee hors tests. **Alternative** : utiliser un schema DB dedie au dossier de tests, cree a la volee par un helper setUp. A trancher a l'implementation.
|
||||
- `/home/m-tristan/workspace/Starseed/tests/Fixtures/SiteAware/FakeSiteAwareEntity.php` : entite Doctrine minimale (`id`, `name`, `site`) utilisee **uniquement** en tests. Mapping Doctrine declare via un `#[ORM\Entity]` mais la table n'existe jamais en prod car la fixture n'est jamais chargee hors tests. **Alternative** : utiliser un schema DB dedie au dossier de tests, cree a la volee par un helper setUp. A trancher a l'implementation.
|
||||
|
||||
## 4. Fichiers à modifier
|
||||
|
||||
- `/home/m-tristan/workspace/Coltura/src/Module/Sites/SitesModule.php` : ajouter la permission `sites.bypass_scope` dans `permissions()` :
|
||||
- `/home/m-tristan/workspace/Starseed/src/Module/Sites/SitesModule.php` : ajouter la permission `sites.bypass_scope` dans `permissions()` :
|
||||
```php
|
||||
['code' => 'sites.bypass_scope', 'label' => 'Voir les donnees site-scoped de tous les sites (bypass du filtrage)'],
|
||||
```
|
||||
**Note importante** : la methode `permissions()` signale l'existence de la permission mais c'est la commande `app:sync-permissions` (inchangee) qui la positionne en base.
|
||||
- `/home/m-tristan/workspace/Coltura/config/services.yaml` : aucun changement requis. `SiteScopedQueryExtension`, `SiteAwareInjectionProcessor` et `CurrentSiteProvider` sont autoconfigures via les `_defaults` du module. Le decorator du persist processor est declare via `#[AsDecorator]` ou via tag (cf. section 8).
|
||||
- `/home/m-tristan/workspace/Coltura/phpunit.dist.xml` : aucune modification requise si la config des fixtures de tests est autonome. Si `FakeSiteAwareEntity` necessite un mapping dedie, l'option la plus propre est un `doctrine.yaml.test` ajoute via `when@test`, sans polluer la config dev/prod (cf. Risque 3).
|
||||
- `/home/m-tristan/workspace/Starseed/config/services.yaml` : aucun changement requis. `SiteScopedQueryExtension`, `SiteAwareInjectionProcessor` et `CurrentSiteProvider` sont autoconfigures via les `_defaults` du module. Le decorator du persist processor est declare via `#[AsDecorator]` ou via tag (cf. section 8).
|
||||
- `/home/m-tristan/workspace/Starseed/phpunit.dist.xml` : aucune modification requise si la config des fixtures de tests est autonome. Si `FakeSiteAwareEntity` necessite un mapping dedie, l'option la plus propre est un `doctrine.yaml.test` ajoute via `when@test`, sans polluer la config dev/prod (cf. Risque 3).
|
||||
|
||||
## 5. Contrat `SiteAwareInterface`
|
||||
|
||||
@@ -459,7 +459,7 @@ A mitiger par un test qui genere une entite `FakeSiteAwareEntity` via un POST `a
|
||||
|
||||
### Risque 8 — Doc developpeur en francais vs anglais
|
||||
|
||||
Le fichier `docs/modules/site-aware.md` s'adresse aux developpeurs de Coltura. Il est redige en **francais**, aligne sur la convention projet (CLAUDE.md : "commentaires en francais, code en anglais"). Aucun extrait de code ne doit etre traduit, seules les explications.
|
||||
Le fichier `docs/modules/site-aware.md` s'adresse aux developpeurs de Starseed. Il est redige en **francais**, aligne sur la convention projet (CLAUDE.md : "commentaires en francais, code en anglais"). Aucun extrait de code ne doit etre traduit, seules les explications.
|
||||
|
||||
## 12. Plan de tests
|
||||
|
||||
|
||||
@@ -0,0 +1,700 @@
|
||||
---
|
||||
# === IDENTITÉ ===
|
||||
module: M0
|
||||
nom: "Gestion des catégories"
|
||||
ecran: gestion-categories
|
||||
type: feature # feature back + UI admin (datatable + drawer)
|
||||
pipeline: ui+back # tickets back ET front (UI admin standard, pas de Figma)
|
||||
owner: Matthieu
|
||||
backup: Tristan
|
||||
date: 2026-05-26
|
||||
version: 1.1 # 1.0 = draft initial ; 1.1 = aligné sur l'archi Starseed réelle
|
||||
|
||||
# === LIENS ===
|
||||
lien_spec_front: ./spec-front.md
|
||||
figma: null # pas de maquette — UI admin standard (datatable + drawer)
|
||||
dependances: [] # M0 = premier module, aucune dépendance
|
||||
regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.04, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13, RG-1.14]
|
||||
roles: [Admin, Bureau, Compta, Commerciale, Usine]
|
||||
|
||||
# === VALIDATION CLIENT ===
|
||||
# Pour le skill ticket-writer : statut = validee (validation implicite, périmètre projet validé en amont).
|
||||
# UI admin interne, pas de Figma, pas de validation client externe requise.
|
||||
client_validation_1:
|
||||
statut: validee
|
||||
date: 2026-05-22
|
||||
canal: ecrit
|
||||
valide_par: "Matthieu (CP MALIO) — validation implicite, périmètre projet"
|
||||
resume: "UI admin standard (datatable + drawer), pas de validation client #1 externe requise (workflow back-only + UI admin standard sans Figma)."
|
||||
trace_archivee: null
|
||||
date_validation: 2026-05-22
|
||||
validateur_client: "Matthieu (CP MALIO) — validation implicite, périmètre projet"
|
||||
|
||||
# === LIEN LESSTIME (rempli après push manuel le 2026-05-26) ===
|
||||
lesstime_taskgroup_id: 22
|
||||
lesstime_project_id: 6 # ERP / Starseed
|
||||
statut_global: en_dev # tickets créés en backlog Lesstime
|
||||
|
||||
# === TAGS LESSTIME suggérés pour les tickets ===
|
||||
tags: [Backend, Frontend]
|
||||
---
|
||||
|
||||
# M0 — Gestion des catégories (back + UI admin)
|
||||
|
||||
## 1. Contexte
|
||||
|
||||
Premier module à intégrer le workflow MALIO. Permet à un administrateur de gérer un référentiel de **catégories** dans Starseed (CRM/ERP). Une catégorie porte un `name` libre et un `type` (FK vers le référentiel `category_type`). Elle servira plus tard à classifier les tiers (clients, fournisseurs, prestataires).
|
||||
|
||||
**Contraintes structurantes :**
|
||||
|
||||
- **Admin uniquement** sur toute la chaîne (Bureau / Compta / Commerciale / Usine : aucun accès, ni lecture ni écriture). Implémenté via la permission RBAC `catalog.categories.view` / `catalog.categories.manage`, jamais attribuée aux autres rôles métier.
|
||||
- **Pas de hard delete** : soft delete via `deleted_at`. La liste exclut les soft-deleted par défaut.
|
||||
- **Pas de Figma** : UI admin standard (datatable + drawer d'édition), composants `@malio/layer-ui`.
|
||||
- **Nouveau module Starseed `Catalog`** créé pour ce M0. Bounded context « référentiels partagés » — n'appartient pas à `Commercial` pour rester réutilisable par les futurs modules Tiers (M-Clients, M-Fournisseurs, M-Prestas).
|
||||
- **Note** : le référentiel `category_type` n'est pas géré par ce module (voir § 9 Hors-périmètre). Migration crée la table vide ; le seed initial sera défini ultérieurement.
|
||||
|
||||
## 2. Décisions d'archi (auto-validation back-only)
|
||||
|
||||
> Section obligatoire pour les specs sans validation client externe (cf. WORKFLOW_ERP.md § 1.bis). Traces écrites des choix techniques.
|
||||
|
||||
### 2.1 Module `Catalog` (nouveau)
|
||||
|
||||
Création du module DDD `App\Module\Catalog` avec la même structure que `Core` / `Commercial` / `Sites` :
|
||||
|
||||
```
|
||||
src/Module/Catalog/
|
||||
├── CatalogModule.php # constantes ID/LABEL/REQUIRED + permissions()
|
||||
├── Domain/
|
||||
│ ├── Entity/
|
||||
│ │ ├── Category.php # #[ApiResource] + #[Auditable]
|
||||
│ │ └── CategoryType.php # #[ApiResource(GetCollection + Get seulement)]
|
||||
│ └── Repository/
|
||||
│ ├── CategoryRepositoryInterface.php
|
||||
│ └── CategoryTypeRepositoryInterface.php
|
||||
└── Infrastructure/
|
||||
├── ApiPlatform/
|
||||
│ └── State/
|
||||
│ ├── Provider/
|
||||
│ │ └── CategoryProvider.php # filtre soft-delete + includeDeleted (admin)
|
||||
│ └── Processor/
|
||||
│ └── CategoryProcessor.php # POST / PATCH / DELETE (soft)
|
||||
└── Doctrine/
|
||||
├── DoctrineCategoryRepository.php
|
||||
├── DoctrineCategoryTypeRepository.php
|
||||
└── Filter/SoftDeletedCategoryFilter.php # filtre Doctrine global (cf. § 4.2)
|
||||
```
|
||||
|
||||
Wire dans `config/modules.php` (ajout d'une ligne) :
|
||||
|
||||
```php
|
||||
return [
|
||||
CoreModule::class,
|
||||
CommercialModule::class,
|
||||
SitesModule::class,
|
||||
CatalogModule::class, // ← AJOUTÉ
|
||||
];
|
||||
```
|
||||
|
||||
**Alternative écartée** : placer dans `Core` (pollue le module noyau avec du métier de référentiel) ou dans `Commercial` (empêche M-Fournisseurs / M-Prestas de réutiliser proprement sans dépendre de Commercial).
|
||||
|
||||
### 2.2 IDs : entier auto-increment Postgres natif
|
||||
|
||||
Convention Starseed confirmée (cf. `Domain/Entity/User.php`, `migrations/Version20260407095546.php`) : tous les IDs sont des `INT GENERATED BY DEFAULT AS IDENTITY`. **PAS de CUID** dans Starseed. La spec applique donc `INT IDENTITY` pour `category.id` et `category_type.id`.
|
||||
|
||||
**Alternative écartée** : `uuid` (utilisé seulement pour `audit_log.id` à cause de la nature append-only / forte croissance).
|
||||
|
||||
### 2.3 Soft delete : pattern à introduire
|
||||
|
||||
Aucun pattern soft delete existant dans Starseed (vérifié, aucune entité ne porte `deleted_at`). Le M0 introduit le pattern :
|
||||
|
||||
- Colonne `deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL` sur `category`.
|
||||
- Filtre Doctrine global enregistré pour cette entité (active par défaut, désactivable via un flag dans le `CategoryProvider`).
|
||||
- Le PATCH ne peut pas écrire `deletedAt` (denormalization group exclut le champ).
|
||||
- Le DELETE pose `deleted_at = now()` via `CategoryProcessor` (override du remove processor Doctrine ORM standard, pattern aligné sur `UserProcessor`).
|
||||
|
||||
**Alternative écartée** : pas de delete du tout (V0 client n'en parle pas) — refusée car le besoin opérationnel reviendra immanquablement. Mieux vaut intégrer le pattern dès le M0 et le réutiliser ailleurs.
|
||||
|
||||
### 2.4 Unicité partielle Postgres
|
||||
|
||||
Index unique partiel sur `(LOWER(name), category_type_id) WHERE deleted_at IS NULL`. Permet de recréer une catégorie avec le même `(name, type)` après suppression logique. Postgres supporte nativement (`CREATE UNIQUE INDEX ... WHERE`). Pattern propre, pas besoin de validator applicatif maison côté unicité — la contrainte SQL fait le job.
|
||||
|
||||
### 2.5 Audit : `#[Auditable]` Starseed standard
|
||||
|
||||
L'entité `Category` porte `#[Auditable]` (`App\Shared\Domain\Attribute\Auditable`). Le `AuditListener` (`src/Module/Core/Infrastructure/Doctrine/AuditListener.php`) intercepte `onFlush` + `postFlush` et écrit une ligne dans `audit_log` à chaque création / modification / suppression logique (le soft delete = un UPDATE pour Doctrine, donc tracé comme un UPDATE).
|
||||
|
||||
**Important** : `#[Auditable]` **ne crée PAS** automatiquement les colonnes `created_by` / `updated_by` / `created_at` / `updated_at` sur l'entité. Il trace les changements dans une table séparée `audit_log` (qui contient `performed_by` + `performed_at` + diff JSON). Conséquence pour le M0 :
|
||||
|
||||
- **Pas de colonnes `created_by` / `updated_by` / `created_at` / `updated_at` sur `category`.** Le « qui a créé / modifié / quand » est lisible via l'endpoint d'historique `GET /api/audit-log?entityType=Category&entityId={id}` (déjà fourni par Core).
|
||||
- C'est cohérent avec les autres entités Starseed (User n'a qu'un `createdAt` géré manuellement dans le constructor, pas de `updatedAt` ; Role n'a rien).
|
||||
|
||||
Si plus tard un besoin de tri par `updated_at` côté front se fait sentir, on pourra rajouter la colonne. Au M0, on ne devine pas.
|
||||
|
||||
### 2.6 Pagination & tri
|
||||
|
||||
Volumétrie cible : **300 max** (cf. Q5 Matthieu). Pas de pagination serveur. L'endpoint liste renvoie tout d'un coup. Tri par défaut côté serveur : `name ASC` (via `OrderFilter` ou ordre par défaut du Provider). La pagination front (`<MalioDataTable>`) gère l'affichage paginé en mémoire.
|
||||
|
||||
### 2.7 Permissions RBAC — granularité
|
||||
|
||||
Pattern Starseed (cf. `CoreModule::permissions()`) : `view` + `manage`, **pas** la granularité `view / create / edit / delete`. Aligné sur `core.users.view` + `core.users.manage`.
|
||||
|
||||
Pour M0 :
|
||||
|
||||
- `catalog.categories.view` — voir la liste + détail (GET) + lecture du référentiel `category_type` (GET)
|
||||
- `catalog.categories.manage` — créer + modifier + supprimer (POST / PATCH / DELETE)
|
||||
|
||||
Les deux permissions seront attachées **uniquement au rôle métier `Admin`** dans `AppFixtures` et `SeedE2ECommand`. Bureau / Compta / Commerciale / Usine n'en reçoivent aucune → 403 systématique.
|
||||
|
||||
## 3. Modèle de données
|
||||
|
||||
### 3.1 Diagramme
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
CATEGORY_TYPE ||--o{ CATEGORY : "classe"
|
||||
CATEGORY {
|
||||
int id PK "INT IDENTITY"
|
||||
string name "VARCHAR(120) NOT NULL"
|
||||
int category_type_id FK "INT NOT NULL"
|
||||
timestamp deleted_at "nullable (soft delete)"
|
||||
}
|
||||
CATEGORY_TYPE {
|
||||
int id PK "INT IDENTITY"
|
||||
string code "VARCHAR(40) NOT NULL UNIQUE"
|
||||
string label "VARCHAR(120) NOT NULL"
|
||||
}
|
||||
AUDIT_LOG }o..o{ CATEGORY : "trace via #[Auditable]"
|
||||
```
|
||||
|
||||
`audit_log` (déjà en place dans Core) trace automatiquement les changements sur `Category` (qui portera `#[Auditable]`). Pas de FK matérielle Postgres entre `audit_log` et `category` : `audit_log.entity_id` est un `VARCHAR(64)` (cf. migration `Version20260420202749`).
|
||||
|
||||
### 3.2 Migration Doctrine — SQL Postgres
|
||||
|
||||
**Placement** : `migrations/VersionYYYYMMDDHHMMSS.php`, namespace racine `DoctrineMigrations` (règle ABSOLUE Starseed n°11 : les migrations d'init des entités d'un module vivent au namespace racine pour éviter le bug de tri FQCN de Doctrine Migrations 3.x).
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* M0 — Catalog : creation des tables `category_type` (referentiel) et `category`.
|
||||
*
|
||||
* Le referentiel `category_type` est cree vide ; ses valeurs seront seedees
|
||||
* ulterieurement (cf. spec-back M0 § 9 HP-1).
|
||||
*
|
||||
* Index unique partiel sur (LOWER(name), category_type_id) WHERE deleted_at
|
||||
* IS NULL : permet la recreation d'une categorie apres suppression logique
|
||||
* (cf. RG-1.07).
|
||||
*/
|
||||
final class VersionYYYYMMDDHHMMSS extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'M0 Catalog : tables category_type et category, index unique partiel.';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE category_type (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
code VARCHAR(40) NOT NULL,
|
||||
label VARCHAR(120) NOT NULL,
|
||||
PRIMARY KEY (id)
|
||||
)
|
||||
SQL);
|
||||
$this->addSql('CREATE UNIQUE INDEX uq_category_type_code ON category_type (code)');
|
||||
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE TABLE category (
|
||||
id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
|
||||
name VARCHAR(120) NOT NULL,
|
||||
category_type_id INT NOT NULL,
|
||||
deleted_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL,
|
||||
PRIMARY KEY (id),
|
||||
CONSTRAINT fk_category_type
|
||||
FOREIGN KEY (category_type_id) REFERENCES category_type (id) ON DELETE RESTRICT
|
||||
)
|
||||
SQL);
|
||||
|
||||
// Unicite (name, type) case-insensitive, seulement sur les non-soft-deleted.
|
||||
$this->addSql(<<<'SQL'
|
||||
CREATE UNIQUE INDEX uq_category_name_type_active
|
||||
ON category (LOWER(name), category_type_id)
|
||||
WHERE deleted_at IS NULL
|
||||
SQL);
|
||||
|
||||
$this->addSql('CREATE INDEX idx_category_deleted_at ON category (deleted_at)');
|
||||
$this->addSql('CREATE INDEX idx_category_type_id ON category (category_type_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE category');
|
||||
$this->addSql('DROP TABLE category_type');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> Rappel convention Starseed/MALIO : noms de colonnes **toujours en minuscules snake_case** dans le SQL brut. Doctrine génère le camelCase côté entité, Postgres stocke en lowercase.
|
||||
|
||||
### 3.3 Entité `Category` — squelette (pattern Starseed)
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Catalog\Domain\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Catalog\Infrastructure\ApiPlatform\State\Processor\CategoryProcessor;
|
||||
use App\Module\Catalog\Infrastructure\ApiPlatform\State\Provider\CategoryProvider;
|
||||
use App\Module\Catalog\Infrastructure\Doctrine\DoctrineCategoryRepository;
|
||||
use App\Shared\Domain\Attribute\Auditable;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
security: "is_granted('catalog.categories.view')",
|
||||
normalizationContext: ['groups' => ['category:read']],
|
||||
provider: CategoryProvider::class,
|
||||
),
|
||||
new Get(
|
||||
security: "is_granted('catalog.categories.view')",
|
||||
normalizationContext: ['groups' => ['category:read']],
|
||||
provider: CategoryProvider::class,
|
||||
),
|
||||
new Post(
|
||||
security: "is_granted('catalog.categories.manage')",
|
||||
normalizationContext: ['groups' => ['category:read']],
|
||||
denormalizationContext: ['groups' => ['category:write']],
|
||||
processor: CategoryProcessor::class,
|
||||
),
|
||||
new Patch(
|
||||
security: "is_granted('catalog.categories.manage')",
|
||||
normalizationContext: ['groups' => ['category:read']],
|
||||
denormalizationContext: ['groups' => ['category:write']],
|
||||
processor: CategoryProcessor::class,
|
||||
),
|
||||
new Delete(
|
||||
security: "is_granted('catalog.categories.manage')",
|
||||
processor: CategoryProcessor::class,
|
||||
),
|
||||
],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineCategoryRepository::class)]
|
||||
#[ORM\Table(name: 'category')]
|
||||
#[Auditable]
|
||||
class Category
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['category:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Assert\NotBlank(message: 'Le nom est obligatoire.')]
|
||||
#[Assert\Length(min: 2, max: 120)]
|
||||
#[Groups(['category:read', 'category:write'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: CategoryType::class)]
|
||||
#[ORM\JoinColumn(name: 'category_type_id', referencedColumnName: 'id', nullable: false, onDelete: 'RESTRICT')]
|
||||
#[Assert\NotNull(message: 'Type de catégorie obligatoire.')]
|
||||
#[Groups(['category:read', 'category:write'])]
|
||||
private ?CategoryType $categoryType = null;
|
||||
|
||||
/**
|
||||
* Soft delete : null = active, valeur = supprimee logiquement le {date}.
|
||||
* Pas exposee en ecriture (DELETE → CategoryProcessor pose la valeur).
|
||||
*/
|
||||
#[ORM\Column(name: 'deleted_at', type: 'datetime_immutable', nullable: true)]
|
||||
#[Groups(['category:read'])]
|
||||
private ?DateTimeImmutable $deletedAt = null;
|
||||
|
||||
// getters / setters classiques (générés par PhpStorm) — omis ici.
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 Entité `CategoryType` — squelette
|
||||
|
||||
Lecture seule au M0. Pas de POST / PATCH / DELETE exposé.
|
||||
|
||||
```php
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new GetCollection(
|
||||
security: "is_granted('catalog.categories.view')",
|
||||
normalizationContext: ['groups' => ['category_type:read']],
|
||||
),
|
||||
new Get(
|
||||
security: "is_granted('catalog.categories.view')",
|
||||
normalizationContext: ['groups' => ['category_type:read']],
|
||||
),
|
||||
],
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: DoctrineCategoryTypeRepository::class)]
|
||||
#[ORM\Table(name: 'category_type')]
|
||||
class CategoryType
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
#[Groups(['category_type:read', 'category:read'])]
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 40, unique: true)]
|
||||
#[Groups(['category_type:read', 'category:read'])]
|
||||
private ?string $code = null;
|
||||
|
||||
#[ORM\Column(length: 120)]
|
||||
#[Groups(['category_type:read', 'category:read'])]
|
||||
private ?string $label = null;
|
||||
|
||||
// getters / setters
|
||||
}
|
||||
```
|
||||
|
||||
> Le groupe `category:read` est ajouté sur les propriétés de `CategoryType` pour qu'il soit **embarqué** dans la réponse `Category` (pattern Starseed cf. `.claude/rules/backend.md § Serialization`).
|
||||
|
||||
## 4. API REST (API Platform)
|
||||
|
||||
Toutes les routes sont préfixées `/api` (cf. `config/routes/api_platform.yaml`). Toutes les opérations sont **réservées au rôle Admin** via les permissions RBAC (cf. § 5).
|
||||
|
||||
### 4.1 `GET /api/categories` — Liste
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.view')`
|
||||
- **Query params** :
|
||||
- `includeDeleted=true|false` (default `false`) — désactivé par défaut, le `CategoryProvider` filtre `deleted_at IS NULL`
|
||||
- `categoryType=<id>` (optionnel) — filtre par type (via `SearchFilter` API Platform standard si activé)
|
||||
- **Pagination** : aucune au M0 (volumétrie ≤ 300). Pagination front via `<MalioDataTable>`.
|
||||
- **Tri par défaut** : `name ASC` (serveur, défini dans le `CategoryProvider`)
|
||||
- **Réponse 200** (format JSON-LD Hydra standard API Platform) :
|
||||
|
||||
```json
|
||||
{
|
||||
"@context": "/api/contexts/Category",
|
||||
"@id": "/api/categories",
|
||||
"@type": "Collection",
|
||||
"totalItems": 42,
|
||||
"member": [
|
||||
{
|
||||
"@id": "/api/categories/12",
|
||||
"@type": "Category",
|
||||
"id": 12,
|
||||
"name": "Vis tête fraisée",
|
||||
"categoryType": {
|
||||
"@id": "/api/category_types/3",
|
||||
"id": 3,
|
||||
"code": "MATIERE",
|
||||
"label": "Matière"
|
||||
},
|
||||
"deletedAt": null
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- **Codes** : `200 OK` / `401` (non authentifié) / `403` (pas la permission)
|
||||
|
||||
### 4.2 `GET /api/categories/{id}` — Détail
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.view')`
|
||||
- **Comportement** : 404 si soft-deleted ET `includeDeleted=false` (default).
|
||||
- **Réponse 200** : identique à un élément de la liste ci-dessus.
|
||||
- **Codes** : `200` / `404` / `401` / `403`
|
||||
|
||||
### 4.3 `POST /api/categories` — Création
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.manage')`
|
||||
- **Content-Type** : `application/ld+json`
|
||||
- **Body** :
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Vis tête fraisée",
|
||||
"categoryType": "/api/category_types/3"
|
||||
}
|
||||
```
|
||||
|
||||
- **Réponse 201** : la ressource créée (cf. § 4.1).
|
||||
- **Codes** :
|
||||
- `201 Created`
|
||||
- `400 Bad Request` payload mal formé
|
||||
- `401` / `403`
|
||||
- `409 Conflict` si doublon `(LOWER(name), categoryType)` parmi les non-soft-deleted (RG-1.07). Détection : le `UniqueConstraintViolation` Postgres remonté par Doctrine est attrapé dans le `CategoryProcessor` et traduit en 409.
|
||||
- `422 Unprocessable Entity` si validation (Assert NotBlank, Assert Length, CategoryType inexistant…)
|
||||
|
||||
### 4.4 `PATCH /api/categories/{id}` — Modification
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.manage')`
|
||||
- **Content-Type** : `application/merge-patch+json`
|
||||
- **Body** (partiel) : `{ "name": "Vis tête fraisée H7" }`
|
||||
- **Réponse 200** : la ressource mise à jour.
|
||||
- **Codes** : `200` / `400` / `401` / `403` / `404` / `409` / `422`
|
||||
- **Champs modifiables** : `name`, `categoryType`. Le champ `deletedAt` n'est PAS dans le groupe `category:write`, donc impossible à modifier via PATCH (séparation propre du DELETE).
|
||||
|
||||
### 4.5 `DELETE /api/categories/{id}` — Suppression logique
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.manage')`
|
||||
- **Comportement** : le `CategoryProcessor` intercepte l'opération Delete, pose `deletedAt = new DateTimeImmutable()` puis flush. **Ne supprime jamais physiquement la ligne.**
|
||||
- **Réponse 204 No Content**
|
||||
- **Codes** : `204` / `401` / `403` / `404` (déjà soft-deleted ou inexistante) / `409` (RG-1.14 — à activer post-M0)
|
||||
|
||||
### 4.6 `GET /api/category_types` — Référentiel (lecture seule)
|
||||
|
||||
- **Security** : `is_granted('catalog.categories.view')` (réutilise la même permission, c'est lié)
|
||||
- **Comportement** : liste de tous les `CategoryType`, triée par `label ASC`.
|
||||
- **Pas d'écriture exposée au M0** — table vide à la livraison.
|
||||
|
||||
## 5. Autorisation
|
||||
|
||||
### 5.1 Déclaration des permissions
|
||||
|
||||
`src/Module/Catalog/CatalogModule.php` (pattern aligné sur `CoreModule.php`) :
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Catalog;
|
||||
|
||||
final class CatalogModule
|
||||
{
|
||||
public const string ID = 'catalog';
|
||||
public const string LABEL = 'Catalogue';
|
||||
public const bool REQUIRED = false;
|
||||
|
||||
/**
|
||||
* Permissions RBAC exposees par le module Catalog. Granularite alignee
|
||||
* sur Core (view + manage), pas view/create/edit/delete.
|
||||
*
|
||||
* @return array<int, array{code: string, label: string}>
|
||||
*/
|
||||
public static function permissions(): array
|
||||
{
|
||||
return [
|
||||
['code' => 'catalog.categories.view', 'label' => 'Voir les catégories'],
|
||||
['code' => 'catalog.categories.manage', 'label' => 'Gérer les catégories (créer, éditer, supprimer)'],
|
||||
];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
À l'issue du dev : `make shell` puis `php bin/console app:sync-permissions` pour upserter les codes dans la table `permission`.
|
||||
|
||||
### 5.2 Mapping rôles MALIO ↔ permissions
|
||||
|
||||
Les 5 rôles MALIO (`Admin / Bureau / Compta / Commerciale / Usine`) sont des **rôles RBAC métier** matérialisés dans la table `role` (pas des rôles Symfony Security — il n'y a que `ROLE_USER` et `ROLE_ADMIN` côté Security). Pour le M0 :
|
||||
|
||||
| Permission | Admin | Bureau | Compta | Commerciale | Usine |
|
||||
|---|---|---|---|---|---|
|
||||
| `catalog.categories.view` | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| `catalog.categories.manage` | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
Tout rôle qui ne porte aucune de ces deux permissions reçoit `403 Forbidden` sur les endpoints `/api/categories/*` et `/api/category_types*`. Un anonyme (sans JWT valide) reçoit `401`.
|
||||
|
||||
### 5.3 Synchronisation RBAC (3 sources OBLIGATOIRES)
|
||||
|
||||
Règle ABSOLUE Starseed n°8 — toute évolution RBAC touche **les 3 sources ensemble** :
|
||||
|
||||
1. **`config/sidebar.php`** — ajouter l'item « Gestion des catégories » dans la section « Administration » :
|
||||
|
||||
```php
|
||||
[
|
||||
'label' => 'sidebar.catalog.categories',
|
||||
'to' => '/admin/categories',
|
||||
'icon' => 'mdi:tag-multiple-outline',
|
||||
'module' => 'catalog',
|
||||
'permission' => 'catalog.categories.view',
|
||||
],
|
||||
```
|
||||
|
||||
2. **`frontend/tests/e2e/_fixtures/personas.ts`** — attribuer les 2 permissions au persona `Admin`, ne rien changer pour les autres personas.
|
||||
|
||||
3. **`src/Module/Core/Infrastructure/Console/SeedE2ECommand.php`** — miroir back : seed le rôle métier `Admin` avec les permissions `catalog.categories.view` + `catalog.categories.manage`.
|
||||
|
||||
### 5.4 Vérification front
|
||||
|
||||
`usePermissions()` côté Nuxt : afficher / cacher l'item de menu « Gestion des catégories » selon `catalog.categories.view`. Les actions « Ajouter / Modifier / Supprimer » sont gatées sur `catalog.categories.manage`.
|
||||
|
||||
## 6. Audit & dates
|
||||
|
||||
### 6.1 Audit automatique via `#[Auditable]`
|
||||
|
||||
`Category` porte `#[Auditable]` (`App\Shared\Domain\Attribute\Auditable`). Le `AuditListener` du module Core (`src/Module/Core/Infrastructure/Doctrine/AuditListener.php`) :
|
||||
|
||||
- Intercepte `onFlush` : capture les insertions / updates / deletions de toute entité `#[Auditable]`.
|
||||
- Intercepte `postFlush` : écrit une ligne dans la table `audit_log` via DBAL (connexion dédiée pour éviter la récursion).
|
||||
- Trace `performed_by` (le user courant), `performed_at`, `changes` (diff JSONB), `request_id`, `ip_address`.
|
||||
|
||||
**Aucun code custom à écrire côté M0.** Il suffit que `Category` porte `#[Auditable]`. Le soft delete (UPDATE `deleted_at`) est tracé comme un UPDATE normal.
|
||||
|
||||
### 6.2 Pas de colonnes `created_by` / `updated_by` / `created_at` / `updated_at` sur `category`
|
||||
|
||||
Contrairement à ce que la V0 brute pourrait suggérer, **`#[Auditable]` n'ajoute PAS ces colonnes**. Il écrit dans `audit_log` séparément. Cohérent avec les autres entités Starseed (`User` n'a qu'un `createdAt` géré manuellement dans le constructor, pas de `updatedAt` ; `Role` n'a rien).
|
||||
|
||||
→ Pour répondre à « qui a créé / modifié / quand », interroger `GET /api/audit-log?entityType=Category&entityId={id}` (endpoint déjà fourni par `Core`).
|
||||
|
||||
→ Si plus tard un besoin de tri front par date de création se fait sentir, on rajoutera la colonne. **Au M0, on ne devine pas.**
|
||||
|
||||
## 7. Règles de gestion (RG)
|
||||
|
||||
> Chaque RG est numérotée, stable, et constituera un critère d'acceptation côté ticket Lesstime.
|
||||
> Convention `RG-1.XX` (les rules numéros 1.XX = règles du M0, premier module workflow).
|
||||
|
||||
### Autorisation
|
||||
|
||||
- **RG-1.01** : Seul un utilisateur authentifié porteur de la permission RBAC `catalog.categories.view` peut consulter `/api/categories/*` ou `/api/category_types*`. Seul un utilisateur authentifié porteur de `catalog.categories.manage` peut faire POST / PATCH / DELETE. Sans permission → **403 Forbidden**. Sans authentification → **401 Unauthorized**. Au M0, ces deux permissions sont attribuées **uniquement au rôle métier Admin** (les rôles Bureau / Compta / Commerciale / Usine reçoivent donc systématiquement 403).
|
||||
|
||||
### Champ `name`
|
||||
|
||||
- **RG-1.02** : Le champ `name` est **obligatoire** à la création et à la modification. Vide / null / whitespace-only → **422** avec violation `name: "Le nom est obligatoire."` (Symfony `Assert\NotBlank`).
|
||||
- **RG-1.03** : Le champ `name` est trim() côté serveur dans le `CategoryProcessor` avant validation et persistance (suppression des espaces de début/fin).
|
||||
- **RG-1.04** : Le champ `name` a une longueur entre **2 et 120 caractères** (après trim). Hors borne → 422 (Symfony `Assert\Length(min: 2, max: 120)`).
|
||||
|
||||
### Champ `categoryType`
|
||||
|
||||
- **RG-1.05** : Le champ `categoryType` est **obligatoire** (IRI vers `/api/category_types/{id}`). Manquant / null → 422 avec violation `categoryType: "Type de catégorie obligatoire."` (Symfony `Assert\NotNull`).
|
||||
- **RG-1.06** : La valeur de `categoryType` doit pointer vers un `CategoryType` existant. Sinon → 422 (Symfony / API Platform résolution IRI → 400 standard, mapping vers 422 ou 400 selon comportement API Platform à confirmer en dev).
|
||||
|
||||
### Unicité
|
||||
|
||||
- **RG-1.07** : Le couple `(LOWER(name), category_type_id)` est unique parmi les catégories **non soft-deleted**. Tentative de doublon (POST ou PATCH qui rend le couple en collision) → **409 Conflict**. Le `CategoryProcessor` attrape la `UniqueConstraintViolation` Postgres remontée par Doctrine et la traduit en 409 avec le message `"Une catégorie nommée \"{name}\" existe déjà pour ce type."`. L'index Postgres est partiel (`WHERE deleted_at IS NULL`), donc on peut recréer une catégorie avec le même `(name, type)` après suppression logique.
|
||||
|
||||
### Liste
|
||||
|
||||
- **RG-1.08** : `GET /api/categories` exclut **par défaut** les catégories soft-deleted (`deleted_at IS NOT NULL`). Implémenté dans le `CategoryProvider`.
|
||||
- **RG-1.09** : Un utilisateur avec `catalog.categories.manage` peut demander à voir les soft-deleted via `?includeDeleted=true`. Pour les autres rôles (qui ont 403 de toute façon), ce paramètre est ignoré.
|
||||
- **RG-1.10** : Tri par défaut côté serveur : `name ASC`. Pas d'autres tris au M0.
|
||||
|
||||
### Détail
|
||||
|
||||
- **RG-1.11** : `GET /api/categories/{id}` renvoie **404** si l'id n'existe pas, ou si la catégorie est soft-deleted ET `includeDeleted` n'est pas activé.
|
||||
|
||||
### Suppression
|
||||
|
||||
- **RG-1.12** : `DELETE /api/categories/{id}` est un **soft delete** (pose `deleted_at = now()`). Réponse `204`. Ne supprime jamais physiquement la ligne.
|
||||
- **RG-1.13** : Le champ `deletedAt` n'est **jamais** modifiable via PATCH (groupe `category:write` ne le contient pas). Seul le DELETE peut le mettre à jour. Restaurer une catégorie supprimée n'est pas un cas d'usage au M0 (HP-3).
|
||||
- **RG-1.14** : *(à activer post-M0, désactivée à la livraison initiale)* Quand les modules Tiers (M-Clients, M-Fournisseurs, M-Prestas) auront ajouté une FK `category_id` nullable, un DELETE sur une catégorie référencée par au moins un tiers → **409 Conflict** avec message `"Impossible de supprimer : N tier(s) référencent cette catégorie."`. **Au M0, cette règle est documentée mais non implémentée** (rien à référencer, donc rien à empêcher).
|
||||
|
||||
## 8. Tests à automatiser
|
||||
|
||||
### 8.1 Cas à couvrir (back — PHPUnit)
|
||||
|
||||
> Pattern Starseed (cf. `tests/` et CLAUDE.md § Tests). Helpers d'auth à utiliser : `createAdminClient()`, `createBureauClient()`, etc. (à créer côté SeedE2E si pas déjà en place).
|
||||
|
||||
- [ ] **RG-1.01** : `Bureau`, `Compta`, `Commerciale`, `Usine`, anonyme → 401 / 403 sur GET / POST / PATCH / DELETE.
|
||||
- [ ] **RG-1.01** : `Admin` → 200 / 201 / 204 selon le verbe sur tous les endpoints.
|
||||
- [ ] **RG-1.02 / RG-1.04** : POST avec `name` vide / null / whitespace / 1 caractère / 121 caractères → 422 avec violation `name`.
|
||||
- [ ] **RG-1.03** : POST `name = " Vis "` → persistance `"Vis"` (trim auto via `CategoryProcessor`).
|
||||
- [ ] **RG-1.05 / RG-1.06** : POST sans `categoryType` ou avec un IRI inexistant → 422.
|
||||
- [ ] **RG-1.07** : POST `(name="Vis", type=MATIERE)` puis POST `(name="vis", type=MATIERE)` (case-insensitive) → 2e → 409.
|
||||
- [ ] **RG-1.07** : POST `(name="Vis", type=MATIERE)` puis `(name="Vis", type=PRODUIT)` → les deux passent (types différents).
|
||||
- [ ] **RG-1.07** : Soft-delete d'une catégorie, puis POST avec exactement les mêmes `(name, type)` → 201 (l'index partiel autorise).
|
||||
- [ ] **RG-1.08 / RG-1.09** : GET liste sans flag → exclut les soft-deleted. GET liste avec `?includeDeleted=true` → inclut.
|
||||
- [ ] **RG-1.10** : GET liste → tri `name ASC` par défaut.
|
||||
- [ ] **RG-1.11** : GET détail d'une catégorie soft-deleted sans flag → 404. Avec flag → 200.
|
||||
- [ ] **RG-1.12** : DELETE → 204, ligne toujours présente en BDD avec `deleted_at IS NOT NULL`.
|
||||
- [ ] **RG-1.13** : PATCH avec body `{"deletedAt": null}` ou autre tentative d'écriture → champ ignoré (groupe write l'exclut).
|
||||
- [ ] **Audit** : POST + PATCH + DELETE → un `audit_log` est créé à chaque fois, avec `entity_type='Category'`, `entity_id={id}`, `performed_by={user.id}`, `action` correct, `changes` JSONB correct.
|
||||
- [ ] **Migration** : `make db-reset` → schéma à jour. Vérifier en Postgres (`\d category`) que `uq_category_name_type_active` apparaît comme index partiel.
|
||||
|
||||
### 8.2 Cas à couvrir (front — Vitest)
|
||||
|
||||
- [ ] Composable `useCategoriesAdmin()` : appel `useApi().get('/categories')` retourne la liste triée, soft-deleted exclus.
|
||||
- [ ] Composable `useCategoryForm()` : validation client-side avant POST (name requis, longueur — miroir RG-1.02/RG-1.04).
|
||||
- [ ] Composant `<CategoriesPage>` : `<MalioDataTable>` + bouton « + Ajouter » → ouverture drawer création. Clic ligne → drawer consultation.
|
||||
- [ ] Permissions : si user sans `catalog.categories.view` (mock store), redirection 403 ; item sidebar masqué.
|
||||
|
||||
### 8.3 Tests E2E
|
||||
|
||||
**Non prévus au M0.** Règle ABSOLUE Starseed n°7 : pas de E2E sauf bug critique passé en prod. Vitest + PHPUnit suffisent.
|
||||
|
||||
## 9. Hors-périmètre (HP)
|
||||
|
||||
- **HP-1** : **CRUD du référentiel `CategoryType`.** Le M0 crée la table vide via migration et expose `GET /api/category_types` en lecture seule. Le seed initial (`PRODUIT` / `SERVICE` / `MATIERE` / `AUTRE` ?) et le module admin pour les gérer feront l'objet d'une spec dédiée plus tard.
|
||||
- **HP-2** : **Référencement par les Tiers.** Les modules M-Clients / M-Fournisseurs / M-Prestas ajouteront une colonne `category_id` nullable dans leurs propres entités. **Aucun changement côté `Category`** au moment où ces modules arriveront, sauf activation de la RG-1.14 (blocage du soft-delete si référencée).
|
||||
- **HP-3** : **Restauration d'une catégorie soft-deleted.** Pas prévue au M0. Si besoin futur → endpoint dédié `POST /api/categories/{id}/restore`, permission `catalog.categories.manage`.
|
||||
- **HP-4** : **Hard delete.** Pas prévu (RGPD / purge → spec dédiée si besoin).
|
||||
- **HP-5** : **Internationalisation du `name`.** Pas d'i18n sur le champ libre saisi par l'admin. L'UI elle-même est en FR fixe.
|
||||
- **HP-6** : **Filtres avancés / recherche serveur** dans la liste. Pas pertinent à 300 entrées (pagination front).
|
||||
- **HP-7** : **Catégories hiérarchiques** (parent / enfant). Pas demandé. Si besoin futur → migration ajout colonne `parent_id` + spec dédiée.
|
||||
- **HP-8** : **Création des rôles métier Bureau / Compta / Commerciale / Usine.** Ces rôles font partie du modèle MALIO mais leur seed initial dans `role` + leur attribution aux users est hors du périmètre M0 (probablement un M-RBAC dédié, ou seedés dans `AppFixtures` / `SeedE2ECommand` au fil des modules).
|
||||
|
||||
## 10. Liens & dépendances
|
||||
|
||||
### Liens
|
||||
|
||||
- **Spec front (V0 client, 2026-05-22)** : [`./spec-front.md`](./spec-front.md)
|
||||
- **Workflow** : [`../../WORKFLOW_ERP.md`](../../WORKFLOW_ERP.md)
|
||||
- **Skill ticket-writer** : [`../../ticket-writer-SKILL.md`](../../ticket-writer-SKILL.md)
|
||||
- **Template spec back** : [`../../templates/spec-back.md`](../../templates/spec-back.md)
|
||||
- **Template ticket back** : [`../../templates/ticket-back.md`](../../templates/ticket-back.md)
|
||||
- **CLAUDE.md Starseed** : `~/dev_malio/Starseed/CLAUDE.md`
|
||||
- **Règles archi Starseed** : `~/dev_malio/Starseed/.claude/rules/architecture.md` (modular monolith DDD, namespace, modules)
|
||||
- **Règles back Starseed** : `~/dev_malio/Starseed/.claude/rules/backend.md` (ApiResource sans controllers, RBAC, Auditable)
|
||||
- **Spec audit** : `~/dev_malio/Starseed/doc/audit-log.md` (référence pour `#[Auditable]`)
|
||||
|
||||
### Dépendances amont (déjà en place dans Starseed)
|
||||
|
||||
- Module `Core` : RBAC (`module.resource.action`), commande `app:sync-permissions`, `PermissionVoter`, `AuditListener` + `AuditLogWriter`.
|
||||
- Module `Shared/Domain/Attribute/` : `Auditable`, `AuditIgnore`.
|
||||
- Table `user` (Lexik JWT) + table `role` + table `permission` + table `audit_log`.
|
||||
- Endpoint `/api/audit-log` (déjà fourni par Core, lit la table `audit_log`).
|
||||
|
||||
### Specs futures qui dépendent du M0
|
||||
|
||||
- **M-? — Gestion du référentiel `CategoryType`** (HP-1).
|
||||
- **M-Clients / M-Fournisseurs / M-Prestas** : ajout FK `category_id` nullable + activation RG-1.14.
|
||||
- **M-RBAC** *(éventuel)* — seed des rôles métier Bureau / Compta / Commerciale / Usine (HP-8).
|
||||
|
||||
---
|
||||
|
||||
## 📦 Tickets Lesstime générés
|
||||
|
||||
**TaskGroup Lesstime** : `#22 — M0 — Gestion des catégories` (projet `ERP / Starseed`, projectId=6)
|
||||
|
||||
> ⚠️ **Bug typing MCP** : le proxy MCP Lesstime stringifie les paramètres scalaires sans `type: "number"` explicite dans le schéma (`groupId`, `effortId`, `priorityId`). Conséquence : les 9 tickets ont été créés **sans rattachement au groupe**, **sans effort** et **sans priorité**. Toutes les infos sont dans le **titre** (`[N.M / Tag / Effort]`) et le **début de la description**. À rattacher manuellement au groupe #22 dans l'UI Lesstime + à renseigner effort/priorité.
|
||||
|
||||
| # | Ticket | Task ID | Number Lesstime | Effort | Tag |
|
||||
|---|---|---|---|---|---|
|
||||
| 0.1 | Migrer les tables Category et CategoryType | `#454` | `#43` | S | Backend |
|
||||
| 0.2 | Créer les entités Category et CategoryType | `#455` | `#44` | M | Backend |
|
||||
| 0.3 | Implémenter Provider et Processor Category | `#456` | `#45` | M | Backend |
|
||||
| 0.4 | Exposer le référentiel CategoryType en lecture seule | `#457` | `#46` | S | Backend |
|
||||
| 0.5 | Déclarer le module Catalog et synchroniser RBAC | `#458` | `#47` | S | Backend |
|
||||
| 0.6 | Écrire les tests PHPUnit RG-1.01 à RG-1.13 | `#459` | `#48` | M | Backend |
|
||||
| 0.7 | Créer la page Gestion des catégories (datatable + drawer) | `#460` | `#49` | L | Frontend |
|
||||
| 0.8 | Implémenter les composables useCategoriesAdmin et useCategoryForm | `#461` | `#50` | M | Frontend |
|
||||
| 0.9 | Écrire les tests Vitest des composables Catalog | `#462` | `#51` | S | Frontend |
|
||||
|
||||
**Total estimé** : ~15-25h (médian ~20h), 9 mini-MR de 1-4h.
|
||||
|
||||
### Actions manuelles à faire dans Lesstime (Matthieu)
|
||||
|
||||
1. Aller sur le projet **STARSEED** (#6) → TaskGroup **#22 « M0 — Gestion des catégories »**
|
||||
2. Pour chaque ticket `#43` à `#51` :
|
||||
- **Rattacher** au TaskGroup #22 (drag & drop ou champ Group)
|
||||
- **Effort** : lire le titre (`S` / `M` / `L`) et sélectionner dans Lesstime
|
||||
- **Tag** : lire le titre (`Backend` / `Frontend`) et sélectionner
|
||||
- **Priorité** : `Moyen` par défaut
|
||||
3. Vérifier que le statut reste `null` (backlog) — DoR pas encore cochée
|
||||
4. Mettre à jour `statut_global` ici en `validee_client` (au lieu de `en_dev`) si tu veux que la spec reflète l'état "tickets en backlog, pas encore pris"
|
||||
@@ -0,0 +1,113 @@
|
||||
---
|
||||
# === IDENTITÉ ===
|
||||
module: M0
|
||||
nom: "Gestion des catégories"
|
||||
ecran: gestion-categories
|
||||
owner_spec: Matthieu
|
||||
backup_spec: Tristan
|
||||
version: V0
|
||||
date_redaction: 2026-05-22
|
||||
|
||||
# === LIENS ===
|
||||
maquette_figma: null # pas de Figma — UI admin standard
|
||||
regles_metier: [RG-1.01, RG-1.02, RG-1.03, RG-1.04, RG-1.05, RG-1.06, RG-1.07, RG-1.08, RG-1.09, RG-1.10, RG-1.11, RG-1.12, RG-1.13]
|
||||
roles: [Admin, Bureau, Compta, Commerciale, Usine]
|
||||
lien_spec_back: ./spec-back.md
|
||||
|
||||
# === VALIDATION CLIENT #1 ===
|
||||
client_validation_1:
|
||||
statut: validee # V0 client validée le 22/05/2026
|
||||
date: 2026-05-22
|
||||
canal: ecrit
|
||||
valide_par: "Matthieu (CP MALIO) — validation implicite, périmètre projet"
|
||||
resume: "Module 0 — Gestion des catégories. Page admin (datatable + drawer). 2 champs (Nom + Type), 3 actions (Ajouter / Consulter / Modifier). Admin only."
|
||||
trace_archivee: "uploads/c4ebb6b4-M0categories.docx (V0 d'origine .docx) — restitué ci-dessous en Markdown."
|
||||
|
||||
# === LIEN LESSTIME ===
|
||||
lesstime_taskgroup_id: 22
|
||||
lesstime_project_id: 6 # ERP / Starseed
|
||||
statut_global: en_dev # tickets créés en backlog Lesstime le 2026-05-26
|
||||
---
|
||||
|
||||
# Module 0 — Gestion des catégories (V0 front)
|
||||
|
||||
> **Origine** : spec front V0 livrée le 22/05/2026 (`c4ebb6b4-M0categories.docx` + `f665acfb-M0categoriesV0.pdf`). Restitution Markdown fidèle pour intégration au workflow MALIO. Le contenu original n'est pas modifié — toute reformulation et précision (en particulier côté back) vit dans [`spec-back.md`](./spec-back.md).
|
||||
|
||||
## But
|
||||
|
||||
Permettre à un administrateur Starseed de gérer un référentiel de **catégories** depuis l'interface admin du logiciel. Ces catégories seront utilisées plus tard pour classifier les tiers (clients, fournisseurs, prestataires).
|
||||
|
||||
## Accès
|
||||
|
||||
- **Depuis** : menu principal → **Administration** → entrée « Gestion des catégories »
|
||||
- **Rôles autorisés** : **Admin uniquement** (Bureau / Compta / Commerciale / Usine n'ont **aucun** accès, ni lecture ni écriture).
|
||||
|
||||
## Navigation
|
||||
|
||||
L'écran est la page d'entrée du Module **Administration**. Titre de la page : « **Gestion des catégories** ».
|
||||
|
||||
- Affichage principal : un **datatable** listant toutes les catégories existantes.
|
||||
- **Clic sur une ligne** → ouverture d'un **drawer** latéral en mode **consultation / modification** (cf. § Action « Consulter »).
|
||||
- **Bouton « + Ajouter »** (en haut à droite du datatable) → ouverture d'un **drawer** en mode **création** (cf. § Action « Ajouter »).
|
||||
- Pas d'onglet, pas de pagination explicite (volumétrie cible faible).
|
||||
|
||||
## Actions
|
||||
|
||||
| Action | Déclencheur | Comportement |
|
||||
|---|---|---|
|
||||
| **Ajouter** | Clic sur le bouton « + Ajouter » | Ouvre le drawer en mode création, formulaire vide. Validation → POST → la catégorie apparaît dans le datatable. |
|
||||
| **Consulter** | Clic sur une ligne du datatable | Ouvre le drawer avec les champs pré-remplis en lecture (et passage en édition si l'utilisateur modifie un champ). |
|
||||
| **Modifier** | Modification d'un champ dans le drawer ouvert en consultation | Validation → PATCH → la ligne du datatable se met à jour. |
|
||||
|
||||
> **Note V0** : la **suppression** n'était pas mentionnée dans la V0 client. Côté workflow MALIO, suite à la revue back (cf. `spec-back.md` § Q3), un soft delete est ajouté (corbeille logique). L'UI peut intégrer ce point lors d'une V1 — au M0 le bouton « Supprimer » n'est pas obligatoire, mais doit être facilement ajoutable.
|
||||
|
||||
## Formulaire — Champs
|
||||
|
||||
Le formulaire (drawer) contient **2 champs**, tous deux obligatoires :
|
||||
|
||||
| Champ | Type | Obligatoire | Contenu / valeur par défaut | Règle |
|
||||
|---|---|---|---|---|
|
||||
| **Nom** | Texte libre | **Oui** | vide à la création | Pas de règle métier détaillée en V0. Détails côté back : RG-1.02 / RG-1.03 / RG-1.04 (obligatoire, trim, longueur 2–120). |
|
||||
| **Type de catégorie** | Select | **Oui** | vide à la création | Le contenu du Select n'était pas précisé en V0. Décision back : entité de référence `CategoryType` séparée (RG-1.05 / RG-1.06). Le référentiel sera alimenté plus tard (cf. HP-1 dans `spec-back.md`). |
|
||||
|
||||
> **Note V0** : la V0 ne précisait ni si le `Type de catégorie` est un enum hardcodé ni si c'est une autre entité. Décision tranchée côté back avant découpe en tickets : **entité de référence** (`category_types`), table créée vide au M0.
|
||||
|
||||
## Permissions par rôle
|
||||
|
||||
| Rôle | Vue (`GET`) | Création (`POST`) | Édition (`PATCH`) | Suppression (`DELETE`) |
|
||||
|---|---|---|---|---|
|
||||
| **Admin** | ✅ | ✅ | ✅ | ✅ (soft delete — ajout post-V0) |
|
||||
| Bureau | ❌ | ❌ | ❌ | ❌ |
|
||||
| Compta | ❌ | ❌ | ❌ | ❌ |
|
||||
| Commerciale | ❌ | ❌ | ❌ | ❌ |
|
||||
| Usine | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
→ Les rôles non-Admin ne voient **pas** l'entrée de menu et reçoivent **403** sur toute requête vers les endpoints `/api/categories/*` (cf. RG-1.01 dans `spec-back.md`).
|
||||
|
||||
## Composants UI à utiliser (Starseed / `@malio/layer-ui`)
|
||||
|
||||
- **Datatable** : `<MalioDataTable>` (avec colonnes `Nom` + `Type` + actions, tri par défaut sur Nom).
|
||||
- **Drawer** : drawer latéral standard `@malio/layer-ui` (à confirmer côté front avec le composant exact).
|
||||
- **Input texte** : `<MalioInputText>` pour le champ Nom.
|
||||
- **Select** : `<MalioSelect>` pour le champ Type de catégorie, alimenté par `GET /api/category_types`.
|
||||
- **Bouton** : `<MalioButton>` (« + Ajouter », « Enregistrer », « Annuler »).
|
||||
- **Toasts succès / erreur** : standards via `useApi()`.
|
||||
|
||||
## Points laissés ouverts par la V0 (résolus côté back)
|
||||
|
||||
| # | Zone d'ombre V0 | Résolution (cf. `spec-back.md`) |
|
||||
|---|---|---|
|
||||
| 1 | Suppression non mentionnée | **Soft delete** ajouté (RG-1.12 + RG-1.13). UI peut ajouter le bouton plus tard. |
|
||||
| 2 | Unicité du nom non précisée | **Unicité sur `(name, type)` case-insensitive**, parmi non-soft-deleted (RG-1.07). |
|
||||
| 3 | Nature du `Type de catégorie` (enum vs entité) | **Entité de référence** `CategoryType` (table vide au M0, créée par migration). |
|
||||
| 4 | Volumétrie & pagination | **300 max** → pagination front (`<MalioDataTable>`), pas de pagination serveur. Tri serveur `name ASC` par défaut. |
|
||||
| 5 | Audit / traçabilité | Pattern `#[Auditable]` Starseed standard. Trace dans la table `audit_log` (qui / quoi / quand / diff). **Pas** de colonnes `created_by` / `updated_by` sur l'entité (cohérent avec User / Role dans Starseed). Historique consultable via `/api/audit-log?entityType=Category&entityId={id}`. |
|
||||
| 6 | Référencement par d'autres entités | **Aucune FK entrante au M0.** Les modules Tiers (M-Clients / M-Fournisseurs / M-Prestas) ajouteront leur propre `category_id` plus tard. |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Tickets Lesstime générés
|
||||
|
||||
**TaskGroup Lesstime** : `#22 — M0 — Gestion des catégories` (projet `ERP / Starseed`, projectId=6)
|
||||
|
||||
> Détail complet, table des tickets et action manuelle dans Lesstime → voir [`spec-back.md § Tickets Lesstime générés`](./spec-back.md#-tickets-lesstime-générés).
|
||||
@@ -62,6 +62,6 @@ watch(() => route.path, () => {
|
||||
})
|
||||
|
||||
useHead({
|
||||
titleTemplate: (title) => title || 'Coltura',
|
||||
titleTemplate: (title) => title || 'Starseed',
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1 +1 @@
|
||||
/* Coltura - Custom styles */
|
||||
/* Starseed - Custom styles */
|
||||
|
||||
@@ -14,7 +14,7 @@ export default await nuxt(
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'coltura/custom-overrides',
|
||||
name: 'starseed/custom-overrides',
|
||||
rules: {
|
||||
// Indentation 4 espaces (convention CLAUDE.md)
|
||||
'vue/html-indent': ['error', 4],
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
},
|
||||
"dashboard": {
|
||||
"title": "Tableau de bord",
|
||||
"welcome": "Bienvenue sur Coltura"
|
||||
"welcome": "Bienvenue sur Starseed"
|
||||
},
|
||||
"commercial": {
|
||||
"title": "Commercial",
|
||||
|
||||
@@ -78,7 +78,7 @@ async function onChange(site: { id: string; name: string; color: string }): Prom
|
||||
// intentionnellement supprimee pour garantir qu'un clic sur le tile
|
||||
// "actif selon cet onglet" envoie quand meme le PATCH et re-synchronise
|
||||
// l'etat. Amelioration future : ecouter l'evenement `storage` sur la
|
||||
// cle `coltura:site-switch` pour mettre a jour les onglets inactifs
|
||||
// cle `starseed:site-switch` pour mettre a jour les onglets inactifs
|
||||
// sans clic via auth.fetchUser() / auth.refreshUser().
|
||||
|
||||
try {
|
||||
|
||||
Generated
+2
-2
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"name": "coltura-frontend",
|
||||
"name": "starseed-frontend",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "coltura-frontend",
|
||||
"name": "starseed-frontend",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@malio/layer-ui": "^1.5.0",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "coltura-frontend",
|
||||
"name": "starseed-frontend",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* Config Playwright pour les tests E2E de Coltura.
|
||||
* Config Playwright pour les tests E2E de Starseed.
|
||||
*
|
||||
* Pre-requis avant de lancer :
|
||||
* 1. Les containers Docker tournent (`make start`)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type {Config} from 'tailwindcss'
|
||||
|
||||
/**
|
||||
* Config Tailwind du projet Coltura.
|
||||
* Config Tailwind du projet Starseed.
|
||||
*
|
||||
* @nuxtjs/tailwindcss merge automatiquement les configs de chaque layer
|
||||
* Nuxt declare dans `nuxt.config.ts:extends`. Le layer `@malio/layer-ui`
|
||||
@@ -11,7 +11,7 @@ import type {Config} from 'tailwindcss'
|
||||
* success,btn-*,site-blue,site-yellow,site-green}
|
||||
* - fontFamily.sans (Helvetica Neue)
|
||||
*
|
||||
* Cette config locale ne redeclare QUE ce qui est specifique a Coltura
|
||||
* Cette config locale ne redeclare QUE ce qui est specifique a Starseed
|
||||
* ou absent de la config Malio — evite la duplication et les derives.
|
||||
*/
|
||||
export default <Partial<Config>>{
|
||||
@@ -19,7 +19,7 @@ export default <Partial<Config>>{
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Couleurs applicatives Coltura (hors namespace `m` reserve
|
||||
// Couleurs applicatives Starseed (hors namespace `m` reserve
|
||||
// au design system Malio partage).
|
||||
primary: {
|
||||
500: '#222783',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
DOCKER_APP_NAME=coltura
|
||||
DOCKER_APP_NAME=starseed
|
||||
DOCKER_PHP_VERSION=8.4.6
|
||||
DOCKER_NODE_VERSION=24.12.0
|
||||
APP_USER=www-data
|
||||
POSTGRES_DB=coltura
|
||||
POSTGRES_DB=starseed
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=root
|
||||
POSTGRES_PORT=5437
|
||||
|
||||
@@ -2,11 +2,12 @@ APP_ENV=prod
|
||||
APP_DEBUG=0
|
||||
APP_SECRET=CHANGE_ME_IN_PRODUCTION
|
||||
|
||||
DATABASE_URL="postgresql://coltura:CHANGE_ME@host.docker.internal:5432/coltura?serverVersion=16&charset=utf8"
|
||||
DATABASE_URL="postgresql://starseed:CHANGE_ME@host.docker.internal:5432/starseed_prod?serverVersion=16&charset=utf8"
|
||||
|
||||
JWT_PASSPHRASE=CHANGE_ME_IN_PRODUCTION
|
||||
JWT_COOKIE_SECURE=1
|
||||
# HTTP en reseau local => cookie non secure
|
||||
JWT_COOKIE_SECURE=0
|
||||
JWT_TOKEN_TTL=86400
|
||||
JWT_COOKIE_TTL=86400
|
||||
|
||||
CORS_ALLOW_ORIGIN='^https://coltura\.malio-dev\.fr$'
|
||||
CORS_ALLOW_ORIGIN='^http://starseed\.malio-dev\.fr$'
|
||||
|
||||
@@ -60,7 +60,7 @@ RUN rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Configs
|
||||
COPY infra/prod/supervisord.conf /etc/supervisor/conf.d/app.conf
|
||||
COPY infra/prod/nginx.conf /etc/nginx/sites-enabled/coltura.conf
|
||||
COPY infra/prod/nginx.conf /etc/nginx/sites-enabled/starseed.conf
|
||||
|
||||
# Backend from stage 1
|
||||
COPY --from=backend-build /app /var/www/html
|
||||
|
||||
@@ -4,9 +4,9 @@ set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
TAG="${1:-latest}"
|
||||
export COLTURA_IMAGE_TAG="$TAG"
|
||||
export STARSEED_IMAGE_TAG="$TAG"
|
||||
|
||||
echo "==> Deploying coltura:${TAG}..."
|
||||
echo "==> Deploying starseed:${TAG}..."
|
||||
|
||||
echo "==> Enabling maintenance mode..."
|
||||
touch maintenance.on
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
services:
|
||||
app:
|
||||
image: gitea.malio.fr/malio-dev/coltura:${COLTURA_IMAGE_TAG:-latest}
|
||||
container_name: coltura-app
|
||||
image: gitea.malio.fr/malio-dev/starseed:${STARSEED_IMAGE_TAG:-latest}
|
||||
container_name: starseed-app
|
||||
env_file: .env
|
||||
ports:
|
||||
- "8086:80"
|
||||
volumes:
|
||||
- ./config/jwt:/var/www/html/config/jwt:ro
|
||||
- coltura_logs:/var/www/html/var/log
|
||||
- starseed_logs:/var/www/html/var/log
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
coltura_logs:
|
||||
starseed_logs:
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name coltura.malio-dev.fr;
|
||||
server_name starseed.malio-dev.fr;
|
||||
|
||||
root /var/www/coltura/public;
|
||||
root /var/www/starseed/public;
|
||||
|
||||
# Maintenance mode
|
||||
if (-f /var/www/coltura/maintenance.on) {
|
||||
if (-f /var/www/starseed/maintenance.on) {
|
||||
return 503;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,12 @@ ENV_FILE := $(if $(wildcard $(ENV_LOCAL)),$(ENV_LOCAL),$(ENV_DEFAULT))
|
||||
include $(ENV_DEFAULT)
|
||||
-include $(ENV_LOCAL)
|
||||
|
||||
# Export du UID/GID host pour que docker compose les voie dans toutes les targets
|
||||
# (sinon le build du Dockerfile dev fail sur `usermod -u ${CURRENT_UID}` quand l'image
|
||||
# n'est pas en cache, ex: apres renommage du compose project).
|
||||
export CURRENT_UID := $(shell id -u)
|
||||
export CURRENT_GID := $(shell id -g)
|
||||
|
||||
PHP_CONTAINER = php-$(DOCKER_APP_NAME)-fpm
|
||||
SYMFONY_CONSOLE = $(EXEC_PHP) php bin/console
|
||||
|
||||
@@ -26,7 +32,7 @@ FILES =
|
||||
|
||||
# Affiche l'aide — cible par defaut (make ou make help)
|
||||
help:
|
||||
@printf "\n \033[1mColtura — Commandes make\033[0m\n\n"
|
||||
@printf "\n \033[1mStarseed — Commandes make\033[0m\n\n"
|
||||
@printf " \033[1;33mContainers\033[0m\n"
|
||||
@printf " \033[36m%-28s\033[0m %s\n" "start" "Demarrer les containers Docker"
|
||||
@printf " \033[36m%-28s\033[0m %s\n" "stop" "Arreter les containers"
|
||||
@@ -64,6 +70,7 @@ help:
|
||||
@printf " \033[36m%-28s\033[0m %s\n" "install-e2e-deps" "One-time : Chromium + libs systeme (sudo)"
|
||||
@printf "\n \033[1;33mQualite code\033[0m\n"
|
||||
@printf " \033[36m%-28s\033[0m %s\n" "php-cs-fixer-allow-risky" "Fix code style PHP (utilise par le pre-commit)"
|
||||
@printf " \033[36m%-28s\033[0m %s\n" "php-cs-fixer-check" "Dry-run du fixer (CI / verif avant push)"
|
||||
@printf "\n Plus de details : \033[4mREADME.md\033[0m, \033[4mCLAUDE.md\033[0m\n\n"
|
||||
|
||||
env-init:
|
||||
@@ -252,6 +259,11 @@ php-cs-fixer-allow-risky:
|
||||
@echo "Fixing files: $(FILES)"
|
||||
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes $(FILES)
|
||||
|
||||
# Dry-run du fixer : echec si au moins un fichier n'est pas conforme.
|
||||
# Utilise par la CI (Gitea pull_request) et avant un push manuel.
|
||||
php-cs-fixer-check:
|
||||
$(EXEC_PHP_CS_FIXER) fix --config=.php-cs-fixer.dist.php --allow-risky=yes --dry-run --diff $(FILES)
|
||||
|
||||
test:
|
||||
$(EXEC_PHP) php -d memory_limit="512M" vendor/bin/phpunit $(FILES)
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ use Doctrine\Migrations\AbstractMigration;
|
||||
* Module Sites - Ticket 1/4 : brique fondatrice de donnees.
|
||||
*
|
||||
* Cree la table `site` qui porte les etablissements physiques de l'instance
|
||||
* Coltura. La table est creee inconditionnellement : meme si SitesModule est
|
||||
* Starseed. La table est creee inconditionnellement : meme si SitesModule est
|
||||
* desactive dans `config/modules.php`, la structure DB existe (pas de
|
||||
* dependance dure depuis Core, mais pas de coin d'ombre schema non plus).
|
||||
*
|
||||
|
||||
@@ -24,7 +24,7 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* Site physique (usine / etablissement) appartenant a l'instance Coltura.
|
||||
* Site physique (usine / etablissement) appartenant a l'instance Starseed.
|
||||
*
|
||||
* Adresse decomposee en champs structures (rue, complement, CP, ville) pour
|
||||
* permettre des recherches/tris fins ulterieurs et eviter les divergences
|
||||
|
||||
@@ -36,7 +36,7 @@ class SitesFixtures extends Fixture
|
||||
|
||||
public function load(ObjectManager $manager): void
|
||||
{
|
||||
// Chatellerault : bleu Coltura.
|
||||
// Chatellerault : bleu Starseed.
|
||||
$this->ensureSite(
|
||||
$manager,
|
||||
name: 'Chatellerault',
|
||||
|
||||
Reference in New Issue
Block a user