<?php
namespace App\Controller;
use App\Entity\Letter;
use App\Entity\Organization;
use App\Entity\ResetPasswordToken;
use App\Entity\User;
use App\Entity\UserProfile;
use App\Exception\UnauthorizedRegistrationException;
use App\Exception\UnmatchedDomainException;
use App\Form\User\AccountCreationFormType;
use App\Form\User\PasswordFormType;
use App\Repository\UserRepository;
use App\Service\Mailer\SattMailer;
use App\Service\Notifier\SattNotificationService;
use App\Service\UserRegisterService;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
use Symfony\Contracts\Translation\TranslatorInterface;
use App\Service\TwoFactorAuthService;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\Session\SessionInterface;
/**
* Class SecurityController.
*/
class SecurityController extends AbstractController
{
private $sattMailer;
private UserPasswordHasherInterface $passwordHasher;
private $sattNotification;
private EntityManagerInterface $em;
private TwoFactorAuthService $twoFactorAuthService;
public function __construct(
SattMailer $sattMailer,
UserPasswordHasherInterface $passwordHasher,
SattNotificationService $sattNotification,
EntityManagerInterface $entityManager,
TwoFactorAuthService $twoFactorAuthService,
) {
$this->sattMailer = $sattMailer;
$this->passwordHasher = $passwordHasher;
$this->sattNotification = $sattNotification;
$this->em = $entityManager;
$this->twoFactorAuthService = $twoFactorAuthService;
}
#[Route(path: '/', name: 'app_login', options: ['expose' => true])]
public function login(AuthenticationUtils $authenticationUtils): Response
{
// if ($this->getUser()) {
// return $this->redirectToRoute('target_path');
// }
// get the login error if there is one
$error = $authenticationUtils->getLastAuthenticationError();
// last username entered by the user
$lastUsername = $authenticationUtils->getLastUsername();
return $this->render('security/login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
}
#[Route(path: '/logout', name: 'app_logout')]
public function logout()
{
throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
}
#[Route(path: '/create', name: 'create_account')]
public function createAccount(
Request $request,
UserRepository $userRepository,
UserRegisterService $userRegisterService,
TranslatorInterface $translator
) {
$form = $this->createForm(AccountCreationFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var User $userForm */
$userForm = $form->getData();
$isFormFlawed = false;
if ($userRepository->findOneByAttribute($form['email']->getData(), 'email')) {
$this->addFlash('error', 'flash.email_taken');
$isFormFlawed = true;
}
// Statut is required by the form, no need to check if null
if (!$userForm->getOrganization() || Organization::TYPE_UNIVERSITY !== $userForm->getOrganization()->getType()) {
if ($userForm->getStatut()->getIsUniversityRequired()) {
$this->addFlash('error', 'flash.statut_require_university');
$isFormFlawed = true;
}
}
/* // Handle usernames
if($this->getDoctrine()->getRepository(User::class)->findOneByAttribute($form['username']->getData(), 'username')){
//$this->addFlash('error', 'flash.username_taken');
$isFormFlawed = true;
} */
if ($isFormFlawed) {
return $this->render('security/create_account.html.twig', ['form' => $form->createView()]);
}
try {
$user = new User();
$user->setEmail($userForm->getEmail())
->setFirstName($userForm->getFirstName())
->setLastName($userForm->getLastName())
->setStatut($userForm->getStatut())
->setOrganization($userForm->getOrganization())
->setPassword($this->passwordHasher->hashPassword($user, $form['password']->getData()))
->setUserProfile((new UserProfile()))
->setUsername('user_'.uniqid());
$entityManager = $this->em;
$userRegisterService->register($user);
$entityManager->persist($user);
$entityManager->flush();
$this->addFLash('success', 'flash.creation_success');
} catch (UnmatchedDomainException $e) {
$this->addFLash('error', $translator->trans('flash.register.unmatched_domain_error', ['%domain%' => $e->getMessage()]));
} catch (UnauthorizedRegistrationException $e) {
$this->addFLash('error', 'flash.register.creation_error');
}
return $this->redirectToRoute('app_login');
}
return $this->render(
'security/create_account.html.twig',
['form' => $form->createView()]
);
}
#[Route(path: '/change-password', name: 'change_password')]
public function forgottenPassword(Request $request, UserRepository $userRepository)
{
$form = $this->createFormBuilder()
->add('email', EmailType::class, ['label' => 'form.label.email'])
->add('submit', SubmitType::class, ['label' => 'form.action.reinitialized'])
->getForm();
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
/** @var User $user */
if ($user = $userRepository->findOneByAttribute($form['email']->getData(), 'email')) {
if (!($token = $user->getResetPasswordToken())) { // No token associated to User
$token = new ResetPasswordToken();
} /* else { // Do not resend a token if the user has a valid reset password token
if ($user->getResetPasswordToken()->isItExpired(time(), $this->getParameter('RESET_PASSWORD_TOKEN_EXPIRATION_DELAY'))) {
return $this->redirectToRoute('app_login');
}
} */ else {
$token = $user->getResetPasswordToken();
}
$entityManager = $this->em;
// Token value
$tokenValue = rtrim(strtr(base64_encode(random_bytes(64)), '+/', '-_'), '=');
$token
->setValue($tokenValue)
->setExpirationDate(time() + $this->getParameter('RESET_PASSWORD_TOKEN_EXPIRATION_DELAY'));
$user->setResetPasswordToken($token);
$entityManager->persist($token);
$entityManager->persist($user);
$entityManager->flush();
$url = $this->generateUrl(
'reset_password',
['token' => $token->getValue()],
UrlGeneratorInterface::ABSOLUTE_URL
);
$this->sattMailer->sendComputedEmailByCode(
[$form['email']->getData()],
Letter::CODE['USER_RESET_PASSWORD'],
[],
array_merge($user->getBindings(), [
'USER.RESET.PASSWORD' => $url
])
);
$this->addFlash('success', 'flash.reset_password');
}
return $this->redirectToRoute('change_password');
}
return $this->render(
'security/change_password.html.twig',
['form' => $form->createView(), 'error' => null]
);
}
/**
* Handle password reset with tokens sent by mail.
*/
#[Route(path: '/reset-password-token', name: 'reset_password')]
public function resetPassword(Request $request, UserRepository $userRepository)
{
if (!$request->query->get('token')) {
return $this->redirectToRoute('app_login');
}
$tokenValue = $request->query->get('token');
/** @var User $tuser */
$user = $userRepository->findOneByResetToken($tokenValue);
//verify if token exists/is expired
if (!$user || $user->getResetPasswordToken()->isItExpired(time(), $this->getParameter('RESET_PASSWORD_TOKEN_EXPIRATION_DELAY'))) {
$this->addFlash('error', 'flash.user_profile.invalid_token');
return $this->redirectToRoute('app_login');
}
$form = $this->createForm(PasswordFormType::class);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager = $this->em;
$user->setPassword($this->passwordHasher->hashPassword($user, $form['newPassword']->getData()));
$token = $user->getResetPasswordToken();
$entityManager->persist($user);
$entityManager->remove($token);
$entityManager->flush();
$this->addFLash('success', 'flash.user_profile.password_reset_success');
return $this->redirectToRoute('app_login');
}
return $this->render(
'security/change_password_form.html.twig',
['form' => $form->createView()]
);
}
#[Route('/2fa', name: 'app_2fa')]
public function twoFactor(Request $request, Security $security, TwoFactorAuthService $twoFactorAuthService, SessionInterface $session): Response
{
/** @var User $user */
$user = $security->getUser();
if (!$user) {
return $this->redirectToRoute('app_login');
}
// si 2FA désactivée ou déjà validée → on rebondit
if (!$user->isTwoFactorEnabled() || $session->get('2fa_verified', false)) {
$route = \array_intersect(['ROLE_ADMIN','ROLE_SCIENTIFIC_ADMIN'], $user->getRoles()) ? 'admin' : 'dashboard';
return $this->redirectToRoute($route);
}
if ($request->isMethod('POST')) {
$code = $request->request->get('code');
$expiresAt = $user->getTwoFactorExpiresAt();
if ($user->getTwoFactorCode() === $code && $expiresAt > new \DateTime()) {
$session->set('2fa_verified', true);
$twoFactorAuthService->clear($user);
if (in_array('ROLE_ADMIN', $user->getRoles(), true)) {
return $this->redirectToRoute('admin');
}
return $this->redirectToRoute('dashboard');
}
$this->addFlash('danger', 'Code invalide ou expiré');
}
return $this->render('security/2fa.html.twig');
}
}