fix deployment db issue and widthrawal administration page
Deploy to Shared Hosting / Shared hosting deploy (push) Failing after 47s
Tests / PHPUnit (push) Failing after 1m19s

This commit is contained in:
root
2026-08-20 20:18:00 -04:00
parent 889c037660
commit d3da699e55
19 changed files with 1170 additions and 194 deletions
+161 -26
View File
@@ -1,9 +1,10 @@
class SessionTimeoutManager {
constructor() {
this.config = {
timeout: 1800, // default 30 min
warning_time: 30, // final 30 seconds before timeout
check_interval: 5000, // 5 seconds
timeout: 1800,
warning_time: 30,
check_interval: 30000,
ping_interval: 60000,
logout_url: '/logout',
keep_alive_url: '/session/ping-activity',
check_url: '/session/check-timeout'
@@ -11,10 +12,19 @@ class SessionTimeoutManager {
this.timers = {
checkTimer: null,
logoutTimer: null,
warningTimer: null
warningTimer: null,
pingTimer: null
};
this.modal = null;
this.warningShown = false;
this.pingInProgress = false;
this.sessionExpired = false;
this.loggedOut = false;
this.consecutiveFailures = 0;
this.nextPingAllowedAt = 0;
this.lastSuccessfulPingAt = 0;
this.activityPending = false;
this.backoffMs = [120000, 300000, 600000, 900000];
}
async init() {
@@ -40,11 +50,11 @@ class SessionTimeoutManager {
},
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.success) {
this.config = { ...this.config, ...data };
@@ -54,43 +64,150 @@ class SessionTimeoutManager {
}
}
shouldPausePings() {
return document.hidden || this.sessionExpired || this.loggedOut;
}
currentBackoffMs() {
if (this.consecutiveFailures <= 0) {
return this.config.ping_interval || 60000;
}
const index = Math.min(this.consecutiveFailures - 1, this.backoffMs.length - 1);
return this.backoffMs[index];
}
setupEventListeners() {
// Reset timers on user activity
const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click', 'input'];
events.forEach(event => {
document.addEventListener(event, () => this.resetActivity(), { passive: true });
events.forEach((event) => {
document.addEventListener(event, () => this.onUserActivity(), { passive: true });
});
// Handle visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
if (document.hidden) {
return;
}
if (!this.shouldPausePings()) {
this.checkSessionStatus();
}
});
}
onUserActivity() {
if (this.shouldPausePings()) {
return;
}
this.activityPending = true;
this.schedulePing();
}
schedulePing() {
if (this.shouldPausePings() || this.pingInProgress) {
return;
}
const now = Date.now();
const waitMs = Math.max(0, this.nextPingAllowedAt - now);
clearTimeout(this.timers.pingTimer);
this.timers.pingTimer = setTimeout(() => {
this.resetActivity();
}, waitMs);
}
async resetActivity() {
if (this.pingInProgress || this.shouldPausePings()) {
return;
}
const now = Date.now();
if (now < this.nextPingAllowedAt) {
this.schedulePing();
return;
}
if (
this.consecutiveFailures === 0
&& this.lastSuccessfulPingAt > 0
&& (now - this.lastSuccessfulPingAt) < (this.config.ping_interval || 60000)
&& !this.activityPending
) {
return;
}
this.pingInProgress = true;
try {
await fetch(this.config.keep_alive_url, {
const response = await fetch(this.config.keep_alive_url, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (response.status === 401 || response.status === 403) {
this.sessionExpired = true;
this.handleSessionExpired({
redirect: this.config.logout_url,
message: 'Your session has expired. Please log in again.'
});
return;
}
if (response.status === 429) {
this.consecutiveFailures += 1;
this.nextPingAllowedAt = Date.now() + this.currentBackoffMs();
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let data = null;
try {
data = await response.json();
} catch (parseError) {
data = { status: 'active' };
}
if (data && data.status === 'expired') {
this.handleSessionExpired(data);
return;
}
this.consecutiveFailures = 0;
this.activityPending = false;
this.lastSuccessfulPingAt = Date.now();
this.nextPingAllowedAt = this.lastSuccessfulPingAt + (this.config.ping_interval || 60000);
this.clearWarning();
} catch (error) {
console.warn('Activity reset failed:', error);
this.consecutiveFailures += 1;
this.nextPingAllowedAt = Date.now() + this.currentBackoffMs();
console.warn('Activity reset failed; backing off:', error);
} finally {
this.pingInProgress = false;
}
}
startPeriodicChecks() {
clearInterval(this.timers.checkTimer);
this.timers.checkTimer = setInterval(() => {
if (this.shouldPausePings()) {
return;
}
this.checkSessionStatus();
}, this.config.check_interval);
}
async checkSessionStatus() {
if (this.shouldPausePings()) {
return;
}
try {
const response = await fetch(this.config.check_url, {
method: 'GET',
@@ -100,12 +217,26 @@ class SessionTimeoutManager {
credentials: 'same-origin'
});
if (response.status === 404) {
console.log('Session check endpoint not available, skipping');
return;
}
if (response.status === 401 || response.status === 403) {
this.sessionExpired = true;
this.handleSessionExpired({
redirect: this.config.logout_url,
message: 'Your session has expired. Please log in again.'
});
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
switch (data.status) {
case 'expired':
this.handleSessionExpired(data);
@@ -123,11 +254,12 @@ class SessionTimeoutManager {
}
handleSessionExpired(data) {
this.sessionExpired = true;
this.clearWarning();
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.pingTimer);
if (data.redirect) {
// Show message before redirect
alert(data.message || 'Your session has expired. Please log in again.');
window.location.href = data.redirect;
} else {
@@ -194,7 +326,7 @@ class SessionTimeoutManager {
createWarningModal(timeRemaining) {
this.hideWarning();
this.modal = document.createElement('div');
this.modal.className = 'session-timeout-modal';
this.modal.style.cssText = `
@@ -210,7 +342,7 @@ class SessionTimeoutManager {
align-items: center;
font-family: Arial, sans-serif;
`;
this.modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 10px; text-align: center; max-width: 400px; box-shadow: 0 5px 25px rgba(0,0,0,0.3);">
<h3 style="color: #d35400; margin-bottom: 20px; font-size: 1.5em;">Session About to Expire</h3>
@@ -221,20 +353,20 @@ class SessionTimeoutManager {
Click Continue Session if you want to keep using this session.
</p>
<div style="margin-top: 25px;">
<button onclick="sessionTimeout.continueSession()"
style="background: #27ae60; color: white; border: none; padding: 12px 24px;
<button onclick="sessionTimeout.continueSession()"
style="background: #27ae60; color: white; border: none; padding: 12px 24px;
border-radius: 5px; cursor: pointer; margin-right: 15px; font-size: 1em;">
Continue Session
</button>
<button onclick="sessionTimeout.logout()"
style="background: #e74c3c; color: white; border: none; padding: 12px 24px;
<button onclick="sessionTimeout.logout()"
style="background: #e74c3c; color: white; border: none; padding: 12px 24px;
border-radius: 5px; cursor: pointer; font-size: 1em;">
Logout Now
</button>
</div>
</div>
`;
document.body.appendChild(this.modal);
document.body.style.overflow = 'hidden';
}
@@ -257,11 +389,16 @@ class SessionTimeoutManager {
}
continueSession() {
this.activityPending = true;
this.nextPingAllowedAt = 0;
this.resetActivity();
this.hideWarning();
}
logout() {
this.loggedOut = true;
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.pingTimer);
window.location.href = this.config.logout_url;
}
@@ -269,19 +406,17 @@ class SessionTimeoutManager {
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.logoutTimer);
clearInterval(this.timers.warningTimer);
clearTimeout(this.timers.pingTimer);
this.hideWarning();
}
}
// Initialize globally
const sessionTimeout = new SessionTimeoutManager();
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => sessionTimeout.init());
} else {
sessionTimeout.init();
}
// Make available globally
window.sessionTimeout = sessionTimeout;
+189 -53
View File
@@ -1,9 +1,10 @@
class SessionTimeoutManager {
constructor() {
this.config = {
timeout: 1800, // 30 minutes
warning_time: 30, // final 30 seconds before timeout
check_interval: 5000, // 5 seconds
timeout: 1800,
warning_time: 30,
check_interval: 30000,
ping_interval: 60000,
logout_url: '/logout',
keep_alive_url: '/session/ping-activity',
check_url: '/session/check-timeout'
@@ -11,100 +12,231 @@ class SessionTimeoutManager {
this.timers = {
checkTimer: null,
logoutTimer: null,
warningTimer: null
warningTimer: null,
pingTimer: null
};
this.modal = null;
this.warningShown = false;
this.csrfToken = this.getCsrfToken();
}
getCsrfToken() {
// Try to get CSRF token from meta tag
const metaTag = document.querySelector('meta[name="csrf-token"]');
if (metaTag) {
return metaTag.getAttribute('content');
}
// Try to get CSRF token from form input (fallback)
const csrfInput = typeof csrf_token !== 'undefined'
? document.querySelector('input[name="' + csrf_token + '"]')
: null;
if (csrfInput) {
return csrfInput.value;
}
console.warn('CSRF token not found');
return '';
this.pingInProgress = false;
this.sessionExpired = false;
this.loggedOut = false;
this.consecutiveFailures = 0;
this.nextPingAllowedAt = 0;
this.lastSuccessfulPingAt = 0;
this.activityPending = false;
this.backoffMs = [120000, 300000, 600000, 900000];
}
async init() {
this.setupEventListeners();
this.startPeriodicChecks();
console.log('Session timeout manager initialized with config:', this.config);
try {
await this.fetchConfig();
this.setupEventListeners();
this.startPeriodicChecks();
console.log('Session timeout manager initialized with config:', this.config);
} catch (error) {
console.warn('Session timeout using default config due to error:', error.message);
this.setupEventListeners();
this.startPeriodicChecks();
}
}
// REMOVE the fetchConfig() method entirely
async fetchConfig() {
try {
const response = await fetch('/session/get-timeout-config', {
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
if (data.success) {
this.config = { ...this.config, ...data };
}
} catch (error) {
console.warn('Failed to load timeout config, using defaults:', error.message);
}
}
shouldPausePings() {
return document.hidden || this.sessionExpired || this.loggedOut;
}
currentBackoffMs() {
if (this.consecutiveFailures <= 0) {
return this.config.ping_interval || 60000;
}
const index = Math.min(this.consecutiveFailures - 1, this.backoffMs.length - 1);
return this.backoffMs[index];
}
setupEventListeners() {
// Reset timers on user activity
const events = ['mousedown', 'keydown', 'scroll', 'touchstart', 'click', 'input'];
events.forEach(event => {
document.addEventListener(event, () => this.resetActivity(), { passive: true });
events.forEach((event) => {
document.addEventListener(event, () => this.onUserActivity(), { passive: true });
});
// Handle visibility change
document.addEventListener('visibilitychange', () => {
if (!document.hidden) {
if (document.hidden) {
return;
}
if (!this.shouldPausePings()) {
this.checkSessionStatus();
}
});
}
onUserActivity() {
if (this.shouldPausePings()) {
return;
}
this.activityPending = true;
this.schedulePing();
}
schedulePing() {
if (this.shouldPausePings() || this.pingInProgress) {
return;
}
const now = Date.now();
const waitMs = Math.max(0, this.nextPingAllowedAt - now);
clearTimeout(this.timers.pingTimer);
this.timers.pingTimer = setTimeout(() => {
this.resetActivity();
}, waitMs);
}
async resetActivity() {
if (this.pingInProgress || this.shouldPausePings()) {
return;
}
const now = Date.now();
if (now < this.nextPingAllowedAt) {
this.schedulePing();
return;
}
if (
this.consecutiveFailures === 0
&& this.lastSuccessfulPingAt > 0
&& (now - this.lastSuccessfulPingAt) < (this.config.ping_interval || 60000)
&& !this.activityPending
) {
return;
}
this.pingInProgress = true;
try {
await fetch(this.config.keep_alive_url, {
const response = await fetch(this.config.keep_alive_url, {
method: 'POST',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': this.csrfToken
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (response.status === 401 || response.status === 403) {
this.sessionExpired = true;
this.handleSessionExpired({
redirect: this.config.logout_url,
message: 'Your session has expired. Please log in again.'
});
return;
}
if (response.status === 429) {
this.consecutiveFailures += 1;
this.nextPingAllowedAt = Date.now() + this.currentBackoffMs();
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
let data = null;
try {
data = await response.json();
} catch (parseError) {
data = { status: 'active' };
}
if (data && data.status === 'expired') {
this.handleSessionExpired(data);
return;
}
this.consecutiveFailures = 0;
this.activityPending = false;
this.lastSuccessfulPingAt = Date.now();
this.nextPingAllowedAt = this.lastSuccessfulPingAt + (this.config.ping_interval || 60000);
this.clearWarning();
} catch (error) {
console.warn('Activity reset failed:', error);
this.consecutiveFailures += 1;
this.nextPingAllowedAt = Date.now() + this.currentBackoffMs();
console.warn('Activity reset failed; backing off:', error);
} finally {
this.pingInProgress = false;
}
}
startPeriodicChecks() {
clearInterval(this.timers.checkTimer);
this.timers.checkTimer = setInterval(() => {
if (this.shouldPausePings()) {
return;
}
this.checkSessionStatus();
}, this.config.check_interval);
}
async checkSessionStatus() {
if (this.shouldPausePings()) {
return;
}
try {
const response = await fetch(this.config.check_url, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'X-CSRF-TOKEN': this.csrfToken
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (response.status === 404) {
// If check endpoint doesn't exist, skip checking
console.log('Session check endpoint not available, skipping');
return;
}
if (response.status === 401 || response.status === 403) {
this.sessionExpired = true;
this.handleSessionExpired({
redirect: this.config.logout_url,
message: 'Your session has expired. Please log in again.'
});
return;
}
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
const data = await response.json();
switch (data.status) {
case 'expired':
this.handleSessionExpired(data);
@@ -121,11 +253,12 @@ class SessionTimeoutManager {
}
}
// ... rest of the methods remain the same
handleSessionExpired(data) {
this.sessionExpired = true;
this.clearWarning();
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.pingTimer);
if (data.redirect) {
alert(data.message || 'Your session has expired. Please log in again.');
window.location.href = data.redirect;
@@ -193,7 +326,7 @@ class SessionTimeoutManager {
createWarningModal(timeRemaining) {
this.hideWarning();
this.modal = document.createElement('div');
this.modal.className = 'session-timeout-modal';
this.modal.style.cssText = `
@@ -209,7 +342,7 @@ class SessionTimeoutManager {
align-items: center;
font-family: Arial, sans-serif;
`;
this.modal.innerHTML = `
<div style="background: white; padding: 30px; border-radius: 10px; text-align: center; max-width: 400px; box-shadow: 0 5px 25px rgba(0,0,0,0.3);">
<h3 style="color: #d35400; margin-bottom: 20px; font-size: 1.5em;">Session About to Expire</h3>
@@ -220,20 +353,20 @@ class SessionTimeoutManager {
Click Continue Session if you want to keep using this session.
</p>
<div style="margin-top: 25px;">
<button onclick="sessionTimeout.continueSession()"
style="background: #27ae60; color: white; border: none; padding: 12px 24px;
<button onclick="sessionTimeout.continueSession()"
style="background: #27ae60; color: white; border: none; padding: 12px 24px;
border-radius: 5px; cursor: pointer; margin-right: 15px; font-size: 1em;">
Continue Session
</button>
<button onclick="sessionTimeout.logout()"
style="background: #e74c3c; color: white; border: none; padding: 12px 24px;
<button onclick="sessionTimeout.logout()"
style="background: #e74c3c; color: white; border: none; padding: 12px 24px;
border-radius: 5px; cursor: pointer; font-size: 1em;">
Logout Now
</button>
</div>
</div>
`;
document.body.appendChild(this.modal);
document.body.style.overflow = 'hidden';
}
@@ -256,11 +389,16 @@ class SessionTimeoutManager {
}
continueSession() {
this.activityPending = true;
this.nextPingAllowedAt = 0;
this.resetActivity();
this.hideWarning();
}
logout() {
this.loggedOut = true;
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.pingTimer);
window.location.href = this.config.logout_url;
}
@@ -268,19 +406,17 @@ class SessionTimeoutManager {
clearInterval(this.timers.checkTimer);
clearTimeout(this.timers.logoutTimer);
clearInterval(this.timers.warningTimer);
clearTimeout(this.timers.pingTimer);
this.hideWarning();
}
}
// Initialize globally
const sessionTimeout = new SessionTimeoutManager();
// Start when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => sessionTimeout.init());
} else {
sessionTimeout.init();
}
// Make available globally
window.sessionTimeout = sessionTimeout;