feat: Attribution de rôles multiples par utilisateur
Les utilisateurs Classeo étaient limités à un seul rôle, alors que dans la réalité scolaire un directeur peut aussi être enseignant, ou un parent peut avoir un rôle vie scolaire. Cette limitation obligeait à créer des comptes distincts par fonction. Le modèle User supporte désormais plusieurs rôles simultanés avec basculement via le header. L'admin peut attribuer/retirer des rôles depuis l'interface de gestion, avec des garde-fous : pas d'auto- destitution, pas d'escalade de privilèges (seul SUPER_ADMIN peut attribuer SUPER_ADMIN), vérification du statut actif pour le switch de rôle, et TTL explicite sur le cache de rôle actif.
This commit is contained in:
36
backend/src/Administration/Domain/Event/RoleAttribue.php
Normal file
36
backend/src/Administration/Domain/Event/RoleAttribue.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class RoleAttribue implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public string $email,
|
||||
public string $role,
|
||||
public TenantId $tenantId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->userId->value;
|
||||
}
|
||||
}
|
||||
36
backend/src/Administration/Domain/Event/RoleRetire.php
Normal file
36
backend/src/Administration/Domain/Event/RoleRetire.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Event;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class RoleRetire implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public string $email,
|
||||
public string $role,
|
||||
public TenantId $tenantId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->userId->value;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\ActivationToken\ActivationTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ActivationTokenAlreadyUsedException extends RuntimeException
|
||||
final class ActivationTokenAlreadyUsedException extends DomainException
|
||||
{
|
||||
public static function forToken(ActivationTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\ActivationToken\ActivationTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ActivationTokenExpiredException extends RuntimeException
|
||||
final class ActivationTokenExpiredException extends DomainException
|
||||
{
|
||||
public static function forToken(ActivationTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\ActivationToken\ActivationTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ActivationTokenNotFoundException extends RuntimeException
|
||||
final class ActivationTokenNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(ActivationTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
final class CannotChangeGradingModeWithExistingGradesException extends RuntimeException
|
||||
final class CannotChangeGradingModeWithExistingGradesException extends DomainException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ClassNameInvalideException extends RuntimeException
|
||||
final class ClassNameInvalideException extends DomainException
|
||||
{
|
||||
public static function pourLongueur(string $value, int $min, int $max): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassName;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ClasseDejaExistanteException extends RuntimeException
|
||||
final class ClasseDejaExistanteException extends DomainException
|
||||
{
|
||||
public static function avecNom(ClassName $name): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ClasseNonSupprimableException extends RuntimeException
|
||||
final class ClasseNonSupprimableException extends DomainException
|
||||
{
|
||||
public static function carElevesAffectes(ClassId $classId, int $nombreEleves): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class ClasseNotFoundException extends RuntimeException
|
||||
final class ClasseNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(ClassId $classId): self
|
||||
{
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\StatutCompte;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class CompteNonActivableException extends RuntimeException
|
||||
final class CompteNonActivableException extends DomainException
|
||||
{
|
||||
public static function carStatutIncompatible(UserId $userId, StatutCompte $statut): self
|
||||
{
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class DernierRoleNonRetirableException extends DomainException
|
||||
{
|
||||
public static function pour(UserId $userId): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Impossible de retirer le dernier rôle de l\'utilisateur %s.',
|
||||
$userId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EmailDejaUtiliseeException extends RuntimeException
|
||||
final class EmailDejaUtiliseeException extends DomainException
|
||||
{
|
||||
public static function dansTenant(Email $email, TenantId $tenantId): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EmailInvalideException extends RuntimeException
|
||||
final class EmailInvalideException extends DomainException
|
||||
{
|
||||
public static function pourAdresse(string $email): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\GradingConfiguration\SchoolGradingConfigurationId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class GradingConfigurationNotFoundException extends RuntimeException
|
||||
final class GradingConfigurationNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(SchoolGradingConfigurationId $id): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidPeriodCountException extends RuntimeException
|
||||
final class InvalidPeriodCountException extends DomainException
|
||||
{
|
||||
public static function forType(string $type, int $expected, int $actual): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class InvalidPeriodDatesException extends RuntimeException
|
||||
final class InvalidPeriodDatesException extends DomainException
|
||||
{
|
||||
public static function endBeforeStart(string $label, string $start, string $end): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenAlreadyUsedException extends RuntimeException
|
||||
final class PasswordResetTokenAlreadyUsedException extends DomainException
|
||||
{
|
||||
public static function forToken(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenExpiredException extends RuntimeException
|
||||
final class PasswordResetTokenExpiredException extends DomainException
|
||||
{
|
||||
public static function forToken(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\PasswordResetToken\PasswordResetTokenId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PasswordResetTokenNotFoundException extends RuntimeException
|
||||
final class PasswordResetTokenNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(PasswordResetTokenId $tokenId): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PeriodeAvecNotesException extends RuntimeException
|
||||
final class PeriodeAvecNotesException extends DomainException
|
||||
{
|
||||
public static function confirmationRequise(string $label): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PeriodeNonTrouveeException extends RuntimeException
|
||||
final class PeriodeNonTrouveeException extends DomainException
|
||||
{
|
||||
public static function pourSequence(int $sequence, string $academicYearId): self
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
final class PeriodesDejaConfigureesException extends RuntimeException
|
||||
final class PeriodesDejaConfigureesException extends DomainException
|
||||
{
|
||||
public static function pourAnnee(string $academicYearId): self
|
||||
{
|
||||
|
||||
@@ -4,9 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
final class PeriodesNonConfigureesException extends RuntimeException
|
||||
final class PeriodesNonConfigureesException extends DomainException
|
||||
{
|
||||
public static function pourAnnee(string $academicYearId): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PeriodsCoverageGapException extends RuntimeException
|
||||
final class PeriodsCoverageGapException extends DomainException
|
||||
{
|
||||
public static function gapBetween(string $periodA, string $periodB): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class PeriodsOverlapException extends RuntimeException
|
||||
final class PeriodsOverlapException extends DomainException
|
||||
{
|
||||
public static function between(string $periodA, string $periodB): self
|
||||
{
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class RoleDejaAttribueException extends DomainException
|
||||
{
|
||||
public static function pour(UserId $userId, Role $role): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le rôle « %s » est déjà attribué à l\'utilisateur %s.',
|
||||
$role->label(),
|
||||
$userId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class RoleNonAttribueException extends DomainException
|
||||
{
|
||||
public static function pour(UserId $userId, Role $role): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Le rôle « %s » n\'est pas attribué à l\'utilisateur %s.',
|
||||
$role->label(),
|
||||
$userId,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
@@ -14,7 +14,7 @@ use function sprintf;
|
||||
*
|
||||
* @see Story 1.6 - Gestion des sessions
|
||||
*/
|
||||
final class SessionNotFoundException extends RuntimeException
|
||||
final class SessionNotFoundException extends DomainException
|
||||
{
|
||||
public function __construct(TokenFamilyId $familyId)
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectCodeInvalideException extends RuntimeException
|
||||
final class SubjectCodeInvalideException extends DomainException
|
||||
{
|
||||
public static function pourFormat(string $value, int $min, int $max): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectColorInvalideException extends RuntimeException
|
||||
final class SubjectColorInvalideException extends DomainException
|
||||
{
|
||||
public static function pourFormat(string $value): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\Subject\SubjectCode;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectDejaExistanteException extends RuntimeException
|
||||
final class SubjectDejaExistanteException extends DomainException
|
||||
{
|
||||
public static function avecCode(SubjectCode $code): self
|
||||
{
|
||||
|
||||
@@ -4,11 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectNameInvalideException extends RuntimeException
|
||||
final class SubjectNameInvalideException extends DomainException
|
||||
{
|
||||
public static function pourLongueur(string $value, int $min, int $max): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectNonSupprimableException extends RuntimeException
|
||||
final class SubjectNonSupprimableException extends DomainException
|
||||
{
|
||||
public static function avecNotes(SubjectId $id): self
|
||||
{
|
||||
|
||||
@@ -5,11 +5,11 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SubjectNotFoundException extends RuntimeException
|
||||
final class SubjectNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(SubjectId $id): self
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
/**
|
||||
* Exception thrown when a refresh token has already been rotated but is still in grace period.
|
||||
@@ -14,7 +14,7 @@ use RuntimeException;
|
||||
*
|
||||
* @see Story 1.4 - Connexion utilisateur
|
||||
*/
|
||||
final class TokenAlreadyRotatedException extends RuntimeException
|
||||
final class TokenAlreadyRotatedException extends DomainException
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
@@ -14,7 +14,7 @@ use function sprintf;
|
||||
* This indicates a concurrent request is processing the same token,
|
||||
* and the client should retry after a short delay.
|
||||
*/
|
||||
final class TokenConsumptionInProgressException extends RuntimeException
|
||||
final class TokenConsumptionInProgressException extends DomainException
|
||||
{
|
||||
public function __construct(string $tokenValue)
|
||||
{
|
||||
|
||||
@@ -5,7 +5,7 @@ declare(strict_types=1);
|
||||
namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\RefreshToken\TokenFamilyId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
@@ -21,7 +21,7 @@ use function sprintf;
|
||||
* - Un audit log doit être créé
|
||||
* - Une alerte de sécurité peut être envoyée
|
||||
*/
|
||||
final class TokenReplayDetectedException extends RuntimeException
|
||||
final class TokenReplayDetectedException extends DomainException
|
||||
{
|
||||
public function __construct(
|
||||
public readonly TokenFamilyId $familyId,
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\Email;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UserNotFoundException extends RuntimeException
|
||||
final class UserNotFoundException extends DomainException
|
||||
{
|
||||
public static function withId(UserId $userId): self
|
||||
{
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\StatutCompte;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UtilisateurDejaInviteException extends RuntimeException
|
||||
final class UtilisateurDejaInviteException extends DomainException
|
||||
{
|
||||
public static function carStatutIncompatible(UserId $userId, StatutCompte $statut): self
|
||||
{
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\StatutCompte;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UtilisateurNonBlocableException extends RuntimeException
|
||||
final class UtilisateurNonBlocableException extends DomainException
|
||||
{
|
||||
public static function carStatutIncompatible(UserId $userId, StatutCompte $statut): self
|
||||
{
|
||||
|
||||
@@ -6,11 +6,11 @@ namespace App\Administration\Domain\Exception;
|
||||
|
||||
use App\Administration\Domain\Model\User\StatutCompte;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use RuntimeException;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class UtilisateurNonDeblocableException extends RuntimeException
|
||||
final class UtilisateurNonDeblocableException extends DomainException
|
||||
{
|
||||
public static function carStatutIncompatible(UserId $userId, StatutCompte $statut): self
|
||||
{
|
||||
|
||||
@@ -8,10 +8,15 @@ use App\Administration\Domain\Event\CompteActive;
|
||||
use App\Administration\Domain\Event\CompteCreated;
|
||||
use App\Administration\Domain\Event\InvitationRenvoyee;
|
||||
use App\Administration\Domain\Event\MotDePasseChange;
|
||||
use App\Administration\Domain\Event\RoleAttribue;
|
||||
use App\Administration\Domain\Event\RoleRetire;
|
||||
use App\Administration\Domain\Event\UtilisateurBloque;
|
||||
use App\Administration\Domain\Event\UtilisateurDebloque;
|
||||
use App\Administration\Domain\Event\UtilisateurInvite;
|
||||
use App\Administration\Domain\Exception\CompteNonActivableException;
|
||||
use App\Administration\Domain\Exception\DernierRoleNonRetirableException;
|
||||
use App\Administration\Domain\Exception\RoleDejaAttribueException;
|
||||
use App\Administration\Domain\Exception\RoleNonAttribueException;
|
||||
use App\Administration\Domain\Exception\UtilisateurDejaInviteException;
|
||||
use App\Administration\Domain\Exception\UtilisateurNonBlocableException;
|
||||
use App\Administration\Domain\Exception\UtilisateurNonDeblocableException;
|
||||
@@ -19,12 +24,20 @@ use App\Administration\Domain\Model\ConsentementParental\ConsentementParental;
|
||||
use App\Administration\Domain\Policy\ConsentementParentalPolicy;
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_values;
|
||||
use function count;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function in_array;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Aggregate Root representing a user in Classeo.
|
||||
*
|
||||
* A user belongs to a school (tenant) and has a role.
|
||||
* A user belongs to a school (tenant) and can have multiple roles (FR5).
|
||||
* The account lifecycle goes through multiple statuses: creation → activation.
|
||||
* Minors (< 15 years) require parental consent before activation.
|
||||
*/
|
||||
@@ -37,10 +50,16 @@ final class User extends AggregateRoot
|
||||
public private(set) ?DateTimeImmutable $blockedAt = null;
|
||||
public private(set) ?string $blockedReason = null;
|
||||
|
||||
/** @var Role[] */
|
||||
public private(set) array $roles;
|
||||
|
||||
/**
|
||||
* @param Role[] $roles
|
||||
*/
|
||||
private function __construct(
|
||||
public private(set) UserId $id,
|
||||
public private(set) Email $email,
|
||||
public private(set) Role $role,
|
||||
array $roles,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) string $schoolName,
|
||||
public private(set) StatutCompte $statut,
|
||||
@@ -49,6 +68,14 @@ final class User extends AggregateRoot
|
||||
public private(set) string $firstName = '',
|
||||
public private(set) string $lastName = '',
|
||||
) {
|
||||
$this->roles = $roles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary role (first assigned role) for backward compatibility.
|
||||
*/
|
||||
public Role $role {
|
||||
get => $this->roles[0];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +92,7 @@ final class User extends AggregateRoot
|
||||
$user = new self(
|
||||
id: UserId::generate(),
|
||||
email: $email,
|
||||
role: $role,
|
||||
roles: [$role],
|
||||
tenantId: $tenantId,
|
||||
schoolName: $schoolName,
|
||||
statut: StatutCompte::EN_ATTENTE,
|
||||
@@ -84,6 +111,73 @@ final class User extends AggregateRoot
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assigns an additional role to the user.
|
||||
*
|
||||
* @throws RoleDejaAttribueException if the role is already assigned
|
||||
*/
|
||||
public function attribuerRole(Role $role, DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->aLeRole($role)) {
|
||||
throw RoleDejaAttribueException::pour($this->id, $role);
|
||||
}
|
||||
|
||||
$this->roles = [...$this->roles, $role];
|
||||
|
||||
$this->recordEvent(new RoleAttribue(
|
||||
userId: $this->id,
|
||||
email: (string) $this->email,
|
||||
role: $role->value,
|
||||
tenantId: $this->tenantId,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a role from the user.
|
||||
*
|
||||
* @throws RoleNonAttribueException if the role is not assigned
|
||||
* @throws DernierRoleNonRetirableException if this is the last role
|
||||
*/
|
||||
public function retirerRole(Role $role, DateTimeImmutable $at): void
|
||||
{
|
||||
if (!$this->aLeRole($role)) {
|
||||
throw RoleNonAttribueException::pour($this->id, $role);
|
||||
}
|
||||
|
||||
if (count($this->roles) === 1) {
|
||||
throw DernierRoleNonRetirableException::pour($this->id);
|
||||
}
|
||||
|
||||
$this->roles = array_values(
|
||||
array_filter($this->roles, static fn (Role $r) => $r !== $role),
|
||||
);
|
||||
|
||||
$this->recordEvent(new RoleRetire(
|
||||
userId: $this->id,
|
||||
email: (string) $this->email,
|
||||
role: $role->value,
|
||||
tenantId: $this->tenantId,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user has a specific role.
|
||||
*/
|
||||
public function aLeRole(Role $role): bool
|
||||
{
|
||||
return in_array($role, $this->roles, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the primary role (first assigned).
|
||||
*/
|
||||
public function rolePrincipal(): Role
|
||||
{
|
||||
return $this->roles[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates the account with the hashed password.
|
||||
*
|
||||
@@ -167,7 +261,7 @@ final class User extends AggregateRoot
|
||||
$user = new self(
|
||||
id: UserId::generate(),
|
||||
email: $email,
|
||||
role: $role,
|
||||
roles: [$role],
|
||||
tenantId: $tenantId,
|
||||
schoolName: $schoolName,
|
||||
statut: StatutCompte::EN_ATTENTE,
|
||||
@@ -296,12 +390,14 @@ final class User extends AggregateRoot
|
||||
/**
|
||||
* Reconstitutes a User from storage.
|
||||
*
|
||||
* @param Role[] $roles
|
||||
*
|
||||
* @internal For Infrastructure use only
|
||||
*/
|
||||
public static function reconstitute(
|
||||
UserId $id,
|
||||
Email $email,
|
||||
Role $role,
|
||||
array $roles,
|
||||
TenantId $tenantId,
|
||||
string $schoolName,
|
||||
StatutCompte $statut,
|
||||
@@ -316,10 +412,14 @@ final class User extends AggregateRoot
|
||||
?DateTimeImmutable $blockedAt = null,
|
||||
?string $blockedReason = null,
|
||||
): self {
|
||||
if ($roles === []) {
|
||||
throw new InvalidArgumentException('Un utilisateur doit avoir au moins un rôle.');
|
||||
}
|
||||
|
||||
$user = new self(
|
||||
id: $id,
|
||||
email: $email,
|
||||
role: $role,
|
||||
roles: $roles,
|
||||
tenantId: $tenantId,
|
||||
schoolName: $schoolName,
|
||||
statut: $statut,
|
||||
|
||||
Reference in New Issue
Block a user