Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reset password #79

Merged
merged 12 commits into from
Jun 4, 2024
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
Loading