mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
feat: add registration
This commit is contained in:
2
.env
2
.env
@@ -57,7 +57,9 @@ LOCK_DSN=flock
|
|||||||
###< symfony/lock ###
|
###< symfony/lock ###
|
||||||
|
|
||||||
|
|
||||||
|
MAILER_SENDER_NAME="Domain Watchdog"
|
||||||
MAILER_SENDER_EMAIL=notifications@example.com
|
MAILER_SENDER_EMAIL=notifications@example.com
|
||||||
|
REGISTRATION_ENABLED=true
|
||||||
LIMITED_FEATURES=false
|
LIMITED_FEATURES=false
|
||||||
OAUTH_CLIENT_ID=
|
OAUTH_CLIENT_ID=
|
||||||
OAUTH_CLIENT_SECRET=
|
OAUTH_CLIENT_SECRET=
|
||||||
|
|||||||
@@ -103,7 +103,9 @@ export default function App() {
|
|||||||
<Typography.Link href='https://github.com/maelgangloff/domain-watchdog/wiki'><Button
|
<Typography.Link href='https://github.com/maelgangloff/domain-watchdog/wiki'><Button
|
||||||
type='text'>{t`Documentation`}</Button></Typography.Link>
|
type='text'>{t`Documentation`}</Button></Typography.Link>
|
||||||
</Space>
|
</Space>
|
||||||
<Typography.Paragraph>{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}</Typography.Paragraph>
|
<Typography.Paragraph style={{marginTop: '1em'}}>
|
||||||
|
{jt`${ProjectLink} is an open source project distributed under the ${LicenseLink} license.`}
|
||||||
|
</Typography.Paragraph>
|
||||||
</Layout.Footer>
|
</Layout.Footer>
|
||||||
</Layout>
|
</Layout>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@
|
|||||||
"symfony/web-link": "7.1.*",
|
"symfony/web-link": "7.1.*",
|
||||||
"symfony/webpack-encore-bundle": "^2.1",
|
"symfony/webpack-encore-bundle": "^2.1",
|
||||||
"symfony/yaml": "7.1.*",
|
"symfony/yaml": "7.1.*",
|
||||||
"symfonycasts/verify-email-bundle": "^1.17",
|
"symfonycasts/verify-email-bundle": "*",
|
||||||
"twig/extra-bundle": "^2.12|^3.0",
|
"twig/extra-bundle": "^2.12|^3.0",
|
||||||
"twig/twig": "^2.12|^3.0"
|
"twig/twig": "^2.12|^3.0"
|
||||||
},
|
},
|
||||||
|
|||||||
2
composer.lock
generated
2
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "6b52bd3ad92da490e7a206500d0c0348",
|
"content-hash": "69d672f9e5a01b48f871fa5c81714f8d",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/core",
|
"name": "api-platform/core",
|
||||||
|
|||||||
@@ -18,4 +18,5 @@ return [
|
|||||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||||
|
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
framework:
|
framework:
|
||||||
rate_limiter:
|
rate_limiter:
|
||||||
# define 2 rate limiters (one for username+IP, the other for IP)
|
|
||||||
username_ip_login:
|
username_ip_login:
|
||||||
policy: token_bucket
|
policy: token_bucket
|
||||||
limit: 5
|
limit: 5
|
||||||
@@ -11,8 +10,17 @@ framework:
|
|||||||
limit: 50
|
limit: 50
|
||||||
interval: '15 minutes'
|
interval: '15 minutes'
|
||||||
|
|
||||||
|
user_register:
|
||||||
|
policy: token_bucket
|
||||||
|
limit: 1
|
||||||
|
rate: { interval: '5 minutes' }
|
||||||
|
|
||||||
|
rdap_requests:
|
||||||
|
policy: sliding_window
|
||||||
|
limit: 10
|
||||||
|
interval: '1 hour'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# our custom login rate limiter
|
|
||||||
app.login_rate_limiter:
|
app.login_rate_limiter:
|
||||||
class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
|
class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
|
||||||
arguments:
|
arguments:
|
||||||
@@ -69,6 +77,7 @@ security:
|
|||||||
access_control:
|
access_control:
|
||||||
- { path: ^/api$, roles: PUBLIC_ACCESS }
|
- { path: ^/api$, roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
- { path: ^/api/docs, roles: PUBLIC_ACCESS }
|
||||||
|
- { path: ^/api/register$, roles: PUBLIC_ACCESS }
|
||||||
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/calendar$", roles: PUBLIC_ACCESS }
|
- { path: "^/api/watchlists/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/calendar$", roles: PUBLIC_ACCESS }
|
||||||
- { path: "^/api/config$", roles: PUBLIC_ACCESS }
|
- { path: "^/api/config$", roles: PUBLIC_ACCESS }
|
||||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||||
|
|||||||
@@ -5,8 +5,10 @@
|
|||||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||||
parameters:
|
parameters:
|
||||||
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
||||||
|
mailer_sender_name: '%env(string:MAILER_SENDER_NAME)'
|
||||||
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
||||||
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
||||||
|
registration_enabled: '%env(bool:REGISTRATION_ENABLED)%'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
# default configuration for services in *this* file
|
# default configuration for services in *this* file
|
||||||
@@ -15,6 +17,7 @@ services:
|
|||||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||||
bind:
|
bind:
|
||||||
$mailerSenderEmail: '%mailer_sender_email%'
|
$mailerSenderEmail: '%mailer_sender_email%'
|
||||||
|
$mailerSenderName: '%mailer_sender_name%'
|
||||||
|
|
||||||
# makes classes in src/ available to be used as services
|
# makes classes in src/ available to be used as services
|
||||||
# this creates a service per class whose id is the fully-qualified class name
|
# this creates a service per class whose id is the fully-qualified class name
|
||||||
|
|||||||
32
migrations/Version20240804214457.php
Normal file
32
migrations/Version20240804214457.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace DoctrineMigrations;
|
||||||
|
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
use Doctrine\Migrations\AbstractMigration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Auto-generated Migration: Please modify to your needs!
|
||||||
|
*/
|
||||||
|
final class Version20240804214457 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function getDescription(): string
|
||||||
|
{
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this up() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" ADD is_verified BOOLEAN NOT NULL DEFAULT true;');
|
||||||
|
$this->addSql('ALTER TABLE "user" ALTER is_verified DROP DEFAULT');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
// this down() migration is auto-generated, please modify it to your needs
|
||||||
|
$this->addSql('ALTER TABLE "user" DROP is_verified');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,7 @@ class DomainRefreshController extends AbstractController
|
|||||||
{
|
{
|
||||||
public function __construct(private readonly DomainRepository $domainRepository,
|
public function __construct(private readonly DomainRepository $domainRepository,
|
||||||
private readonly RDAPService $RDAPService,
|
private readonly RDAPService $RDAPService,
|
||||||
private readonly RateLimiterFactory $authenticatedApiLimiter,
|
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||||
private readonly MessageBusInterface $bus,
|
private readonly MessageBusInterface $bus,
|
||||||
private readonly LoggerInterface $logger
|
private readonly LoggerInterface $logger
|
||||||
) {
|
) {
|
||||||
@@ -63,7 +63,8 @@ class DomainRefreshController extends AbstractController
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (false === $kernel->isDebug() && true === $this->getParameter('limited_features')) {
|
if (false === $kernel->isDebug() && true === $this->getParameter('limited_features')) {
|
||||||
$limiter = $this->authenticatedApiLimiter->create($userId);
|
$limiter = $this->rdapRequestsLimiter->create($userId);
|
||||||
|
|
||||||
if (false === $limiter->consume()->isAccepted()) {
|
if (false === $limiter->consume()->isAccepted()) {
|
||||||
$this->logger->warning('User {username} was rate limited by the API.', [
|
$this->logger->warning('User {username} was rate limited by the API.', [
|
||||||
'username' => $this->getUser()->getUserIdentifier(),
|
'username' => $this->getUser()->getUserIdentifier(),
|
||||||
|
|||||||
119
src/Controller/RegistrationController.php
Normal file
119
src/Controller/RegistrationController.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\UserRepository;
|
||||||
|
use App\Security\EmailVerifier;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
|
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||||
|
use Symfony\Component\RateLimiter\RateLimiterFactory;
|
||||||
|
use Symfony\Component\Routing\Attribute\Route;
|
||||||
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
|
|
||||||
|
class RegistrationController extends AbstractController
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EmailVerifier $emailVerifier,
|
||||||
|
private readonly string $mailerSenderEmail,
|
||||||
|
private readonly string $mailerSenderName,
|
||||||
|
private readonly RateLimiterFactory $userRegisterLimiter,
|
||||||
|
private readonly EntityManagerInterface $em,
|
||||||
|
private readonly SerializerInterface $serializer,
|
||||||
|
private readonly LoggerInterface $logger,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
#[Route(
|
||||||
|
path: '/api/register',
|
||||||
|
name: 'user_register',
|
||||||
|
defaults: [
|
||||||
|
'_api_resource_class' => User::class,
|
||||||
|
'_api_operation_name' => 'register',
|
||||||
|
],
|
||||||
|
methods: ['POST']
|
||||||
|
)]
|
||||||
|
public function register(Request $request, UserPasswordHasherInterface $userPasswordHasher): Response
|
||||||
|
{
|
||||||
|
if (false === $this->getParameter('registration_enabled')) {
|
||||||
|
throw new UnauthorizedHttpException('', 'Registration is disabled on this instance');
|
||||||
|
}
|
||||||
|
|
||||||
|
$limiter = $this->userRegisterLimiter->create($request->getClientIp());
|
||||||
|
|
||||||
|
if (false === $limiter->consume()->isAccepted()) {
|
||||||
|
$this->logger->warning('IP address {ip} was rate limited by the Registration API.', [
|
||||||
|
'ip' => $request->getClientIp(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new TooManyRequestsHttpException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $this->serializer->deserialize($request->getContent(), User::class, 'json', ['groups' => 'user:register']);
|
||||||
|
if (null === $user->getEmail() || null === $user->getPassword()) {
|
||||||
|
throw new BadRequestHttpException('Bad request');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user->setPassword(
|
||||||
|
$userPasswordHasher->hashPassword(
|
||||||
|
$user,
|
||||||
|
$user->getPassword()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->em->persist($user);
|
||||||
|
$this->em->flush();
|
||||||
|
|
||||||
|
$this->logger->info('A new user has registered ({username}).', [
|
||||||
|
'username' => $user->getUserIdentifier(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||||
|
(new TemplatedEmail())
|
||||||
|
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||||
|
->to($user->getEmail())
|
||||||
|
->locale('en')
|
||||||
|
->subject('Please Confirm your Email')
|
||||||
|
->htmlTemplate('emails/success/confirmation_email.html.twig')
|
||||||
|
);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
#[Route('/verify/email', name: 'app_verify_email')]
|
||||||
|
public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
|
||||||
|
{
|
||||||
|
$id = $request->query->get('id');
|
||||||
|
|
||||||
|
if (null === $id) {
|
||||||
|
return $this->redirectToRoute('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $userRepository->find($id);
|
||||||
|
|
||||||
|
if (null === $user) {
|
||||||
|
return $this->redirectToRoute('index');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->emailVerifier->handleEmailConfirmation($request, $user);
|
||||||
|
|
||||||
|
$this->logger->info('User {username} has validated his email address.', [
|
||||||
|
'username' => $user->getUserIdentifier(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->redirectToRoute('index');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,7 +4,9 @@ namespace App\Entity;
|
|||||||
|
|
||||||
use ApiPlatform\Metadata\ApiResource;
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
use ApiPlatform\Metadata\Get;
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\Metadata\Post;
|
||||||
use App\Controller\MeController;
|
use App\Controller\MeController;
|
||||||
|
use App\Controller\RegistrationController;
|
||||||
use App\Repository\UserRepository;
|
use App\Repository\UserRepository;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
@@ -26,6 +28,14 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
|||||||
normalizationContext: ['groups' => 'user:list'],
|
normalizationContext: ['groups' => 'user:list'],
|
||||||
read: false
|
read: false
|
||||||
),
|
),
|
||||||
|
new Post(
|
||||||
|
uriTemplate: '/register',
|
||||||
|
routeName: 'user_register',
|
||||||
|
controller: RegistrationController::class,
|
||||||
|
denormalizationContext: ['groups' => ['user:register']],
|
||||||
|
read: false,
|
||||||
|
name: 'register'
|
||||||
|
),
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||||
@@ -36,7 +46,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
private ?int $id = null;
|
private ?int $id = null;
|
||||||
|
|
||||||
#[ORM\Column(length: 180)]
|
#[ORM\Column(length: 180)]
|
||||||
#[Groups(['user:list'])]
|
#[Groups(['user:list', 'user:register'])]
|
||||||
private ?string $email = null;
|
private ?string $email = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -50,6 +60,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
* @var string|null The hashed password
|
* @var string|null The hashed password
|
||||||
*/
|
*/
|
||||||
#[ORM\Column(nullable: true)]
|
#[ORM\Column(nullable: true)]
|
||||||
|
#[Groups(['user:register'])]
|
||||||
private ?string $password = null;
|
private ?string $password = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,6 +75,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
#[ORM\OneToMany(targetEntity: Connector::class, mappedBy: 'user', orphanRemoval: true)]
|
#[ORM\OneToMany(targetEntity: Connector::class, mappedBy: 'user', orphanRemoval: true)]
|
||||||
private Collection $connectors;
|
private Collection $connectors;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private bool $isVerified = false;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->watchLists = new ArrayCollection();
|
$this->watchLists = new ArrayCollection();
|
||||||
@@ -201,4 +215,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function isVerified(): bool
|
||||||
|
{
|
||||||
|
return $this->isVerified;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setVerified(bool $isVerified): static
|
||||||
|
{
|
||||||
|
$this->isVerified = $isVerified;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ use Symfony\Component\HttpKernel\KernelInterface;
|
|||||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
use Symfony\Component\Mailer\MailerInterface;
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
use Symfony\Component\Mime\Email;
|
use Symfony\Component\Mime\Email;
|
||||||
|
|
||||||
#[AsMessageHandler]
|
#[AsMessageHandler]
|
||||||
@@ -27,6 +28,7 @@ final readonly class ProcessDomainTriggerHandler
|
|||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private string $mailerSenderEmail,
|
private string $mailerSenderEmail,
|
||||||
|
private string $mailerSenderName,
|
||||||
private MailerInterface $mailer,
|
private MailerInterface $mailer,
|
||||||
private WatchListRepository $watchListRepository,
|
private WatchListRepository $watchListRepository,
|
||||||
private DomainRepository $domainRepository,
|
private DomainRepository $domainRepository,
|
||||||
@@ -97,7 +99,7 @@ final readonly class ProcessDomainTriggerHandler
|
|||||||
private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void
|
private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void
|
||||||
{
|
{
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->from($this->mailerSenderEmail)
|
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->priority(Email::PRIORITY_HIGHEST)
|
->priority(Email::PRIORITY_HIGHEST)
|
||||||
->subject('A domain name has been ordered')
|
->subject('A domain name has been ordered')
|
||||||
@@ -117,7 +119,7 @@ final readonly class ProcessDomainTriggerHandler
|
|||||||
private function sendEmailDomainOrderError(Domain $domain, User $user): void
|
private function sendEmailDomainOrderError(Domain $domain, User $user): void
|
||||||
{
|
{
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->from($this->mailerSenderEmail)
|
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->subject('An error occurred while ordering a domain name')
|
->subject('An error occurred while ordering a domain name')
|
||||||
->htmlTemplate('emails/errors/domain_order.html.twig')
|
->htmlTemplate('emails/errors/domain_order.html.twig')
|
||||||
@@ -135,7 +137,7 @@ final readonly class ProcessDomainTriggerHandler
|
|||||||
private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void
|
private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void
|
||||||
{
|
{
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->from($this->mailerSenderEmail)
|
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->priority(Email::PRIORITY_HIGHEST)
|
->priority(Email::PRIORITY_HIGHEST)
|
||||||
->subject('A domain name has been changed')
|
->subject('A domain name has been changed')
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ use Symfony\Component\Mailer\MailerInterface;
|
|||||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||||
use Symfony\Component\Messenger\MessageBusInterface;
|
use Symfony\Component\Messenger\MessageBusInterface;
|
||||||
|
use Symfony\Component\Mime\Address;
|
||||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||||
|
|
||||||
#[AsMessageHandler]
|
#[AsMessageHandler]
|
||||||
@@ -25,6 +26,7 @@ final readonly class ProcessWatchListTriggerHandler
|
|||||||
private RDAPService $RDAPService,
|
private RDAPService $RDAPService,
|
||||||
private MailerInterface $mailer,
|
private MailerInterface $mailer,
|
||||||
private string $mailerSenderEmail,
|
private string $mailerSenderEmail,
|
||||||
|
private string $mailerSenderName,
|
||||||
private MessageBusInterface $bus,
|
private MessageBusInterface $bus,
|
||||||
private WatchListRepository $watchListRepository,
|
private WatchListRepository $watchListRepository,
|
||||||
private LoggerInterface $logger
|
private LoggerInterface $logger
|
||||||
@@ -90,7 +92,7 @@ final readonly class ProcessWatchListTriggerHandler
|
|||||||
private function sendEmailDomainUpdateError(Domain $domain, User $user): void
|
private function sendEmailDomainUpdateError(Domain $domain, User $user): void
|
||||||
{
|
{
|
||||||
$email = (new TemplatedEmail())
|
$email = (new TemplatedEmail())
|
||||||
->from($this->mailerSenderEmail)
|
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||||
->to($user->getEmail())
|
->to($user->getEmail())
|
||||||
->subject('An error occurred while updating a domain name')
|
->subject('An error occurred while updating a domain name')
|
||||||
->htmlTemplate('emails/errors/domain_update.html.twig')
|
->htmlTemplate('emails/errors/domain_update.html.twig')
|
||||||
|
|||||||
53
src/Security/EmailVerifier.php
Normal file
53
src/Security/EmailVerifier.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Security;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||||
|
use Symfony\Component\Mailer\MailerInterface;
|
||||||
|
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
|
||||||
|
|
||||||
|
readonly class EmailVerifier
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private VerifyEmailHelperInterface $verifyEmailHelper,
|
||||||
|
private MailerInterface $mailer,
|
||||||
|
private EntityManagerInterface $entityManager
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @throws TransportExceptionInterface
|
||||||
|
*/
|
||||||
|
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void
|
||||||
|
{
|
||||||
|
$signatureComponents = $this->verifyEmailHelper->generateSignature(
|
||||||
|
$verifyEmailRouteName,
|
||||||
|
(string) $user->getId(),
|
||||||
|
$user->getEmail(),
|
||||||
|
['id' => $user->getId()]
|
||||||
|
);
|
||||||
|
|
||||||
|
$context = $email->getContext();
|
||||||
|
$context['signedUrl'] = $signatureComponents->getSignedUrl();
|
||||||
|
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
|
||||||
|
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
|
||||||
|
|
||||||
|
$email->context($context);
|
||||||
|
|
||||||
|
$this->mailer->send($email);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handleEmailConfirmation(Request $request, User $user): void
|
||||||
|
{
|
||||||
|
$this->verifyEmailHelper->validateEmailConfirmationFromRequest($request, (string) $user->getId(), $user->getEmail());
|
||||||
|
|
||||||
|
$user->setVerified(true);
|
||||||
|
|
||||||
|
$this->entityManager->persist($user);
|
||||||
|
$this->entityManager->flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEn
|
|||||||
}
|
}
|
||||||
|
|
||||||
$user = new User();
|
$user = new User();
|
||||||
$user->setEmail($userFromToken->getEmail());
|
$user->setEmail($userFromToken->getEmail())->setVerified(true);
|
||||||
$this->em->persist($user);
|
$this->em->persist($user);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
|
|||||||
@@ -56,18 +56,21 @@
|
|||||||
<h1>Domain Watchdog Error</h1>
|
<h1>Domain Watchdog Error</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Hello,</p>
|
<p>Hello, <br/>
|
||||||
<p>We would like to inform you that an error occurred while ordering the following domain name:</p>
|
We would like to inform you that an error occurred while ordering the following domain name:<br/>
|
||||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||||
|
</p>
|
||||||
<p>Here are some possible explanations:</p>
|
<p>Here are some possible explanations:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>The Connector configuration you provided is no longer valid.</li>
|
<li>The Connector configuration you provided is no longer valid.</li>
|
||||||
<li>It is likely that the domain is no longer available for registration.</li>
|
<li>It is likely that the domain is no longer available for registration.</li>
|
||||||
<li>A temporary interruption is affecting the registration of this domain name.</li>
|
<li>A temporary interruption is affecting the registration of this domain name.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Thank you for your understanding,</p>
|
<br/><br/>
|
||||||
<p>Sincerely,</p>
|
<p>Thank you for your understanding,<br/>
|
||||||
<p>Domain Watchdog</p>
|
Sincerely,<br/>
|
||||||
|
Domain Watchdog<br/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© Domain Watchdog</p>
|
<p>© Domain Watchdog</p>
|
||||||
|
|||||||
@@ -56,18 +56,22 @@
|
|||||||
<h1>Domain Watchdog Error</h1>
|
<h1>Domain Watchdog Error</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Hello,</p>
|
<p>Hello, <br/>
|
||||||
<p>We would like to inform you that an error occurred while updating the information for the following domain
|
We would like to inform you that an error occurred while updating the information for the following domain
|
||||||
name:</p>
|
name:<br/>
|
||||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||||
|
</p>
|
||||||
<p>Here are some possible explanations:</p>
|
<p>Here are some possible explanations:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li>It is likely that the domain will be available for registration again.</li>
|
<li>It is likely that the domain will be available for registration again.</li>
|
||||||
<li>A temporary outage affects the provision of domain name information.</li>
|
<li>A temporary outage affects the provision of domain name information.</li>
|
||||||
</ul>
|
</ul>
|
||||||
<p>Thank you for your understanding,</p>
|
<br/><br/>
|
||||||
<p>Sincerely,</p>
|
<p>Thank you for your understanding,<br/>
|
||||||
<p>Domain Watchdog</p>
|
Sincerely,<br/>
|
||||||
|
Domain Watchdog<br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© Domain Watchdog</p>
|
<p>© Domain Watchdog</p>
|
||||||
|
|||||||
66
templates/emails/success/confirmation_email.html.twig
Normal file
66
templates/emails/success/confirmation_email.html.twig
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header h1 {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
text-align: center;
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<title>Domain Watchdog - Confirm Email</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="header">
|
||||||
|
<h1>Domain Watchdog - Confirm Email</h1>
|
||||||
|
</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>Hello, <br/>
|
||||||
|
Please confirm your email address by clicking the following link: <br><br>
|
||||||
|
<a href="{{ signedUrl|raw }}">Confirm my Email</a>.
|
||||||
|
This link will expire in {{ expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle') }}.
|
||||||
|
<br/><br/>
|
||||||
|
Thank you for your understanding,<br/>
|
||||||
|
Sincerely,<br/>
|
||||||
|
Domain Watchdog<br/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">
|
||||||
|
<p>© Domain Watchdog</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -48,15 +48,16 @@
|
|||||||
<h1>Domain Watchdog - Domain Ordered</h1>
|
<h1>Domain Watchdog - Domain Ordered</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Hello,</p>
|
<p>Hello, <br/>
|
||||||
<p>We are pleased to inform you that a domain name present in your Watchlist has been ordered using the
|
We are pleased to inform you that a domain name present in your Watchlist has been ordered using the
|
||||||
connector you have chosen.</p>
|
connector you have chosen.<br/>
|
||||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||||
<p><strong>Connector provider :</strong> {{ provider }}</p>
|
<strong>Connector provider :</strong> {{ provider }}<br/>
|
||||||
<br/>
|
<br/><br/>
|
||||||
<p>Thank you for your understanding,</p>
|
Thank you for your understanding,<br/>
|
||||||
<p>Sincerely,</p>
|
Sincerely,<br/>
|
||||||
<p>Domain Watchdog</p>
|
Domain Watchdog<br/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© Domain Watchdog</p>
|
<p>© Domain Watchdog</p>
|
||||||
|
|||||||
@@ -48,15 +48,16 @@
|
|||||||
<h1>Domain Watchdog Alert</h1>
|
<h1>Domain Watchdog Alert</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p>Hello,</p>
|
<p>Hello, <br/>
|
||||||
<p>We are pleased to inform you that a new action has been detected on a domain name in your watchlist.</p>
|
We are pleased to inform you that a new action has been detected on a domain name in your watchlist.<br/>
|
||||||
<p><strong>Domain name:</strong> {{ event.domain.ldhName }}</p>
|
<strong>Domain name:</strong> {{ event.domain.ldhName }}<br/>
|
||||||
<p><strong>Action:</strong> {{ event.action }}</p>
|
<strong>Action:</strong> {{ event.action }}<br/>
|
||||||
<p><strong>Effective Date:</strong> {{ event.date | date("c") }}</p>
|
<strong>Effective Date:</strong> {{ event.date | date("c") }}<br/>
|
||||||
<br/>
|
<br/><br/>
|
||||||
<p>Thank you for your understanding,</p>
|
Thank you for your understanding,<br/>
|
||||||
<p>Sincerely,</p>
|
Sincerely,<br/>
|
||||||
<p>Domain Watchdog</p>
|
Domain Watchdog<br/>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<p>© Domain Watchdog</p>
|
<p>© Domain Watchdog</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user