8313c759c6
Auto Tag Develop / tag (push) Successful in 9s
## Migration modular monolith DDD — Lesstime (0.1 → 3.3) Cette MR regroupe l'intégralité de la refonte en monolithe modulaire (strangler progressif, additif). Elle remplace les MR stackées de Phase 1 (#12–#16), désormais incluses ici. **Ne pas merger avant validation fonctionnelle** : branche destinée à être testée telle quelle. ### Périmètre — 9 modules sous `src/Module/` | Phase | Module | Contenu | |------|--------|---------| | 0.1 | (socle) | infrastructure modulaire, `ModuleInterface`, mapping Doctrine par module | | 0.2 | (socle front) | auto-détection des layers Nuxt sous `frontend/modules/*` | | 1.1 | **Core** | Identité (User/Auth), Notifications, Notifier | | 1.2 | Core | RBAC fin (permissions `module.resource.action`, sidebar gated) | | 1.3 | Core | Audit log (`#[Auditable]`, listener, provider DBAL) | | 2.1 | **TimeTracking** | TimeEntry + MCP + export | | 2.2 | **ProjectManagement** | cœur métier Projets/Tâches + 38 MCP tools | | 2.3 | **Absence** | demandes, soldes, policies, justificatifs | | 2.4 | **Directory** | Clients (migrés) + **Prospects** (nouveau, conversion → Client) | | 2.5 | **Mail** | intégration IMAP OVH + liens tâches | | 2.6 | **Integration** | Gitea / BookStack / Zimbra / Share | | 3.1 | **Reporting** | rapports transverses (DBAL read-only, 0 import inter-module) | | 3.2 | **ClientPortal** | portail client (ROLE_CLIENT cloisonné, tickets, notifications) | | 3.3 | (finition) | nettoyage legacy — `src/Entity` vide, app 100% modulaire | ### Architecture - Découplage inter-modules par **contrats** (`UserInterface`, `ProjectInterface`, `TaskInterface`, `TaskTagInterface`, `ClientInterface`, `ClientTicketInterface`, `LeaveProfileInterface`) + `resolve_target_entities` 100% modulaire (aucune cible legacy). - Repositories : interface `Domain/Repository` + implémentation `Infrastructure/Doctrine`, bindées. - Reporting en DBAL read-only pur (aucun import d'entité d'un autre module). - Chaque migration de module : déplacement à comportement préservé (API publique et noms d'outils MCP inchangés), migrations **additives** uniquement (zéro destructif). ### Sécurité - ROLE_CLIENT cloisonné : un utilisateur client n'accède qu'à `/portal` et à ses propres tickets (filtrés par `allowedProjects`), interdit sur toute l'API interne. - Correctif : interdiction pour un client de créer un lien vers le partage SMB (upload uniquement). ### QA non-régression (branche reconstruite from scratch) - Migrations from scratch + fixtures : OK. - Compilation dev + prod : OK. - **180 tests PHPUnit verts**, php-cs-fixer clean, ~96 routes, **66 outils MCP** tous sous `App\Module\*`. - Smoke test runtime multi-rôles (admin / ROLE_USER / ROLE_CLIENT) : 44 vérifications HTTP, **0 écart**, cloisonnement client étanche. - Build Nuxt OK, 9 layers, 0 import legacy résiduel. ### Points à arbitrer (hors périmètre de cette migration) - Durcissement MCP/IDOR pré-existant (`userId` explicite sans scoping sur certains tools TimeTracking/Absence/TaskDocument) — ticket dédié recommandé. - Validation fonctionnelle de **Prospect** et **ClientPortal** (conçus depuis les specs disque). - **Harmonisation visuelle Malio finale** (3.3) — finition esthétique inter-modules laissée au PO. --- ## ⚠️ Déploiement / migration des données — à ne pas oublier ### 1. Resynchroniser les séquences PostgreSQL après tout import/restore de dump Si la prod (ou tout environnement) est **montée depuis un dump** (`pg_restore` / `COPY`), les lignes sont chargées avec leurs `id` explicites **sans avancer les séquences** → au premier `INSERT` : `duplicate key value violates unique constraint "..._pkey"` (constaté en local sur `notification`, `task`, `time_entry`…). À lancer **juste après chaque restore/import** : ```sql DO $$ DECLARE r RECORD; maxid BIGINT; seq TEXT; BEGIN FOR r IN SELECT table_name, column_name FROM information_schema.columns WHERE table_schema='public' LOOP seq := pg_get_serial_sequence(quote_ident(r.table_name), r.column_name); IF seq IS NOT NULL THEN EXECUTE format('SELECT COALESCE(MAX(%I),0) FROM %I', r.column_name, r.table_name) INTO maxid; PERFORM setval(seq, GREATEST(maxid,1), maxid > 0); END IF; END LOOP; END $$; ``` > Ne concerne **pas** une prod qui tourne déjà (séquences avancées organiquement) — uniquement le cas restore/import. Idempotent, sans risque. ### 2. Fix dénormalisation des collections typées-contrat (code, inclus dans la branche) Les relations **to-many** typées par une interface `Shared\Domain\Contract\*` (`TimeEntry::tags` → `TaskTagInterface`, `Task::collaborators` → `UserInterface`) étaient **indénormalisables par API Platform** (mono-valué OK via IRI, collection KO) → **tout POST/PATCH portant une telle collection renvoyait 400/500**. Corrigé par un dénormaliseur générique `ContractRelationDenormalizer` (réutilise `resolve_target_entities`, zéro couplage par-entité) + test fonctionnel de non-régression. --------- Co-authored-by: Matthieu <contact@malio.fr> Reviewed-on: #17
121 lines
7.8 KiB
PHP
121 lines
7.8 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace DoctrineMigrations;
|
|
|
|
use Doctrine\DBAL\Schema\Schema;
|
|
use Doctrine\Migrations\AbstractMigration;
|
|
|
|
/**
|
|
* Remove the client portal entirely (reverses phases 1 & 3).
|
|
*
|
|
* - Drops notification.related_ticket_id (FK/index/column).
|
|
* - Restores task_document to a task-only model: drops the CHECK constraint and
|
|
* client_ticket_id, removes orphan client-ticket-only documents, makes task_id
|
|
* NOT NULL again.
|
|
* - Drops task.client_ticket_id.
|
|
* - Drops the user_allowed_projects join table and user.client_id.
|
|
* - Drops the client_ticket table.
|
|
* - Deletes leftover client accounts (roles contains ROLE_CLIENT): with the portal
|
|
* gone every user now resolves to ROLE_USER, so external client accounts MUST be
|
|
* removed to avoid silently granting them internal access.
|
|
*
|
|
* Lowercase SQL columns; "user" table is quoted. down() recreates the schema
|
|
* (structure only — deleted client accounts/tickets are not restored).
|
|
*/
|
|
final class Version20260622090000 extends AbstractMigration
|
|
{
|
|
public function getDescription(): string
|
|
{
|
|
return 'Remove client portal: drop client_ticket, related portal columns/links and external client accounts';
|
|
}
|
|
|
|
public function up(Schema $schema): void
|
|
{
|
|
// --- notification.related_ticket_id (phase 3) ---
|
|
$this->addSql('ALTER TABLE notification DROP CONSTRAINT FK_BF5476CA98F144DB');
|
|
$this->addSql('DROP INDEX idx_notification_related_ticket');
|
|
$this->addSql('ALTER TABLE notification DROP related_ticket_id');
|
|
|
|
// --- task_document: back to task-only ---
|
|
$this->addSql('ALTER TABLE task_document DROP CONSTRAINT chk_task_document_target');
|
|
$this->addSql('ALTER TABLE task_document DROP CONSTRAINT FK_98A9603A9B2097DD');
|
|
$this->addSql('DROP INDEX IDX_98A9603A9B2097DD');
|
|
// Remove documents attached only to a client ticket before re-enforcing NOT NULL.
|
|
$this->addSql('DELETE FROM task_document WHERE task_id IS NULL');
|
|
$this->addSql('ALTER TABLE task_document DROP client_ticket_id');
|
|
$this->addSql('ALTER TABLE task_document ALTER task_id SET NOT NULL');
|
|
|
|
// --- task.client_ticket_id ---
|
|
$this->addSql('ALTER TABLE task DROP CONSTRAINT FK_527EDB259B2097DD');
|
|
$this->addSql('DROP INDEX IDX_527EDB259B2097DD');
|
|
$this->addSql('ALTER TABLE task DROP client_ticket_id');
|
|
|
|
// --- user_allowed_projects ---
|
|
$this->addSql('ALTER TABLE user_allowed_projects DROP CONSTRAINT FK_B3E0FC97A76ED395');
|
|
$this->addSql('ALTER TABLE user_allowed_projects DROP CONSTRAINT FK_B3E0FC97166D1F9C');
|
|
$this->addSql('DROP TABLE user_allowed_projects');
|
|
|
|
// --- user.client_id ---
|
|
$this->addSql('ALTER TABLE "user" DROP CONSTRAINT FK_8D93D64919EB6921');
|
|
$this->addSql('DROP INDEX IDX_8D93D64919EB6921');
|
|
$this->addSql('ALTER TABLE "user" DROP client_id');
|
|
|
|
// --- client_ticket table ---
|
|
$this->addSql('ALTER TABLE client_ticket DROP CONSTRAINT FK_C206E610166D1F9C');
|
|
$this->addSql('ALTER TABLE client_ticket DROP CONSTRAINT FK_C206E61079F7D87D');
|
|
$this->addSql('ALTER TABLE client_ticket DROP CONSTRAINT FK_C206E610DE12AB56');
|
|
$this->addSql('ALTER TABLE client_ticket DROP CONSTRAINT FK_C206E61016FE72E1');
|
|
$this->addSql('DROP TABLE client_ticket');
|
|
|
|
// --- external client accounts ---
|
|
$this->addSql('DELETE FROM "user" WHERE roles::text LIKE \'%ROLE_CLIENT%\'');
|
|
}
|
|
|
|
public function down(Schema $schema): void
|
|
{
|
|
// --- client_ticket table ---
|
|
$this->addSql('CREATE TABLE client_ticket (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, number INT NOT NULL, type VARCHAR(16) NOT NULL, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, url VARCHAR(1024) DEFAULT NULL, status VARCHAR(16) NOT NULL, status_comment TEXT DEFAULT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, updated_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, project_id INT NOT NULL, submitted_by_id INT DEFAULT NULL, created_by INT DEFAULT NULL, updated_by INT DEFAULT NULL, PRIMARY KEY (id))');
|
|
$this->addSql('CREATE UNIQUE INDEX uniq_client_ticket_project_number ON client_ticket (project_id, number)');
|
|
$this->addSql('CREATE INDEX idx_client_ticket_project ON client_ticket (project_id)');
|
|
$this->addSql('CREATE INDEX idx_client_ticket_submitted_by ON client_ticket (submitted_by_id)');
|
|
$this->addSql('CREATE INDEX idx_client_ticket_status_project ON client_ticket (status, project_id)');
|
|
$this->addSql('CREATE INDEX IDX_C206E610DE12AB56 ON client_ticket (created_by)');
|
|
$this->addSql('CREATE INDEX IDX_C206E61016FE72E1 ON client_ticket (updated_by)');
|
|
$this->addSql('ALTER TABLE client_ticket ADD CONSTRAINT FK_C206E610166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE NOT DEFERRABLE');
|
|
$this->addSql('ALTER TABLE client_ticket ADD CONSTRAINT FK_C206E61079F7D87D FOREIGN KEY (submitted_by_id) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
$this->addSql('ALTER TABLE client_ticket ADD CONSTRAINT FK_C206E610DE12AB56 FOREIGN KEY (created_by) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
$this->addSql('ALTER TABLE client_ticket ADD CONSTRAINT FK_C206E61016FE72E1 FOREIGN KEY (updated_by) REFERENCES "user" (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
|
|
// --- user.client_id ---
|
|
$this->addSql('ALTER TABLE "user" ADD client_id INT DEFAULT NULL');
|
|
$this->addSql('ALTER TABLE "user" ADD CONSTRAINT FK_8D93D64919EB6921 FOREIGN KEY (client_id) REFERENCES client (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
$this->addSql('CREATE INDEX IDX_8D93D64919EB6921 ON "user" (client_id)');
|
|
|
|
// --- user_allowed_projects ---
|
|
$this->addSql('CREATE TABLE user_allowed_projects (user_id INT NOT NULL, project_id INT NOT NULL, PRIMARY KEY (user_id, project_id))');
|
|
$this->addSql('CREATE INDEX IDX_B3E0FC97A76ED395 ON user_allowed_projects (user_id)');
|
|
$this->addSql('CREATE INDEX IDX_B3E0FC97166D1F9C ON user_allowed_projects (project_id)');
|
|
$this->addSql('ALTER TABLE user_allowed_projects ADD CONSTRAINT FK_B3E0FC97A76ED395 FOREIGN KEY (user_id) REFERENCES "user" (id) ON DELETE CASCADE NOT DEFERRABLE');
|
|
$this->addSql('ALTER TABLE user_allowed_projects ADD CONSTRAINT FK_B3E0FC97166D1F9C FOREIGN KEY (project_id) REFERENCES project (id) ON DELETE CASCADE NOT DEFERRABLE');
|
|
|
|
// --- task.client_ticket_id ---
|
|
$this->addSql('ALTER TABLE task ADD client_ticket_id INT DEFAULT NULL');
|
|
$this->addSql('ALTER TABLE task ADD CONSTRAINT FK_527EDB259B2097DD FOREIGN KEY (client_ticket_id) REFERENCES client_ticket (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
$this->addSql('CREATE INDEX IDX_527EDB259B2097DD ON task (client_ticket_id)');
|
|
|
|
// --- task_document generalisation ---
|
|
$this->addSql('ALTER TABLE task_document ALTER task_id DROP NOT NULL');
|
|
$this->addSql('ALTER TABLE task_document ADD client_ticket_id INT DEFAULT NULL');
|
|
$this->addSql('ALTER TABLE task_document ADD CONSTRAINT FK_98A9603A9B2097DD FOREIGN KEY (client_ticket_id) REFERENCES client_ticket (id) ON DELETE CASCADE NOT DEFERRABLE');
|
|
$this->addSql('CREATE INDEX IDX_98A9603A9B2097DD ON task_document (client_ticket_id)');
|
|
$this->addSql('ALTER TABLE task_document ADD CONSTRAINT chk_task_document_target CHECK (task_id IS NOT NULL OR client_ticket_id IS NOT NULL)');
|
|
|
|
// --- notification.related_ticket_id ---
|
|
$this->addSql('ALTER TABLE notification ADD related_ticket_id INT DEFAULT NULL');
|
|
$this->addSql('ALTER TABLE notification ADD CONSTRAINT FK_BF5476CA98F144DB FOREIGN KEY (related_ticket_id) REFERENCES client_ticket (id) ON DELETE SET NULL NOT DEFERRABLE');
|
|
$this->addSql('CREATE INDEX idx_notification_related_ticket ON notification (related_ticket_id)');
|
|
}
|
|
}
|