src/Controller/DefaultController.php line 53

Open in your IDE?
  1. <?php
  2. namespace MedBrief\MSR\Controller;
  3. use DateTime;
  4. use Doctrine\ORM\EntityManagerInterface;
  5. use Exception;
  6. use MedBrief\MSR\Controller\BaseController as Controller;
  7. use MedBrief\MSR\Entity\Analytics\AnalyticsReport;
  8. use MedBrief\MSR\Entity\Invitation;
  9. use MedBrief\MSR\Entity\Project;
  10. use MedBrief\MSR\Entity\RoleInvitation;
  11. use MedBrief\MSR\Entity\User;
  12. use MedBrief\MSR\Form\ChangePasswordFormType;
  13. use MedBrief\MSR\Form\Filter\MatterFilterType;
  14. use MedBrief\MSR\Form\Order\MatterOrderType;
  15. use MedBrief\MSR\Repository\ProjectRepository;
  16. use MedBrief\MSR\Repository\RoleInvitationRepository;
  17. use MedBrief\MSR\Repository\SystemNotificationRepository;
  18. use MedBrief\MSR\Service\Analytics\AnalyticsService;
  19. use MedBrief\MSR\Service\Analytics\Model\AnalyticsEmbeddedReport;
  20. use MedBrief\MSR\Service\DeviceDetectorService;
  21. use MedBrief\MSR\Service\EntityHelper\ProjectHelper;
  22. use MedBrief\MSR\Service\EntityHelper\UserHelper;
  23. use MedBrief\MSR\Service\LicenceRenewal\RenewalHelperService;
  24. use MedBrief\MSR\Service\ProjectMatch\MatchExpertUser;
  25. use MedBrief\MSR\Service\Role\RoleParserService;
  26. use MedBrief\MSR\Service\Security\GeneralSecurityService;
  27. use MedBrief\MSR\Traits\PaginatorAwareTrait;
  28. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
  29. use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
  30. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  31. use Symfony\Component\HttpFoundation\Cookie;
  32. use Symfony\Component\HttpFoundation\JsonResponse;
  33. use Symfony\Component\HttpFoundation\RedirectResponse;
  34. use Symfony\Component\HttpFoundation\Request;
  35. use Symfony\Component\HttpFoundation\Response;
  36. use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
  37. use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
  38. use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken;
  39. use Symfony\Component\Security\Http\Event\InteractiveLoginEvent;
  40. class DefaultController extends Controller
  41. {
  42. use PaginatorAwareTrait;
  43. /**
  44. * When the user navigates to the MedBrief home page, if they are logged in, then we
  45. * log them out. We then redirect to login. This is per Steve England's
  46. * request that for security purposes, when the landing page is hit, we log
  47. * users out.
  48. */
  49. public function indexAction(): JsonResponse|RedirectResponse
  50. {
  51. // if the user is currently logged in
  52. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  53. // Forward to the dashboard. We used to log the user out but this caused more problems than it solved.
  54. return $this->redirectWithAjaxSupport($this->generateUrl('infology_medbrief_dashboard'));
  55. }
  56. // if we have a redirect page specified in the parameters
  57. $redirectUrl = $this->getParameter('root_page_redirect_url');
  58. if ($redirectUrl) {
  59. // redirect the user to it
  60. return $this->redirectWithAjaxSupport($redirectUrl);
  61. }
  62. // otherwise just sent them to login
  63. return $this->redirectWithAjaxSupport($this->generateUrl('login'));
  64. }
  65. /**
  66. * @param EntityManagerInterface $entityManager
  67. * @param AnalyticsService $analyticsService
  68. * @param UserHelper $userHelper
  69. * @param ProjectRepository $projectRepository
  70. * @param RoleInvitationRepository $roleInvitationRepository
  71. * @param SystemNotificationRepository $systemNotificationRepository
  72. * @param RoleParserService $roleParser
  73. * @param RenewalHelperService $renewalHelperService
  74. * @param Request $request
  75. * @param ProjectHelper $projectHelper
  76. * @param MatchExpertUser $matchExpertUserService
  77. * @param GeneralSecurityService $generalSecurityService
  78. *
  79. * @throws Exception
  80. *
  81. */
  82. public function dashboardAction(
  83. AnalyticsService $analyticsService,
  84. UserHelper $userHelper,
  85. ProjectRepository $projectRepository,
  86. RoleInvitationRepository $roleInvitationRepository,
  87. SystemNotificationRepository $systemNotificationRepository,
  88. RoleParserService $roleParser,
  89. RenewalHelperService $renewalHelperService,
  90. ProjectHelper $projectHelper,
  91. MatchExpertUser $matchExpertUserService,
  92. Request $request,
  93. GeneralSecurityService $generalSecurityService
  94. ): JsonResponse|RedirectResponse|Response {
  95. // Get the response message from url if we have one from the (match expert) complete profile confirm button.
  96. $responseMessage = $request->query->get('message', null);
  97. $referrer = $request->headers->get('referer');
  98. if ($responseMessage && $generalSecurityService->isRequestFromMedBrief($referrer)) {
  99. $this->addFlash('success', $responseMessage);
  100. }
  101. // Redirect to pending invitations page if User has pending role invitations.
  102. if ($this->userHasPendingInvitations()) {
  103. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithoutFlash());
  104. }
  105. /** @var User $user */
  106. $user = $this->getUser();
  107. $userHelper->setUser($user);
  108. // If the user has 2FA enabled and is rate limited, redirect them to the login page.
  109. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('rateLimited')) {
  110. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times. Please try again in fifteen minutes.');
  111. return $this->redirectToRoute('logout');
  112. }
  113. // If the user has 2FA enabled and their IP Address has been blacklisted, redirect them to the login page.
  114. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('ipBlacklisted')) {
  115. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times from this IP Address. Please try again in fifteen minutes.');
  116. return $this->redirectToRoute('logout');
  117. }
  118. $analyticsEmbeddedReport = $analyticsService->generateAnalyticsEmbeddedReport($user, AnalyticsReport::REPORT_TYPE_DASHBOARD);
  119. // We use this to determine if we should display the 'Explore Analytics' button.
  120. $analyticsEmbeddedReportMain = $analyticsService->generateAnalyticsEmbeddedReport($user, AnalyticsReport::REPORT_TYPE_MAIN);
  121. $recentlyViewedProjects = $this->getRecentlyViewedProjects($projectRepository, $userHelper, $projectHelper, $matchExpertUserService, false)['projects'];
  122. $favouriteProjects = $this->getFavouriteProjects($projectHelper, $matchExpertUserService, false)['projects'];
  123. $recentlyAcceptedProjects = $this->getRecentlyAcceptedProjects($roleInvitationRepository, $roleParser, $projectHelper, $matchExpertUserService, false)['projects'];
  124. $recentlyViewedProjectCount = count($recentlyViewedProjects);
  125. $favouritesCount = count($favouriteProjects);
  126. $recentlyAcceptedCount = count($recentlyAcceptedProjects);
  127. // Fetch renewal projects and filter by active licence renewal terms to get accurate count
  128. $renewalProjectsForCount = $projectRepository->findRenewalsDueWithin30DaysForUser($userHelper);
  129. $eligibleRenewalProjects = $this->getEligibleRenewalProjects($renewalProjectsForCount, $renewalHelperService);
  130. $renewalsCount = count($eligibleRenewalProjects);
  131. // Create a combined array of all unique Projects fetched in this controller action.
  132. $uniqueProjects = array_unique(array_merge(
  133. $recentlyViewedProjects,
  134. $favouriteProjects,
  135. $recentlyAcceptedProjects,
  136. ));
  137. $grossTotalProjectsCount = count($uniqueProjects);
  138. // All Projects that have a renewal date
  139. $eligibleProjects = $this->getEligibleRenewalProjects($uniqueProjects, $renewalHelperService);
  140. // Get the latest active system notification
  141. $systemNotification = $systemNotificationRepository->findLatestSystemNotification();
  142. /**
  143. * Create a form we will use to allow the user to Filter this list
  144. */
  145. $filterForm = $this->createForm(MatterFilterType::class, null, [
  146. 'userHelper' => $userHelper,
  147. 'em' => $this->getDoctrine()->getManager(),
  148. ]);
  149. // Create and user a form to order this list
  150. $orderForm = $this->createForm(MatterOrderType::class);
  151. $isExpert = $user->isExpertOnly() || $user->isExpertViewerOnly();
  152. // Order A applies to all user roles, excluding the two expert user roles
  153. // Order B applies to the two expert user roles only
  154. $tabOrder = $isExpert ? 'order-b' : 'order-a';
  155. return $this->renderTurbo('Default/dashboard.html.twig', [
  156. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  157. 'filterForm' => $filterForm->createView(),
  158. 'orderForm' => $orderForm->createView(),
  159. 'systemNotification' => $systemNotification,
  160. 'analyticsEmbeddedReport' => $analyticsEmbeddedReport,
  161. 'analyticsEmbeddedReportMain' => $analyticsEmbeddedReportMain,
  162. 'recentlyViewedCount' => $recentlyViewedProjectCount,
  163. 'recentlyAcceptedCount' => $recentlyAcceptedCount,
  164. 'favouritesCount' => $favouritesCount,
  165. 'renewalsCount' => $renewalsCount,
  166. 'grossTotalProjectsCount' => $grossTotalProjectsCount,
  167. 'tabOrder' => $tabOrder,
  168. 'isExpert' => $isExpert,
  169. ]);
  170. }
  171. /**
  172. * List Matters most recently viewed
  173. *
  174. * @Template("Default/Partials/viewedTab.html.twig")
  175. *
  176. * @param RoleInvitationRepository $roleInvitationRepository
  177. * @param RoleParserService $roleParser
  178. * @param UserHelper $userHelper
  179. * @param ProjectRepository $projectRepository
  180. * @param RenewalHelperService $renewalHelperService
  181. * @param ProjectHelper $projectHelper
  182. * @param MatchExpertUser $matchExpertUserService
  183. *
  184. * @throws Exception
  185. *
  186. */
  187. public function dashboardViewedAjaxAction(
  188. UserHelper $userHelper,
  189. ProjectRepository $projectRepository,
  190. RenewalHelperService $renewalHelperService,
  191. MatchExpertUser $matchExpertUserService,
  192. ProjectHelper $projectHelper,
  193. ): JsonResponse|RedirectResponse|array {
  194. // Redirect to pending invitations page if User has pending role invitations.
  195. if ($this->userHasPendingInvitations()) {
  196. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  197. }
  198. $userHelper->setUser($this->getUser());
  199. $recentlyViewed = $this->getRecentlyViewedProjects($projectRepository, $userHelper, $projectHelper, $matchExpertUserService);
  200. $recentlyViewedProjects = $recentlyViewed['projects'];
  201. $userType = $recentlyViewed['userType'];
  202. // Return if no qualifying Projects
  203. if ($recentlyViewedProjects === []) {
  204. return [
  205. 'blurb' => 'You do not have any recently viewed matters.',
  206. ];
  207. }
  208. // All Projects that have a renewal date
  209. $eligibleProjects = $this->getEligibleRenewalProjects($recentlyViewedProjects, $renewalHelperService);
  210. return [
  211. 'projects' => $recentlyViewedProjects,
  212. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  213. 'blurb' => 'Most recently viewed matters (up to 20).',
  214. 'userType' => $userType,
  215. ];
  216. }
  217. /**
  218. * List the User's favourite Matters
  219. *
  220. * @Template("Default/Partials/favouritesTab.html.twig")
  221. *
  222. * @param Request $request
  223. * @param RenewalHelperService $renewalHelperService
  224. * @param ProjectHelper $projectHelper
  225. * @param MatchExpertUser $matchExpertUserService
  226. *
  227. * @throws Exception
  228. */
  229. public function dashboardFavouritesAjaxAction(Request $request, RenewalHelperService $renewalHelperService, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService): JsonResponse|RedirectResponse|array
  230. {
  231. // Redirect to pending invitations page if User has pending role invitations.
  232. if ($this->userHasPendingInvitations()) {
  233. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  234. }
  235. $favouriteProjectsArray = $this->getFavouriteProjects($projectHelper, $matchExpertUserService);
  236. $favouriteProjects = $favouriteProjectsArray['projects'];
  237. $userType = $favouriteProjectsArray['userType'];
  238. // Return if no qualifying Projects
  239. if ($favouriteProjects === []) {
  240. return [
  241. 'blurb' => 'You do not have any matters added as a favourite.',
  242. ];
  243. }
  244. // All favourite Projects that have a renewal date
  245. $eligibleProjects = $this->getEligibleRenewalProjects($favouriteProjects, $renewalHelperService);
  246. $projectCount = count($favouriteProjects);
  247. $blurbText = $projectCount === 1 ? '1 Matter added as a favourite.' : sprintf('%d %s', $projectCount, 'Matters added as a favourite.');
  248. // Paginate with a specific page and limit
  249. $pagination = $this->paginator->paginate(
  250. $favouriteProjects, // the array to paginate
  251. $request->query->getInt('page', 1), // current page number, default is 1
  252. 10 // items per page
  253. );
  254. return [
  255. 'pagination' => $pagination,
  256. 'totalProjectCount' => $projectCount,
  257. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  258. 'blurb' => $blurbText,
  259. 'userType' => $userType,
  260. ];
  261. }
  262. /**
  263. * List Matters accepted in the past 30 days
  264. *
  265. * @Template("Default/Partials/invitationsTab.html.twig")
  266. *
  267. * @param Request $request
  268. * @param RoleInvitationRepository $roleInvitationRepository
  269. * @param RoleParserService $roleParser
  270. * @param RenewalHelperService $renewalHelperService
  271. * @param ProjectHelper $projectHelper
  272. * @param MatchExpertUser $matchExpertUserService
  273. *
  274. * @throws Exception
  275. */
  276. public function dashboardInvitationsAjaxAction(
  277. Request $request,
  278. RoleInvitationRepository $roleInvitationRepository,
  279. RoleParserService $roleParser,
  280. RenewalHelperService $renewalHelperService,
  281. MatchExpertUser $matchExpertUserService,
  282. ProjectHelper $projectHelper
  283. ): JsonResponse|RedirectResponse|array {
  284. // Redirect to pending invitations page if User has pending role invitations.
  285. if ($this->userHasPendingInvitations()) {
  286. return $this->redirectWithAjaxSupport($this->pendingInvitationsUrlWithFlash());
  287. }
  288. $recentlyAcceptedArray = $this->getRecentlyAcceptedProjects($roleInvitationRepository, $roleParser, $projectHelper, $matchExpertUserService);
  289. $recentlyAccepted = $recentlyAcceptedArray['projects'];
  290. $userType = $recentlyAcceptedArray['userType'];
  291. // Return if no qualifying Projects
  292. if ($recentlyAccepted === []) {
  293. return [
  294. 'blurb' => 'You have not accepted any invitations in the last 30 days.',
  295. ];
  296. }
  297. // All recently accepted Projects that have a renewal date
  298. $eligibleProjects = $this->getEligibleRenewalProjects($recentlyAccepted, $renewalHelperService);
  299. $projectCount = count($recentlyAccepted);
  300. $blurbText = $projectCount === 1 ? '1 Invitation accepted in the last 30 days.' : sprintf('%d %s', $projectCount, 'Invitations accepted in the last 30 days.');
  301. // Paginate with a specific page and limit
  302. $pagination = $this->paginator->paginate(
  303. $recentlyAccepted, // the array to paginate
  304. $request->query->getInt('page', 1), // current page number, default is 1
  305. 10 // items per page
  306. );
  307. return [
  308. 'pagination' => $pagination,
  309. 'totalProjectCount' => $projectCount,
  310. 'eligibleProjectsToDisplayRenewalDate' => $eligibleProjects,
  311. 'blurb' => $blurbText,
  312. 'userType' => $userType,
  313. ];
  314. }
  315. /**
  316. * List Matters due for renewal
  317. *
  318. * @Template("Default/Partials/renewalsTab.html.twig")
  319. *
  320. * @param Request $request
  321. * @param ProjectRepository $projectRepository
  322. * @param UserHelper $userHelper
  323. * @param ProjectHelper $projectHelper
  324. * @param MatchExpertUser $matchExpertUserService
  325. *
  326. * @return array
  327. */
  328. public function dashboardRenewalsAjaxAction(Request $request, ProjectRepository $projectRepository, UserHelper $userHelper, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService): RedirectResponse|array
  329. {
  330. // Redirect to pending invitations page if User has pending role invitations.
  331. if ($this->userHasPendingInvitations()) {
  332. return $this->redirect($this->pendingInvitationsUrlWithFlash());
  333. }
  334. $userHelper->setUser($this->getUser());
  335. // All Projects that are due for renewal within the next 30 days
  336. $renewalProjects = $projectRepository->findRenewalsDueWithin30DaysForUser($userHelper);
  337. // Filter out projects whose accounts no longer have active licence renewal terms.
  338. // Use array_values(array_filter()) instead of getEligibleRenewalProjects() to preserve
  339. // the DB query's nextRenewalDate ASC sort order (soonest renewals first).
  340. $renewalProjects = array_values(array_filter(
  341. $renewalProjects,
  342. fn (Project $project): ?bool => $renewalHelperService->isMatterEligibleForRenewal($project)
  343. ));
  344. $projectCount = count($renewalProjects);
  345. $blurbText = $projectCount === 1 ? '1 Matter due for renewal within the next 30 days.' : sprintf('%d %s', $projectCount, 'Matters due for renewal within the next 30 days.');
  346. // Return if no qualifying Projects
  347. if (empty($renewalProjects)) {
  348. return [
  349. 'blurb' => 'There are no matters due for renewal in the next 30 days.',
  350. ];
  351. }
  352. // Paginate with a specific page and limit
  353. $pagination = $this->paginator->paginate(
  354. $renewalProjects, // the array to paginate
  355. $request->query->getInt('page', 1), // current page number, default is 1
  356. 10 // items per page
  357. );
  358. // Determine user type for displaying unread message counts (firm/expert)
  359. $userType = $matchExpertUserService->getUserType($this->getUser());
  360. // This sets the unread message counts on each Project entity
  361. $projectHelper->setUnreadMessageCount(iterator_to_array($pagination), $userType, $this->getUser());
  362. return [
  363. 'pagination' => $pagination,
  364. 'totalProjectCount' => $projectCount,
  365. 'eligibleProjectsToDisplayRenewalDate' => $renewalProjects,
  366. 'blurb' => $blurbText,
  367. 'userType' => $userType,
  368. ];
  369. }
  370. /**
  371. * @Template("Default/analytics.html.twig")
  372. *
  373. * @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
  374. *
  375. * @param AnalyticsService $analyticsService
  376. */
  377. public function analyticsAction(AnalyticsService $analyticsService): RedirectResponse|array
  378. {
  379. $analyticsEmbeddedReport = $analyticsService->generateAnalyticsEmbeddedReport($this->getUser(), AnalyticsReport::REPORT_TYPE_MAIN);
  380. if (!$analyticsEmbeddedReport instanceof AnalyticsEmbeddedReport) {
  381. $this->addFlash('warning', 'No Analytics Report for this case');
  382. return $this->redirectToRoute('infology_medbrief_dashboard');
  383. }
  384. return [
  385. 'analyticsEmbeddedReport' => $analyticsEmbeddedReport,
  386. ];
  387. }
  388. /**
  389. * This action is the click through from the email that is sent to a user asking
  390. * them to finalise their registration in order to enable their account
  391. *
  392. * @Template("Default/enableAccount.html.twig")
  393. *
  394. * @param Request $request
  395. * @param TokenStorageInterface $tokenStorage
  396. * @param EventDispatcherInterface $eventDispatcher
  397. * @param EntityManagerInterface $entityManager
  398. * @param UserPasswordEncoderInterface $userPasswordEncoder
  399. * @param UserHelper $userHelper
  400. *
  401. * @throws Exception
  402. */
  403. public function enableAccountAction(
  404. Request $request,
  405. TokenStorageInterface $tokenStorage,
  406. EventDispatcherInterface $eventDispatcher,
  407. EntityManagerInterface $entityManager,
  408. UserPasswordEncoderInterface $userPasswordEncoder,
  409. UserHelper $userHelper
  410. ): RedirectResponse|array {
  411. // if the user is logged in
  412. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  413. // log them out
  414. $tokenStorage->setToken();
  415. $request->getSession()->invalidate();
  416. // redirect them back here
  417. return $this->redirect($request->getRequestUri());
  418. }
  419. // grab the invitation code from the URL
  420. $invitationCode = $request->query->get('i');
  421. // if none was provided, we have a problem
  422. if (empty($invitationCode)) {
  423. throw $this->createNotFoundException('Invitation could not be found.');
  424. }
  425. // try to find a matching invitation
  426. $invitation = $this->getDoctrine()->getRepository(Invitation::class)->findOneByCode($invitationCode);
  427. // if we cant find one, we have a problem
  428. if (!$invitation) {
  429. throw $this->createNotFoundException('Unable to find Invitation.');
  430. }
  431. // try to find a user who matches this invitation
  432. $matchingUser = $entityManager->getRepository(User::class)
  433. ->findOneByEmail($invitation->getEmail())
  434. ;
  435. if (!$matchingUser) {
  436. throw $this->createNotFoundException('Unable to find User.');
  437. }
  438. // if the user is already enabled, then we assume this invitation has been processed already
  439. if ($matchingUser->isEnabled()) {
  440. return $this->redirectToRoute('infology_medbrief_invitation_already_processed');
  441. }
  442. // if we get here then we have found a user who is not yet enabled and may now complete their registration
  443. $form = $this->createForm(ChangePasswordFormType::class, $matchingUser, [
  444. 'action' => $this->generateUrl('infology_medbrief_user_complete_registration', ['id' => $matchingUser->getId()]),
  445. ]);
  446. $form->handleRequest($request);
  447. if ($form->isSubmitted() && $form->isValid()) {
  448. // Accept the Medbrief policies
  449. $userHelper->acceptDeclineMedbriefPolicies($matchingUser);
  450. $encodedPassword = $userPasswordEncoder->encodePassword(
  451. $matchingUser,
  452. $form->get('plainPassword')->getData()
  453. );
  454. // Set the password
  455. $matchingUser->setPassword($encodedPassword);
  456. // set the user enabled now
  457. $matchingUser->setEnabled(true);
  458. $entityManager->flush();
  459. // log the user in
  460. $token = new PostAuthenticationGuardToken($matchingUser, 'main', $matchingUser->getRoles());
  461. $tokenStorage->setToken($token); //now the user is logged in
  462. //now dispatch the login event
  463. $eventDispatcher->dispatch(
  464. new InteractiveLoginEvent($request, $token),
  465. 'security.interactive_login'
  466. );
  467. // Determine if th user requires 2FA to be enabled (if they don't already) and store it in the session
  468. // to prevent navigating to any part of the site, except the 2FA enable page, or logout page.
  469. // @see MedBrief\MSR\EventSubscriber\Enforce2FASubscriber
  470. /** @var User $user */
  471. $user = $this->getUser();
  472. $userHelper->setUser($user);
  473. if ($userHelper->twoFactoryAuthEnabledUser() && !$user->hasMultiMfaEnabled()) {
  474. $request->getSession()->set('enforce-two-factor-authentication-detail', [
  475. '2faRequired' => true,
  476. 'targetPath' => null,
  477. ]);
  478. return $this->redirectToRoute('mfa_enable_force');
  479. }
  480. // If the user has 2FA enabled and is rate limited, redirect them to the login page.
  481. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('rateLimited')) {
  482. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times. Please try again in five minutes.');
  483. return $this->redirectToRoute('logout');
  484. }
  485. // If the user has 2FA enabled and their IP Address has been blacklisted, redirect them to the login page.
  486. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('ipBlacklisted')) {
  487. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times from this IP address. Please try again in fifteen minutes.');
  488. return $this->redirectToRoute('logout');
  489. }
  490. return $this->redirectToRoute('infology_medbrief_dashboard');
  491. }
  492. return [
  493. 'user' => $matchingUser,
  494. 'form' => $form->createView(),
  495. ];
  496. }
  497. /**
  498. * Is the user logged in?
  499. *
  500. *
  501. *
  502. * @param Request $request
  503. *
  504. * @return JsonResponse 1/0
  505. */
  506. public function isUserLoggedInAction(Request $request)
  507. {
  508. // if the user is currently logged in
  509. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  510. return new JsonResponse(['status' => 1], 200);
  511. }
  512. return new JsonResponse(['status' => 0], 200);
  513. }
  514. /**
  515. * Refresh the session upon user wishing to remain logged in.
  516. *
  517. *
  518. *
  519. * @param Request $request
  520. *
  521. * @return JsonResponse
  522. */
  523. public function refreshSessionAction(Request $request)
  524. {
  525. // if the user is currently logged in
  526. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  527. // Extend the session
  528. try {
  529. $cookie_lifetime = ini_get('session.cookie_lifetime');
  530. if ($cookie_lifetime === false || $cookie_lifetime === '') {
  531. throw new Exception('session.cookie_lifetime not found!');
  532. }
  533. $lifeTime = (int) $cookie_lifetime;
  534. } catch (Exception) {
  535. $lifeTime = $request->getSession()->getMetadataBag()->getLifetime();
  536. }
  537. $request->getSession()->getMetadataBag()->stampNew($lifeTime);
  538. $now = new DateTime();
  539. $expire = $request->getSession()->getMetadataBag()->getCreated() + $request->getSession()->getMetadataBag()->getLifetime();
  540. $response = new JsonResponse([
  541. 'sessionExpire' => $expire,
  542. ]);
  543. $sessionName = $this->getParameter('session.name');
  544. $sessionCookieValue = $request->cookies->get($sessionName);
  545. $sessionCookie = Cookie::create($sessionName, $sessionCookieValue, $expire);
  546. $response->headers->setCookie($sessionCookie);
  547. return $response;
  548. }
  549. return new JsonResponse([], 404);
  550. }
  551. /**
  552. * Refresh the session upon user wishing to remain logged in.
  553. *
  554. *
  555. *
  556. * @param Request $request
  557. *
  558. * @return JsonResponse
  559. */
  560. public function refreshUserSessionAction(Request $request)
  561. {
  562. // if the user is currently logged in
  563. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  564. // Expiration time
  565. $expire = time() + (20 * 60 * 1000);
  566. return new JsonResponse([
  567. 'sessionExpire' => $expire,
  568. ]);
  569. }
  570. return new JsonResponse(['status' => 0], Response::HTTP_FORBIDDEN);
  571. }
  572. /**
  573. * Redirect the user to the login page with a message
  574. *
  575. * Used by the session timeout feature to logout inactive users while preserving
  576. * the return URL so they can resume where they left off after logging back in.
  577. *
  578. * @param Request $request
  579. */
  580. public function userInactivityLoginRedirectAction(Request $request): RedirectResponse
  581. {
  582. $this->addFlash('warning', 'You have been automatically logged out due to inactivity');
  583. // Get the return URL from the query parameter (passed from JavaScript)
  584. $returnUrl = $request->query->get('returnUrl');
  585. // Note: Uses 'logout' route (not legacy 'fos_user_security_logout' which no longer exists)
  586. $response = $this->redirectToRoute('logout');
  587. // Store return URL in a cookie that survives logout (session is cleared on logout)
  588. // The login authenticator will read this cookie and redirect after successful login
  589. if ($returnUrl) {
  590. $response->headers->setCookie(
  591. new Cookie(
  592. 'inactivity_return_url',
  593. $returnUrl,
  594. time() + 3600, // 1 hour expiry
  595. '/',
  596. null,
  597. $request->isSecure(),
  598. true, // httpOnly
  599. false,
  600. Cookie::SAMESITE_LAX
  601. )
  602. );
  603. }
  604. return $response;
  605. }
  606. /**
  607. * Helper function which determines if the user is using a deprecated browser.
  608. *
  609. *
  610. *
  611. * @param Request $request
  612. * @param DeviceDetectorService $deviceDetector
  613. *
  614. * @return bool
  615. */
  616. protected function isDeprecatedBrowser(Request $request, DeviceDetectorService $deviceDetector)
  617. {
  618. $dci = $deviceDetector->getClientInfoFromRequest($request);
  619. if (is_array($dci)) {
  620. // Check if the browser matches IE10 or lower (which we won't support in future).
  621. return $dci['type'] === 'browser' && $dci['short_name'] === 'IE' && floor($dci['version']) <= 10;
  622. }
  623. // If we didn't get an array of results, this is probably an automated action
  624. return true;
  625. }
  626. /**
  627. * Helper function to determine whether the User has any pending Role Invitations
  628. */
  629. private function userHasPendingInvitations(): bool
  630. {
  631. return $this->getUser()->hasUnsuppressedRoleInvitationsPendingApproval();
  632. }
  633. /**
  634. * Generate the Url to the role invitation listing page and
  635. * add a flash message to current session
  636. */
  637. private function pendingInvitationsUrlWithFlash(): string
  638. {
  639. $this->addFlash('info', 'You have pending invitations. Before you continue, please accept or decline all of your invitations.');
  640. return $this->generateUrl('infology_medbrief_role_invitation_list_pending');
  641. }
  642. /**
  643. * Generate the Url to the role invitation listing page without adding a flash message to current session
  644. * Used for AJAX calls where we want to redirect the user but not show a flash message as well
  645. */
  646. private function pendingInvitationsUrlWithoutFlash(): string
  647. {
  648. return $this->generateUrl('infology_medbrief_role_invitation_list_pending');
  649. }
  650. /**
  651. * Get all Projects the User has recently viewed
  652. *
  653. * @param ProjectRepository $projectRepository
  654. * @param UserHelper $userHelper
  655. * @param ProjectHelper $projectHelper
  656. * @param int $limit
  657. * @param bool $calculateUnreadMessages
  658. * @param MatchExpertUser $matchExpertUserService
  659. *
  660. * @return array ['projects' => Project[], 'userType' => string|null]
  661. */
  662. private function getRecentlyViewedProjects(
  663. ProjectRepository $projectRepository,
  664. UserHelper $userHelper,
  665. ProjectHelper $projectHelper,
  666. MatchExpertUser $matchExpertUserService,
  667. bool $calculateUnreadMessages = true,
  668. int $limit = 20
  669. ): array {
  670. $projects = $projectRepository->findRecentlyAccessedByUser($userHelper, $limit);
  671. // if calculating unread messages, set them on each Project entity
  672. if ($calculateUnreadMessages) {
  673. // Determine user type for displaying unread message counts (firm/expert)
  674. $userType = $matchExpertUserService->getUserType($this->getUser());
  675. // This sets the unread message counts on each Project entity
  676. $projectHelper->setUnreadMessageCount(iterator_to_array($projects), $userType, $userHelper->getUser());
  677. return ['projects' => $projects, 'userType' => $userType];
  678. }
  679. return ['projects' => $projects, 'userType' => null];
  680. }
  681. /**
  682. * Get all Projects the User has recently accepted
  683. *
  684. * @param RoleInvitationRepository $roleInvitationRepository
  685. * @param RoleParserService $roleParser
  686. * @param ProjectHelper $projectHelper
  687. * @param bool $calculateUnreadMessages
  688. * @param MatchExpertUser $matchExpertUserService
  689. *
  690. * @return array ['projects' => Project[], 'userType' => string|null]
  691. */
  692. private function getRecentlyAcceptedProjects(RoleInvitationRepository $roleInvitationRepository, RoleParserService $roleParser, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  693. {
  694. /**
  695. * Not Accessed
  696. */
  697. $recentlyAccepted = $roleInvitationRepository->findLatestProjectRoleInvitationsByUser($this->getUser());
  698. // Map Role Invitations to obtain Projects via the RoleParserService
  699. $recentlyAccepted = array_map(fn (RoleInvitation $roleInvitation)
  700. => $roleParser->parseRole($roleInvitation)->getSubject(), $recentlyAccepted);
  701. // Filter out Projects with any type of deleted or archived status
  702. $recentlyAccepted = array_filter($recentlyAccepted, fn ($project): bool => !$project->isCloseInProgressOrComplete());
  703. // if calculating unread messages, set them on each Project entity
  704. if ($calculateUnreadMessages) {
  705. // Determine user type for displaying unread message counts (firm/expert)
  706. $userType = $matchExpertUserService->getUserType($this->getUser());
  707. // This sets the unread message counts on each Project entity
  708. $projectHelper->setUnreadMessageCount(iterator_to_array($recentlyAccepted), $userType, $this->getUser());
  709. return ['projects' => $recentlyAccepted, 'userType' => $userType];
  710. }
  711. return ['projects' => $recentlyAccepted, 'userType' => null];
  712. }
  713. /**
  714. * Get all the User's favourite Projects
  715. *
  716. * @param ProjectHelper $projectHelper
  717. * @param bool $calculateUnreadMessages
  718. * @param MatchExpertUser $matchExpertUserService
  719. *
  720. * @return array ['projects' => Project[], 'userType' => string|null]
  721. */
  722. private function getFavouriteProjects(ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  723. {
  724. $favouriteProjects = $this->getUser()->getFavouriteProjects()->toArray();
  725. // Filter out deleted and archived Projects
  726. $favouriteProjects = array_filter($favouriteProjects, fn ($project): bool => !$project->isDeleteStatusComplete() && !$project->isArchiveStatusComplete());
  727. // Converts favourite projects to project_id's and removes ones where access is no longer granted
  728. foreach ($favouriteProjects as $key => $project) {
  729. if (!$this->isGranted('READ', $project)) {
  730. unset($favouriteProjects[$key]);
  731. }
  732. }
  733. // if calculating unread messages, set them on each Project entity
  734. if ($calculateUnreadMessages) {
  735. // Determine user type for displaying unread message counts (firm/expert)
  736. $userType = $matchExpertUserService->getUserType($this->getUser());
  737. // This sets the unread message counts on each Project entity
  738. $projectHelper->setUnreadMessageCount(iterator_to_array($favouriteProjects), $userType, $this->getUser());
  739. return ['projects' => $favouriteProjects, 'userType' => $userType];
  740. }
  741. // TODO: create a UserProject Entity to store the date when the Project was favourited
  742. return ['projects' => $favouriteProjects, 'userType' => null];
  743. }
  744. /**
  745. * @param Project[] $projects
  746. * @param RenewalHelperService $renewalHelperService
  747. *
  748. * @return Projects[]|[]
  749. */
  750. private function getEligibleRenewalProjects(array $projects, RenewalHelperService $renewalHelperService): array
  751. {
  752. $eligibleProjectsToDisplayRenewalDate = array_filter($projects, fn ($project): ?bool => $renewalHelperService->isMatterEligibleForRenewal($project));
  753. return $this->sortProjectsByDateOrder($eligibleProjectsToDisplayRenewalDate, 'descending');
  754. }
  755. /**
  756. * Sort an array of Projects by their Updated date in specified order.
  757. *
  758. *
  759. *
  760. * @param array $projects
  761. * @param string $order
  762. *
  763. * @return array $projects
  764. */
  765. private function sortProjectsByDateOrder(array $projects, string $order = 'descending'): array
  766. {
  767. // Use usort to sort the array using the custom comparison function
  768. if ($order === 'descending') {
  769. usort($projects, self::compareByDateDescending(...));
  770. } else {
  771. usort($projects, self::compareByDateAscending(...));
  772. }
  773. return $projects;
  774. }
  775. /**
  776. * Callback function for the comparison of two Projects by Updated date Descending.
  777. *
  778. * @param Project $project1
  779. * @param Project $project2
  780. */
  781. private function compareByDateDescending($project1, $project2): bool
  782. {
  783. $date1 = $project1->getUpdated();
  784. $date2 = $project2->getUpdated();
  785. return $date2 <=> $date1;
  786. }
  787. /**
  788. * Callback function for the comparison of two Projects by Updated date Ascending.
  789. *
  790. * @param Project $project1
  791. * @param Project $project2
  792. */
  793. private function compareByDateAscending($project1, $project2): bool
  794. {
  795. $date1 = $project1->getUpdated();
  796. $date2 = $project2->getUpdated();
  797. return $date1 <=> $date2;
  798. }
  799. }