Skip to content

Commit

Permalink
Reset password (#79)
Browse files Browse the repository at this point in the history
* Inital password reset commit

* Revert composer.json

* Add headers

* Apply php-cs-fixer changes

* Consistency check

* Try to reformat login mail text

* Align at column 24

* move text to const

* Apply php-cs-fixer changes

* Do not return json response, just return response

---------

Co-authored-by: mattamon <[email protected]>
  • Loading branch information
mattamon and mattamon authored Jun 4, 2024
1 parent 8369c21 commit d29b769
Show file tree
Hide file tree
Showing 15 changed files with 628 additions and 1 deletion.
12 changes: 11 additions & 1 deletion config/authorization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,14 @@ services:


Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenServiceInterface:
class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenService
class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\TokenService

Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter\RateLimiterInterface:
class: Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter\RateLimiter

Pimcore\Bundle\StudioBackendBundle\Authorization\Service\UserServiceInterface:
class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\UserService

Pimcore\Bundle\StudioBackendBundle\Authorization\Service\MailServiceInterface:
class: Pimcore\Bundle\StudioBackendBundle\Authorization\Service\MailService
arguments: ['@Pimcore\Bundle\StudioBackendBundle\Setting\Provider\SystemSettingsProvider']
34 changes: 34 additions & 0 deletions src/Authorization/Attributes/Request/ResetPasswordRequestBody.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\Attributes\Request;

use Attribute;
use OpenApi\Attributes\JsonContent;
use OpenApi\Attributes\RequestBody;
use Pimcore\Bundle\StudioBackendBundle\Authorization\Schema\ResetPassword;

#[Attribute(Attribute::TARGET_METHOD)]
final class ResetPasswordRequestBody extends RequestBody
{
public function __construct()
{
parent::__construct(
required: true,
content: new JsonContent(ref: ResetPassword::class)
);
}
}
68 changes: 68 additions & 0 deletions src/Authorization/Controller/ResetPasswordController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\Controller;

use OpenApi\Attributes\Post;
use Pimcore\Bundle\StudioBackendBundle\Authorization\Attributes\Request\ResetPasswordRequestBody;
use Pimcore\Bundle\StudioBackendBundle\Authorization\Schema\ResetPassword;
use Pimcore\Bundle\StudioBackendBundle\Authorization\Service\UserServiceInterface;
use Pimcore\Bundle\StudioBackendBundle\Controller\AbstractApiController;
use Pimcore\Bundle\StudioBackendBundle\Exception\DomainConfigurationException;
use Pimcore\Bundle\StudioBackendBundle\Exception\RateLimitException;
use Pimcore\Bundle\StudioBackendBundle\Exception\SendMailException;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\DefaultResponses;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Attributes\Response\SuccessResponse;
use Pimcore\Bundle\StudioBackendBundle\OpenApi\Config\Tags;
use Pimcore\Bundle\StudioBackendBundle\Util\Constants\HttpResponseCodes;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\MapRequestPayload;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Serializer\SerializerInterface;

/**
* @internal
*/
final class ResetPasswordController extends AbstractApiController
{
public function __construct(
SerializerInterface $serializer,
private readonly UserServiceInterface $userService
) {
parent::__construct($serializer);
}

/**
* @throws RateLimitException|DomainConfigurationException|SendMailException
*/
#[Route('/reset-password', name: 'pimcore_studio_api_reset_password', methods: ['POST'])]
#[Post(
path: self::API_PATH . '/reset-password',
operationId: 'rest-password',
summary: 'Sending username to reset password',
tags: [Tags::Authorization->name]
)]
#[ResetPasswordRequestBody]
#[SuccessResponse]
#[DefaultResponses([
HttpResponseCodes::TOO_MANY_REQUESTS
])]
public function resetPassword(#[MapRequestPayload] ResetPassword $resetPassword): Response
{
$this->userService->resetPassword($resetPassword);
return new Response();
}
}
59 changes: 59 additions & 0 deletions src/Authorization/Event/LostPasswordEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\Event;

use Pimcore\Model\User;
use Symfony\Contracts\EventDispatcher\Event;

final class LostPasswordEvent extends Event
{
public const EVENT_NAME = 'pimcore.admin.login.lostpassword';

protected bool $sendMail = true;

public function __construct(private User $user, private string $loginUrl)
{
}

public function getUser(): User
{
return $this->user;
}

public function getLoginUrl(): string
{
return $this->loginUrl;
}

/**
* Determines if lost password mail should be sent
*/
public function getSendMail(): bool
{
return $this->sendMail;
}

/**
* Sets flag whether to send lost password mail or not
*/
public function setSendMail(bool $sendMail): LostPasswordEvent
{
$this->sendMail = $sendMail;

return $this;
}
}
51 changes: 51 additions & 0 deletions src/Authorization/RateLimiter/RateLimiter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter;

