Skip to content

Commit e772d27

Browse files
Merge pull request #15 from aligent/feature/BEG-223_Add_session_expiry_warning_modal
BEG-223: Add session expiry warning modal
2 parents 0abf7fd + 47a5169 commit e772d27

File tree

5 files changed

+329
-0
lines changed

5 files changed

+329
-0
lines changed
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Aligent\Pci4Compatibility\Controller\Adminhtml\Session;
6+
7+
use Magento\Backend\App\Action;
8+
use Magento\Backend\App\Action\Context;
9+
use Magento\Backend\Model\Auth\Session as AuthSession;
10+
use Magento\Framework\App\Action\HttpPostActionInterface;
11+
use Magento\Framework\Controller\Result\Json;
12+
use Magento\Framework\Controller\Result\JsonFactory;
13+
use Magento\Framework\Data\Form\FormKey\Validator as FormKeyValidator;
14+
use Magento\Framework\Exception\LocalizedException;
15+
use Psr\Log\LoggerInterface;
16+
17+
/**
18+
* Controller to extend the admin session via AJAX
19+
*/
20+
class ExtendSession extends Action implements HttpPostActionInterface
21+
{
22+
23+
/**
24+
* @param Context $context
25+
* @param JsonFactory $resultJsonFactory
26+
* @param AuthSession $authSession
27+
* @param FormKeyValidator $formKeyValidator
28+
* @param LoggerInterface $logger
29+
*/
30+
public function __construct(
31+
Context $context,
32+
private readonly JsonFactory $resultJsonFactory,
33+
private readonly AuthSession $authSession,
34+
private readonly FormKeyValidator $formKeyValidator,
35+
private readonly LoggerInterface $logger
36+
) {
37+
parent::__construct($context);
38+
}
39+
40+
/**
41+
* Execute action to extend the admin session
42+
*
43+
* @return Json
44+
*/
45+
public function execute(): Json
46+
{
47+
$result = $this->resultJsonFactory->create();
48+
49+
try {
50+
// Validate form key for CSRF protection
51+
if (!$this->formKeyValidator->validate($this->getRequest())) {
52+
throw new LocalizedException(__('Invalid form key. Please refresh the page.'));
53+
}
54+
55+
// Verify the user is still authenticated
56+
if (!$this->authSession->isLoggedIn()) {
57+
throw new LocalizedException(__('Session has expired. Please log in again.'));
58+
}
59+
60+
// Accessing session data automatically refreshes the session timeout
61+
// The session lifetime is extended by making this authenticated request
62+
$this->authSession->prolong();
63+
64+
$this->logger->info(
65+
'Admin session extended',
66+
['user_id' => $this->authSession->getUser()?->getId()]
67+
);
68+
69+
return $result->setData([
70+
'success' => true,
71+
'message' => __('Your session has been extended.')
72+
]);
73+
} catch (LocalizedException $e) {
74+
$this->logger->warning('Failed to extend admin session: ' . $e->getMessage());
75+
return $result->setData([
76+
'success' => false,
77+
'message' => $e->getMessage()
78+
]);
79+
} catch (\Exception $e) {
80+
$this->logger->error('Error extending admin session: ' . $e->getMessage());
81+
return $result->setData([
82+
'success' => false,
83+
'message' => __('An error occurred while extending your session. Please try again.')
84+
]);
85+
}
86+
}
87+
}

ViewModel/Modal.php

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Aligent\Pci4Compatibility\ViewModel;
6+
7+
use Magento\Backend\Model\UrlInterface;
8+
use Magento\Framework\App\Config\ScopeConfigInterface;
9+
use Magento\Framework\View\Element\Block\ArgumentInterface;
10+
11+
class Modal implements ArgumentInterface
12+
{
13+
/**
14+
* @param ScopeConfigInterface $scopeConfig
15+
* @param UrlInterface $backendUrl
16+
*/
17+
public function __construct(
18+
private readonly ScopeConfigInterface $scopeConfig,
19+
private readonly UrlInterface $backendUrl,
20+
) {
21+
}
22+
23+
/**
24+
* Returns the duration in ms after refresh when the session expiry warning modal should appear
25+
*
26+
* @return int
27+
*/
28+
public function getModalPopupTimeout(): int
29+
{
30+
// This value is in seconds
31+
$sessionTimeout = $this->scopeConfig->getValue(
32+
'admin/security/session_lifetime',
33+
ScopeConfigInterface::SCOPE_TYPE_DEFAULT
34+
);
35+
36+
// Then we want the popup to appear one minute before timeout
37+
return ($sessionTimeout - 60) * 1000;
38+
}
39+
40+
/**
41+
* Get the session lifetime in seconds
42+
*
43+
* @return int
44+
*/
45+
public function getSessionLifetime(): int
46+
{
47+
return (int) $this->scopeConfig->getValue(
48+
'admin/security/session_lifetime',
49+
ScopeConfigInterface::SCOPE_TYPE_DEFAULT
50+
);
51+
}
52+
53+
/**
54+
* Get URL for extending admin session
55+
*
56+
* @return string
57+
*/
58+
public function getExtendSessionUrl(): string
59+
{
60+
return $this->backendUrl->getUrl('pci4/session/extendsession');
61+
}
62+
}

etc/adminhtml/routes.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
/**
4+
* Copyright © Aligent. All rights reserved.
5+
*/
6+
-->
7+
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
8+
xsi:noNamespaceSchemaLocation="urn:magento:framework:App/etc/routes.xsd">
9+
<router id="admin">
10+
<route id="pci4" frontName="pci4">
11+
<module name="Aligent_Pci4Compatibility"/>
12+
</route>
13+
</router>
14+
</config>

