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 ###
|
||||
|
||||
|
||||
MAILER_SENDER_NAME="Domain Watchdog"
|
||||
MAILER_SENDER_EMAIL=notifications@example.com
|
||||
REGISTRATION_ENABLED=true
|
||||
LIMITED_FEATURES=false
|
||||
OAUTH_CLIENT_ID=
|
||||
OAUTH_CLIENT_SECRET=
|
||||
|
||||
@@ -103,7 +103,9 @@ export default function App() {
|
||||
<Typography.Link href='https://github.com/maelgangloff/domain-watchdog/wiki'><Button
|
||||
type='text'>{t`Documentation`}</Button></Typography.Link>
|
||||
</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>
|
||||
</Layout>
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
"symfony/web-link": "7.1.*",
|
||||
"symfony/webpack-encore-bundle": "^2.1",
|
||||
"symfony/yaml": "7.1.*",
|
||||
"symfonycasts/verify-email-bundle": "^1.17",
|
||||
"symfonycasts/verify-email-bundle": "*",
|
||||
"twig/extra-bundle": "^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",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "6b52bd3ad92da490e7a206500d0c0348",
|
||||
"content-hash": "69d672f9e5a01b48f871fa5c81714f8d",
|
||||
"packages": [
|
||||
{
|
||||
"name": "api-platform/core",
|
||||
|
||||
@@ -18,4 +18,5 @@ return [
|
||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||
KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true],
|
||||
Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
|
||||
SymfonyCasts\Bundle\VerifyEmail\SymfonyCastsVerifyEmailBundle::class => ['all' => true],
|
||||
];
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
framework:
|
||||
rate_limiter:
|
||||
# define 2 rate limiters (one for username+IP, the other for IP)
|
||||
username_ip_login:
|
||||
policy: token_bucket
|
||||
limit: 5
|
||||
@@ -11,8 +10,17 @@ framework:
|
||||
limit: 50
|
||||
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:
|
||||
# our custom login rate limiter
|
||||
app.login_rate_limiter:
|
||||
class: Symfony\Component\Security\Http\RateLimiter\DefaultLoginRateLimiter
|
||||
arguments:
|
||||
@@ -69,6 +77,7 @@ security:
|
||||
access_control:
|
||||
- { path: ^/api$, 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/config$", roles: PUBLIC_ACCESS }
|
||||
- { path: ^/api, roles: IS_AUTHENTICATED_FULLY }
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
# https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration
|
||||
parameters:
|
||||
mailer_sender_email: '%env(string:MAILER_SENDER_EMAIL)%'
|
||||
mailer_sender_name: '%env(string:MAILER_SENDER_NAME)'
|
||||
oauth_enabled: '%env(OAUTH_CLIENT_ID)%'
|
||||
limited_features: '%env(bool:LIMITED_FEATURES)%'
|
||||
registration_enabled: '%env(bool:REGISTRATION_ENABLED)%'
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
@@ -15,6 +17,7 @@ services:
|
||||
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
|
||||
bind:
|
||||
$mailerSenderEmail: '%mailer_sender_email%'
|
||||
$mailerSenderName: '%mailer_sender_name%'
|
||||
|
||||
# makes classes in src/ available to be used as services
|
||||
# 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,
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly RateLimiterFactory $authenticatedApiLimiter,
|
||||
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly LoggerInterface $logger
|
||||
) {
|
||||
@@ -63,7 +63,8 @@ class DomainRefreshController extends AbstractController
|
||||
}
|
||||
|
||||
if (false === $kernel->isDebug() && true === $this->getParameter('limited_features')) {
|
||||
$limiter = $this->authenticatedApiLimiter->create($userId);
|
||||
$limiter = $this->rdapRequestsLimiter->create($userId);
|
||||
|
||||
if (false === $limiter->consume()->isAccepted()) {
|
||||
$this->logger->warning('User {username} was rate limited by the API.', [
|
||||
'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\Get;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Controller\MeController;
|
||||
use App\Controller\RegistrationController;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
@@ -26,6 +28,14 @@ use Symfony\Component\Serializer\Attribute\Groups;
|
||||
normalizationContext: ['groups' => 'user:list'],
|
||||
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
|
||||
@@ -36,7 +46,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
private ?int $id = null;
|
||||
|
||||
#[ORM\Column(length: 180)]
|
||||
#[Groups(['user:list'])]
|
||||
#[Groups(['user:list', 'user:register'])]
|
||||
private ?string $email = null;
|
||||
|
||||
/**
|
||||
@@ -50,6 +60,7 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
* @var string|null The hashed password
|
||||
*/
|
||||
#[ORM\Column(nullable: true)]
|
||||
#[Groups(['user:register'])]
|
||||
private ?string $password = null;
|
||||
|
||||
/**
|
||||
@@ -64,6 +75,9 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\OneToMany(targetEntity: Connector::class, mappedBy: 'user', orphanRemoval: true)]
|
||||
private Collection $connectors;
|
||||
|
||||
#[ORM\Column]
|
||||
private bool $isVerified = false;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->watchLists = new ArrayCollection();
|
||||
@@ -201,4 +215,16 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
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\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
|
||||
#[AsMessageHandler]
|
||||
@@ -27,6 +28,7 @@ final readonly class ProcessDomainTriggerHandler
|
||||
{
|
||||
public function __construct(
|
||||
private string $mailerSenderEmail,
|
||||
private string $mailerSenderName,
|
||||
private MailerInterface $mailer,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
@@ -97,7 +99,7 @@ final readonly class ProcessDomainTriggerHandler
|
||||
private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->mailerSenderEmail)
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->subject('A domain name has been ordered')
|
||||
@@ -117,7 +119,7 @@ final readonly class ProcessDomainTriggerHandler
|
||||
private function sendEmailDomainOrderError(Domain $domain, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->mailerSenderEmail)
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->subject('An error occurred while ordering a domain name')
|
||||
->htmlTemplate('emails/errors/domain_order.html.twig')
|
||||
@@ -135,7 +137,7 @@ final readonly class ProcessDomainTriggerHandler
|
||||
private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->mailerSenderEmail)
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->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\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
@@ -25,6 +26,7 @@ final readonly class ProcessWatchListTriggerHandler
|
||||
private RDAPService $RDAPService,
|
||||
private MailerInterface $mailer,
|
||||
private string $mailerSenderEmail,
|
||||
private string $mailerSenderName,
|
||||
private MessageBusInterface $bus,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private LoggerInterface $logger
|
||||
@@ -90,7 +92,7 @@ final readonly class ProcessWatchListTriggerHandler
|
||||
private function sendEmailDomainUpdateError(Domain $domain, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from($this->mailerSenderEmail)
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->subject('An error occurred while updating a domain name')
|
||||
->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->setEmail($userFromToken->getEmail());
|
||||
$user->setEmail($userFromToken->getEmail())->setVerified(true);
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
|
||||
@@ -56,18 +56,21 @@
|
||||
<h1>Domain Watchdog Error</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello,</p>
|
||||
<p>We would like to inform you that an error occurred while ordering the following domain name:</p>
|
||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
||||
<p>Hello, <br/>
|
||||
We would like to inform you that an error occurred while ordering the following domain name:<br/>
|
||||
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||
</p>
|
||||
<p>Here are some possible explanations:</p>
|
||||
<ul>
|
||||
<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>A temporary interruption is affecting the registration of this domain name.</li>
|
||||
</ul>
|
||||
<p>Thank you for your understanding,</p>
|
||||
<p>Sincerely,</p>
|
||||
<p>Domain Watchdog</p>
|
||||
<br/><br/>
|
||||
<p>Thank you for your understanding,<br/>
|
||||
Sincerely,<br/>
|
||||
Domain Watchdog<br/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© Domain Watchdog</p>
|
||||
|
||||
@@ -56,18 +56,22 @@
|
||||
<h1>Domain Watchdog Error</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello,</p>
|
||||
<p>We would like to inform you that an error occurred while updating the information for the following domain
|
||||
name:</p>
|
||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
||||
<p>Hello, <br/>
|
||||
We would like to inform you that an error occurred while updating the information for the following domain
|
||||
name:<br/>
|
||||
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||
</p>
|
||||
<p>Here are some possible explanations:</p>
|
||||
<ul>
|
||||
<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>
|
||||
</ul>
|
||||
<p>Thank you for your understanding,</p>
|
||||
<p>Sincerely,</p>
|
||||
<p>Domain Watchdog</p>
|
||||
<br/><br/>
|
||||
<p>Thank you for your understanding,<br/>
|
||||
Sincerely,<br/>
|
||||
Domain Watchdog<br/>
|
||||
</p>
|
||||
|
||||
</div>
|
||||
<div class="footer">
|
||||
<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>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello,</p>
|
||||
<p>We are pleased to inform you that a domain name present in your Watchlist has been ordered using the
|
||||
connector you have chosen.</p>
|
||||
<p><strong>Domain name:</strong> {{ domain.ldhName }}</p>
|
||||
<p><strong>Connector provider :</strong> {{ provider }}</p>
|
||||
<br/>
|
||||
<p>Thank you for your understanding,</p>
|
||||
<p>Sincerely,</p>
|
||||
<p>Domain Watchdog</p>
|
||||
<p>Hello, <br/>
|
||||
We are pleased to inform you that a domain name present in your Watchlist has been ordered using the
|
||||
connector you have chosen.<br/>
|
||||
<strong>Domain name:</strong> {{ domain.ldhName }}<br/>
|
||||
<strong>Connector provider :</strong> {{ provider }}<br/>
|
||||
<br/><br/>
|
||||
Thank you for your understanding,<br/>
|
||||
Sincerely,<br/>
|
||||
Domain Watchdog<br/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© Domain Watchdog</p>
|
||||
|
||||
@@ -48,15 +48,16 @@
|
||||
<h1>Domain Watchdog Alert</h1>
|
||||
</div>
|
||||
<div class="content">
|
||||
<p>Hello,</p>
|
||||
<p>We are pleased to inform you that a new action has been detected on a domain name in your watchlist.</p>
|
||||
<p><strong>Domain name:</strong> {{ event.domain.ldhName }}</p>
|
||||
<p><strong>Action:</strong> {{ event.action }}</p>
|
||||
<p><strong>Effective Date:</strong> {{ event.date | date("c") }}</p>
|
||||
<br/>
|
||||
<p>Thank you for your understanding,</p>
|
||||
<p>Sincerely,</p>
|
||||
<p>Domain Watchdog</p>
|
||||
<p>Hello, <br/>
|
||||
We are pleased to inform you that a new action has been detected on a domain name in your watchlist.<br/>
|
||||
<strong>Domain name:</strong> {{ event.domain.ldhName }}<br/>
|
||||
<strong>Action:</strong> {{ event.action }}<br/>
|
||||
<strong>Effective Date:</strong> {{ event.date | date("c") }}<br/>
|
||||
<br/><br/>
|
||||
Thank you for your understanding,<br/>
|
||||
Sincerely,<br/>
|
||||
Domain Watchdog<br/>
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<p>© Domain Watchdog</p>
|
||||
|
||||
Reference in New Issue
Block a user