feat(client-portal) : phase 1 foundations — ROLE_CLIENT hardening + ClientTicket (back)
LST-69 (3.2) phase 1. New ClientPortal module + security foundations for the client portal (spec docs/superpowers/specs/2026-03-15-client-portal-design.md). - Security: User::getRoles() no longer adds ROLE_USER to ROLE_CLIENT users; role_hierarchy ROLE_ADMIN: [ROLE_USER, ROLE_CLIENT]. Existing Task/Project/ Client/TimeEntry/metadata endpoints already required ROLE_USER -> a pure ROLE_CLIENT is walled off (verified: 403). - User (Core): client (ManyToOne ClientInterface, SET NULL) + allowedProjects (ManyToMany ProjectInterface). UserInterface extended (getClient/ getAllowedProjects). - New ClientTicket entity (module ClientPortal) + enums + repository + API with per-client isolation (ClientTicketProvider: own tickets ∩ allowedProjects), per-project numbering under advisory lock (rejects if user.client null), status transition rules. ClientTicketInterface contract for Task/TaskDocument. - TaskDocument generalized: task nullable + clientTicket (CASCADE) + CHECK; per-role access. Task.clientTicket exposed in task:read. - Additive migration; demo client fixtures. - Tenancy tests assert the isolation invariant (a client never sees another client's tickets) rather than brittle absolute counts (shared test DB). 178 tests green, mapping valid, cs-fixer clean.
This commit is contained in:
@@ -10,6 +10,9 @@ use App\Module\Absence\Domain\Entity\AbsencePolicy;
|
||||
use App\Module\Absence\Domain\Entity\AbsenceRequest;
|
||||
use App\Module\Absence\Domain\Enum\AbsenceStatus;
|
||||
use App\Module\Absence\Domain\Enum\AbsenceType;
|
||||
use App\Module\ClientPortal\Domain\Entity\ClientTicket;
|
||||
use App\Module\ClientPortal\Domain\Enum\ClientTicketStatus;
|
||||
use App\Module\ClientPortal\Domain\Enum\ClientTicketType;
|
||||
use App\Module\Core\Application\Rbac\RbacSeeder;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use App\Module\Directory\Domain\Entity\Client;
|
||||
@@ -215,6 +218,53 @@ class AppFixtures extends Fixture
|
||||
$projectInterne->setWorkflow($standardWorkflow);
|
||||
$manager->persist($projectInterne);
|
||||
|
||||
// Client portal users (ROLE_CLIENT) — linked to a client + allowed projects.
|
||||
$clientUserLiot = new User();
|
||||
$clientUserLiot->setUsername('client-liot');
|
||||
$clientUserLiot->setFirstName('Camille');
|
||||
$clientUserLiot->setLastName('LIOT');
|
||||
$clientUserLiot->setRoles(['ROLE_CLIENT']);
|
||||
$clientUserLiot->setPassword($this->passwordHasher->hashPassword($clientUserLiot, 'client-liot'));
|
||||
$clientUserLiot->setClient($clientLiot);
|
||||
$clientUserLiot->addAllowedProject($projectSirh);
|
||||
$manager->persist($clientUserLiot);
|
||||
|
||||
$clientUserAcme = new User();
|
||||
$clientUserAcme->setUsername('client-acme');
|
||||
$clientUserAcme->setFirstName('Sophie');
|
||||
$clientUserAcme->setLastName('ACME');
|
||||
$clientUserAcme->setRoles(['ROLE_CLIENT']);
|
||||
$clientUserAcme->setPassword($this->passwordHasher->hashPassword($clientUserAcme, 'client-acme'));
|
||||
$clientUserAcme->setClient($clientAcme);
|
||||
$clientUserAcme->addAllowedProject($projectCrm);
|
||||
$manager->persist($clientUserAcme);
|
||||
|
||||
// Demo client tickets.
|
||||
$ticketLiot = new ClientTicket();
|
||||
$ticketLiot->setNumber(1);
|
||||
$ticketLiot->setType(ClientTicketType::Bug);
|
||||
$ticketLiot->setTitle('Erreur lors de l\'export des congés');
|
||||
$ticketLiot->setDescription('L\'export PDF des congés échoue avec une erreur 500.');
|
||||
$ticketLiot->setUrl('https://app.example.com/sirh/conges');
|
||||
$ticketLiot->setStatus(ClientTicketStatus::New);
|
||||
$ticketLiot->setProject($projectSirh);
|
||||
$ticketLiot->setSubmittedBy($clientUserLiot);
|
||||
$ticketLiot->setCreatedAt(new DateTimeImmutable());
|
||||
$ticketLiot->setUpdatedAt(new DateTimeImmutable());
|
||||
$manager->persist($ticketLiot);
|
||||
|
||||
$ticketAcme = new ClientTicket();
|
||||
$ticketAcme->setNumber(1);
|
||||
$ticketAcme->setType(ClientTicketType::Improvement);
|
||||
$ticketAcme->setTitle('Ajouter un filtre par commercial');
|
||||
$ticketAcme->setDescription('Pouvoir filtrer la liste des opportunités par commercial assigné.');
|
||||
$ticketAcme->setStatus(ClientTicketStatus::InProgress);
|
||||
$ticketAcme->setProject($projectCrm);
|
||||
$ticketAcme->setSubmittedBy($clientUserAcme);
|
||||
$ticketAcme->setCreatedAt(new DateTimeImmutable());
|
||||
$ticketAcme->setUpdatedAt(new DateTimeImmutable());
|
||||
$manager->persist($ticketAcme);
|
||||
|
||||
// Task Efforts
|
||||
$effortS = new TaskEffort();
|
||||
$effortS->setLabel('S');
|
||||
|
||||
Reference in New Issue
Block a user