src/Controller/Front/Account/AccountController.php line 60

Open in your IDE?
  1. <?php
  2. namespace App\Controller\Front\Account;
  3. use App\Application\Service\Authentication\MultifactorAuthService;
  4. use App\Application\Service\Helper\LinkGenerator;
  5. use App\Application\Service\Session\SessionService;
  6. use App\Client\Microsoft\Service\TokenService;
  7. use App\Client\Microsoft\Service\UserService;
  8. use App\Entity\System\Customer;
  9. use App\Manager\System\CustomerManager;
  10. use App\Manager\System\TokenManager;
  11. use App\Service\AuthenticatedSessionService;
  12. use App\Service\CustomerInformationUpdate\CustomerInformationUpdateService;
  13. use App\Service\EventDispatcher;
  14. use App\Service\RefererService;
  15. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  16. use Symfony\Component\HttpFoundation\Cookie;
  17. use Symfony\Component\HttpFoundation\JsonResponse;
  18. use Symfony\Component\HttpFoundation\RedirectResponse;
  19. use Symfony\Component\HttpFoundation\Request;
  20. use Symfony\Component\HttpFoundation\RequestStack;
  21. use Symfony\Component\HttpFoundation\Response;
  22. use Symfony\Component\Routing\Annotation\Route;
  23. use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
  24. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  25. use Symfony\Component\Security\Http\Event\LogoutEvent;
  26. use Symfony\Component\Uid\Uuid;
  27. use Symfony\Contracts\Translation\TranslatorInterface;
  28. use function Sentry\captureException;
  29. use function Sentry\captureMessage;
  30. class AccountController extends AbstractController
  31. {
  32. public const URL_FORGOT_PASSWORD_TOKEN_EXPIRED = 'forgot-password-token-expired?token=';
  33. public const URL_UPDATE_PASSWORD = 'update-password?token=';
  34. public function __construct(
  35. private readonly CustomerManager $customerManager,
  36. private readonly MultifactorAuthService $multifactorAuthService,
  37. private readonly AuthenticatedSessionService $authenticatedSessionService,
  38. private readonly TranslatorInterface $translator,
  39. private readonly TokenStorageInterface $tokenStorage,
  40. private readonly LinkGenerator $linkGenerator,
  41. private readonly SessionService $sessionService,
  42. private readonly TokenManager $tokenManager,
  43. private readonly TokenService $tokenService,
  44. private readonly UserService $userService,
  45. private readonly CustomerInformationUpdateService $customerInformationUpdateService,
  46. private readonly RequestStack $requestStack,
  47. private readonly EventDispatcher $eventDispatcher,
  48. private readonly RefererService $refererService,
  49. ) {
  50. }
  51. /**
  52. * @Route("/login", name="customer_login")
  53. */
  54. public function login(): Response
  55. {
  56. return $this->redirect($this->linkGenerator->getFrontUrl('login'));
  57. }
  58. /**
  59. * @Route("/{lang}/account/logout", name="customer_logout_lang", requirements={"lang"="[a-zA-Z]{2}"})
  60. * @Route("/account/logout", name="customer_logout_customized")
  61. */
  62. public function logout(): Response
  63. {
  64. $user = $this->getUser();
  65. if (!$user) {
  66. return $this->redirectToRoute('homepage_default');
  67. }
  68. if ($user instanceof Customer) {
  69. $this->tokenStorage->setToken();
  70. return $this->redirectToRoute('customer_login');
  71. }
  72. $this->tokenStorage->setToken();
  73. $this->requestStack->getSession()->invalidate();
  74. return $this->redirectToRoute('admin_login');
  75. }
  76. /**
  77. * @Route("/{lang}/account/multifactor-auth", name="customer_confirm_auth", requirements={"lang"="[a-zA-Z]{2}"})
  78. */
  79. public function confirmAuth(Request $request): Response
  80. {
  81. if ($request->getMethod() === Request::METHOD_GET) {
  82. return $this->render(
  83. 'front/account/confirm_auth.html.twig',
  84. [
  85. 'email' => $request->get('email'),
  86. ]
  87. );
  88. }
  89. $code = trim($request->request->get('authcode'));
  90. $email = $request->request->get('email');
  91. $user = $this->customerManager->findOneBy(['email' => $email]);
  92. $multifactorAuth = $this->multifactorAuthService->findValidMultifactorAuth($user, $code);
  93. if (!$multifactorAuth) {
  94. return new JsonResponse(
  95. ['isLogged' => false, 'errors' => ['authcode' => $this->translator->trans('mfa.error.wrong_code')]]
  96. );
  97. }
  98. $this->authenticatedSessionService->loadCustomerSession($user);
  99. $this->authenticatedSessionService->authenticateCustomer($user);
  100. [$cookieHash, $cookieExpirationTimestamp] = $this->multifactorAuthService->updateMultifactorAuthWithCookieData(
  101. $user,
  102. $multifactorAuth
  103. );
  104. $request->getSession()->remove(MultifactorAuthService::SESSION_EMAIL_CUSTOMER_KEY);
  105. $response = new JsonResponse(['isLogged' => true, 'redirect' => $this->refererService->getCustomerReferer()]);
  106. $browserCookie = Cookie::create(
  107. MultifactorAuthService::MULTIFACTOR_AUTH_CUSTOMER_COOKIE_NAME,
  108. $cookieHash,
  109. $cookieExpirationTimestamp
  110. );
  111. $response->headers->setCookie($browserCookie);
  112. return $response;
  113. }
  114. /**
  115. * @Route("/account/resend-mfa-code", name="multifactor_auth_resend_code", requirements={"lang"="[a-zA-Z]{2}"})
  116. */
  117. public function resendMultifactorAuthCode(Request $request): Response
  118. {
  119. $email = $request->getSession()->get(MultifactorAuthService::SESSION_EMAIL_CUSTOMER_KEY);
  120. $customer = $this->customerManager->findOneBy(['email' => $email]);
  121. try {
  122. $this->multifactorAuthService->sendAuthCodeToUser($customer);
  123. } catch (\Throwable $t) {
  124. return new JsonResponse(['error' => 'Something went wrong']);
  125. }
  126. return new JsonResponse(['message' => $this->translator->trans('mfa.popup.code_sent')]);
  127. }
  128. /**
  129. * @Route("/{lang}/account/update-password/{updatePasswordToken}", name="app_front_account_update_password", requirements={"lang"="[a-zA-Z]{2}", "updatePasswordToken"="[a-zA-Z0-9]+"})
  130. */
  131. public function updatePassword(?string $updatePasswordToken = null): Response
  132. {
  133. $localeId = $this->sessionService->getLocaleId();
  134. if (empty($updatePasswordToken)) {
  135. return $this->redirect(
  136. $this->linkGenerator->getFrontUrl(
  137. self::URL_FORGOT_PASSWORD_TOKEN_EXPIRED.$updatePasswordToken,
  138. $localeId
  139. )
  140. );
  141. }
  142. $token = $this->tokenManager->findOneByToken($updatePasswordToken);
  143. if ($token === null || $token->isUsed()) {
  144. return $this->redirect(
  145. $this->linkGenerator->getFrontUrl(
  146. self::URL_FORGOT_PASSWORD_TOKEN_EXPIRED.$updatePasswordToken,
  147. $localeId
  148. )
  149. );
  150. }
  151. return $this->redirect(
  152. $this->linkGenerator->getFrontUrl('update-password?token='.$updatePasswordToken, $localeId)
  153. );
  154. }
  155. /**
  156. * @Route("/account/connect/microsoft", name="microsoft_login")
  157. */
  158. public function microsoftLogin(Request $request): RedirectResponse
  159. {
  160. if ($this->getUser()) {
  161. return $this->redirectToRoute('homepage');
  162. }
  163. $redirectUri = $this->generateUrl('microsoft_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
  164. $state = Uuid::v4()->toRfc4122();
  165. $request->getSession()->set('oauth_state', $state);
  166. return new RedirectResponse($this->tokenService->generateAuthorizationUrl($state, $redirectUri));
  167. }
  168. /**
  169. * @Route("/connect/microsoft/check", name="microsoft_callback")
  170. */
  171. public function microsoftCallback(Request $request): RedirectResponse
  172. {
  173. $code = $request->query->get('code');
  174. $state = $request->query->get('state');
  175. $storedState = $request->getSession()->get('oauth_state');
  176. if ($state !== $storedState || empty($state)) {
  177. $this->addFlash('danger', 'Invalid state parameter.');
  178. return $this->redirectToRoute('admin_login');
  179. }
  180. if (!$code) {
  181. $this->addFlash('danger', 'Authentication failed: No code received.');
  182. return $this->redirectToRoute('admin_login');
  183. }
  184. $redirectUri = $this->generateUrl('microsoft_callback', [], UrlGeneratorInterface::ABSOLUTE_URL);
  185. try {
  186. $token = $this->tokenService->getTokenWithCode($redirectUri, $code);
  187. $microsoftUser = $this->userService->getMe($token->getAccessToken());
  188. } catch (\Exception $exception) {
  189. $this->addFlash('danger', 'Authentication failed: '.$exception->getMessage());
  190. captureException($exception);
  191. return $this->redirectToRoute('admin_login');
  192. }
  193. $user = $this->customerManager->findOneBy(['email' => $microsoftUser->getMail()]);
  194. if (!$user) {
  195. $this->addFlash('danger', 'Authentication failed: User not found.');
  196. captureMessage('Microsoft user not found with email: '.$microsoftUser->getMail());
  197. return $this->redirectToRoute('admin_login');
  198. }
  199. if (!$user->isActive()) {
  200. $this->addFlash('danger', 'Authentication failed: User disabled.');
  201. return $this->redirectToRoute('admin_login');
  202. }
  203. $this->customerInformationUpdateService->updateCustomerEmployee($user, $microsoftUser);
  204. $this->authenticatedSessionService->authenticateCustomer($user);
  205. return $this->redirect($this->refererService->getCustomerReferer());
  206. }
  207. /**
  208. * @Route("/change-to-employee", name="admin_change_to_employee")
  209. */
  210. public function changeToEmployee(): Response
  211. {
  212. $logoutEvent = new LogoutEvent($this->requestStack->getCurrentRequest(), $this->tokenStorage->getToken());
  213. $this->eventDispatcher->dispatch($logoutEvent);
  214. $this->tokenStorage->setToken();
  215. $this->requestStack->getSession()->invalidate();
  216. return $this->redirectToRoute('admin_microsoft_login');
  217. }
  218. /**
  219. * @Route("/microsoft/user/change", name="microsoft_user_change_notification")
  220. */
  221. public function microsoftUserChangeNotification(Request $request): Response
  222. {
  223. $validationToken = $request->query->get('validationToken');
  224. if ($validationToken) {
  225. return new Response($validationToken, Response::HTTP_OK, ['Content-Type' => 'text/plain']);
  226. }
  227. $notifications = json_decode($request->getContent(), true);
  228. $userId = null;
  229. foreach ($notifications['value'] as $value) {
  230. if ($value['resourceData']['@odata.type'] === '#Microsoft.Graph.User') {
  231. $userId = 'a1b2c3d4-5678-90ab-cdef-1234567890ab';
  232. break;
  233. }
  234. }
  235. if (!$userId) {
  236. return new Response(null, Response::HTTP_ACCEPTED);
  237. }
  238. try {
  239. $token = $this->tokenService->getToken();
  240. $microsoftUser = $this->userService->getUserByIdOrEmail($userId, $token->getAccessToken());
  241. } catch (\Exception $exception) {
  242. $this->addFlash('danger', 'Authentication failed: '.$exception->getMessage());
  243. captureException($exception);
  244. return new Response(null, Response::HTTP_NOT_ACCEPTABLE);
  245. }
  246. if (!$microsoftUser->isAccountEnabled()) {
  247. $customer = $this->customerManager->findOneByEmail($microsoftUser->getMail());
  248. if ($customer) {
  249. $customer->setActive(false);
  250. $this->customerManager->save($customer);
  251. }
  252. }
  253. return new Response(null, Response::HTTP_ACCEPTED);
  254. }
  255. }