feat: local password reset
This commit is contained in:
1 parent
fe562e5e73
commit
8bb7f08074
11 files changed
+688
-19
No files matched your search
@@ -0,0 +1,138 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Email\Mailer;
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\UriSigner;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class PasswordResetManager
|
||||
{
|
||||
private const int RESET_TTL_SECONDS = 3600;
|
||||
|
||||
public function __construct(
|
||||
private readonly UserRepository $userRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
private readonly UserPasswordHasherInterface $passwordHasher,
|
||||
private readonly UrlGeneratorInterface $urlGenerator,
|
||||
private readonly UriSigner $uriSigner,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly LoggerInterface $logger,
|
||||
) {
|
||||
}
|
||||
|
||||
public function requestPasswordReset(string $email): bool
|
||||
{
|
||||
$normalizedEmail = $this->normalizeEmail($email);
|
||||
$user = $this->userRepository->findOneByNormalizedEmail($normalizedEmail);
|
||||
|
||||
if (null === $user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$previousNonce = $user->getPasswordResetNonce();
|
||||
$nonce = $this->generateNonce();
|
||||
$user->setPasswordResetNonce($nonce);
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$resetUrl = $this->buildResetUrl($normalizedEmail, $nonce);
|
||||
|
||||
try {
|
||||
$this->mailer->createAndSendEmail(
|
||||
[
|
||||
'resetUrl' => $resetUrl,
|
||||
],
|
||||
[
|
||||
'to' => $normalizedEmail,
|
||||
'subject' => 'Dein MyE&P Passwort zurücksetzen',
|
||||
'template' => 'email/password_reset.html.twig',
|
||||
],
|
||||
);
|
||||
} catch (\Throwable $exception) {
|
||||
$user->setPasswordResetNonce($previousNonce);
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->error('Could not send password reset email', [
|
||||
'email' => $normalizedEmail,
|
||||
'error' => $exception->getMessage(),
|
||||
]);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->logger->info('Password reset requested', [
|
||||
'email' => $normalizedEmail,
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function validateResetRequest(Request $request): User
|
||||
{
|
||||
if (false === $this->uriSigner->checkRequest($request)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$expires = $request->query->getInt('expires');
|
||||
if (0 >= $expires || $expires < time()) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$email = $this->normalizeEmail($request->query->getString('email'));
|
||||
$nonce = trim($request->query->getString('nonce'));
|
||||
|
||||
if ('' === $email || '' === $nonce) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$user = $this->userRepository->findOneByNormalizedEmail($email);
|
||||
if (null === $user || null === $user->getPasswordResetNonce() || false === hash_equals($user->getPasswordResetNonce(), $nonce)) {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function resetPassword(User $user, string $plainPassword): void
|
||||
{
|
||||
$user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword));
|
||||
$user->setPasswordResetNonce($this->generateNonce());
|
||||
|
||||
$this->entityManager->flush();
|
||||
|
||||
$this->logger->info('Password reset completed', [
|
||||
'email' => $user->getUserIdentifier(),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildResetUrl(string $normalizedEmail, string $nonce): string
|
||||
{
|
||||
$url = $this->urlGenerator->generate('app_reset_password_confirm', [
|
||||
'email' => $normalizedEmail,
|
||||
'nonce' => $nonce,
|
||||
'expires' => time() + self::RESET_TTL_SECONDS,
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
return $this->uriSigner->sign($url);
|
||||
}
|
||||
|
||||
private function normalizeEmail(string $email): string
|
||||
{
|
||||
return mb_strtolower(trim($email));
|
||||
}
|
||||
|
||||
private function generateNonce(): string
|
||||
{
|
||||
return bin2hex(random_bytes(32));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user