feat(monorepo): migrate to typescript monorepo
This commit is contained in:
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\AccountConfirmedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountConfirmed;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class AccountConfirmedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountConfirmedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AccountConfirmed $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new AccountConfirmedEmail($user->email, false, null);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\AccountLockedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountLocked;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class AccountLockedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountLockedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AccountLocked $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new AccountLockedEmail($user->email, $event->token);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\AccountUnlockedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\AccountUnlocked;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class AccountUnlockedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountUnlockedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AccountUnlocked $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new AccountUnlockedEmail($user->email);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\ConfirmationRequestedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\ConfirmationRequested;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class UserRegisteredListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ConfirmationRequestedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ConfirmationRequested $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new ConfirmationRequestedEmail($user->email, $user->name, $event->token);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\LoginProfileChangedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\LoginProfileChanged;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class LoginProfileChangedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class LoginProfileChangedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(LoginProfileChanged $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new LoginProfileChangedEmail($user->email, $event->device, $event->location);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\PasswordCreatedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordCreated;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class PasswordCreatedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordCreatedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PasswordCreated $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new PasswordCreatedEmail($user->email, $event->password);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\PasswordForgottenEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordForgotten;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class PasswordForgottenListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordForgottenListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PasswordForgotten $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new PasswordForgottenEmail($user->email, $event->token);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\PasswordResetEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordReset;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class PasswordForgottenListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordResetListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PasswordReset $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new PasswordResetEmail($user->email);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\Mailing\PasswordUpdatedEmail;
|
||||
use Basango\IdentityAndAccess\Domain\Event\PasswordUpdated;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Mailing\Mailer;
|
||||
use Basango\SharedKernel\Domain\EventListener\EventListener;
|
||||
|
||||
/**
|
||||
* Class PasswordUpdatedListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordUpdatedListener implements EventListener
|
||||
{
|
||||
public function __construct(
|
||||
private Mailer $mailer,
|
||||
private UserRepository $userRepository
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PasswordUpdated $event): void
|
||||
{
|
||||
$user = $this->userRepository->getById($event->userId);
|
||||
$email = new PasswordUpdatedEmail($user->email);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class UserConfirmedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountConfirmedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private bool $isSocialLogin,
|
||||
private ?string $socialLoginService
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.account_confirmed';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/account_confirmed';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'is_social_login' => $this->isSocialLogin,
|
||||
'social_login_service' => $this->socialLoginService,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class AccountBlockedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountLockedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.account_locked';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/account_locked';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'token' => $this->token,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class AccountUnlockedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountUnlockedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.account_unlocked';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/account_unlocked';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class UserRegisteredEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ConfirmationRequestedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private string $name,
|
||||
private GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.user_registered';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/user_registered';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'token' => $this->token,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\Device;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\GeoLocation;
|
||||
|
||||
/**
|
||||
* Class LoginProfileChangedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class LoginProfileChangedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private Device $device,
|
||||
private GeoLocation $location
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.login_profile_changed';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/login_profile_changed';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'device' => $this->device,
|
||||
'location' => $this->location,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedCode;
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class PasswordCreatedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordCreatedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private GeneratedCode $code
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.password_created';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/password_created';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class PasswordForgottenEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordForgottenEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
private GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.password_forgotten';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/password_forgotten';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [
|
||||
'token' => $this->token,
|
||||
];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class PasswordResetEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordResetEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.password_reset';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/password_reset';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\Mailing;
|
||||
|
||||
use Basango\SharedKernel\Application\Mailing\EmailDefinition;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class PasswordUpdatedEmail.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordUpdatedEmail implements EmailDefinition
|
||||
{
|
||||
public function __construct(
|
||||
private EmailAddress $recipient
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function recipient(): EmailAddress
|
||||
{
|
||||
return $this->recipient;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subject(): string
|
||||
{
|
||||
return 'identity_and_access.emails.subjects.password_updated';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function subjectVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function template(): string
|
||||
{
|
||||
return 'identity_and_access/password_updated';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function templateVariables(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function locale(): string
|
||||
{
|
||||
return 'fr';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\ReadModel;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\DataTransfert\DataMapping;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class UserProfile.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UserProfile
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $id,
|
||||
public string $name,
|
||||
public EmailAddress $email,
|
||||
public \DateTimeImmutable $createdAt,
|
||||
public ?\DateTimeImmutable $updatedAt = null,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(array $item): self
|
||||
{
|
||||
return new self(
|
||||
UserId::fromString(DataMapping::string($item, 'user_id')),
|
||||
DataMapping::string($item, 'user_name'),
|
||||
EmailAddress::from(DataMapping::string($item, 'user_email')),
|
||||
DataMapping::dateTime($item, 'user_created_at'),
|
||||
DataMapping::nullableDateTime($item, 'user_updated_at'),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class ConfirmRegistration.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ConfirmAccount
|
||||
{
|
||||
public function __construct(
|
||||
public GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class LockAccount.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class LockAccount
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Roles;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class Register.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class Register
|
||||
{
|
||||
public function __construct(
|
||||
public string $name,
|
||||
public EmailAddress $email,
|
||||
public ?string $password,
|
||||
public Roles $roles = new Roles()
|
||||
) {
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class AddLoginAttempt.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RegisterLoginAttempt
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\ClientProfile;
|
||||
|
||||
/**
|
||||
* Class RegisterLogin.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RegisterLoginSuccess
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public ClientProfile $profile
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class RequestPassword.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RequestPassword
|
||||
{
|
||||
public function __construct(
|
||||
public EmailAddress $email
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class ResetPassword.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ResetPassword
|
||||
{
|
||||
public function __construct(
|
||||
public GeneratedToken $token,
|
||||
public string $password
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class UnlockAccount.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UnlockAccount
|
||||
{
|
||||
public function __construct(
|
||||
public GeneratedToken $token,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Command;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class UpdatePassword.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UpdatePassword
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public string $current,
|
||||
public string $password,
|
||||
) {
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\ConfirmAccount;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class ConfirmRegistrationHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ConfirmAccountHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ConfirmAccount $command): void
|
||||
{
|
||||
$token = $this->verificationTokenRepository->getByToken(
|
||||
$command->token,
|
||||
TokenPurpose::CONFIRM_ACCOUNT
|
||||
);
|
||||
|
||||
$user = $this->userRepository->getById($token->user->id);
|
||||
$user->confirmAccount();
|
||||
|
||||
$this->verificationTokenRepository->remove($token);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\LockAccount;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\VerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\IdentityAndAccess\Domain\Service\SecretGenerator;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class ResetPasswordHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class LockAccountHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private SecretGenerator $secretGenerator,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(LockAccount $command): void
|
||||
{
|
||||
$user = $this->userRepository->getById($command->userId);
|
||||
$token = $this->createVerificationToken($user);
|
||||
$user->lockAccount($token);
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->verificationTokenRepository->add($token);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
|
||||
private function createVerificationToken(User $user): VerificationToken
|
||||
{
|
||||
$secret = $this->secretGenerator->generateToken();
|
||||
return VerificationToken::create($user, $secret, TokenPurpose::UNLOCK_ACCOUNT);
|
||||
}
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\Register;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\EmailAlreadyUsed;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\VerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\IdentityAndAccess\Domain\Service\PasswordHasher;
|
||||
use Basango\IdentityAndAccess\Domain\Service\SecretGenerator;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class RegisterHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RegisterHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private PasswordHasher $passwordHasher,
|
||||
private SecretGenerator $secretGenerator,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(Register $command): void
|
||||
{
|
||||
$user = $this->userRepository->getByEmail($command->email);
|
||||
if ($user instanceof User) {
|
||||
throw EmailAlreadyUsed::with($command->email);
|
||||
}
|
||||
|
||||
$user = User::register($command->name, $command->email, $command->roles);
|
||||
$password = $command->password ?? $this->secretGenerator->generateCode();
|
||||
$token = $this->createVerificationToken($user);
|
||||
|
||||
$user
|
||||
->definePassword($password, $this->passwordHasher)
|
||||
->requestAccountConfirmation($token);
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->verificationTokenRepository->add($token);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
|
||||
private function createVerificationToken(User $user): VerificationToken
|
||||
{
|
||||
$secret = $this->secretGenerator->generateToken();
|
||||
return VerificationToken::create($user, $secret, TokenPurpose::CONFIRM_ACCOUNT);
|
||||
}
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\LockAccount;
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RegisterLoginAttempt;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginAttempt;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginAttemptRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
|
||||
/**
|
||||
* Class RegisterLoginAttemptHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RegisterLoginAttemptHandler implements CommandHandler
|
||||
{
|
||||
private const int ATTEMPTS_LIMIT = 3;
|
||||
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private LoginAttemptRepository $loginAttemptRepository,
|
||||
private CommandBus $commandBus
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RegisterLoginAttempt $command): void
|
||||
{
|
||||
$user = $this->userRepository->getById($command->userId);
|
||||
$attempts = $this->loginAttemptRepository->countBy($user);
|
||||
|
||||
if ($attempts < self::ATTEMPTS_LIMIT) {
|
||||
$this->loginAttemptRepository->add(LoginAttempt::create($user));
|
||||
} elseif ($user->isLocked === false) {
|
||||
$this->commandBus->handle(new LockAccount($user->id));
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RegisterLoginSuccess;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginHistory;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginAttemptRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginHistoryRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
use Basango\SharedKernel\Domain\Tracking\ClientProfiler;
|
||||
|
||||
/**
|
||||
* Class RegisterLoginSuccessHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RegisterLoginSuccessHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private LoginHistoryRepository $loginHistoryRepository,
|
||||
private LoginAttemptRepository $loginAttemptRepository,
|
||||
private ClientProfiler $clientProfiler,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RegisterLoginSuccess $command): void
|
||||
{
|
||||
$user = $this->userRepository->getById($command->userId);
|
||||
$device = $this->clientProfiler->detect($command->profile);
|
||||
$location = $this->clientProfiler->locate($command->profile);
|
||||
|
||||
$current = LoginHistory::create($user, $command->profile->userIp, $device, $location);
|
||||
$previous = $this->loginHistoryRepository->getLastBy($user);
|
||||
if ($previous instanceof LoginHistory) {
|
||||
$current->matchPreviousProfile($previous);
|
||||
}
|
||||
|
||||
$this->loginHistoryRepository->add($current);
|
||||
$this->loginAttemptRepository->deleteBy($user);
|
||||
$this->eventDispatcher->dispatch($current->releaseEvents());
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RequestPassword;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\UserNotFound;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\VerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\IdentityAndAccess\Domain\Service\SecretGenerator;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class RequestPasswordHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RequestPasswordHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private SecretGenerator $secretGenerator,
|
||||
private EventDispatcher $eventDispatcher,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RequestPassword $command): void
|
||||
{
|
||||
$user = $this->userRepository->getByEmail($command->email);
|
||||
if (! $user instanceof User) {
|
||||
throw UserNotFound::withEmail($command->email);
|
||||
}
|
||||
|
||||
$token = $this->createVerificationToken($user);
|
||||
$user->requestPasswordReset($token);
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->verificationTokenRepository->add($token);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
|
||||
private function createVerificationToken(User $user): VerificationToken
|
||||
{
|
||||
$secret = $this->secretGenerator->generateToken();
|
||||
return VerificationToken::create($user, $secret, TokenPurpose::PASSWORD_RESET);
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\ResetPassword;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\IdentityAndAccess\Domain\Service\PasswordHasher;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class ResetPasswordHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ResetPasswordHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private PasswordHasher $passwordHasher,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(ResetPassword $command): void
|
||||
{
|
||||
$token = $this->verificationTokenRepository->getByToken(
|
||||
$command->token,
|
||||
TokenPurpose::PASSWORD_RESET
|
||||
);
|
||||
|
||||
$user = $this->userRepository->getById($token->user->id);
|
||||
$user->resetPassword($command->password, $this->passwordHasher);
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->verificationTokenRepository->remove($token);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\UnlockAccount;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginAttemptRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class ResetPasswordHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UnlockAccountHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private VerificationTokenRepository $verificationTokenRepository,
|
||||
private LoginAttemptRepository $loginAttemptRepository,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UnlockAccount $command): void
|
||||
{
|
||||
$token = $this->verificationTokenRepository->getByToken(
|
||||
$command->token,
|
||||
TokenPurpose::UNLOCK_ACCOUNT
|
||||
);
|
||||
|
||||
$user = $this->userRepository->getById($token->user->id);
|
||||
$user->unlockAccount();
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->verificationTokenRepository->remove($token);
|
||||
$this->loginAttemptRepository->deleteBy($user);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\CommandHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\UpdatePassword;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Service\PasswordHasher;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandHandler;
|
||||
use Basango\SharedKernel\Domain\EventDispatcher\EventDispatcher;
|
||||
|
||||
/**
|
||||
* Class UpdatePasswordHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UpdatePasswordHandler implements CommandHandler
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
private PasswordHasher $passwordHasher,
|
||||
private EventDispatcher $eventDispatcher
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(UpdatePassword $command): void
|
||||
{
|
||||
$user = $this->userRepository->getById($command->userId);
|
||||
$user->updatePassword($command->current, $command->password, $this->passwordHasher);
|
||||
|
||||
$this->userRepository->add($user);
|
||||
$this->eventDispatcher->dispatch($user->releaseEvents());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\Query;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class GetUserProfile.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class GetUserProfile
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Application\UseCase\QueryHandler;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\ReadModel\UserProfile;
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Query\GetUserProfile;
|
||||
use Basango\SharedKernel\Application\Messaging\QueryHandler;
|
||||
|
||||
/**
|
||||
* Interface GetUserProfileHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface GetUserProfileHandler extends QueryHandler
|
||||
{
|
||||
public function __invoke(GetUserProfile $query): UserProfile;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class UserConfirmed.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountConfirmed
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class AccountLocked.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountLocked
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class AccountUnlocked.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class AccountUnlocked
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class AccountConfirmationRequested.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class ConfirmationRequested
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public GeneratedToken $token
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class EmailUpdated.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class EmailUpdated
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public EmailAddress $previous,
|
||||
public EmailAddress $current
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\Device;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\GeoLocation;
|
||||
|
||||
/**
|
||||
* Class LoginProfileChanged.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class LoginProfileChanged
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public Device $device,
|
||||
public GeoLocation $location
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedCode;
|
||||
|
||||
/**
|
||||
* Class PasswordCreated.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordCreated
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
#[\SensitiveParameter] public GeneratedCode $password
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class PasswordForgotten.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordForgotten
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public GeneratedToken $token,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class PasswordReset.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordReset
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Event;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
|
||||
/**
|
||||
* Class PasswordUpdated.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class PasswordUpdated
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class TooManyLoginAttempts.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class AccountIsLocked extends \DomainException implements UserFacingError
|
||||
{
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.account_is_locked';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class UserNotConfirmed.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class AccountNotConfirmed extends \DomainException implements UserFacingError
|
||||
{
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.account_not_confirmed';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class EmailAlreadyUsed.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class EmailAlreadyUsed extends \DomainException implements UserFacingError
|
||||
{
|
||||
public static function with(EmailAddress $email): self
|
||||
{
|
||||
return new self(sprintf('the %s email is already used by another user', $email->value));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.email_already_used';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class InvalidCurrentPassword.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class InvalidCurrentPassword extends \DomainException implements UserFacingError
|
||||
{
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.invalid_current_password';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class InvalidToken.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class InvalidVerificationToken extends \DomainException implements UserFacingError
|
||||
{
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.invalid_verification_token';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class PasswordAlreadyDefined.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class PasswordAlreadyDefined extends \DomainException implements UserFacingError
|
||||
{
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.password_already_defined';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
|
||||
/**
|
||||
* Class PermissionNotGranted.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class PermissionNotGranted extends \DomainException implements UserFacingError
|
||||
{
|
||||
public static function withReason(string $message): self
|
||||
{
|
||||
return new self($message);
|
||||
}
|
||||
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.permission_not_granted';
|
||||
}
|
||||
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [
|
||||
'{reason}' => $this->message,
|
||||
];
|
||||
}
|
||||
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Exception;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Exception\UserFacingError;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
|
||||
/**
|
||||
* Class UserNotFound.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class UserNotFound extends \DomainException implements UserFacingError
|
||||
{
|
||||
public static function withEmail(EmailAddress $email): self
|
||||
{
|
||||
return new self(\sprintf('User with email %s was not found', $email->value));
|
||||
}
|
||||
|
||||
public static function withId(UserId $userId): self
|
||||
{
|
||||
return new self(\sprintf('User with id %s was not found', $userId->toString()));
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationId(): string
|
||||
{
|
||||
return 'identity_and_access.exceptions.user_not_found';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationParameters(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function translationDomain(): string
|
||||
{
|
||||
return 'identity_and_access';
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Service;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
|
||||
/**
|
||||
* Interface PasswordHasher.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface PasswordHasher
|
||||
{
|
||||
public function hash(User $user, string $password): string;
|
||||
|
||||
public function verify(User $user, string $password): bool;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Domain\Service;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedCode;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
|
||||
/**
|
||||
* Class SecretGenerator.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
interface SecretGenerator
|
||||
{
|
||||
public function generateToken(int $length = 60): GeneratedToken;
|
||||
|
||||
public function generateCode(int $length = 6): GeneratedCode;
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RegisterLoginAttempt;
|
||||
use Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\SecurityUser;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||
use Symfony\Component\Security\Http\Event\LoginFailureEvent;
|
||||
|
||||
/**
|
||||
* Class LoginFailureListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
#[AsEventListener(LoginFailureEvent::class)]
|
||||
final readonly class LoginFailureListener
|
||||
{
|
||||
public function __construct(
|
||||
private CommandBus $commandBus
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(LoginFailureEvent $event): void
|
||||
{
|
||||
/** @var SecurityUser|null $user */
|
||||
$user = $event->getPassport()?->getUser();
|
||||
|
||||
if ($user && $event->getException() instanceof BadCredentialsException) {
|
||||
$this->commandBus->handle(new RegisterLoginAttempt($user->userId));
|
||||
}
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\EventListener;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RegisterLoginSuccess;
|
||||
use Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security\SecurityUser;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\Tracking\ClientProfile;
|
||||
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
|
||||
use Symfony\Component\HttpFoundation\IpUtils;
|
||||
use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
|
||||
|
||||
/**
|
||||
* Class LoginSuccessListener.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
#[AsEventListener(InteractiveLoginEvent::class)]
|
||||
final readonly class LoginSuccessListener
|
||||
{
|
||||
public function __construct(
|
||||
private CommandBus $commandBus,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(InteractiveLoginEvent $event): void
|
||||
{
|
||||
/** @var SecurityUser|null $user */
|
||||
$user = $event->getAuthenticationToken()->getUser();
|
||||
|
||||
if ($user !== null) {
|
||||
$profile = new ClientProfile(
|
||||
IpUtils::anonymize((string) $event->getRequest()->getClientIp(), 1),
|
||||
$event->getRequest()->headers->get('User-Agent'),
|
||||
$event->getRequest()->server->all()
|
||||
);
|
||||
|
||||
$this->commandBus->handle(new RegisterLoginSuccess($user->userId, $profile));
|
||||
}
|
||||
}
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Symfony\Component\Security\Core\User\EquatableInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Class SecurityUser.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class SecurityUser implements UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface
|
||||
{
|
||||
public function __construct(
|
||||
public UserId $userId,
|
||||
public EmailAddress $email,
|
||||
public ?string $password,
|
||||
public array $roles,
|
||||
public bool $isLocked,
|
||||
public bool $isConfirmed
|
||||
) {
|
||||
}
|
||||
|
||||
public static function create(User $user): self
|
||||
{
|
||||
return new self(
|
||||
$user->id,
|
||||
$user->email,
|
||||
(string) $user->password,
|
||||
$user->roles->toArray(),
|
||||
$user->isLocked,
|
||||
$user->isConfirmed
|
||||
);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getRoles(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getUserIdentifier(): string
|
||||
{
|
||||
/** @var non-empty-string $email */
|
||||
$email = $this->email->value;
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function isEqualTo(UserInterface $user): bool
|
||||
{
|
||||
if (! $user instanceof self) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->userId->equals($user->userId);
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Symfony\Component\Security\Core\Exception\UserNotFoundException;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserProviderInterface;
|
||||
|
||||
/**
|
||||
* Class SecurityUserProvider.
|
||||
*
|
||||
* @implements UserProviderInterface<SecurityUser>
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class SecurityUserProvider implements UserProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UserRepository $userRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function refreshUser(UserInterface $user): UserInterface
|
||||
{
|
||||
return $this->loadUserByIdentifier($user->getUserIdentifier());
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function loadUserByIdentifier(string $identifier): UserInterface
|
||||
{
|
||||
$user = $this->userRepository->getByEmail(EmailAddress::from($identifier));
|
||||
if (! $user instanceof User) {
|
||||
throw new UserNotFoundException();
|
||||
}
|
||||
|
||||
return SecurityUser::create($user);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function supportsClass(string $class): bool
|
||||
{
|
||||
return $class === SecurityUser::class;
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Exception\AccountIsLocked;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\AccountNotConfirmed;
|
||||
use Symfony\Component\Security\Core\User\UserCheckerInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
/**
|
||||
* Class UserChecker.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UserChecker implements UserCheckerInterface
|
||||
{
|
||||
#[\Override]
|
||||
public function checkPreAuth(UserInterface $user): void
|
||||
{
|
||||
if ($user instanceof SecurityUser && $user->isLocked) {
|
||||
throw new AccountIsLocked();
|
||||
}
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function checkPostAuth(UserInterface $user): void
|
||||
{
|
||||
if ($user instanceof SecurityUser && $user->isConfirmed === false) {
|
||||
throw new AccountNotConfirmed();
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Framework\Symfony\Security;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Service\PasswordHasher;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
/**
|
||||
* Class UserPasswordHasher.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class UserPasswordHasher implements PasswordHasher
|
||||
{
|
||||
public function __construct(
|
||||
private UserPasswordHasherInterface $passwordHasher
|
||||
) {
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function hash(User $user, string $password): string
|
||||
{
|
||||
$securityUser = SecurityUser::create($user);
|
||||
return $this->passwordHasher->hashPassword($securityUser, $password);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function verify(User $user, string $password): bool
|
||||
{
|
||||
$securityUser = SecurityUser::create($user);
|
||||
return $this->passwordHasher->isPasswordValid($securityUser, $password);
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\ReadModel\UserProfile;
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Query\GetUserProfile;
|
||||
use Basango\IdentityAndAccess\Application\UseCase\QueryHandler\GetUserProfileHandler;
|
||||
use Basango\IdentityAndAccess\Domain\Exception\UserNotFound;
|
||||
use Doctrine\DBAL\Connection;
|
||||
|
||||
/**
|
||||
* Class GetUserProfileDbalHandler.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class GetUserProfileDbalHandler implements GetUserProfileHandler
|
||||
{
|
||||
public function __construct(
|
||||
private Connection $connection
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(GetUserProfile $query): UserProfile
|
||||
{
|
||||
$qb = $this->connection->createQueryBuilder()
|
||||
->select(
|
||||
'u.id as user_id',
|
||||
'u.name as user_name',
|
||||
'u.email as user_email',
|
||||
'u.created_at as user_created_at',
|
||||
'u.updated_at as user_updated_at'
|
||||
)
|
||||
->from('user', 'u')
|
||||
->where('u.id = :userId')
|
||||
->setParameter('userId', $query->userId->toString());
|
||||
|
||||
/** @var array<string, mixed>|false $data */
|
||||
$data = $qb->executeQuery()->fetchAssociative();
|
||||
|
||||
if ($data === false) {
|
||||
throw UserNotFound::withId($query->userId);
|
||||
}
|
||||
|
||||
return UserProfile::create($data);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\LoginAttemptId;
|
||||
use Symfony\Bridge\Doctrine\Types\AbstractUidType;
|
||||
|
||||
/**
|
||||
* Class LoginAttemptId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginAttemptIdType extends AbstractUidType
|
||||
{
|
||||
#[\Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return 'login_attempt_id';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return LoginAttemptId::class;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\LoginHistoryId;
|
||||
use Symfony\Bridge\Doctrine\Types\AbstractUidType;
|
||||
|
||||
/**
|
||||
* Class LoginHistoryId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginHistoryIdType extends AbstractUidType
|
||||
{
|
||||
#[\Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return 'login_history_id';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return LoginHistoryId::class;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Symfony\Bridge\Doctrine\Types\AbstractUidType;
|
||||
|
||||
/**
|
||||
* Class UserIdType.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class UserIdType extends AbstractUidType
|
||||
{
|
||||
#[\Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return 'user_id';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return UserId::class;
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\DBAL\Types;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\VerificationTokenId;
|
||||
use Symfony\Bridge\Doctrine\Types\AbstractUidType;
|
||||
|
||||
/**
|
||||
* Class VerificationTokenId.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class VerificationTokenIdType extends AbstractUidType
|
||||
{
|
||||
#[\Override]
|
||||
public function getName(): string
|
||||
{
|
||||
return 'verification_token_id';
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function getUidClass(): string
|
||||
{
|
||||
return VerificationTokenId::class;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\ORM;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginAttempt;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginAttemptRepository;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* Class LoginAttemptOrmRepository.
|
||||
*
|
||||
* @extends ServiceEntityRepository<LoginAttempt>
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginAttemptOrmRepository extends ServiceEntityRepository implements LoginAttemptRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, LoginAttempt::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add(LoginAttempt $loginAttempt): void
|
||||
{
|
||||
$this->getEntityManager()->persist($loginAttempt);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function remove(LoginAttempt $loginAttempt): void
|
||||
{
|
||||
$this->getEntityManager()->remove($loginAttempt);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function countBy(User $user): int
|
||||
{
|
||||
return $this->count([
|
||||
'user' => $user,
|
||||
]);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function deleteBy(User $user): void
|
||||
{
|
||||
$this->createQueryBuilder('la')
|
||||
->delete(LoginAttempt::class, 'la')
|
||||
->where('la.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->getQuery()
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\ORM;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\LoginHistory;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\LoginHistoryRepository;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* Class LoginHistoryOrmRepository.
|
||||
*
|
||||
* @extends ServiceEntityRepository<LoginHistory>
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class LoginHistoryOrmRepository extends ServiceEntityRepository implements LoginHistoryRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, LoginHistory::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add(LoginHistory $loginHistory): void
|
||||
{
|
||||
$this->getEntityManager()->persist($loginHistory);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function remove(LoginHistory $loginHistory): void
|
||||
{
|
||||
$this->getEntityManager()->remove($loginHistory);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getLastBy(User $user): ?LoginHistory
|
||||
{
|
||||
/** @var LoginHistory|null $loginHistory */
|
||||
$loginHistory = $this->createQueryBuilder('lh')
|
||||
->andWhere('lh.user = :user')
|
||||
->setParameter('user', $user)
|
||||
->orderBy('lh.createdAt', 'DESC')
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
|
||||
return $loginHistory;
|
||||
}
|
||||
}
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\ORM;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Exception\UserNotFound;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\User;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Identity\UserId;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\UserRepository;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* Class UserOrmRepository.
|
||||
*
|
||||
* @extends ServiceEntityRepository<User>
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class UserOrmRepository extends ServiceEntityRepository implements UserRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, User::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add(User $user): void
|
||||
{
|
||||
$this->getEntityManager()->persist($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function remove(User $user): void
|
||||
{
|
||||
$this->getEntityManager()->remove($user);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getById(UserId $userId): User
|
||||
{
|
||||
$user = $this->findOneBy([
|
||||
'id' => $userId,
|
||||
]);
|
||||
|
||||
if ($user === null) {
|
||||
throw UserNotFound::withId($userId);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getByEmail(EmailAddress $email): ?User
|
||||
{
|
||||
return $this->findOneBy([
|
||||
'email' => $email,
|
||||
]);
|
||||
}
|
||||
}
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Persistence\Doctrine\ORM;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Exception\InvalidVerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Entity\VerificationToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\Repository\VerificationTokenRepository;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\TokenPurpose;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* Class VerificationTokenOrmRepository.
|
||||
*
|
||||
* @extends ServiceEntityRepository<VerificationToken>
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class VerificationTokenOrmRepository extends ServiceEntityRepository implements VerificationTokenRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, VerificationToken::class);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function add(VerificationToken $verificationToken): void
|
||||
{
|
||||
$this->getEntityManager()->persist($verificationToken);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function remove(VerificationToken $verificationToken): void
|
||||
{
|
||||
$this->getEntityManager()->remove($verificationToken);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function getByToken(GeneratedToken $token, TokenPurpose $purpose): VerificationToken
|
||||
{
|
||||
$token = $this->findOneBy([
|
||||
'token.token' => $token->token,
|
||||
'purpose' => $purpose->value,
|
||||
]);
|
||||
|
||||
if ($token === null) {
|
||||
throw new InvalidVerificationToken();
|
||||
}
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Infrastructure\Secret;
|
||||
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedCode;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\IdentityAndAccess\Domain\Service\SecretGenerator;
|
||||
use Random\Randomizer;
|
||||
|
||||
/**
|
||||
* Class RandomizerSecretGenerator.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final readonly class RandomizerSecretGenerator implements SecretGenerator
|
||||
{
|
||||
private const string ALLOWED_CHARACTERS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||
|
||||
#[\Override]
|
||||
public function generateToken(int $length = 60): GeneratedToken
|
||||
{
|
||||
$value = new Randomizer()
|
||||
->getBytesFromString(self::ALLOWED_CHARACTERS, $length);
|
||||
|
||||
return new GeneratedToken($value);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
public function generateCode(int $length = 6): GeneratedCode
|
||||
{
|
||||
$min = 10 ** ($length - 1);
|
||||
$max = 10 ** $length - 1;
|
||||
|
||||
$value = new Randomizer()
|
||||
->getInt($min, $max);
|
||||
|
||||
return new GeneratedCode((string) $value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Console;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\Register;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Roles;
|
||||
use Basango\SharedKernel\Application\Messaging\CommandBus;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Basango\SharedKernel\Presentation\Console\AskArgumentFeature;
|
||||
use Symfony\Component\Console\Attribute\AsCommand;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* Class RegisterConsole.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
#[AsCommand('app:user-register', 'register a new user')]
|
||||
final class RegisterConsole extends Command
|
||||
{
|
||||
use AskArgumentFeature;
|
||||
|
||||
private SymfonyStyle $io;
|
||||
|
||||
public function __construct(
|
||||
private readonly CommandBus $commandBus,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setDescription('Creates users and stores them in the database')
|
||||
->addArgument('name', InputArgument::OPTIONAL, 'The name of the new user')
|
||||
->addArgument('email', InputArgument::OPTIONAL, 'The email of the new user')
|
||||
->addArgument('password', InputArgument::OPTIONAL, 'The plain password of the new user')
|
||||
->addOption('admin', null, InputOption::VALUE_NONE, 'If set, the user is created as an administrator');
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function initialize(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$this->io = new SymfonyStyle($input, $output);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function interact(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
if (
|
||||
$input->getArgument('name') !== null &&
|
||||
$input->getArgument('email') !== null &&
|
||||
$input->getArgument('password') !== null
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->askArgument($input, 'name');
|
||||
$this->askArgument($input, 'email');
|
||||
$this->askArgument($input, 'password', true);
|
||||
}
|
||||
|
||||
#[\Override]
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
/** @var string $name */
|
||||
$name = $input->getArgument('name');
|
||||
|
||||
/** @var string $email */
|
||||
$email = $input->getArgument('email');
|
||||
|
||||
/** @var string $password */
|
||||
$password = $input->getArgument('password');
|
||||
|
||||
/** @var bool $admin */
|
||||
$admin = $input->getOption('admin');
|
||||
|
||||
$command = new Register($name, EmailAddress::from($email), $password, $admin ? Roles::admin() : Roles::user());
|
||||
$this->commandBus->handle($command);
|
||||
$this->io->success(\sprintf('%s was created: %s', $admin ? 'ADMIN' : 'USER', $email));
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\ConfirmAccount;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
|
||||
/**
|
||||
* Class UnlockAccountController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class ConfirmAccountController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/account/confirm/{token}',
|
||||
name: 'identity_and_access_confirm_account',
|
||||
requirements: [
|
||||
'token' => Requirement::ASCII_SLUG,
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function __invoke(string $token): JsonResponse
|
||||
{
|
||||
$token = new GeneratedToken($token);
|
||||
$this->handleCommand(new ConfirmAccount($token));
|
||||
|
||||
return new JsonResponse(status: 200);
|
||||
}
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Query\GetUserProfile;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Class GetUserProfileController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class GetUserProfileController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/me',
|
||||
name: 'identity_and_access_me',
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function __invoke(): JsonResponse
|
||||
{
|
||||
$security = $this->getSecurityUser();
|
||||
$data = $this->handleQuery(new GetUserProfile($security->userId));
|
||||
|
||||
return JsonResponse::fromJsonString($this->serialize($data));
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\Register;
|
||||
use Basango\IdentityAndAccess\Presentation\WriteModel\RegisterModel;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Class RegisterController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class RegisterController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/register',
|
||||
name: 'identity_and_access_register',
|
||||
methods: ['POST']
|
||||
)]
|
||||
public function __invoke(#[MapRequestPayload] RegisterModel $model): JsonResponse
|
||||
{
|
||||
$this->handleCommand(new Register(
|
||||
$model->name,
|
||||
EmailAddress::from($model->email),
|
||||
$model->password
|
||||
));
|
||||
|
||||
return new JsonResponse(status: 201);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\RequestPassword;
|
||||
use Basango\IdentityAndAccess\Presentation\WriteModel\RequestPasswordModel;
|
||||
use Basango\SharedKernel\Domain\Model\ValueObject\EmailAddress;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
/**
|
||||
* Class RequestPasswordController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class RequestPasswordController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/password/request',
|
||||
name: 'identity_and_access_request_password',
|
||||
methods: ['POST']
|
||||
)]
|
||||
public function __invoke(#[MapRequestPayload] RequestPasswordModel $model): JsonResponse
|
||||
{
|
||||
$email = EmailAddress::from($model->email);
|
||||
$this->handleCommand(new RequestPassword($email));
|
||||
|
||||
return new JsonResponse(status: 200);
|
||||
}
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\ResetPassword;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\IdentityAndAccess\Presentation\WriteModel\ResetPasswordModel;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
|
||||
/**
|
||||
* Class RequestPasswordController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class ResetPasswordController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/password/reset/{token}',
|
||||
name: 'identity_and_access_reset_password',
|
||||
requirements: [
|
||||
'token' => Requirement::ASCII_SLUG,
|
||||
],
|
||||
methods: ['POST']
|
||||
)]
|
||||
public function __invoke(#[MapRequestPayload] ResetPasswordModel $model, string $token): JsonResponse
|
||||
{
|
||||
$token = new GeneratedToken($token);
|
||||
$this->handleCommand(new ResetPassword($token, $model->password));
|
||||
|
||||
return new JsonResponse(status: 200);
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Basango\IdentityAndAccess\Presentation\Web\Controller;
|
||||
|
||||
use Basango\IdentityAndAccess\Application\UseCase\Command\UnlockAccount;
|
||||
use Basango\IdentityAndAccess\Domain\Model\ValueObject\Secret\GeneratedToken;
|
||||
use Basango\SharedKernel\Presentation\Web\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Routing\Requirement\Requirement;
|
||||
|
||||
/**
|
||||
* Class UnlockAccountController.
|
||||
*
|
||||
* @author bernard-ng <bernard@devscast.tech>
|
||||
*/
|
||||
final class UnlockAccountController extends AbstractController
|
||||
{
|
||||
#[Route(
|
||||
path: '/api/account/unlock/{token}',
|
||||
name: 'identity_and_access_unlock_account',
|
||||
requirements: [
|
||||
'token' => Requirement::ASCII_SLUG,
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function __invoke(string $token): JsonResponse
|
||||
{
|
||||
$token = new GeneratedToken($token);
|
||||
$this->handleCommand(new UnlockAccount($token));
|
||||
|
||||
return new JsonResponse(status: 200);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user