This commit is contained in:
Flo 2024-08-26 14:05:55 +00:00
parent 624e4e4b97
commit 9237801bc1
26 changed files with 430 additions and 45 deletions

View File

@ -18,3 +18,11 @@ BEE_API_KEY=
INIT_USER_NAME=admin
INIT_USER_PASSWORD=password
INIT_USER_MAIL=admin@test.com
# Mail
MAIL_DEFAULT_SENDER=info@bee.local
SMTP_USERNAME=smtp@stack-up.de
SMTP_PASSWORD=
SMTP_HOST= #srv-mail-01.lab.jonasf.de
SMTP_PORT=465
SMTP_ENCRYPTION=ssl

View File

@ -19,7 +19,9 @@
"monolog/monolog": "^3.4",
"laminas/laminas-mail": "^2.23",
"teewurst/pipeline": "^3.0",
"guzzlehttp/guzzle": "^7.8"
"guzzlehttp/guzzle": "^7.8",
"nette/mail": "^4.0",
"latte/latte": "^3.0"
},
"autoload": {
"psr-4": {

View File

@ -0,0 +1,16 @@
<?php
return [
'mail' => [
'default' => [
'sender' => $_ENV['MAIL_DEFAULT_SENDER']
],
'smtp-server' => [
'host' => $_ENV['SMTP_HOST'],
'port' => (int)$_ENV['SMTP_PORT'] ?? 25,
'encryption' => $_ENV['SMTP_ENCRYPTION'],
'username' => $_ENV['SMTP_USERNAME'],
'password' => $_ENV['SMTP_PASSWORD'],
]
]
];

View File

@ -52,6 +52,7 @@ $aggregator = new ConfigAggregator([
\Bee\Infrastructure\Rbac\ConfigProvider::class,
\Bee\Infrastructure\Request\ConfigProvider::class,
\Bee\Infrastructure\Session\ConfigProvider::class,
\Bee\Infrastructure\Mail\ConfigProvider::class,
// HandlingDomain
\Bee\Handling\User\ConfigProvider::class,

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,63 @@
<html>
<head>
<meta charset="utf-8">
<title>Willkommen beim Beekeeper!</title>
<style>
body {
}
.message-block {
font-family: Arial, sans-serif;
max-width: 600px;
margin: 0 auto;
margin-top: 20px;
padding: 20px;
}
.header {
background-color: rgb(255, 199, 44);
width: 100%;
height: 15%;
text-align: center;
}
.title {
display: flex;
flex-wrap: wrap;
}
.wrapper {
display: inline-block;
}
.quote {
font-style: italic;
font-size: x-small;
color: slategray;
}
img {
margin: 1rem;
}
h1 {
margin-top: auto;
margin-bottom: auto;
}
</style>
</head>
<body>
<div id="header" class="header">
<div class="wrapper">
<div id="title" class="title">
<img src="assets/icon.png" />
<h1>Beekeeper</h1>
</div>
</div>
</div>
<div id="message-block" class="message-block">
<p>Hallo {$username},</p>
<br />
<p>Herzlich willkommen beim Beekeeper!</p>
<br />
<p>Bitte klicke auf <a href="{$confirmationLink}">diesen Link</a> um dein Passwort festzulegen.</p>
<p>Danach ist deine Registierung abgeschlossen und du kannst loslegen!</p>
<br />
<p>Mit fleißigen Grüßen,</p>
<p>Der Beekeeper</p>
</div>
</body>
</html>

View File

@ -1,5 +1,7 @@
<?php
use Bee\API\External\Authentication\Formatter\ConfirmRegistrationFormatter;
use Bee\API\External\Authentication\Formatter\LoginUserFormatter;
use Bee\API\External\Authentication\Handler\ConfirmRegistrationHandler;
use Bee\API\External\Authentication\Handler\LoginUserHandler;
use Bee\API\External\Authentication\Handler\LogoutUserHandler;
@ -8,6 +10,10 @@ use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [
'factories' => [
// Formatter
ConfirmRegistrationFormatter::class => AutoWiringFactory::class,
LoginUserFormatter::class => AutoWiringFactory::class,
// Handler
LoginUserHandler::class => AutoWiringFactory::class,
LogoutUserHandler::class => AutoWiringFactory::class,

View File

@ -0,0 +1,27 @@
<?php
namespace Bee\API\External\Authentication\Formatter;
use DateTime;
use Bee\Data\Business\Entity\User;
class ConfirmRegistrationFormatter {
public function format(User $user): array {
$userArray = [
'id' => $user->getId()->toString(),
'username' => $user->getUsername(),
'roleIdentifier' => $user->getRole()->getIdentifier(),
'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
];
$userArray['permissions'] = [];
foreach ($user->getRole()->getPermissions()->toArray() as $permission) {
$userArray['permissions'][] = $permission->getIdentifier();
}
return $userArray;
}
}

View File

@ -0,0 +1,14 @@
<?php
namespace Bee\API\External\Authentication\Formatter;
use Bee\Data\Business\Entity\UserSession;
class LoginUserFormatter {
public function format(UserSession $session): array {
return [
'sessionId' => $session->getId()->toString()
];
}
}

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
use Bee\API\External\Authentication\Formatter\ConfirmRegistrationFormatter;
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\Handling\Registration\Handler\Command\ConfirmRegistration\ConfirmRegistrationCommandBuilder;
use Bee\Handling\Registration\Handler\Command\ConfirmRegistration\ConfirmRegistrationCommandHandler;
@ -20,7 +21,7 @@ class ConfirmRegistrationHandler implements RequestHandlerInterface
public function __construct(
private readonly ConfirmRegistrationCommandHandler $handler,
private readonly ConfirmRegistrationCommandBuilder $builder,
private readonly UserFormatter $userFormatter
private readonly ConfirmRegistrationFormatter $formatter
) {
}
@ -36,7 +37,7 @@ class ConfirmRegistrationHandler implements RequestHandlerInterface
$result = $this->handler->execute($query);
return new JsonResponse(
$this->userFormatter->format($result)
$this->formatter->format($result)
);
}
}

View File

@ -4,9 +4,18 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
use Bee\API\External\Authentication\Formatter\LoginUserFormatter;
use Bee\Handling\User\Exception\UserNotFoundByIdentifierException;
use Bee\Handling\User\Exception\UserWrongPasswordException;
use Bee\Handling\UserSession\Handler\Command\LoginUser\LoginUserCommandBuilder;
use Bee\Handling\UserSession\Handler\Command\LoginUser\LoginUserCommandHandler;
use Bee\Infrastructure\Exception\ErrorCode;
use Bee\Infrastructure\Exception\ErrorDomain;
use Bee\Infrastructure\Exception\Exception\Exception;
use Bee\Infrastructure\Logging\Logger\Logger;
use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Bee\Infrastructure\Response\ErrorResponse;
use Bee\Infrastructure\Response\UnauthorizedResponse;
use Bee\Infrastructure\Session\Middleware\SessionMiddleware;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@ -17,10 +26,16 @@ class LoginUserHandler implements RequestHandlerInterface
{
public function __construct(
private readonly LoginUserCommandHandler $handler,
private readonly LoginUserCommandBuilder $builder
private readonly LoginUserCommandBuilder $builder,
private readonly LoginUserFormatter $formatter,
private readonly Logger $logger,
) {
}
/**
* @throws UserWrongPasswordException
* @throws UserNotFoundByIdentifierException
*/
public function handle(ServerRequestInterface $request): ResponseInterface
{
$session = $request->getAttribute(SessionMiddleware::SESSION_ATTRIBUTE);
@ -32,9 +47,12 @@ class LoginUserHandler implements RequestHandlerInterface
$data['password'],
);
try {
$result = $this->handler->execute($query);
return new JsonResponse([
'sessionId' => $result->getId()->toString()
]);
return new JsonResponse($this->formatter->format($result));
} catch (Exception $e) {
$this->logger->exception($e);
return new ErrorResponse(ErrorDomain::Login, ErrorCode::SomethingWentWrong);
}
}
}

View File

@ -30,6 +30,6 @@ class LogoutUserHandler implements RequestHandlerInterface
);
$this->handler->execute($query);
return new JsonResponse('OK');
return new SuccessResponse();
}
}

