Add profile page for changing password and email

New /profile route (nav link between Admin and Logout) with two
independent forms, both requiring the current password before
applying a change:
- Change password: standard current/new/repeat flow, same password
  strength rules as registration.
- Change email: re-checks the new address isn't already taken (avoids
  a 500 from the unique constraint), then marks the account
  unverified and re-sends the confirmation email via the existing
  EmailVerifier/verify-email flow, same as registration. The current
  session stays logged in, but the user needs to verify the new
  address before their next login (UserChecker already blocks
  unverified accounts).
This commit is contained in:
Frank
2026-07-11 23:23:38 +02:00
parent 2941cfca49
commit 886208c5c0
7 changed files with 244 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
<?php
declare(strict_types=1);
namespace App\Tech\Controller;
use App\Tech\Entity\User;
use App\Tech\Form\ProfileChangeEmailFormType;
use App\Tech\Form\ProfileChangePasswordFormType;
use App\Tech\Repository\UserRepository;
use App\Tech\Service\EmailVerifier;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
final class ProfileController extends AbstractController
{
public function __construct(
private readonly EntityManagerInterface $entityManager,
private readonly UserPasswordHasherInterface $passwordHasher,
private readonly UserRepository $userRepository,
private readonly EmailVerifier $emailVerifier,
) {
}
#[Route('/profile', name: 'app_profile', methods: ['GET', 'POST'])]
#[IsGranted('ROLE_USER')]
public function index(Request $request): Response
{
$user = $this->getUser();
if (!$user instanceof User) {
throw $this->createAccessDeniedException();
}
$passwordForm = $this->createForm(ProfileChangePasswordFormType::class);
$passwordForm->handleRequest($request);
if ($passwordForm->isSubmitted() && $passwordForm->isValid()) {
$currentPassword = (string) $passwordForm->get('currentPassword')->getData();
if (!$this->passwordHasher->isPasswordValid($user, $currentPassword)) {
$passwordForm->get('currentPassword')->addError(new FormError('Incorrect password.'));
} else {
$plainPassword = (string) $passwordForm->get('plainPassword')->getData();
$user->setPassword($this->passwordHasher->hashPassword($user, $plainPassword));
$this->entityManager->flush();
$this->addFlash('success', 'Your password has been updated.');
return $this->redirectToRoute('app_profile');
}
}
$emailForm = $this->createForm(ProfileChangeEmailFormType::class, ['email' => $user->getEmail()]);
$emailForm->handleRequest($request);
if ($emailForm->isSubmitted() && $emailForm->isValid()) {
$currentPassword = (string) $emailForm->get('currentPassword')->getData();
if (!$this->passwordHasher->isPasswordValid($user, $currentPassword)) {
$emailForm->get('currentPassword')->addError(new FormError('Incorrect password.'));
} else {
$newEmail = (string) $emailForm->get('email')->getData();
if ($newEmail === $user->getEmail()) {
$this->addFlash('info', 'That is already your email address.');
return $this->redirectToRoute('app_profile');
}
$existing = $this->userRepository->findOneBy(['email' => $newEmail]);
if ($existing) {
$emailForm->get('email')->addError(new FormError('This email address is already in use.'));
} else {
$user->setEmail($newEmail);
$user->setIsVerified(false);
$this->entityManager->flush();
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from($this->getParameter('mailer_from'))
->to($newEmail)
->subject('Please confirm your new email address')
->htmlTemplate('tech/registration/confirmation_email.html.twig')
);
$this->addFlash('success', 'Your email address has been updated. You must verify it (check your inbox) before you can log in again.');
return $this->redirectToRoute('app_profile');
}
}
}
return $this->render('tech/profile/index.html.twig', [
'user' => $user,
'passwordForm' => $passwordForm,
'emailForm' => $emailForm,
]);
}
}