feat(monorepo): migrate to typescript monorepo
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Entity;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\LoginAttemptId;
|
||||
|
||||
/**
|
||||
* Class LoginAttempt.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
readonly class LoginAttempt
|
||||
{
|
||||
public LoginAttemptId $id;
|
||||
|
||||
private function __construct(
|
||||
public User $user,
|
||||
public \DateTimeImmutable $createdAt = new \DateTimeImmutable()
|
||||
) {
|
||||
$this->id = new LoginAttemptId();
|
||||
}
|
||||
|
||||
public static function create(User $user): self
|
||||
{
|
||||
return new self($user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Entity;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Event\LoginProfileChanged;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\LoginHistoryId;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventEmitterTrait;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\Device;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\GeoLocation;
|
||||
|
||||
/**
|
||||
* Class LoginHistory.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
class LoginHistory
|
||||
{
|
||||
use EventEmitterTrait;
|
||||
|
||||
public readonly LoginHistoryId $id;
|
||||
|
||||
private function __construct(
|
||||
public readonly User $user,
|
||||
public readonly ?string $ipAddress,
|
||||
public readonly Device $device,
|
||||
public readonly GeoLocation $location,
|
||||
public readonly \DateTimeImmutable $createdAt = new \DateTimeImmutable()
|
||||
) {
|
||||
$this->id = new LoginHistoryId();
|
||||
}
|
||||
|
||||
public static function create(User $user, ?string $userIp, Device $device, GeoLocation $location): self
|
||||
{
|
||||
return new self($user, $userIp, $device, $location);
|
||||
}
|
||||
|
||||
public function matchPreviousProfile(self $previous): self
|
||||
{
|
||||
if (
|
||||
$this->ipAddress !== $previous->ipAddress ||
|
||||
$this->location->city !== $previous->location->city ||
|
||||
$this->location->country !== $previous->location->country ||
|
||||
$this->device->operatingSystem !== $previous->device->operatingSystem
|
||||
) {
|
||||
$this->emitEvent(new LoginProfileChanged($this->user->id, $this->device, $this->location));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Entity;
|
||||
|
||||
use Gesdinet\JWTRefreshTokenBundle\Model\AbstractRefreshToken;
|
||||
|
||||
class RefreshToken extends AbstractRefreshToken
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Entity;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountConfirmed;
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountLocked;
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountUnlocked;
|
||||
use Basango\IdentityAndAccess\Domain\Event\ConfirmationRequested;
|
||||
use Basango\IdentityAndAccess\Domain\Event\EmailUpdated;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordCreated;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordForgotten;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordReset;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordUpdated;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\InvalidCurrentPassword;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\PasswordAlreadyDefined;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Roles;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedCode;
|
||||
use Basango\IdentityAndAccess\Domain\Service\PasswordHasher;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventEmitterTrait;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class User.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
class User
|
||||
{
|
||||
use EventEmitterTrait;
|
||||
|
||||
public readonly UserId $id;
|
||||
|
||||
public function __construct(
|
||||
private(set) string $name,
|
||||
private(set) EmailAddress $email,
|
||||
private(set) Roles $roles,
|
||||
private(set) ?string $password = null,
|
||||
private(set) bool $isLocked = false,
|
||||
private(set) bool $isConfirmed = false,
|
||||
private(set) ?\DateTimeImmutable $updatedAt = null,
|
||||
public readonly ?\DateTimeImmutable $createdAt = new \DateTimeImmutable(),
|
||||
) {
|
||||
$this->id = new UserId();
|
||||
}
|
||||
|
||||
public function lockAccount(VerificationToken $verificationToken): self
|
||||
{
|
||||
$this->isLocked = true;
|
||||
$this->emitEvent(new AccountLocked($this->id, $verificationToken->token));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function unlockAccount(): self
|
||||
{
|
||||
$this->isLocked = false;
|
||||
$this->emitEvent(new AccountUnlocked($this->id));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function confirmAccount(): self
|
||||
{
|
||||
$this->isConfirmed = true;
|
||||
$this->emitEvent(new AccountConfirmed($this->id));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public static function register(string $name, EmailAddress $email, ?Roles $roles): self
|
||||
{
|
||||
return new self($name, $email, $roles ?? Roles::user());
|
||||
}
|
||||
|
||||
public function updateProfile(string $name, Roles $roles): static
|
||||
{
|
||||
$this->name = $name;
|
||||
$this->roles = $roles;
|
||||
$this->updatedAt = new \DateTimeImmutable();
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function updateEmail(EmailAddress $email): self
|
||||
{
|
||||
$previous = $this->email;
|
||||
$this->email = $email;
|
||||
$this->emitEvent(new EmailUpdated($this->id, $previous, $email));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function resetPassword(string $password, PasswordHasher $passwordHasher): void
|
||||
{
|
||||
$this->password = $passwordHasher->hash($this, $password);
|
||||
$this->emitEvent(new PasswordReset($this->id));
|
||||
}
|
||||
|
||||
public function updatePassword(string $current, string $new, PasswordHasher $passwordHasher): self
|
||||
{
|
||||
if ($this->password === null || ! $passwordHasher->verify($this, $current)) {
|
||||
throw new InvalidCurrentPassword();
|
||||
}
|
||||
|
||||
$this->password = $passwordHasher->hash($this, $new);
|
||||
$this->emitEvent(new PasswordUpdated($this->id));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function definePassword(GeneratedCode|string $password, PasswordHasher $passwordHasher): self
|
||||
{
|
||||
if ($this->password !== null) {
|
||||
throw new PasswordAlreadyDefined();
|
||||
}
|
||||
|
||||
$this->password = $passwordHasher->hash($this, (string) $password);
|
||||
$this->updatedAt = new \DateTimeImmutable();
|
||||
|
||||
if ($password instanceof GeneratedCode) {
|
||||
$this->emitEvent(new PasswordCreated($this->id, $password));
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function requestPasswordReset(VerificationToken $verificationToken): self
|
||||
{
|
||||
$this->emitEvent(new PasswordForgotten($this->id, $verificationToken->token));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function requestAccountConfirmation(VerificationToken $verificationToken): self
|
||||
{
|
||||
$this->emitEvent(new ConfirmationRequested($this->id, $verificationToken->token));
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Entity;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Exception\InvalidVerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\VerificationTokenId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
|
||||
/**
|
||||
* Class VerificationToken.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
readonly class VerificationToken
|
||||
{
|
||||
public const string DEFAULT_VALIDITY = 'PT2H';
|
||||
|
||||
public VerificationTokenId $id;
|
||||
|
||||
public function __construct(
|
||||
public User $user,
|
||||
public GeneratedToken $token,
|
||||
public TokenPurpose $purpose,
|
||||
public \DateTimeImmutable $createdAt = new \DateTimeImmutable()
|
||||
) {
|
||||
$this->id = new VerificationTokenId();
|
||||
}
|
||||
|
||||
public static function create(User $user, GeneratedToken $token, TokenPurpose $purpose): self
|
||||
{
|
||||
return new self($user, $token, $purpose);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
$now = new \DateTimeImmutable();
|
||||
$validUntil = (\DateTime::createFromImmutable($this->createdAt))
|
||||
->add(new \DateInterval(self::DEFAULT_VALIDITY));
|
||||
|
||||
return $now > $validUntil;
|
||||
}
|
||||
|
||||
public function assertValid(): void
|
||||
{
|
||||
if ($this->isExpired()) {
|
||||
throw new InvalidVerificationToken();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Identity;
|
||||
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
/**
|
||||
* Class LoginAttemptId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginAttemptId extends UuidV7
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Identity;
|
||||
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
/**
|
||||
* Class LoginHistoryId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginHistoryId extends UuidV7
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Identity;
|
||||
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
/**
|
||||
* Class UserId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class UserId extends UuidV7
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Identity;
|
||||
|
||||
use Symfony\Component\Uid\UuidV7;
|
||||
|
||||
/**
|
||||
* Class VerificationTokenId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class VerificationTokenId extends UuidV7
|
||||
{
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Repository;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginAttempt;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
|
||||
/**
|
||||
* Interface LoginAttemptRepository.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface LoginAttemptRepository
|
||||
{
|
||||
public function add(LoginAttempt $loginAttempt): void;
|
||||
|
||||
public function remove(LoginAttempt $loginAttempt): void;
|
||||
|
||||
public function countBy(User $user): int;
|
||||
|
||||
public function deleteBy(User $user): void;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Repository;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginHistory;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
|
||||
/**
|
||||
* Interface LoginHistoryRepository.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface LoginHistoryRepository
|
||||
{
|
||||
public function add(LoginHistory $loginHistory): void;
|
||||
|
||||
public function remove(LoginHistory $loginHistory): void;
|
||||
|
||||
public function getLastBy(User $user): ?LoginHistory;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Repository;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Interface UserRepository.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface UserRepository
|
||||
{
|
||||
public function add(User $user): void;
|
||||
|
||||
public function remove(User $user): void;
|
||||
|
||||
public function getById(UserId $userId): User;
|
||||
|
||||
public function getByEmail(EmailAddress $email): ?User;
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\Repository;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\VerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
|
||||
/**
|
||||
* Interface LoginAttemptRepository.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface VerificationTokenRepository
|
||||
{
|
||||
public function add(VerificationToken $verificationToken): void;
|
||||
|
||||
public function remove(VerificationToken $verificationToken): void;
|
||||
|
||||
public function getByToken(GeneratedToken $token, TokenPurpose $purpose): VerificationToken;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\ValueObject;
|
||||
|
||||
/**
|
||||
* Class Role.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
enum Role: string
|
||||
{
|
||||
case USER = 'ROLE_USER';
|
||||
case ADMIN = 'ROLE_ADMIN';
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\ValueObject;
|
||||
|
||||
use Basango\SharedKernel\Domain\Assert;
|
||||
|
||||
/**
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
readonly class Roles implements \Stringable
|
||||
{
|
||||
private array $roles;
|
||||
|
||||
public function __construct(array $roles = [Role::USER])
|
||||
{
|
||||
Assert::notEmpty($roles, 'identity_and_access.validations.empty_roles');
|
||||
Assert::allIsInstanceOf($roles, Role::class);
|
||||
|
||||
$roles[] = Role::USER;
|
||||
$this->roles = array_unique(\array_map(fn (Role $role) => $role->value, $roles));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
return implode(',', $this->roles);
|
||||
}
|
||||
|
||||
public static function admin(): self
|
||||
{
|
||||
return new self([Role::USER, Role::ADMIN]);
|
||||
}
|
||||
|
||||
public static function user(): self
|
||||
{
|
||||
return new self();
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
public static function fromArray(array $roles): self
|
||||
{
|
||||
return new self($roles);
|
||||
}
|
||||
|
||||
public function contains(string $role): bool
|
||||
{
|
||||
return in_array($role, $this->roles, true);
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret;
|
||||
|
||||
use Basango\SharedKernel\Domain\Assert;
|
||||
|
||||
/**
|
||||
* Class GeneratedCode.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class GeneratedCode implements \Stringable
|
||||
{
|
||||
public function __construct(
|
||||
public string $code
|
||||
) {
|
||||
Assert::notEmpty($this->code);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret;
|
||||
|
||||
use Basango\SharedKernel\Domain\Assert;
|
||||
|
||||
/**
|
||||
* Class GeneratedToken.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class GeneratedToken implements \Stringable
|
||||
{
|
||||
public function __construct(
|
||||
public string $token,
|
||||
) {
|
||||
Assert::notEmpty($this->token);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function isEqualTo(self $token): bool
|
||||
{
|
||||
return $this->token === $token->token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Model\ValueObject;
|
||||
|
||||
/**
|
||||
* Enum TokenPurpose.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
enum TokenPurpose: string
|
||||
{
|
||||
case CONFIRM_ACCOUNT = 'confirm_account';
|
||||
case PASSWORD_RESET = 'password_reset';
|
||||
case UNLOCK_ACCOUNT = 'unlock_account';
|
||||
case DELETE_ACCOUNT = 'delete_account';
|
||||
}
|
||||
Reference in New Issue
Block a user