View File

@ -4,7 +4,13 @@ declare(strict_types=1);
namespace Bee\API\External\Authentication\Handler;
use Bee\Handling\Registration\Exception\RegistrationWithIdentifierAlreadyExistsException;
use Bee\Handling\User\Exception\UserWithIdentifierAlreadyExistsException;
use Bee\Infrastructure\Exception\ErrorCode;
use Bee\Infrastructure\Exception\ErrorDomain;
use Bee\Infrastructure\Mail\Exception\SendMailFailedException;
use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Bee\Infrastructure\Response\ErrorResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\RequestHandlerInterface;
@ -18,7 +24,8 @@ class RegisterUserHandler implements RequestHandlerInterface
{
public function __construct(
private readonly RegisterUserCommandHandler $handler,
private readonly RegisterUserCommandBuilder $builder
private readonly RegisterUserCommandBuilder $builder,
private readonly Logger $logger,
) {
}
@ -33,7 +40,19 @@ class RegisterUserHandler implements RequestHandlerInterface
$host
);
try {
$this->handler->execute($query);
} catch (RegistrationWithIdentifierAlreadyExistsException $e) {
$this->logger->exception($e);
return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
} catch (UserWithIdentifierAlreadyExistsException $e) {
$this->logger->exception($e);
return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
} catch (SendMailFailedException $e) {
$this->logger->exception($e);
return new ErrorResponse(ErrorDomain::Registration, ErrorCode::SomethingWentWrong);
}
return new SuccessResponse();
}
}