view/adminhtml/layout/default.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?xml version="1.0" ?>
2+
<page xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:framework:View/Layout/etc/page_configuration.xsd">
3+
<body>
4+
<referenceContainer name="page.main.actions">
5+
<block class="Magento\Backend\Block\Template" name="session.expiry.modal" as="modal" template="Aligent_Pci4Compatibility::modal.phtml">
6+
<arguments>
7+
<argument name="view_model" xsi:type="object">Aligent\Pci4Compatibility\ViewModel\Modal</argument>
8+
</arguments>
9+
</block>
10+
</referenceContainer>
11+
</body>
12+
</page>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
use Aligent\Pci4Compatibility\ViewModel\Modal;
5+
use Magento\Backend\Block\Template;
6+
use Magento\Framework\Escaper;
7+
8+
/**
9+
* @var Template $block
10+
* @var Escaper $escaper
11+
* @var Modal $viewModel
12+
*/
13+
$viewModel = $block->getViewModel();
14+
$modalTimeout = $viewModel->getModalPopupTimeout();
15+
$sessionLifetime = $viewModel->getSessionLifetime();
16+
$extendSessionUrl = $viewModel->getExtendSessionUrl();
17+
18+
?>
19+
20+
<div id="session-expire-warning-modal">
21+
<p style="font-size:48px;color:#FF0000">&#x26A0;</p>
22+
<p id="session-warning-message">
23+
<?= __(
24+
'Your session will expire in one minute. ' .
25+
'Please save your changes or extend your session to continue working.'
26+
) ?>
27+
</p>
28+
<input type="hidden" id="session-extend-url" value="<?= $escaper->escapeUrl($extendSessionUrl) ?>"/>
29+
<input type="hidden" id="session-lifetime" value="<?= $escaper->escapeHtmlAttr((string)$sessionLifetime) ?>"/>
30+
</div>
31+
32+
<script>
33+
require(
34+
[
35+
'jquery',
36+
'Magento_Ui/js/modal/modal',
37+
'mage/translate'
38+
],
39+
function(
40+
$,
41+
modal,
42+
$t
43+
) {
44+
let sessionTimeoutHandle = null;
45+
const sessionLifetime = parseInt($('#session-lifetime').val()) || 900;
46+
const warningOffset = 60; // Show warning 60 seconds before expiry
47+
48+
/**
49+
* Initialize and show the session expiry warning modal
50+
*/
51+
function initSessionWarningModal() {
52+
const options = {
53+
type: 'popup',
54+
innerScroll: true,
55+
title: $t('Session Expiring Soon'),
56+
modalClass: 'modal-admintimeout',
57+
buttons: [
58+
{
59+
text: $t('Extend Session'),
60+
class: 'action-primary',
61+
click: function () {
62+
extendSession(this);
63+
}
64+
},
65+
{
66+
text: $t('Close'),
67+
class: 'action-secondary',
68+
click: function () {
69+
this.closeModal();
70+
}
71+
}
72+
]
73+
};
74+
75+
modal(options, $('#session-expire-warning-modal'));
76+
}
77+
78+
/**
79+
* Schedule the session warning modal to appear
80+
*/
81+
function scheduleSessionWarning() {
82+
// Clear any existing timeout
83+
if (sessionTimeoutHandle) {
84+
clearTimeout(sessionTimeoutHandle);
85+
}
86+
87+
// Calculate delay: (sessionLifetime - warningOffset) * 1000 milliseconds
88+
const delay = (sessionLifetime - warningOffset) * 1000;
89+
90+
sessionTimeoutHandle = setTimeout(function() {
91+
$('#session-expire-warning-modal').modal('openModal');
92+
}, delay);
93+
}
94+
95+
/**
96+
* Extend the admin session via AJAX
97+
*/
98+
function extendSession(modalContext) {
99+
const $message = $('#session-warning-message');
100+
const originalMessage = $message.text();
101+
102+
// Show loading state
103+
$message.text($t('Extending your session...'));
104+
105+
$.ajax({
106+
url: $('#session-extend-url').val(),
107+
type: 'POST',
108+
dataType: 'json',
109+
data: {
110+
form_key: window.FORM_KEY
111+
},
112+
success: function(response) {
113+
if (response.success) {
114+
// Show a success message
115+
$message.text($t('Session extended successfully!'));
116+
117+
// Close modal after a short delay
118+
setTimeout(function() {
119+
modalContext.closeModal();
120+
$message.text(originalMessage);
121+
122+
// Reschedule the warning modal
123+
scheduleSessionWarning();
124+
}, 1500);
125+
} else {
126+
// Show error message
127+
$message.text(response.message || $t('Failed to extend session. Please try again.'));
128+
129+
// Restore the original message after delay
130+
setTimeout(function() {
131+
$message.text(originalMessage);
132+
}, 3000);
133+
}
134+
},
135+
error: function() {
136+
// Show error message
137+
$message.text($t('An error occurred. Please refresh the page.'));
138+
139+
// Restore the original message after delay
140+
setTimeout(function() {
141+
$message.text(originalMessage);
142+
}, 3000);
143+
}
144+
});
145+
}
146+
147+
// Initialize on document ready
148+
$(document).ready(function() {
149+
initSessionWarningModal();
150+
scheduleSessionWarning();
151+
});
152+
}
153+
);
154+
</script>

0 commit comments

Comments
 (0)