Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes, emploi du temps, devoirs). Cela nécessite un lien formalisé entre le compte parent et le compte élève, géré par les administrateurs. Le lien est établi soit manuellement via l'interface d'administration, soit automatiquement lors de l'activation du compte parent lorsque l'invitation inclut un élève cible. Ce lien conditionne l'accès aux données scolaires de l'enfant (autorisations vérifiées par un voter dédié).
160 lines
4.7 KiB
PHP
160 lines
4.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Administration\Domain\Model\ActivationToken;
|
|
|
|
use App\Administration\Domain\Event\ActivationTokenGenerated;
|
|
use App\Administration\Domain\Event\ActivationTokenUsed;
|
|
use App\Administration\Domain\Exception\ActivationTokenAlreadyUsedException;
|
|
use App\Administration\Domain\Exception\ActivationTokenExpiredException;
|
|
use App\Shared\Domain\AggregateRoot;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use Ramsey\Uuid\Uuid;
|
|
|
|
use function sprintf;
|
|
|
|
final class ActivationToken extends AggregateRoot
|
|
{
|
|
private const int EXPIRATION_DAYS = 7;
|
|
|
|
public private(set) ?DateTimeImmutable $usedAt = null;
|
|
|
|
private function __construct(
|
|
public private(set) ActivationTokenId $id,
|
|
public private(set) string $tokenValue,
|
|
public private(set) string $userId,
|
|
public private(set) string $email,
|
|
public private(set) TenantId $tenantId,
|
|
public private(set) string $role,
|
|
public private(set) string $schoolName,
|
|
public private(set) DateTimeImmutable $createdAt,
|
|
public private(set) DateTimeImmutable $expiresAt,
|
|
public private(set) ?string $studentId = null,
|
|
public private(set) ?string $relationshipType = null,
|
|
) {
|
|
}
|
|
|
|
public static function generate(
|
|
string $userId,
|
|
string $email,
|
|
TenantId $tenantId,
|
|
string $role,
|
|
string $schoolName,
|
|
DateTimeImmutable $createdAt,
|
|
?string $studentId = null,
|
|
?string $relationshipType = null,
|
|
): self {
|
|
$token = new self(
|
|
id: ActivationTokenId::generate(),
|
|
tokenValue: Uuid::uuid7()->toString(),
|
|
userId: $userId,
|
|
email: $email,
|
|
tenantId: $tenantId,
|
|
role: $role,
|
|
schoolName: $schoolName,
|
|
createdAt: $createdAt,
|
|
expiresAt: $createdAt->modify(sprintf('+%d days', self::EXPIRATION_DAYS)),
|
|
studentId: $studentId,
|
|
relationshipType: $relationshipType,
|
|
);
|
|
|
|
$token->recordEvent(new ActivationTokenGenerated(
|
|
tokenId: $token->id,
|
|
userId: $userId,
|
|
email: $email,
|
|
tenantId: $tenantId,
|
|
occurredOn: $createdAt,
|
|
));
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Reconstitute an ActivationToken from storage.
|
|
* Does NOT record domain events (this is not a new creation).
|
|
*
|
|
* @internal For use by Infrastructure layer only
|
|
*/
|
|
public static function reconstitute(
|
|
ActivationTokenId $id,
|
|
string $tokenValue,
|
|
string $userId,
|
|
string $email,
|
|
TenantId $tenantId,
|
|
string $role,
|
|
string $schoolName,
|
|
DateTimeImmutable $createdAt,
|
|
DateTimeImmutable $expiresAt,
|
|
?DateTimeImmutable $usedAt,
|
|
?string $studentId = null,
|
|
?string $relationshipType = null,
|
|
): self {
|
|
$token = new self(
|
|
id: $id,
|
|
tokenValue: $tokenValue,
|
|
userId: $userId,
|
|
email: $email,
|
|
tenantId: $tenantId,
|
|
role: $role,
|
|
schoolName: $schoolName,
|
|
createdAt: $createdAt,
|
|
expiresAt: $expiresAt,
|
|
studentId: $studentId,
|
|
relationshipType: $relationshipType,
|
|
);
|
|
|
|
$token->usedAt = $usedAt;
|
|
|
|
return $token;
|
|
}
|
|
|
|
public function isExpired(DateTimeImmutable $at): bool
|
|
{
|
|
return $at >= $this->expiresAt;
|
|
}
|
|
|
|
public function isUsed(): bool
|
|
{
|
|
return $this->usedAt !== null;
|
|
}
|
|
|
|
/**
|
|
* Validate that the token can be used (not expired, not already used).
|
|
* Does NOT mark the token as used - use use() for that after successful activation.
|
|
*
|
|
* @throws ActivationTokenAlreadyUsedException if token was already used
|
|
* @throws ActivationTokenExpiredException if token is expired
|
|
*/
|
|
public function validateForUse(DateTimeImmutable $at): void
|
|
{
|
|
if ($this->isUsed()) {
|
|
throw ActivationTokenAlreadyUsedException::forToken($this->id);
|
|
}
|
|
|
|
if ($this->isExpired($at)) {
|
|
throw ActivationTokenExpiredException::forToken($this->id);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark the token as used. Should only be called after successful user activation.
|
|
*
|
|
* @throws ActivationTokenAlreadyUsedException if token was already used
|
|
* @throws ActivationTokenExpiredException if token is expired
|
|
*/
|
|
public function use(DateTimeImmutable $at): void
|
|
{
|
|
$this->validateForUse($at);
|
|
|
|
$this->usedAt = $at;
|
|
|
|
$this->recordEvent(new ActivationTokenUsed(
|
|
tokenId: $this->id,
|
|
userId: $this->userId,
|
|
occurredOn: $at,
|
|
));
|
|
}
|
|
}
|