View File

@ -1,6 +1,7 @@
<?php
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\API\External\User\Formatter\CreateUserFormatter;
use Bee\API\External\User\Formatter\UserStateFormatter;
use Bee\API\External\User\Handler\ChangePasswordHandler;
use Bee\API\External\User\Handler\ChangeUsernameHandler;
use Bee\API\External\User\Handler\CreateUserHandler;
@ -10,7 +11,8 @@ use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
return [
'factories' => [
// Formatter
UserFormatter::class => AutoWiringFactory::class,
UserStateFormatter::class => AutoWiringFactory::class,
CreateUserFormatter::class => AutoWiringFactory::class,
// Handler
CreateUserHandler::class => AutoWiringFactory::class,

View File

@ -5,15 +5,15 @@ namespace Bee\API\External\User\Formatter;
use DateTime;
use Bee\Data\Business\Entity\User;
class UserFormatter {
class CreateUserFormatter {
public function format(User $user): array {
$userArray = [
'id' => $user->getId()->toString(),
'username' => $user->getUsername(),
'role' => $user->getRole()->getIdentifier(),
'created' => $user->getCreatedAt()->format(DateTime::ATOM),
'updated' => $user->getUpdatedAt()->format(DateTime::ATOM)
'roleIdentifier' => $user->getRole()->getIdentifier(),
'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
];
$userArray['permissions'] = [];

View File

@ -0,0 +1,28 @@
<?php
namespace Bee\API\External\User\Formatter;
use DateTime;
use Bee\Data\Business\Entity\User;
class UserStateFormatter {
public function format(User $user): array {
$userArray = [
'id' => $user->getId()->toString(),
'sessionId' => $user->getSession()->getId()->toString(),
'username' => $user->getUsername(),
'roleIdentifier' => $user->getRole()->getIdentifier(),
'createdAt' => $user->getCreatedAt()->format(DateTime::ATOM),
'updatedAt' => $user->getUpdatedAt()->format(DateTime::ATOM)
];
$userArray['permissions'] = [];
foreach ($user->getRole()->getPermissions()->toArray() as $permission) {
$userArray['permissions'][] = $permission->getIdentifier();
}
return $userArray;
}
}

View File

@ -7,6 +7,7 @@ namespace Bee\API\External\User\Handler;
use Bee\Data\Business\Entity\User;
use Bee\Handling\User\Handler\Command\ChangeUsername\ChangeUsernameCommandBuilder;
use Bee\Handling\User\Handler\Command\ChangeUsername\ChangeUsernameCommandHandler;
use Bee\Infrastructure\Request\Middleware\AnalyzeBodyMiddleware;
use Bee\Infrastructure\Response\SuccessResponse;
use Bee\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Psr\Http\Message\ResponseInterface;
@ -25,7 +26,6 @@ class ChangeUsernameHandler implements RequestHandlerInterface
{
/** @var User $user */
$user = $request->getAttribute(LoggedInUserMiddleware::USER_KEY);
$data = $request->getAttribute(AnalyzeBodyMiddleware::JSON_DATA);
$query = $this->builder->build(

View File

@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Bee\API\External\User\Handler;
use Bee\API\External\User\Formatter\CreateUserFormatter;
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandBuilder;
use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandHandler;
@ -18,7 +19,7 @@ class CreateUserHandler implements RequestHandlerInterface
public function __construct(
private readonly CreateUserCommandHandler $handler,
private readonly CreateUserCommandBuilder $builder,
private readonly UserFormatter $userFormatter,
private readonly CreateUserFormatter $formatter,
) {
}
@ -34,7 +35,7 @@ class CreateUserHandler implements RequestHandlerInterface
$result = $this->handler->execute($query);
return new JsonResponse(
$this->userFormatter->format($result)
$this->formatter->format($result)
);
}
}

View File

@ -4,10 +4,8 @@ declare(strict_types=1);
namespace Bee\API\External\User\Handler;
use Bee\API\External\User\Formatter\UserFormatter;
use Bee\API\External\User\Formatter\UserStateFormatter;
use Bee\Data\Business\Entity\User;
use Bee\Handling\User\Handler\Command\CreateUser\CreateUserCommandBuilder;
use Bee\Handling\User\Handler\Command\CreateUser\ChangePasswordCommandHandler;
use Bee\Infrastructure\Session\Middleware\LoggedInUserMiddleware;
use Laminas\Diactoros\Response\JsonResponse;
use Psr\Http\Message\ResponseInterface;
@ -17,7 +15,7 @@ use Psr\Http\Server\RequestHandlerInterface;
class UserStateHandler implements RequestHandlerInterface
{
public function __construct(
private readonly UserFormatter $userFormatter,
private readonly UserStateFormatter $userFormatter,
) {
}
@ -27,7 +25,6 @@ class UserStateHandler implements RequestHandlerInterface
$user = $request->getAttribute(LoggedInUserMiddleware::USER_KEY);
$response = $this->userFormatter->format($user);
$response['sessionId'] = $user->getSession()->getId()->toString();
return new JsonResponse($response);
}

View File

@ -4,7 +4,8 @@ declare(strict_types=1);
namespace Bee\Handling\Registration\Pipeline\RegisterUser\Step;
use Bee\Infrastructure\Request\Service\RequestService;
use Bee\Infrastructure\Mail\Exception\SendMailFailedException;
use Bee\Infrastructure\Mail\Service\MailService;
use teewurst\Pipeline\PipelineInterface;
use teewurst\Pipeline\TaskInterface;
@ -13,10 +14,13 @@ class SendMailStep implements TaskInterface
private const CONFIRM_LINK = 'https://%s/auth/registration/%s';
public function __construct(
private RequestService $requestService
private MailService $mailService,
) {
}
/**
* @throws SendMailFailedException
*/
public function __invoke(
$payload,
PipelineInterface $pipeline
@ -25,14 +29,9 @@ class SendMailStep implements TaskInterface
$command = $payload->getCommand();
$registration = $payload->getRegistration();
$this->requestService->request(
'notification',
'send-mail',
[
'bee-identifier' => 'new-account',
'sender' => 'info@stack-up.de',
'recipient' => $command->getMail(),
'data' => [
$this->mailService->send(
template: 'registration',
templateData: [
'username' => $command->getUsername(),
'confirmationLink' =>
sprintf(
@ -40,8 +39,9 @@ class SendMailStep implements TaskInterface
$command->getHost(),
$registration->getId()->toString()
)
]
]
],
recipient: $command->getMail(),
senderName: "Beekeeper"
);
$pipeline->next()($payload, $pipeline);

View File

@ -9,4 +9,5 @@ enum ErrorCode : string {
case AlreadyExists = 'AlreadyExists';
case WrongCredentials = 'WrongCredentials';
case Mismatch = 'Mismatch';
case Failed = 'Failed';
}

View File

@ -10,4 +10,6 @@ enum ErrorDomain : string {
case UserPassword = 'UserPassword';
case Registration = 'Registration';
case Product = 'Product';
case Mail = 'Mail';
case Login = 'Login';
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
use Reinfi\DependencyInjection\Factory\AutoWiringFactory;
use Bee\Infrastructure\Mail\Service\MailService;
return [
'factories' => [
MailService::class => AutoWiringFactory::class,
],
];

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Bee\Infrastructure\Mail;
class ConfigProvider
{
public function __invoke(): array
{
return [
'dependencies' => require __DIR__ . './../config/service_manager.php',
];
}
}

View File

@ -0,0 +1,32 @@
<?php
namespace Bee\Infrastructure\Mail\Exception;
use Throwable;
use Bee\Infrastructure\Exception\ErrorCode;
use Bee\Infrastructure\Exception\ErrorDomain;
use Bee\Infrastructure\Exception\Exception\Exception;
class SendMailFailedException extends Exception
{
private const MESSAGE = 'Das Mail template %s konnte mit den Daten {%s} nicht an %s gesendet werden. Grund: %s';
public function __construct(
string $templateIdentification,
array $templateData,
string $recipient,
Throwable $reason,
) {
parent::__construct(
sprintf(
self::MESSAGE,
$templateIdentification,
json_encode($templateData),
$recipient,
$reason->getMessage(),
),
ErrorDomain::Mail,
ErrorCode::Failed
);
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Bee\Infrastructure\Mail\Service;
use Exception;
use Latte\Engine;
use Nette\Mail\Mailer;
use Nette\Mail\Message;
use Nette\Mail\SmtpMailer;
use Reinfi\DependencyInjection\Service\ConfigService;
use Throwable;
use Bee\Infrastructure\Logging\Logger\Logger;
use Bee\Infrastructure\Mail\Exception\SendMailFailedException;
class MailService
{
private const TEMPLATE_PATH = APP_ROOT . '/data/mails/';
private readonly Engine $engine;
public function __construct(
private readonly ConfigService $configService,
private readonly Logger $logger,
) {
$this->engine = new Engine();
}
/**
* @throws SendMailFailedException
*/
public function send(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): void {
try {
$mail = $this->getMail(
template: $template,
templateData: $templateData,
recipient: $recipient,
sender: $sender,
senderName: $senderName
);
$mailer = $this->getMailer();
$mailer->send($mail);
} catch (Throwable $e) {
$this->logger->exception($e);
throw new SendMailFailedException(
$template,
$templateData,
$recipient,
$e
);
}
}
private function getMailer(): Mailer
{
$smtpConfig = $this->configService->resolve('mail.smtp-server');
return new SmtpMailer(
host: $smtpConfig['host'],
username: $smtpConfig['username'],
password: $smtpConfig['password'],
encryption: $smtpConfig['encryption'],
port: $smtpConfig['port'],
);
}
private function getMail(
string $template,
array $templateData,
string $recipient,
?string $sender = null,
?string $senderName = null
): Message {
$templatePath = self::TEMPLATE_PATH . $template . '/template.latte';
$assetsPath = self::TEMPLATE_PATH . $template . "/";
if (!file_exists($templatePath)) {
throw new Exception("Template File does not exist");
}
if (!is_dir($assetsPath)) {
$assetsPath = null;
}
$mail = new Message();
$mail->setFrom($this->getSenderName($sender, $senderName));
$mail->addTo($recipient);
$mail->setHtmlBody(
$this->engine->renderToString(
$templatePath,
$templateData,
),
$assetsPath
);
return $mail;
}
private function getSenderName(
?string $sender,
?string $senderName
): string {
$defaultConfig = $this->configService->resolve('mail.default');
$from = $sender ?? $defaultConfig['sender'] ?? throw new Exception('Could not determine Sender');
if ($senderName !== null) {
$from = sprintf('%s <%s>', $senderName, $from);
}
return $from;
}
}