Is there a way to set an inactivity timeout on uibakery? If you don’t use the web page for a period of time it will time you out and make you log in again. It’s a security thing just like banks have.
Yes please to this. Ideally configurable, at application level. Really needed for things like PCI-DSS.
Yes please. this is very important for security.
You can do it with Javascript in an action that initiates on app launch:
/**
* Inactivity logout with custom modal warning 1 minute before logout.
* HARDENED:
* - Modal does NOT auto-hide on activity
* - While modal is open, activity does NOT reset timers (forces explicit choice)
* - Only buttons close the modal
*/
if ({{app.env === "dev"}}) return "dev";
const timerKey = 'AppInactivityTimer';
const warningKey = 'AppInactivityWarning';
const countdownKey = 'AppInactivityCountdown';
const handlerKey = 'AppResetInactivityHandler';
const TOTAL_TIMEOUT = 5 * 60 * 1000; // 5 minutes
const WARNING_OFFSET = 1 * 60 * 1000; // warn 1 minute before logout
const WARNING_DURATION = WARNING_OFFSET;
const MODAL_ID = 'inactivity-warning-modal';
let warningOpen = false;
// ---------- Modal helpers ----------
function ensureModalStyles() {
if (document.getElementById(`${MODAL_ID}-styles`)) return;
const style = document.createElement('style');
style.id = `${MODAL_ID}-styles`;
style.textContent = `
#${MODAL_ID}-backdrop{
position: fixed; inset: 0;
background: rgba(0,0,0,.45);
display: none;
align-items: center;
justify-content: center;
z-index: 999999;
}
#${MODAL_ID}{
width: min(520px, calc(100vw - 32px));
background: #fff;
border-radius: 14px;
box-shadow: 0 20px 60px rgba(0,0,0,.25);
overflow: hidden;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
}
#${MODAL_ID} .hdr{
padding: 16px 18px;
border-bottom: 1px solid rgba(0,0,0,.08);
font-weight: 700;
font-size: 16px;
}
#${MODAL_ID} .body{
padding: 14px 18px 6px 18px;
font-size: 14px;
line-height: 1.35;
color: rgba(0,0,0,.85);
}
#${MODAL_ID} .countdown{
margin-top: 10px;
font-weight: 700;
font-size: 14px;
}
#${MODAL_ID} .ftr{
display: flex;
gap: 10px;
justify-content: flex-end;
padding: 14px 18px 16px 18px;
border-top: 1px solid rgba(0,0,0,.08);
}
#${MODAL_ID} button{
border: 0;
border-radius: 10px;
padding: 10px 12px;
font-weight: 650;
cursor: pointer;
font-size: 14px;
}
#${MODAL_ID} .btn-secondary{
background: rgba(0,0,0,.08);
color: rgba(0,0,0,.85);
}
#${MODAL_ID} .btn-primary{
background: #092E6E;
color: #fff;
}
#${MODAL_ID} .btn-secondary:hover{ filter: brightness(.98); }
#${MODAL_ID} .btn-primary:hover{ filter: brightness(.95); }
@media (prefers-color-scheme: dark){
#${MODAL_ID}{
background: #141414;
color: rgba(255,255,255,.92);
}
#${MODAL_ID} .hdr,
#${MODAL_ID} .ftr{ border-color: rgba(255,255,255,.10); }
#${MODAL_ID} .body{ color: rgba(255,255,255,.86); }
#${MODAL_ID} .btn-secondary{
background: rgba(255,255,255,.12);
color: rgba(255,255,255,.92);
}
}
`;
document.head.appendChild(style);
}
function ensureModalDom() {
if (document.getElementById(`${MODAL_ID}-backdrop`)) return;
const backdrop = document.createElement('div');
backdrop.id = `${MODAL_ID}-backdrop`;
backdrop.innerHTML = `
<div id="${MODAL_ID}" role="dialog" aria-modal="true" aria-labelledby="${MODAL_ID}-title">
<div class="hdr" id="${MODAL_ID}-title">Still there?</div>
<div class="body">
<div>You’ll be logged out in <span class="countdown" id="${MODAL_ID}-countdown">60</span> seconds due to inactivity.</div>
<div style="margin-top:8px; opacity:.9;">Click <b>Stay Logged In</b> to continue your session.</div>
</div>
<div class="ftr">
<button class="btn-secondary" id="${MODAL_ID}-logout" type="button">Log Out Now</button>
<button class="btn-primary" id="${MODAL_ID}-stay" type="button">Stay Logged In</button>
</div>
</div>
`;
document.body.appendChild(backdrop);
// Disable backdrop click (force explicit choice)
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) {
// do nothing
}
});
// Harden: disable Escape from closing anything
document.addEventListener('keydown', (e) => {
if (!warningOpen) return;
if (e.key === 'Escape') {
e.preventDefault();
e.stopPropagation();
}
}, true);
}
function showModal() {
ensureModalStyles();
ensureModalDom();
warningOpen = true;
const backdrop = document.getElementById(`${MODAL_ID}-backdrop`);
backdrop.style.display = 'flex';
// focus primary button
document.getElementById(`${MODAL_ID}-stay`)?.focus();
}
function hideModal() {
warningOpen = false;
const backdrop = document.getElementById(`${MODAL_ID}-backdrop`);
if (backdrop) backdrop.style.display = 'none';
}
function setCountdown(seconds) {
const el = document.getElementById(`${MODAL_ID}-countdown`);
if (el) el.textContent = String(seconds);
}
// ---------- Timer logic ----------
const clearAllTimers = () => {
if (document[timerKey]) clearTimeout(document[timerKey]);
if (document[warningKey]) clearTimeout(document[warningKey]);
if (document[countdownKey]) clearInterval(document[countdownKey]);
document[timerKey] = null;
document[warningKey] = null;
document[countdownKey] = null;
};
const resetTimer = () => {
// HARDENING: ignore activity while warning modal is open.
// User must explicitly click Stay Logged In.
if (warningOpen) return;
clearAllTimers();
// Schedule warning
document[warningKey] = setTimeout(() => {
showModal();
// Start countdown from 60 seconds
let remaining = Math.max(1, Math.round(WARNING_DURATION / 1000));
setCountdown(remaining);
const stayBtn = document.getElementById(`${MODAL_ID}-stay`);
const logoutBtn = document.getElementById(`${MODAL_ID}-logout`);
if (stayBtn) {
stayBtn.onclick = () => {
hideModal();
resetTimer(); // resets both timers (warningOpen is now false)
};
}
if (logoutBtn) {
logoutBtn.onclick = () => {
hideModal();
user.logout();
};
}
// Countdown interval
document[countdownKey] = setInterval(() => {
remaining -= 1;
if (remaining <= 0) {
clearInterval(document[countdownKey]);
document[countdownKey] = null;
return;
}
setCountdown(remaining);
}, 1000);
}, TOTAL_TIMEOUT - WARNING_OFFSET);
// Schedule logout
document[timerKey] = setTimeout(() => {
clearAllTimers();
hideModal();
console.log('User is inactive for 5 minutes. Logging out...');
user.logout();
}, TOTAL_TIMEOUT);
};
// Remove old handlers if they exist
if (document[handlerKey]) {
document.removeEventListener('mousemove', document[handlerKey]);
document.removeEventListener('keydown', document[handlerKey]);
document.removeEventListener('mousedown', document[handlerKey]);
document.removeEventListener('touchstart', document[handlerKey]);
}
// Assign and register new handler
document[handlerKey] = resetTimer;
// Activity events
document.addEventListener('mousemove', document[handlerKey], { passive: true });
document.addEventListener('keydown', document[handlerKey]);
document.addEventListener('mousedown', document[handlerKey], { passive: true });
document.addEventListener('touchstart', document[handlerKey], { passive: true });
// Start timers immediately
resetTimer();
Shoutout to Matvey for the foundation of this function!
2 Likes