use Pimcore\Bundle\StudioBackendBundle\Exception\RateLimitException;
use Pimcore\Bundle\StudioBackendBundle\Util\Traits\RequestTrait;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\RateLimiter\Exception\RateLimitExceededException;
use Symfony\Component\RateLimiter\RateLimiterFactory;

final readonly class RateLimiter implements RateLimiterInterface
{
use RequestTrait;
public function __construct(
private RateLimiterFactory $resetPasswordLimiter,
private RequestStack $requestStack,
)
{
}

/**
* @throws RateLimitException
*/
public function check(): void
{
$request = $this->getCurrentRequest($this->requestStack);

$limiter = $this->resetPasswordLimiter->create($request->getClientIp());

try {
$limiter->consume()->ensureAccepted();
} catch (RateLimitExceededException) {
throw new RateLimitException();
}

}
}
27 changes: 27 additions & 0 deletions src/Authorization/RateLimiter/RateLimiterInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\RateLimiter;

use Pimcore\Bundle\StudioBackendBundle\Exception\RateLimitException;

interface RateLimiterInterface
{
/**
* @throws RateLimitException
*/
public function check(): void;
}
42 changes: 42 additions & 0 deletions src/Authorization/Schema/ResetPassword.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\Schema;

use OpenApi\Attributes\Property;
use OpenApi\Attributes\Schema;

/**
* @internal
*/
#[Schema(
title: 'ResetPassword',
description: 'Username',
type: 'object'
)]
final readonly class ResetPassword
{
public function __construct(
#[Property(description: 'Username', type: 'string', example: 'shaquille.oatmeal')]
private string $username
) {
}

public function getUsername(): string
{
return $this->username;
}
}
88 changes: 88 additions & 0 deletions src/Authorization/Service/MailService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);

/**
* Pimcore
*
* This source file is available under two different licenses:
* - GNU General Public License version 3 (GPLv3)
* - Pimcore Commercial License (PCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (http://www.pimcore.org)
* @license http://www.pimcore.org/license GPLv3 and PCL
*/

namespace Pimcore\Bundle\StudioBackendBundle\Authorization\Service;

use Exception;
use Pimcore\Bundle\StaticResolverBundle\Lib\ToolResolverInterface;
use Pimcore\Bundle\StudioBackendBundle\Authorization\Event\LostPasswordEvent;
use Pimcore\Bundle\StudioBackendBundle\Exception\DomainConfigurationException;
use Pimcore\Bundle\StudioBackendBundle\Exception\SendMailException;
use Pimcore\Bundle\StudioBackendBundle\Setting\Provider\SettingsProviderInterface;
use Pimcore\Model\User;
use Pimcore\Model\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Routing\RouterInterface;

/**
* @internal
*/
final readonly class MailService implements MailServiceInterface
{
private const RESET_MAIL_TEXT = "Login to pimcore and change your password using the following link.
This temporary login link will expire in 24 hours: \r\n\r\n %s";
private string $domain;

public function __construct(
private SettingsProviderInterface $systemSettingsProvider,
private RouterInterface $router,
private EventDispatcherInterface $eventDispatcher,
private ToolResolverInterface $toolResolver,
)
{
$settings = $this->systemSettingsProvider->getSettings();
$this->domain = $settings['main_domain'];
}

/**
* @throws DomainConfigurationException|SendMailException
*/
public function sendResetPasswordMail(UserInterface $user, string $token): void
{
if (!$this->domain) {
throw new DomainConfigurationException();
}

$context = $this->router->getContext();
$context->setHost($this->domain);

$loginUrl = $this->router->generate(
'pimcore_admin_login',
[
'token' => $token,
'reset' => 'true',
],
UrlGeneratorInterface::ABSOLUTE_URL
);

/** @var User $user */
$event = new LostPasswordEvent($user, $loginUrl);
$this->eventDispatcher->dispatch($event, LostPasswordEvent::EVENT_NAME);

// only send mail if it wasn't prevented in event
if ($event->getSendMail()) {
try {
$mail = $this->toolResolver->getMail([$user->getEmail()], 'Pimcore lost password service');
$mail->setIgnoreDebugMode(true);
$mail->text(sprintf(self::RESET_MAIL_TEXT, $loginUrl));
$mail->send();
} catch (Exception $exception) {
throw new SendMailException($exception->getMessage());
}
}
}
}
Loading

0 comments on commit d29b769

Please sign in to comment.