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. * @param RenewalHelperService $renewalHelperService
  326. *
  327. * @return array
  328. */
  329. public function dashboardRenewalsAjaxAction(Request $request, ProjectRepository $projectRepository, UserHelper $userHelper, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, RenewalHelperService $renewalHelperService): RedirectResponse|array
  330. {
  331. // Redirect to pending invitations page if User has pending role invitations.
  332. if ($this->userHasPendingInvitations()) {
  333. return $this->redirect($this->pendingInvitationsUrlWithFlash());
  334. }
  335. $userHelper->setUser($this->getUser());
  336. // All Projects that are due for renewal within the next 30 days
  337. $renewalProjects = $projectRepository->findRenewalsDueWithin30DaysForUser($userHelper);
  338. // Filter out projects whose accounts no longer have active licence renewal terms.
  339. // Use array_values(array_filter()) instead of getEligibleRenewalProjects() to preserve
  340. // the DB query's nextRenewalDate ASC sort order (soonest renewals first).
  341. $renewalProjects = array_values(array_filter(
  342. $renewalProjects,
  343. fn (Project $project): ?bool => $renewalHelperService->isMatterEligibleForRenewal($project)
  344. ));
  345. $projectCount = count($renewalProjects);
  346. $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.');
  347. // Return if no qualifying Projects
  348. if (empty($renewalProjects)) {
  349. return [
  350. 'blurb' => 'There are no matters due for renewal in the next 30 days.',
  351. ];
  352. }
  353. // Paginate with a specific page and limit
  354. $pagination = $this->paginator->paginate(
  355. $renewalProjects, // the array to paginate
  356. $request->query->getInt('page', 1), // current page number, default is 1
  357. 10 // items per page
  358. );
  359. // Determine user type for displaying unread message counts (firm/expert)
  360. $userType = $matchExpertUserService->getUserType($this->getUser());
  361. // This sets the unread message counts on each Project entity
  362. $projectHelper->setUnreadMessageCount(iterator_to_array($pagination), $userType, $this->getUser());
  363. return [
  364. 'pagination' => $pagination,
  365. 'totalProjectCount' => $projectCount,
  366. 'eligibleProjectsToDisplayRenewalDate' => $renewalProjects,
  367. 'blurb' => $blurbText,
  368. 'userType' => $userType,
  369. ];
  370. }
  371. /**
  372. * @Template("Default/analytics.html.twig")
  373. *
  374. * @Security("is_granted('IS_AUTHENTICATED_REMEMBERED')")
  375. *
  376. * @param AnalyticsService $analyticsService
  377. */
  378. public function analyticsAction(AnalyticsService $analyticsService): RedirectResponse|array
  379. {
  380. $analyticsEmbeddedReport = $analyticsService->generateAnalyticsEmbeddedReport($this->getUser(), AnalyticsReport::REPORT_TYPE_MAIN);
  381. if (!$analyticsEmbeddedReport instanceof AnalyticsEmbeddedReport) {
  382. $this->addFlash('warning', 'No Analytics Report for this case');
  383. return $this->redirectToRoute('infology_medbrief_dashboard');
  384. }
  385. return [
  386. 'analyticsEmbeddedReport' => $analyticsEmbeddedReport,
  387. ];
  388. }
  389. /**
  390. * This action is the click through from the email that is sent to a user asking
  391. * them to finalise their registration in order to enable their account
  392. *
  393. * @Template("Default/enableAccount.html.twig")
  394. *
  395. * @param Request $request
  396. * @param TokenStorageInterface $tokenStorage
  397. * @param EventDispatcherInterface $eventDispatcher
  398. * @param EntityManagerInterface $entityManager
  399. * @param UserPasswordEncoderInterface $userPasswordEncoder
  400. * @param UserHelper $userHelper
  401. *
  402. * @throws Exception
  403. */
  404. public function enableAccountAction(
  405. Request $request,
  406. TokenStorageInterface $tokenStorage,
  407. EventDispatcherInterface $eventDispatcher,
  408. EntityManagerInterface $entityManager,
  409. UserPasswordEncoderInterface $userPasswordEncoder,
  410. UserHelper $userHelper
  411. ): RedirectResponse|array {
  412. // if the user is logged in
  413. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  414. // log them out
  415. $tokenStorage->setToken();
  416. $request->getSession()->invalidate();
  417. // redirect them back here
  418. return $this->redirect($request->getRequestUri());
  419. }
  420. // grab the invitation code from the URL
  421. $invitationCode = $request->query->get('i');
  422. // if none was provided, we have a problem
  423. if (empty($invitationCode)) {
  424. throw $this->createNotFoundException('Invitation could not be found.');
  425. }
  426. // try to find a matching invitation
  427. $invitation = $this->getDoctrine()->getRepository(Invitation::class)->findOneByCode($invitationCode);
  428. // if we cant find one, we have a problem
  429. if (!$invitation) {
  430. throw $this->createNotFoundException('Unable to find Invitation.');
  431. }
  432. // try to find a user who matches this invitation
  433. $matchingUser = $entityManager->getRepository(User::class)
  434. ->findOneByEmail($invitation->getEmail())
  435. ;
  436. if (!$matchingUser) {
  437. throw $this->createNotFoundException('Unable to find User.');
  438. }
  439. // if the user is already enabled, then we assume this invitation has been processed already
  440. if ($matchingUser->isEnabled()) {
  441. return $this->redirectToRoute('infology_medbrief_invitation_already_processed');
  442. }
  443. // if we get here then we have found a user who is not yet enabled and may now complete their registration
  444. $form = $this->createForm(ChangePasswordFormType::class, $matchingUser, [
  445. 'action' => $this->generateUrl('infology_medbrief_user_complete_registration', ['id' => $matchingUser->getId()]),
  446. ]);
  447. $form->handleRequest($request);
  448. if ($form->isSubmitted() && $form->isValid()) {
  449. // Accept the Medbrief policies
  450. $userHelper->acceptDeclineMedbriefPolicies($matchingUser);
  451. $encodedPassword = $userPasswordEncoder->encodePassword(
  452. $matchingUser,
  453. $form->get('plainPassword')->getData()
  454. );
  455. // Set the password
  456. $matchingUser->setPassword($encodedPassword);
  457. // set the user enabled now
  458. $matchingUser->setEnabled(true);
  459. $entityManager->flush();
  460. // log the user in
  461. $token = new PostAuthenticationGuardToken($matchingUser, 'main', $matchingUser->getRoles());
  462. $tokenStorage->setToken($token); //now the user is logged in
  463. //now dispatch the login event
  464. $eventDispatcher->dispatch(
  465. new InteractiveLoginEvent($request, $token),
  466. 'security.interactive_login'
  467. );
  468. // Determine if th user requires 2FA to be enabled (if they don't already) and store it in the session
  469. // to prevent navigating to any part of the site, except the 2FA enable page, or logout page.
  470. // @see MedBrief\MSR\EventSubscriber\Enforce2FASubscriber
  471. /** @var User $user */
  472. $user = $this->getUser();
  473. $userHelper->setUser($user);
  474. if ($userHelper->twoFactoryAuthEnabledUser() && !$user->hasMultiMfaEnabled()) {
  475. $request->getSession()->set('enforce-two-factor-authentication-detail', [
  476. '2faRequired' => true,
  477. 'targetPath' => null,
  478. ]);
  479. return $this->redirectToRoute('mfa_enable_force');
  480. }
  481. // If the user has 2FA enabled and is rate limited, redirect them to the login page.
  482. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('rateLimited')) {
  483. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times. Please try again in five minutes.');
  484. return $this->redirectToRoute('logout');
  485. }
  486. // If the user has 2FA enabled and their IP Address has been blacklisted, redirect them to the login page.
  487. if ($user->hasMultiMfaEnabled() && $request->getSession()->has('ipBlacklisted')) {
  488. $this->addFlash('danger', 'You have entered the incorrect authentication code too many times from this IP address. Please try again in fifteen minutes.');
  489. return $this->redirectToRoute('logout');
  490. }
  491. return $this->redirectToRoute('infology_medbrief_dashboard');
  492. }
  493. return [
  494. 'user' => $matchingUser,
  495. 'form' => $form->createView(),
  496. ];
  497. }
  498. /**
  499. * Is the user logged in?
  500. *
  501. *
  502. *
  503. * @param Request $request
  504. *
  505. * @return JsonResponse 1/0
  506. */
  507. public function isUserLoggedInAction(Request $request)
  508. {
  509. // if the user is currently logged in
  510. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  511. return new JsonResponse(['status' => 1], 200);
  512. }
  513. return new JsonResponse(['status' => 0], 200);
  514. }
  515. /**
  516. * Refresh the session upon user wishing to remain logged in.
  517. *
  518. *
  519. *
  520. * @param Request $request
  521. *
  522. * @return JsonResponse
  523. */
  524. public function refreshSessionAction(Request $request)
  525. {
  526. // if the user is currently logged in
  527. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  528. // Extend the session
  529. try {
  530. $cookie_lifetime = ini_get('session.cookie_lifetime');
  531. if ($cookie_lifetime === false || $cookie_lifetime === '') {
  532. throw new Exception('session.cookie_lifetime not found!');
  533. }
  534. $lifeTime = (int) $cookie_lifetime;
  535. } catch (Exception) {
  536. $lifeTime = $request->getSession()->getMetadataBag()->getLifetime();
  537. }
  538. $request->getSession()->getMetadataBag()->stampNew($lifeTime);
  539. $now = new DateTime();
  540. $expire = $request->getSession()->getMetadataBag()->getCreated() + $request->getSession()->getMetadataBag()->getLifetime();
  541. $response = new JsonResponse([
  542. 'sessionExpire' => $expire,
  543. ]);
  544. $sessionName = $this->getParameter('session.name');
  545. $sessionCookieValue = $request->cookies->get($sessionName);
  546. $sessionCookie = Cookie::create($sessionName, $sessionCookieValue, $expire);
  547. $response->headers->setCookie($sessionCookie);
  548. return $response;
  549. }
  550. return new JsonResponse([], 404);
  551. }
  552. /**
  553. * Refresh the session upon user wishing to remain logged in.
  554. *
  555. *
  556. *
  557. * @param Request $request
  558. *
  559. * @return JsonResponse
  560. */
  561. public function refreshUserSessionAction(Request $request)
  562. {
  563. // if the user is currently logged in
  564. if ($this->isGranted('IS_AUTHENTICATED_REMEMBERED')) {
  565. // Expiration time
  566. $expire = time() + (20 * 60 * 1000);
  567. return new JsonResponse([
  568. 'sessionExpire' => $expire,
  569. ]);
  570. }
  571. return new JsonResponse(['status' => 0], Response::HTTP_FORBIDDEN);
  572. }
  573. /**
  574. * Redirect the user to the login page with a message
  575. *
  576. * Used by the session timeout feature to logout inactive users while preserving
  577. * the return URL so they can resume where they left off after logging back in.
  578. *
  579. * @param Request $request
  580. */
  581. public function userInactivityLoginRedirectAction(Request $request): RedirectResponse
  582. {
  583. $this->addFlash('warning', 'You have been automatically logged out due to inactivity');
  584. // Get the return URL from the query parameter (passed from JavaScript)
  585. $returnUrl = $request->query->get('returnUrl');
  586. // Note: Uses 'logout' route (not legacy 'fos_user_security_logout' which no longer exists)
  587. $response = $this->redirectToRoute('logout');
  588. // Store return URL in a cookie that survives logout (session is cleared on logout)
  589. // The login authenticator will read this cookie and redirect after successful login
  590. if ($returnUrl) {
  591. $response->headers->setCookie(
  592. new Cookie(
  593. 'inactivity_return_url',
  594. $returnUrl,
  595. time() + 3600, // 1 hour expiry
  596. '/',
  597. null,
  598. $request->isSecure(),
  599. true, // httpOnly
  600. false,
  601. Cookie::SAMESITE_LAX
  602. )
  603. );
  604. }
  605. return $response;
  606. }
  607. /**
  608. * Helper function which determines if the user is using a deprecated browser.
  609. *
  610. *
  611. *
  612. * @param Request $request
  613. * @param DeviceDetectorService $deviceDetector
  614. *
  615. * @return bool
  616. */
  617. protected function isDeprecatedBrowser(Request $request, DeviceDetectorService $deviceDetector)
  618. {
  619. $dci = $deviceDetector->getClientInfoFromRequest($request);
  620. if (is_array($dci)) {
  621. // Check if the browser matches IE10 or lower (which we won't support in future).
  622. return $dci['type'] === 'browser' && $dci['short_name'] === 'IE' && floor($dci['version']) <= 10;
  623. }
  624. // If we didn't get an array of results, this is probably an automated action
  625. return true;
  626. }
  627. /**
  628. * Helper function to determine whether the User has any pending Role Invitations
  629. */
  630. private function userHasPendingInvitations(): bool
  631. {
  632. return $this->getUser()->hasUnsuppressedRoleInvitationsPendingApproval();
  633. }
  634. /**
  635. * Generate the Url to the role invitation listing page and
  636. * add a flash message to current session
  637. */
  638. private function pendingInvitationsUrlWithFlash(): string
  639. {
  640. $this->addFlash('info', 'You have pending invitations. Before you continue, please accept or decline all of your invitations.');
  641. return $this->generateUrl('infology_medbrief_role_invitation_list_pending');
  642. }
  643. /**
  644. * Generate the Url to the role invitation listing page without adding a flash message to current session
  645. * Used for AJAX calls where we want to redirect the user but not show a flash message as well
  646. */
  647. private function pendingInvitationsUrlWithoutFlash(): string
  648. {
  649. return $this->generateUrl('infology_medbrief_role_invitation_list_pending');
  650. }
  651. /**
  652. * Get all Projects the User has recently viewed
  653. *
  654. * @param ProjectRepository $projectRepository
  655. * @param UserHelper $userHelper
  656. * @param ProjectHelper $projectHelper
  657. * @param int $limit
  658. * @param bool $calculateUnreadMessages
  659. * @param MatchExpertUser $matchExpertUserService
  660. *
  661. * @return array ['projects' => Project[], 'userType' => string|null]
  662. */
  663. private function getRecentlyViewedProjects(
  664. ProjectRepository $projectRepository,
  665. UserHelper $userHelper,
  666. ProjectHelper $projectHelper,
  667. MatchExpertUser $matchExpertUserService,
  668. bool $calculateUnreadMessages = true,
  669. int $limit = 20
  670. ): array {
  671. $projects = $projectRepository->findRecentlyAccessedByUser($userHelper, $limit);
  672. // if calculating unread messages, set them on each Project entity
  673. if ($calculateUnreadMessages) {
  674. // Determine user type for displaying unread message counts (firm/expert)
  675. $userType = $matchExpertUserService->getUserType($this->getUser());
  676. // This sets the unread message counts on each Project entity
  677. $projectHelper->setUnreadMessageCount(iterator_to_array($projects), $userType, $userHelper->getUser());
  678. return ['projects' => $projects, 'userType' => $userType];
  679. }
  680. return ['projects' => $projects, 'userType' => null];
  681. }
  682. /**
  683. * Get all Projects the User has recently accepted
  684. *
  685. * @param RoleInvitationRepository $roleInvitationRepository
  686. * @param RoleParserService $roleParser
  687. * @param ProjectHelper $projectHelper
  688. * @param bool $calculateUnreadMessages
  689. * @param MatchExpertUser $matchExpertUserService
  690. *
  691. * @return array ['projects' => Project[], 'userType' => string|null]
  692. */
  693. private function getRecentlyAcceptedProjects(RoleInvitationRepository $roleInvitationRepository, RoleParserService $roleParser, ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  694. {
  695. /**
  696. * Not Accessed
  697. */
  698. $recentlyAccepted = $roleInvitationRepository->findLatestProjectRoleInvitationsByUser($this->getUser());
  699. // Map Role Invitations to obtain Projects via the RoleParserService
  700. $recentlyAccepted = array_map(fn (RoleInvitation $roleInvitation)
  701. => $roleParser->parseRole($roleInvitation)->getSubject(), $recentlyAccepted);
  702. // Filter out Projects with any type of deleted or archived status
  703. $recentlyAccepted = array_filter($recentlyAccepted, fn ($project): bool => !$project->isCloseInProgressOrComplete());
  704. // if calculating unread messages, set them on each Project entity
  705. if ($calculateUnreadMessages) {
  706. // Determine user type for displaying unread message counts (firm/expert)
  707. $userType = $matchExpertUserService->getUserType($this->getUser());
  708. // This sets the unread message counts on each Project entity
  709. $projectHelper->setUnreadMessageCount(iterator_to_array($recentlyAccepted), $userType, $this->getUser());
  710. return ['projects' => $recentlyAccepted, 'userType' => $userType];
  711. }
  712. return ['projects' => $recentlyAccepted, 'userType' => null];
  713. }
  714. /**
  715. * Get all the User's favourite Projects
  716. *
  717. * @param ProjectHelper $projectHelper
  718. * @param bool $calculateUnreadMessages
  719. * @param MatchExpertUser $matchExpertUserService
  720. *
  721. * @return array ['projects' => Project[], 'userType' => string|null]
  722. */
  723. private function getFavouriteProjects(ProjectHelper $projectHelper, MatchExpertUser $matchExpertUserService, bool $calculateUnreadMessages = true): array
  724. {
  725. $favouriteProjects = $this->getUser()->getFavouriteProjects()->toArray();
  726. // Filter out deleted and archived Projects
  727. $favouriteProjects = array_filter($favouriteProjects, fn ($project): bool => !$project->isDeleteStatusComplete() && !$project->isArchiveStatusComplete());
  728. // Converts favourite projects to project_id's and removes ones where access is no longer granted
  729. foreach ($favouriteProjects as $key => $project) {
  730. if (!$this->isGranted('READ', $project)) {
  731. unset($favouriteProjects[$key]);
  732. }
  733. }
  734. // if calculating unread messages, set them on each Project entity
  735. if ($calculateUnreadMessages) {
  736. // Determine user type for displaying unread message counts (firm/expert)
  737. $userType = $matchExpertUserService->getUserType($this->getUser());
  738. // This sets the unread message counts on each Project entity
  739. $projectHelper->setUnreadMessageCount(iterator_to_array($favouriteProjects), $userType, $this->getUser());
  740. return ['projects' => $favouriteProjects, 'userType' => $userType];
  741. }
  742. // TODO: create a UserProject Entity to store the date when the Project was favourited
  743. return ['projects' => $favouriteProjects, 'userType' => null];
  744. }
  745. /**
  746. * @param Project[] $projects
  747. * @param RenewalHelperService $renewalHelperService
  748. *
  749. * @return Projects[]|[]
  750. */
  751. private function getEligibleRenewalProjects(array $projects, RenewalHelperService $renewalHelperService): array
  752. {
  753. $eligibleProjectsToDisplayRenewalDate = array_filter($projects, fn ($project): ?bool => $renewalHelperService->isMatterEligibleForRenewal($project));
  754. return $this->sortProjectsByDateOrder($eligibleProjectsToDisplayRenewalDate, 'descending');
  755. }
  756. /**
  757. * Sort an array of Projects by their Updated date in specified order.
  758. *
  759. *
  760. *
  761. * @param array $projects
  762. * @param string $order
  763. *
  764. * @return array $projects
  765. */
  766. private function sortProjectsByDateOrder(array $projects, string $order = 'descending'): array
  767. {
  768. // Use usort to sort the array using the custom comparison function
  769. if ($order === 'descending') {
  770. usort($projects, self::compareByDateDescending(...));
  771. } else {
  772. usort($projects, self::compareByDateAscending(...));
  773. }
  774. return $projects;
  775. }
  776. /**
  777. * Callback function for the comparison of two Projects by Updated date Descending.
  778. *
  779. * @param Project $project1
  780. * @param Project $project2
  781. */
  782. private function compareByDateDescending($project1, $project2): bool
  783. {
  784. $date1 = $project1->getUpdated();
  785. $date2 = $project2->getUpdated();
  786. return $date2 <=> $date1;
  787. }
  788. /**
  789. * Callback function for the comparison of two Projects by Updated date Ascending.
  790. *
  791. * @param Project $project1
  792. * @param Project $project2
  793. */
  794. private function compareByDateAscending($project1, $project2): bool
  795. {
  796. $date1 = $project1->getUpdated();
  797. $date2 = $project2->getUpdated();
  798. return $date1 <=> $date2;
  799. }
  800. }