feat: Permettre au super admin de se connecter et accéder à son dashboard
Le super admin (table super_admins, master DB) ne pouvait pas se connecter via /api/login car ce firewall n'utilisait que le provider tenant. De même, le JWT n'était pas enrichi pour les super admins, l'endpoint /api/me/roles les rejetait, et le frontend redirigeait systématiquement vers /dashboard. Un chain provider (super_admin + tenant) résout l'authentification, le JwtPayloadEnricher et MyRolesProvider gèrent désormais les deux types d'utilisateurs, et le frontend redirige selon le rôle après login.
This commit is contained in:
@@ -13,6 +13,7 @@ use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\UserRepository;
|
||||
use App\Administration\Infrastructure\Api\Resource\MyRolesOutput;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\SuperAdmin\Infrastructure\Security\SecuritySuperAdmin;
|
||||
|
||||
use function array_map;
|
||||
|
||||
@@ -37,6 +38,16 @@ final readonly class MyRolesProvider implements ProviderInterface
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): MyRolesOutput
|
||||
{
|
||||
$currentUser = $this->security->getUser();
|
||||
|
||||
if ($currentUser instanceof SecuritySuperAdmin) {
|
||||
$output = new MyRolesOutput();
|
||||
$output->roles = [['value' => 'ROLE_SUPER_ADMIN', 'label' => 'Super Admin']];
|
||||
$output->activeRole = 'ROLE_SUPER_ADMIN';
|
||||
$output->activeRoleLabel = 'Super Admin';
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
if (!$currentUser instanceof SecurityUser) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Authentification requise.');
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Security;
|
||||
|
||||
use App\SuperAdmin\Infrastructure\Security\SecuritySuperAdmin;
|
||||
use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
|
||||
|
||||
/**
|
||||
@@ -12,7 +13,8 @@ use Lexik\Bundle\JWTAuthenticationBundle\Event\JWTCreatedEvent;
|
||||
* Added claims:
|
||||
* - sub: User email (Symfony Security identifier)
|
||||
* - user_id: User UUID (for API consumers)
|
||||
* - tenant_id: Tenant UUID for multi-tenant isolation
|
||||
* - tenant_id: Tenant UUID for multi-tenant isolation (regular users only)
|
||||
* - user_type: "super_admin" for super admins
|
||||
* - roles: List of Symfony roles for authorization
|
||||
*
|
||||
* @see Story 1.4 - User login
|
||||
@@ -22,13 +24,21 @@ final readonly class JwtPayloadEnricher
|
||||
public function onJWTCreated(JWTCreatedEvent $event): void
|
||||
{
|
||||
$user = $event->getUser();
|
||||
$payload = $event->getData();
|
||||
|
||||
if ($user instanceof SecuritySuperAdmin) {
|
||||
$payload['user_id'] = $user->superAdminId();
|
||||
$payload['user_type'] = 'super_admin';
|
||||
$payload['roles'] = $user->getRoles();
|
||||
$event->setData($payload);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payload = $event->getData();
|
||||
|
||||
// Business claims for multi-tenant isolation and authorization
|
||||
$payload['user_id'] = $user->userId();
|
||||
$payload['tenant_id'] = $user->tenantId();
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Command\CreateEstablishment;
|
||||
|
||||
final readonly class CreateEstablishmentCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public string $subdomain,
|
||||
public string $adminEmail,
|
||||
public string $superAdminId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Command\CreateEstablishment;
|
||||
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\Establishment;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
|
||||
final readonly class CreateEstablishmentHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EstablishmentRepository $establishmentRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(CreateEstablishmentCommand $command): CreateEstablishmentResult
|
||||
{
|
||||
$establishment = Establishment::creer(
|
||||
name: $command->name,
|
||||
subdomain: $command->subdomain,
|
||||
createdBy: SuperAdminId::fromString($command->superAdminId),
|
||||
createdAt: $this->clock->now(),
|
||||
);
|
||||
|
||||
$this->establishmentRepository->save($establishment);
|
||||
|
||||
return new CreateEstablishmentResult(
|
||||
establishmentId: (string) $establishment->id,
|
||||
tenantId: (string) $establishment->tenantId,
|
||||
name: $establishment->name,
|
||||
subdomain: $establishment->subdomain,
|
||||
databaseName: $establishment->databaseName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Command\CreateEstablishment;
|
||||
|
||||
final readonly class CreateEstablishmentResult
|
||||
{
|
||||
public function __construct(
|
||||
public string $establishmentId,
|
||||
public string $tenantId,
|
||||
public string $name,
|
||||
public string $subdomain,
|
||||
public string $databaseName,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Command\SwitchTenant;
|
||||
|
||||
final readonly class SwitchTenantCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Command\SwitchTenant;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Exception\EstablishmentIntrouvableException;
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
use App\SuperAdmin\Infrastructure\Security\SuperAdminTenantContext;
|
||||
|
||||
final readonly class SwitchTenantHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EstablishmentRepository $establishmentRepository,
|
||||
private SuperAdminTenantContext $superAdminTenantContext,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(SwitchTenantCommand $command): void
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
|
||||
$establishment = $this->establishmentRepository->findByTenantId($tenantId);
|
||||
|
||||
if ($establishment === null) {
|
||||
throw EstablishmentIntrouvableException::avecTenantId($tenantId);
|
||||
}
|
||||
|
||||
$this->superAdminTenantContext->switchTo($tenantId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishments;
|
||||
|
||||
final readonly class EstablishmentView
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $tenantId,
|
||||
public string $name,
|
||||
public string $subdomain,
|
||||
public string $status,
|
||||
public string $createdAt,
|
||||
public ?string $lastActivityAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishments;
|
||||
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
|
||||
final readonly class GetEstablishmentsHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EstablishmentRepository $establishmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EstablishmentView[]
|
||||
*/
|
||||
public function __invoke(GetEstablishmentsQuery $query): array
|
||||
{
|
||||
$establishments = $this->establishmentRepository->findAll();
|
||||
|
||||
return array_map(
|
||||
static fn ($e) => new EstablishmentView(
|
||||
id: (string) $e->id,
|
||||
tenantId: (string) $e->tenantId,
|
||||
name: $e->name,
|
||||
subdomain: $e->subdomain,
|
||||
status: $e->status->value,
|
||||
createdAt: $e->createdAt->format('c'),
|
||||
lastActivityAt: $e->lastActivityAt?->format('c'),
|
||||
),
|
||||
$establishments,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishments;
|
||||
|
||||
final readonly class GetEstablishmentsQuery
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishmentsMetrics;
|
||||
|
||||
/**
|
||||
* Anonymized metrics for an establishment — no personal data.
|
||||
*/
|
||||
final readonly class EstablishmentMetricsView
|
||||
{
|
||||
public function __construct(
|
||||
public string $establishmentId,
|
||||
public string $name,
|
||||
public string $status,
|
||||
public int $userCount,
|
||||
public int $studentCount,
|
||||
public int $teacherCount,
|
||||
public ?string $lastLoginAt,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishmentsMetrics;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
/**
|
||||
* Returns anonymized aggregate metrics for all establishments.
|
||||
* No personal data (names, emails, grades) is ever exposed.
|
||||
*/
|
||||
final readonly class GetEstablishmentsMetricsHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EstablishmentMetricsView[]
|
||||
*/
|
||||
public function __invoke(GetEstablishmentsMetricsQuery $query): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
<<<'SQL'
|
||||
SELECT
|
||||
e.id AS establishment_id,
|
||||
e.name,
|
||||
e.status,
|
||||
COALESCE(m.user_count, 0) AS user_count,
|
||||
COALESCE(m.student_count, 0) AS student_count,
|
||||
COALESCE(m.teacher_count, 0) AS teacher_count,
|
||||
m.last_login_at
|
||||
FROM establishments e
|
||||
LEFT JOIN establishment_metrics m ON m.establishment_id = e.id
|
||||
ORDER BY e.name ASC
|
||||
SQL,
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static function (array $row): EstablishmentMetricsView {
|
||||
/** @var string $establishmentId */
|
||||
$establishmentId = $row['establishment_id'];
|
||||
/** @var string $name */
|
||||
$name = $row['name'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var int $userCount */
|
||||
$userCount = $row['user_count'];
|
||||
/** @var int $studentCount */
|
||||
$studentCount = $row['student_count'];
|
||||
/** @var int $teacherCount */
|
||||
$teacherCount = $row['teacher_count'];
|
||||
/** @var string|null $lastLoginAt */
|
||||
$lastLoginAt = $row['last_login_at'];
|
||||
|
||||
return new EstablishmentMetricsView(
|
||||
establishmentId: $establishmentId,
|
||||
name: $name,
|
||||
status: $status,
|
||||
userCount: $userCount,
|
||||
studentCount: $studentCount,
|
||||
teacherCount: $teacherCount,
|
||||
lastLoginAt: $lastLoginAt,
|
||||
);
|
||||
},
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Application\Query\GetEstablishmentsMetrics;
|
||||
|
||||
final readonly class GetEstablishmentsMetricsQuery
|
||||
{
|
||||
}
|
||||
36
backend/src/SuperAdmin/Domain/Event/EtablissementCree.php
Normal file
36
backend/src/SuperAdmin/Domain/Event/EtablissementCree.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Event;
|
||||
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EtablissementCree implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EstablishmentId $establishmentId,
|
||||
public TenantId $tenantId,
|
||||
public string $name,
|
||||
public string $subdomain,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->establishmentId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Event;
|
||||
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class EtablissementDesactive implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public EstablishmentId $establishmentId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->establishmentId->value;
|
||||
}
|
||||
}
|
||||
33
backend/src/SuperAdmin/Domain/Event/SuperAdminCree.php
Normal file
33
backend/src/SuperAdmin/Domain/Event/SuperAdminCree.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Event;
|
||||
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class SuperAdminCree implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public SuperAdminId $superAdminId,
|
||||
public string $email,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->superAdminId->value;
|
||||
}
|
||||
}
|
||||
32
backend/src/SuperAdmin/Domain/Event/SuperAdminDesactive.php
Normal file
32
backend/src/SuperAdmin/Domain/Event/SuperAdminDesactive.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Event;
|
||||
|
||||
use App\Shared\Domain\DomainEvent;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DateTimeImmutable;
|
||||
use Override;
|
||||
use Ramsey\Uuid\UuidInterface;
|
||||
|
||||
final readonly class SuperAdminDesactive implements DomainEvent
|
||||
{
|
||||
public function __construct(
|
||||
public SuperAdminId $superAdminId,
|
||||
private DateTimeImmutable $occurredOn,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function occurredOn(): DateTimeImmutable
|
||||
{
|
||||
return $this->occurredOn;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function aggregateId(): UuidInterface
|
||||
{
|
||||
return $this->superAdminId->value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Exception;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EstablishmentDejaInactifException extends DomainException
|
||||
{
|
||||
public static function pour(EstablishmentId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('L\'établissement %s est déjà inactif.', $id),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Exception;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class EstablishmentIntrouvableException extends DomainException
|
||||
{
|
||||
public static function avecId(EstablishmentId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Établissement introuvable avec l\'id %s.', $id),
|
||||
);
|
||||
}
|
||||
|
||||
public static function avecTenantId(TenantId $tenantId): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Établissement introuvable avec le tenant_id %s.', $tenantId),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Exception;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SuperAdminDejaActifException extends DomainException
|
||||
{
|
||||
public static function pourReactivation(SuperAdminId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Le Super Admin %s est déjà actif.', $id),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Exception;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SuperAdminIntrouvableException extends DomainException
|
||||
{
|
||||
public static function avecId(SuperAdminId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Super Admin introuvable avec l\'id %s.', $id),
|
||||
);
|
||||
}
|
||||
|
||||
public static function avecEmail(string $email): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Super Admin introuvable avec l\'email %s.', $email),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Exception;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DomainException;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
final class SuperAdminNonActifException extends DomainException
|
||||
{
|
||||
public static function pourConnexion(SuperAdminId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Le Super Admin %s ne peut pas se connecter car son compte est inactif.', $id),
|
||||
);
|
||||
}
|
||||
|
||||
public static function pourDesactivation(SuperAdminId $id): self
|
||||
{
|
||||
return new self(
|
||||
sprintf('Le Super Admin %s est déjà inactif.', $id),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\Establishment;
|
||||
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Event\EtablissementCree;
|
||||
use App\SuperAdmin\Domain\Event\EtablissementDesactive;
|
||||
use App\SuperAdmin\Domain\Exception\EstablishmentDejaInactifException;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
/**
|
||||
* Aggregate Root for an Establishment (tenant) — lives in master database.
|
||||
*
|
||||
* Each Establishment maps to a tenant with its own database.
|
||||
*/
|
||||
final class Establishment extends AggregateRoot
|
||||
{
|
||||
public private(set) ?DateTimeImmutable $lastActivityAt = null;
|
||||
|
||||
private function __construct(
|
||||
public private(set) EstablishmentId $id,
|
||||
public private(set) TenantId $tenantId,
|
||||
public private(set) string $name,
|
||||
public private(set) string $subdomain,
|
||||
public private(set) string $databaseName,
|
||||
public private(set) EstablishmentStatus $status,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
public private(set) ?SuperAdminId $createdBy,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
string $name,
|
||||
string $subdomain,
|
||||
SuperAdminId $createdBy,
|
||||
DateTimeImmutable $createdAt,
|
||||
): self {
|
||||
$tenantId = TenantId::generate();
|
||||
|
||||
$establishment = new self(
|
||||
id: EstablishmentId::generate(),
|
||||
tenantId: $tenantId,
|
||||
name: $name,
|
||||
subdomain: $subdomain,
|
||||
databaseName: sprintf('classeo_tenant_%s', str_replace('-', '', (string) $tenantId)),
|
||||
status: EstablishmentStatus::ACTIF,
|
||||
createdAt: $createdAt,
|
||||
createdBy: $createdBy,
|
||||
);
|
||||
|
||||
$establishment->recordEvent(new EtablissementCree(
|
||||
establishmentId: $establishment->id,
|
||||
tenantId: $establishment->tenantId,
|
||||
name: $name,
|
||||
subdomain: $subdomain,
|
||||
occurredOn: $createdAt,
|
||||
));
|
||||
|
||||
return $establishment;
|
||||
}
|
||||
|
||||
public function desactiver(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status !== EstablishmentStatus::ACTIF) {
|
||||
throw EstablishmentDejaInactifException::pour($this->id);
|
||||
}
|
||||
|
||||
$this->status = EstablishmentStatus::INACTIF;
|
||||
|
||||
$this->recordEvent(new EtablissementDesactive(
|
||||
establishmentId: $this->id,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
public function enregistrerActivite(DateTimeImmutable $at): void
|
||||
{
|
||||
$this->lastActivityAt = $at;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal For Infrastructure use only
|
||||
*/
|
||||
public static function reconstitute(
|
||||
EstablishmentId $id,
|
||||
TenantId $tenantId,
|
||||
string $name,
|
||||
string $subdomain,
|
||||
string $databaseName,
|
||||
EstablishmentStatus $status,
|
||||
DateTimeImmutable $createdAt,
|
||||
?SuperAdminId $createdBy = null,
|
||||
?DateTimeImmutable $lastActivityAt = null,
|
||||
): self {
|
||||
$establishment = new self(
|
||||
id: $id,
|
||||
tenantId: $tenantId,
|
||||
name: $name,
|
||||
subdomain: $subdomain,
|
||||
databaseName: $databaseName,
|
||||
status: $status,
|
||||
createdAt: $createdAt,
|
||||
createdBy: $createdBy,
|
||||
);
|
||||
|
||||
$establishment->lastActivityAt = $lastActivityAt;
|
||||
|
||||
return $establishment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\Establishment;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class EstablishmentId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\Establishment;
|
||||
|
||||
enum EstablishmentStatus: string
|
||||
{
|
||||
case ACTIF = 'active';
|
||||
case INACTIF = 'inactive';
|
||||
}
|
||||
125
backend/src/SuperAdmin/Domain/Model/SuperAdmin/SuperAdmin.php
Normal file
125
backend/src/SuperAdmin/Domain/Model/SuperAdmin/SuperAdmin.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\SuperAdmin;
|
||||
|
||||
use App\Shared\Domain\AggregateRoot;
|
||||
use App\SuperAdmin\Domain\Event\SuperAdminCree;
|
||||
use App\SuperAdmin\Domain\Event\SuperAdminDesactive;
|
||||
use App\SuperAdmin\Domain\Exception\SuperAdminDejaActifException;
|
||||
use App\SuperAdmin\Domain\Exception\SuperAdminNonActifException;
|
||||
use DateTimeImmutable;
|
||||
|
||||
/**
|
||||
* Aggregate Root for Super Admin — lives in the master database, not per-tenant.
|
||||
*
|
||||
* A Super Admin can manage all establishments and switch between tenant contexts.
|
||||
* Authentication is separate from regular users (different user provider, different table).
|
||||
*/
|
||||
final class SuperAdmin extends AggregateRoot
|
||||
{
|
||||
public private(set) ?DateTimeImmutable $lastLoginAt = null;
|
||||
|
||||
private function __construct(
|
||||
public private(set) SuperAdminId $id,
|
||||
public private(set) string $email,
|
||||
public private(set) string $hashedPassword,
|
||||
public private(set) string $firstName,
|
||||
public private(set) string $lastName,
|
||||
public private(set) SuperAdminStatus $status,
|
||||
public private(set) DateTimeImmutable $createdAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function creer(
|
||||
string $email,
|
||||
string $hashedPassword,
|
||||
string $firstName,
|
||||
string $lastName,
|
||||
DateTimeImmutable $createdAt,
|
||||
): self {
|
||||
$superAdmin = new self(
|
||||
id: SuperAdminId::generate(),
|
||||
email: $email,
|
||||
hashedPassword: $hashedPassword,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
status: SuperAdminStatus::ACTIF,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$superAdmin->recordEvent(new SuperAdminCree(
|
||||
superAdminId: $superAdmin->id,
|
||||
email: $superAdmin->email,
|
||||
occurredOn: $createdAt,
|
||||
));
|
||||
|
||||
return $superAdmin;
|
||||
}
|
||||
|
||||
public function enregistrerConnexion(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status !== SuperAdminStatus::ACTIF) {
|
||||
throw SuperAdminNonActifException::pourConnexion($this->id);
|
||||
}
|
||||
|
||||
$this->lastLoginAt = $at;
|
||||
}
|
||||
|
||||
public function desactiver(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status !== SuperAdminStatus::ACTIF) {
|
||||
throw SuperAdminNonActifException::pourDesactivation($this->id);
|
||||
}
|
||||
|
||||
$this->status = SuperAdminStatus::INACTIF;
|
||||
|
||||
$this->recordEvent(new SuperAdminDesactive(
|
||||
superAdminId: $this->id,
|
||||
occurredOn: $at,
|
||||
));
|
||||
}
|
||||
|
||||
public function reactiver(DateTimeImmutable $at): void
|
||||
{
|
||||
if ($this->status === SuperAdminStatus::ACTIF) {
|
||||
throw SuperAdminDejaActifException::pourReactivation($this->id);
|
||||
}
|
||||
|
||||
$this->status = SuperAdminStatus::ACTIF;
|
||||
}
|
||||
|
||||
public function peutSeConnecter(): bool
|
||||
{
|
||||
return $this->status === SuperAdminStatus::ACTIF;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal For Infrastructure use only
|
||||
*/
|
||||
public static function reconstitute(
|
||||
SuperAdminId $id,
|
||||
string $email,
|
||||
string $hashedPassword,
|
||||
string $firstName,
|
||||
string $lastName,
|
||||
SuperAdminStatus $status,
|
||||
DateTimeImmutable $createdAt,
|
||||
?DateTimeImmutable $lastLoginAt = null,
|
||||
): self {
|
||||
$superAdmin = new self(
|
||||
id: $id,
|
||||
email: $email,
|
||||
hashedPassword: $hashedPassword,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
status: $status,
|
||||
createdAt: $createdAt,
|
||||
);
|
||||
|
||||
$superAdmin->lastLoginAt = $lastLoginAt;
|
||||
|
||||
return $superAdmin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\SuperAdmin;
|
||||
|
||||
use App\Shared\Domain\EntityId;
|
||||
|
||||
final readonly class SuperAdminId extends EntityId
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Model\SuperAdmin;
|
||||
|
||||
enum SuperAdminStatus: string
|
||||
{
|
||||
case ACTIF = 'active';
|
||||
case INACTIF = 'inactive';
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Repository;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\Establishment;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
|
||||
interface EstablishmentRepository
|
||||
{
|
||||
public function save(Establishment $establishment): void;
|
||||
|
||||
/**
|
||||
* @throws \App\SuperAdmin\Domain\Exception\EstablishmentIntrouvableException
|
||||
*/
|
||||
public function get(EstablishmentId $id): Establishment;
|
||||
|
||||
public function findByTenantId(TenantId $tenantId): ?Establishment;
|
||||
|
||||
/**
|
||||
* @return Establishment[]
|
||||
*/
|
||||
public function findAll(): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Domain\Repository;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdmin;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
|
||||
interface SuperAdminRepository
|
||||
{
|
||||
public function save(SuperAdmin $superAdmin): void;
|
||||
|
||||
/**
|
||||
* @throws \App\SuperAdmin\Domain\Exception\SuperAdminIntrouvableException
|
||||
*/
|
||||
public function get(SuperAdminId $id): SuperAdmin;
|
||||
|
||||
public function findByEmail(string $email): ?SuperAdmin;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\SuperAdmin\Application\Command\CreateEstablishment\CreateEstablishmentCommand;
|
||||
use App\SuperAdmin\Application\Command\CreateEstablishment\CreateEstablishmentHandler;
|
||||
use App\SuperAdmin\Infrastructure\Api\Resource\EstablishmentResource;
|
||||
use App\SuperAdmin\Infrastructure\Security\SecuritySuperAdmin;
|
||||
use Override;
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<EstablishmentResource, EstablishmentResource>
|
||||
*/
|
||||
final readonly class CreateEstablishmentProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CreateEstablishmentHandler $handler,
|
||||
private Security $security,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EstablishmentResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): EstablishmentResource
|
||||
{
|
||||
/** @var SecuritySuperAdmin $user */
|
||||
$user = $this->security->getUser();
|
||||
|
||||
$result = ($this->handler)(new CreateEstablishmentCommand(
|
||||
name: $data->name,
|
||||
subdomain: $data->subdomain,
|
||||
adminEmail: $data->adminEmail,
|
||||
superAdminId: $user->superAdminId(),
|
||||
));
|
||||
|
||||
$resource = new EstablishmentResource();
|
||||
$resource->id = $result->establishmentId;
|
||||
$resource->tenantId = $result->tenantId;
|
||||
$resource->name = $result->name;
|
||||
$resource->subdomain = $result->subdomain;
|
||||
$resource->databaseName = $result->databaseName;
|
||||
$resource->status = 'active';
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\SuperAdmin\Application\Command\SwitchTenant\SwitchTenantCommand;
|
||||
use App\SuperAdmin\Application\Command\SwitchTenant\SwitchTenantHandler;
|
||||
use App\SuperAdmin\Domain\Exception\EstablishmentIntrouvableException;
|
||||
use App\SuperAdmin\Infrastructure\Api\Resource\SwitchTenantInput;
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<SwitchTenantInput, void>
|
||||
*/
|
||||
final readonly class SwitchTenantProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SwitchTenantHandler $handler,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param SwitchTenantInput $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): void
|
||||
{
|
||||
try {
|
||||
($this->handler)(new SwitchTenantCommand(
|
||||
tenantId: $data->tenantId,
|
||||
));
|
||||
} catch (EstablishmentIntrouvableException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage(), $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\SuperAdmin\Application\Query\GetEstablishments\GetEstablishmentsHandler;
|
||||
use App\SuperAdmin\Application\Query\GetEstablishments\GetEstablishmentsQuery;
|
||||
use App\SuperAdmin\Infrastructure\Api\Resource\EstablishmentResource;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<EstablishmentResource>
|
||||
*/
|
||||
final readonly class EstablishmentCollectionProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetEstablishmentsHandler $handler,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return EstablishmentResource[]
|
||||
*/
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
$views = ($this->handler)(new GetEstablishmentsQuery());
|
||||
|
||||
return array_map(
|
||||
static function ($view): EstablishmentResource {
|
||||
$resource = new EstablishmentResource();
|
||||
$resource->id = $view->id;
|
||||
$resource->tenantId = $view->tenantId;
|
||||
$resource->name = $view->name;
|
||||
$resource->subdomain = $view->subdomain;
|
||||
$resource->status = $view->status;
|
||||
$resource->createdAt = $view->createdAt;
|
||||
$resource->lastActivityAt = $view->lastActivityAt;
|
||||
|
||||
return $resource;
|
||||
},
|
||||
$views,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
use App\SuperAdmin\Infrastructure\Api\Resource\EstablishmentResource;
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<EstablishmentResource>
|
||||
*/
|
||||
final readonly class EstablishmentItemProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private EstablishmentRepository $establishmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): EstablishmentResource
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $uriVariables['id'] ?? throw new NotFoundHttpException();
|
||||
|
||||
try {
|
||||
$establishment = $this->establishmentRepository->get(
|
||||
EstablishmentId::fromString($id),
|
||||
);
|
||||
} catch (Throwable) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$resource = new EstablishmentResource();
|
||||
$resource->id = (string) $establishment->id;
|
||||
$resource->tenantId = (string) $establishment->tenantId;
|
||||
$resource->name = $establishment->name;
|
||||
$resource->subdomain = $establishment->subdomain;
|
||||
$resource->databaseName = $establishment->databaseName;
|
||||
$resource->status = $establishment->status->value;
|
||||
$resource->createdAt = $establishment->createdAt->format('c');
|
||||
$resource->lastActivityAt = $establishment->lastActivityAt?->format('c');
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\SuperAdmin\Application\Query\GetEstablishmentsMetrics\GetEstablishmentsMetricsHandler;
|
||||
use App\SuperAdmin\Application\Query\GetEstablishmentsMetrics\GetEstablishmentsMetricsQuery;
|
||||
use App\SuperAdmin\Infrastructure\Api\Resource\MetricsResource;
|
||||
use Override;
|
||||
|
||||
/**
|
||||
* @implements ProviderInterface<MetricsResource>
|
||||
*/
|
||||
final readonly class MetricsProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetEstablishmentsMetricsHandler $handler,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MetricsResource[]
|
||||
*/
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
$metrics = ($this->handler)(new GetEstablishmentsMetricsQuery());
|
||||
|
||||
return array_map(
|
||||
static function ($view): MetricsResource {
|
||||
$resource = new MetricsResource();
|
||||
$resource->establishmentId = $view->establishmentId;
|
||||
$resource->name = $view->name;
|
||||
$resource->status = $view->status;
|
||||
$resource->userCount = $view->userCount;
|
||||
$resource->studentCount = $view->studentCount;
|
||||
$resource->teacherCount = $view->teacherCount;
|
||||
$resource->lastLoginAt = $view->lastLoginAt;
|
||||
|
||||
return $resource;
|
||||
},
|
||||
$metrics,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\SuperAdmin\Infrastructure\Api\Processor\CreateEstablishmentProcessor;
|
||||
use App\SuperAdmin\Infrastructure\Api\Provider\EstablishmentCollectionProvider;
|
||||
use App\SuperAdmin\Infrastructure\Api\Provider\EstablishmentItemProvider;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'Establishment',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/super-admin/establishments',
|
||||
provider: EstablishmentCollectionProvider::class,
|
||||
security: "is_granted('ROLE_SUPER_ADMIN')",
|
||||
name: 'super_admin_establishments_list',
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/super-admin/establishments/{id}',
|
||||
provider: EstablishmentItemProvider::class,
|
||||
security: "is_granted('ROLE_SUPER_ADMIN')",
|
||||
name: 'super_admin_establishments_detail',
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/super-admin/establishments',
|
||||
processor: CreateEstablishmentProcessor::class,
|
||||
security: "is_granted('ROLE_SUPER_ADMIN')",
|
||||
validationContext: ['groups' => ['Default', 'create']],
|
||||
name: 'super_admin_establishments_create',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class EstablishmentResource
|
||||
{
|
||||
public ?string $id = null;
|
||||
public ?string $tenantId = null;
|
||||
|
||||
#[Assert\NotBlank(groups: ['create'])]
|
||||
#[Assert\Length(max: 255, groups: ['create'])]
|
||||
public string $name = '';
|
||||
|
||||
#[Assert\NotBlank(groups: ['create'])]
|
||||
#[Assert\Length(max: 100, groups: ['create'])]
|
||||
#[Assert\Regex(pattern: '/^[a-z0-9-]+$/', message: 'Le sous-domaine ne peut contenir que des lettres minuscules, chiffres et tirets.', groups: ['create'])]
|
||||
public string $subdomain = '';
|
||||
|
||||
#[Assert\NotBlank(groups: ['create'])]
|
||||
#[Assert\Email(groups: ['create'])]
|
||||
public string $adminEmail = '';
|
||||
|
||||
public ?string $databaseName = null;
|
||||
public ?string $status = null;
|
||||
public ?string $createdAt = null;
|
||||
public ?string $lastActivityAt = null;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\SuperAdmin\Infrastructure\Api\Provider\MetricsProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'EstablishmentMetrics',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/super-admin/metrics',
|
||||
provider: MetricsProvider::class,
|
||||
security: "is_granted('ROLE_SUPER_ADMIN')",
|
||||
name: 'super_admin_metrics',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class MetricsResource
|
||||
{
|
||||
public string $establishmentId = '';
|
||||
public string $name = '';
|
||||
public string $status = '';
|
||||
public int $userCount = 0;
|
||||
public int $studentCount = 0;
|
||||
public int $teacherCount = 0;
|
||||
public ?string $lastLoginAt = null;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\SuperAdmin\Infrastructure\Api\Processor\SwitchTenantProcessor;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'SwitchTenant',
|
||||
operations: [
|
||||
new Post(
|
||||
uriTemplate: '/super-admin/switch-tenant',
|
||||
processor: SwitchTenantProcessor::class,
|
||||
security: "is_granted('ROLE_SUPER_ADMIN')",
|
||||
output: false,
|
||||
status: 204,
|
||||
name: 'super_admin_switch_tenant',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class SwitchTenantInput
|
||||
{
|
||||
#[Assert\NotBlank]
|
||||
#[Assert\Uuid]
|
||||
public string $tenantId = '';
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Console;
|
||||
|
||||
use App\Administration\Application\Port\PasswordHasher;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdmin;
|
||||
use App\SuperAdmin\Domain\Repository\SuperAdminRepository;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Creates an already-activated test super admin for E2E tests.
|
||||
*/
|
||||
#[AsCommand(
|
||||
name: 'app:dev:create-test-super-admin',
|
||||
description: 'Creates a test super admin for E2E tests',
|
||||
)]
|
||||
final class CreateTestSuperAdminCommand extends Command
|
||||
{
|
||||
public function __construct(
|
||||
private readonly SuperAdminRepository $superAdminRepository,
|
||||
private readonly PasswordHasher $passwordHasher,
|
||||
private readonly Clock $clock,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->addOption('email', null, InputOption::VALUE_OPTIONAL, 'Email address', 'sadmin@test.com')
|
||||
->addOption('password', null, InputOption::VALUE_OPTIONAL, 'Password (plain text)', 'SuperAdmin123')
|
||||
->addOption('first-name', null, InputOption::VALUE_OPTIONAL, 'First name', 'Super')
|
||||
->addOption('last-name', null, InputOption::VALUE_OPTIONAL, 'Last name', 'Admin');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
/** @var string $email */
|
||||
$email = $input->getOption('email');
|
||||
/** @var string $password */
|
||||
$password = $input->getOption('password');
|
||||
/** @var string $firstName */
|
||||
$firstName = $input->getOption('first-name');
|
||||
/** @var string $lastName */
|
||||
$lastName = $input->getOption('last-name');
|
||||
|
||||
// Check if super admin already exists
|
||||
$existing = $this->superAdminRepository->findByEmail($email);
|
||||
if ($existing !== null) {
|
||||
$io->warning(sprintf('Super admin with email "%s" already exists.', $email));
|
||||
|
||||
$io->table(
|
||||
['Property', 'Value'],
|
||||
[
|
||||
['Super Admin ID', (string) $existing->id],
|
||||
['Email', $email],
|
||||
['Password', $password],
|
||||
['Status', $existing->status->value],
|
||||
]
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$hashedPassword = $this->passwordHasher->hash($password);
|
||||
$now = $this->clock->now();
|
||||
|
||||
$superAdmin = SuperAdmin::creer(
|
||||
email: $email,
|
||||
hashedPassword: $hashedPassword,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
createdAt: $now,
|
||||
);
|
||||
|
||||
$this->superAdminRepository->save($superAdmin);
|
||||
|
||||
$io->success('Test super admin created successfully!');
|
||||
|
||||
$io->table(
|
||||
['Property', 'Value'],
|
||||
[
|
||||
['Super Admin ID', (string) $superAdmin->id],
|
||||
['Email', $email],
|
||||
['Password', $password],
|
||||
['First Name', $firstName],
|
||||
['Last Name', $lastName],
|
||||
['Status', $superAdmin->status->value],
|
||||
]
|
||||
);
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Exception\EstablishmentIntrouvableException;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\Establishment;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentStatus;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineEstablishmentRepository implements EstablishmentRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(Establishment $establishment): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
<<<'SQL'
|
||||
INSERT INTO establishments (id, tenant_id, name, subdomain, database_name, status, created_at, created_by, last_activity_at)
|
||||
VALUES (:id, :tenant_id, :name, :subdomain, :database_name, :status, :created_at, :created_by, :last_activity_at)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
status = EXCLUDED.status,
|
||||
last_activity_at = EXCLUDED.last_activity_at
|
||||
SQL,
|
||||
[
|
||||
'id' => (string) $establishment->id,
|
||||
'tenant_id' => (string) $establishment->tenantId,
|
||||
'name' => $establishment->name,
|
||||
'subdomain' => $establishment->subdomain,
|
||||
'database_name' => $establishment->databaseName,
|
||||
'status' => $establishment->status->value,
|
||||
'created_at' => $establishment->createdAt->format('Y-m-d H:i:sP'),
|
||||
'created_by' => $establishment->createdBy !== null ? (string) $establishment->createdBy : null,
|
||||
'last_activity_at' => $establishment->lastActivityAt?->format('Y-m-d H:i:sP'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(EstablishmentId $id): Establishment
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM establishments WHERE id = :id',
|
||||
['id' => (string) $id],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
throw EstablishmentIntrouvableException::avecId($id);
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTenantId(TenantId $tenantId): ?Establishment
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM establishments WHERE tenant_id = :tenant_id',
|
||||
['tenant_id' => (string) $tenantId],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Establishment[]
|
||||
*/
|
||||
#[Override]
|
||||
public function findAll(): array
|
||||
{
|
||||
$rows = $this->connection->fetchAllAssociative(
|
||||
'SELECT * FROM establishments ORDER BY name ASC',
|
||||
);
|
||||
|
||||
return array_map($this->hydrate(...), $rows);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function hydrate(array $row): Establishment
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $tenantId */
|
||||
$tenantId = $row['tenant_id'];
|
||||
/** @var string $name */
|
||||
$name = $row['name'];
|
||||
/** @var string $subdomain */
|
||||
$subdomain = $row['subdomain'];
|
||||
/** @var string $databaseName */
|
||||
$databaseName = $row['database_name'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string|null $createdBy */
|
||||
$createdBy = $row['created_by'];
|
||||
/** @var string|null $lastActivityAt */
|
||||
$lastActivityAt = $row['last_activity_at'];
|
||||
|
||||
return Establishment::reconstitute(
|
||||
id: EstablishmentId::fromString($id),
|
||||
tenantId: TenantId::fromString($tenantId),
|
||||
name: $name,
|
||||
subdomain: $subdomain,
|
||||
databaseName: $databaseName,
|
||||
status: EstablishmentStatus::from($status),
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
createdBy: $createdBy !== null ? SuperAdminId::fromString($createdBy) : null,
|
||||
lastActivityAt: $lastActivityAt !== null ? new DateTimeImmutable($lastActivityAt) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Persistence\Doctrine;
|
||||
|
||||
use App\SuperAdmin\Domain\Exception\SuperAdminIntrouvableException;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdmin;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminStatus;
|
||||
use App\SuperAdmin\Domain\Repository\SuperAdminRepository;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Override;
|
||||
|
||||
final readonly class DoctrineSuperAdminRepository implements SuperAdminRepository
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function save(SuperAdmin $superAdmin): void
|
||||
{
|
||||
$this->connection->executeStatement(
|
||||
<<<'SQL'
|
||||
INSERT INTO super_admins (id, email, password_hash, first_name, last_name, status, created_at, last_login_at)
|
||||
VALUES (:id, :email, :password_hash, :first_name, :last_name, :status, :created_at, :last_login_at)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
email = EXCLUDED.email,
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
first_name = EXCLUDED.first_name,
|
||||
last_name = EXCLUDED.last_name,
|
||||
status = EXCLUDED.status,
|
||||
last_login_at = EXCLUDED.last_login_at
|
||||
SQL,
|
||||
[
|
||||
'id' => (string) $superAdmin->id,
|
||||
'email' => $superAdmin->email,
|
||||
'password_hash' => $superAdmin->hashedPassword,
|
||||
'first_name' => $superAdmin->firstName,
|
||||
'last_name' => $superAdmin->lastName,
|
||||
'status' => $superAdmin->status->value,
|
||||
'created_at' => $superAdmin->createdAt->format('Y-m-d H:i:sP'),
|
||||
'last_login_at' => $superAdmin->lastLoginAt?->format('Y-m-d H:i:sP'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(SuperAdminId $id): SuperAdmin
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM super_admins WHERE id = :id',
|
||||
['id' => (string) $id],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
throw SuperAdminIntrouvableException::avecId($id);
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByEmail(string $email): ?SuperAdmin
|
||||
{
|
||||
$row = $this->connection->fetchAssociative(
|
||||
'SELECT * FROM super_admins WHERE email = :email',
|
||||
['email' => $email],
|
||||
);
|
||||
|
||||
if ($row === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->hydrate($row);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $row
|
||||
*/
|
||||
private function hydrate(array $row): SuperAdmin
|
||||
{
|
||||
/** @var string $id */
|
||||
$id = $row['id'];
|
||||
/** @var string $email */
|
||||
$email = $row['email'];
|
||||
/** @var string $passwordHash */
|
||||
$passwordHash = $row['password_hash'];
|
||||
/** @var string $firstName */
|
||||
$firstName = $row['first_name'];
|
||||
/** @var string $lastName */
|
||||
$lastName = $row['last_name'];
|
||||
/** @var string $status */
|
||||
$status = $row['status'];
|
||||
/** @var string $createdAt */
|
||||
$createdAt = $row['created_at'];
|
||||
/** @var string|null $lastLoginAt */
|
||||
$lastLoginAt = $row['last_login_at'];
|
||||
|
||||
return SuperAdmin::reconstitute(
|
||||
id: SuperAdminId::fromString($id),
|
||||
email: $email,
|
||||
hashedPassword: $passwordHash,
|
||||
firstName: $firstName,
|
||||
lastName: $lastName,
|
||||
status: SuperAdminStatus::from($status),
|
||||
createdAt: new DateTimeImmutable($createdAt),
|
||||
lastLoginAt: $lastLoginAt !== null ? new DateTimeImmutable($lastLoginAt) : null,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\SuperAdmin\Domain\Exception\EstablishmentIntrouvableException;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\Establishment;
|
||||
use App\SuperAdmin\Domain\Model\Establishment\EstablishmentId;
|
||||
use App\SuperAdmin\Domain\Repository\EstablishmentRepository;
|
||||
use Override;
|
||||
|
||||
final class InMemoryEstablishmentRepository implements EstablishmentRepository
|
||||
{
|
||||
/** @var array<string, Establishment> */
|
||||
private array $establishments = [];
|
||||
|
||||
#[Override]
|
||||
public function save(Establishment $establishment): void
|
||||
{
|
||||
$this->establishments[(string) $establishment->id] = $establishment;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(EstablishmentId $id): Establishment
|
||||
{
|
||||
return $this->establishments[(string) $id]
|
||||
?? throw EstablishmentIntrouvableException::avecId($id);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByTenantId(TenantId $tenantId): ?Establishment
|
||||
{
|
||||
foreach ($this->establishments as $establishment) {
|
||||
if ($establishment->tenantId->equals($tenantId)) {
|
||||
return $establishment;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Establishment[]
|
||||
*/
|
||||
#[Override]
|
||||
public function findAll(): array
|
||||
{
|
||||
return array_values($this->establishments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Persistence\InMemory;
|
||||
|
||||
use App\SuperAdmin\Domain\Exception\SuperAdminIntrouvableException;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdmin;
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use App\SuperAdmin\Domain\Repository\SuperAdminRepository;
|
||||
use Override;
|
||||
|
||||
final class InMemorySuperAdminRepository implements SuperAdminRepository
|
||||
{
|
||||
/** @var array<string, SuperAdmin> */
|
||||
private array $superAdmins = [];
|
||||
|
||||
#[Override]
|
||||
public function save(SuperAdmin $superAdmin): void
|
||||
{
|
||||
$this->superAdmins[(string) $superAdmin->id] = $superAdmin;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function get(SuperAdminId $id): SuperAdmin
|
||||
{
|
||||
return $this->superAdmins[(string) $id]
|
||||
?? throw SuperAdminIntrouvableException::avecId($id);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function findByEmail(string $email): ?SuperAdmin
|
||||
{
|
||||
foreach ($this->superAdmins as $superAdmin) {
|
||||
if ($superAdmin->email === $email) {
|
||||
return $superAdmin;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Security;
|
||||
|
||||
use App\SuperAdmin\Domain\Model\SuperAdmin\SuperAdminId;
|
||||
use Override;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final readonly class SecuritySuperAdmin implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
/** @var non-empty-string */
|
||||
private string $email;
|
||||
|
||||
/**
|
||||
* @param non-empty-string $email
|
||||
*/
|
||||
public function __construct(
|
||||
private SuperAdminId $superAdminId,
|
||||
string $email,
|
||||
private string $hashedPassword,
|
||||
) {
|
||||
$this->email = $email;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function superAdminId(): string
|
||||
{
|
||||
return (string) $this->superAdminId;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->hashedPassword;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
#[Override]
|
||||
public function getRoles(): array
|
||||
{
|
||||
return ['ROLE_SUPER_ADMIN'];
|
||||
}
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Security;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
/**
|
||||
* Stores the currently selected tenant for a Super Admin session.
|
||||
*
|
||||
* When a Super Admin switches to an establishment context,
|
||||
* they get ROLE_ADMIN rights within that tenant scope.
|
||||
*/
|
||||
final class SuperAdminTenantContext
|
||||
{
|
||||
private ?TenantId $currentTenantId = null;
|
||||
|
||||
public function switchTo(TenantId $tenantId): void
|
||||
{
|
||||
$this->currentTenantId = $tenantId;
|
||||
}
|
||||
|
||||
public function currentTenantId(): ?TenantId
|
||||
{
|
||||
return $this->currentTenantId;
|
||||
}
|
||||
|
||||
public function hasTenant(): bool
|
||||
{
|
||||
return $this->currentTenantId !== null;
|
||||
}
|
||||
|
||||
public function clear(): void
|
||||
{
|
||||
$this->currentTenantId = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\SuperAdmin\Infrastructure\Security;
|
||||
|
||||
use App\SuperAdmin\Domain\Repository\SuperAdminRepository;
|
||||
use InvalidArgumentException;
|
||||
use Override;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException as SymfonyUserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* User provider for Super Admin authentication.
|
||||
*
|
||||
* Separate from the regular DatabaseUserProvider because Super Admins
|
||||
* live in the master database and are not scoped by tenant.
|
||||
*
|
||||
* @implements UserProviderInterface<SecuritySuperAdmin>
|
||||
*/
|
||||
final readonly class SuperAdminUserProvider implements UserProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private SuperAdminRepository $superAdminRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
$superAdmin = $this->superAdminRepository->findByEmail($identifier);
|
||||
|
||||
if ($superAdmin === null) {
|
||||
throw new SymfonyUserNotFoundException();
|
||||
}
|
||||
|
||||
if (!$superAdmin->peutSeConnecter()) {
|
||||
throw new SymfonyUserNotFoundException();
|
||||
}
|
||||
|
||||
/** @var non-empty-string $email */
|
||||
$email = $superAdmin->email;
|
||||
|
||||
return new SecuritySuperAdmin(
|
||||
superAdminId: $superAdmin->id,
|
||||
email: $email,
|
||||
hashedPassword: $superAdmin->hashedPassword,
|
||||
);
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
if (!$user instanceof SecuritySuperAdmin) {
|
||||
throw new InvalidArgumentException('Expected instance of ' . SecuritySuperAdmin::class);
|
||||
}
|
||||
|
||||
return $this->loadUserByIdentifier($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return $class === SecuritySuperAdmin::class;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user