Add mobile Safari reload loop prevention for auth pages
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
All checks were successful
Build And Push Image / docker (push) Successful in 3m2s
- Implement comprehensive reload loop prevention utility - Add initialization checks to setup-password, verify, and signup pages - Include timeout protection and error handling for config loading - Add fallback defaults to prevent page failures on mobile devices - Document mobile reload loop prevention system
This commit is contained in:
310
utils/reload-loop-prevention.ts
Normal file
310
utils/reload-loop-prevention.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
/**
|
||||
* Reload Loop Detection and Prevention Utility
|
||||
* Advanced mobile Safari reload loop prevention system
|
||||
*/
|
||||
|
||||
interface PageLoadInfo {
|
||||
url: string;
|
||||
timestamp: number;
|
||||
userAgent: string;
|
||||
loadCount: number;
|
||||
}
|
||||
|
||||
interface ReloadLoopState {
|
||||
enabled: boolean;
|
||||
pageLoads: PageLoadInfo[];
|
||||
blockedPages: Set<string>;
|
||||
emergencyMode: boolean;
|
||||
debugMode: boolean;
|
||||
}
|
||||
|
||||
const RELOAD_LOOP_THRESHOLD = 5; // Max page loads in time window
|
||||
const TIME_WINDOW = 10000; // 10 seconds
|
||||
const EMERGENCY_BLOCK_TIME = 30000; // 30 seconds
|
||||
const STORAGE_KEY = 'reload_loop_prevention';
|
||||
|
||||
/**
|
||||
* Get or initialize reload loop state
|
||||
*/
|
||||
function getReloadLoopState(): ReloadLoopState {
|
||||
if (typeof window === 'undefined') {
|
||||
return {
|
||||
enabled: false,
|
||||
pageLoads: [],
|
||||
blockedPages: new Set(),
|
||||
emergencyMode: false,
|
||||
debugMode: false
|
||||
};
|
||||
}
|
||||
|
||||
// Use sessionStorage for persistence across reloads
|
||||
try {
|
||||
const stored = sessionStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
const parsed = JSON.parse(stored);
|
||||
return {
|
||||
...parsed,
|
||||
blockedPages: new Set(parsed.blockedPages || [])
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to parse stored state:', error);
|
||||
}
|
||||
|
||||
// Initialize new state
|
||||
const initialState: ReloadLoopState = {
|
||||
enabled: true,
|
||||
pageLoads: [],
|
||||
blockedPages: new Set(),
|
||||
emergencyMode: false,
|
||||
debugMode: process.env.NODE_ENV === 'development'
|
||||
};
|
||||
|
||||
saveReloadLoopState(initialState);
|
||||
return initialState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save reload loop state to sessionStorage
|
||||
*/
|
||||
function saveReloadLoopState(state: ReloadLoopState): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const toStore = {
|
||||
...state,
|
||||
blockedPages: Array.from(state.blockedPages)
|
||||
};
|
||||
sessionStorage.setItem(STORAGE_KEY, JSON.stringify(toStore));
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to save state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a page load and check for reload loops
|
||||
*/
|
||||
export function recordPageLoad(url: string): boolean {
|
||||
const state = getReloadLoopState();
|
||||
|
||||
if (!state.enabled) {
|
||||
return true; // Allow load
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
const userAgent = navigator.userAgent || '';
|
||||
|
||||
// Clean old page loads
|
||||
state.pageLoads = state.pageLoads.filter(
|
||||
load => now - load.timestamp < TIME_WINDOW
|
||||
);
|
||||
|
||||
// Find existing load for this URL
|
||||
const existingLoad = state.pageLoads.find(load => load.url === url);
|
||||
|
||||
if (existingLoad) {
|
||||
existingLoad.loadCount++;
|
||||
existingLoad.timestamp = now;
|
||||
} else {
|
||||
state.pageLoads.push({
|
||||
url,
|
||||
timestamp: now,
|
||||
userAgent,
|
||||
loadCount: 1
|
||||
});
|
||||
}
|
||||
|
||||
// Check for reload loop
|
||||
const urlLoads = state.pageLoads.filter(load => load.url === url);
|
||||
const totalLoads = urlLoads.reduce((sum, load) => sum + load.loadCount, 0);
|
||||
|
||||
if (totalLoads >= RELOAD_LOOP_THRESHOLD) {
|
||||
console.error(`[reload-prevention] Reload loop detected for ${url} (${totalLoads} loads)`);
|
||||
|
||||
// Block this page
|
||||
state.blockedPages.add(url);
|
||||
state.emergencyMode = true;
|
||||
|
||||
// Set emergency timeout
|
||||
setTimeout(() => {
|
||||
state.emergencyMode = false;
|
||||
state.blockedPages.delete(url);
|
||||
saveReloadLoopState(state);
|
||||
console.log(`[reload-prevention] Emergency block lifted for ${url}`);
|
||||
}, EMERGENCY_BLOCK_TIME);
|
||||
|
||||
saveReloadLoopState(state);
|
||||
return false; // Block load
|
||||
}
|
||||
|
||||
saveReloadLoopState(state);
|
||||
return true; // Allow load
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is currently blocked
|
||||
*/
|
||||
export function isPageBlocked(url: string): boolean {
|
||||
const state = getReloadLoopState();
|
||||
return state.blockedPages.has(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize reload loop prevention for a page
|
||||
*/
|
||||
export function initReloadLoopPrevention(pageName: string): boolean {
|
||||
if (typeof window === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const currentUrl = window.location.pathname;
|
||||
const canLoad = recordPageLoad(currentUrl);
|
||||
|
||||
if (!canLoad) {
|
||||
console.error(`[reload-prevention] Page load blocked: ${pageName} (${currentUrl})`);
|
||||
|
||||
// Show emergency message
|
||||
showEmergencyMessage(pageName);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getReloadLoopState().debugMode) {
|
||||
console.log(`[reload-prevention] Page load allowed: ${pageName} (${currentUrl})`);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Show emergency message when page is blocked
|
||||
*/
|
||||
function showEmergencyMessage(pageName: string): void {
|
||||
const message = `
|
||||
<div style="
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(163, 21, 21, 0.95);
|
||||
color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 10000;
|
||||
font-family: Arial, sans-serif;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
">
|
||||
<h1 style="font-size: 24px; margin-bottom: 20px;">Page Loading Temporarily Blocked</h1>
|
||||
<p style="font-size: 16px; margin-bottom: 20px;">
|
||||
Multiple rapid page loads detected for ${pageName}.<br>
|
||||
This is a safety measure to prevent infinite loading loops.
|
||||
</p>
|
||||
<p style="font-size: 14px; margin-bottom: 30px;">
|
||||
The block will be automatically lifted in 30 seconds.
|
||||
</p>
|
||||
<button onclick="location.href='/'" style="
|
||||
background: white;
|
||||
color: #a31515;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
margin-right: 15px;
|
||||
">Return to Home</button>
|
||||
<button onclick="window.history.back()" style="
|
||||
background: transparent;
|
||||
color: white;
|
||||
border: 2px solid white;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
">Go Back</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.innerHTML = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear reload loop prevention state (for testing)
|
||||
*/
|
||||
export function clearReloadLoopPrevention(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
sessionStorage.removeItem(STORAGE_KEY);
|
||||
console.log('[reload-prevention] State cleared');
|
||||
} catch (error) {
|
||||
console.warn('[reload-prevention] Failed to clear state:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current reload loop prevention status
|
||||
*/
|
||||
export function getReloadLoopStatus(): {
|
||||
enabled: boolean;
|
||||
emergencyMode: boolean;
|
||||
blockedPages: string[];
|
||||
pageLoads: PageLoadInfo[];
|
||||
} {
|
||||
const state = getReloadLoopState();
|
||||
return {
|
||||
enabled: state.enabled,
|
||||
emergencyMode: state.emergencyMode,
|
||||
blockedPages: Array.from(state.blockedPages),
|
||||
pageLoads: state.pageLoads
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Mobile Safari specific optimizations
|
||||
*/
|
||||
export function applyMobileSafariReloadLoopFixes(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const userAgent = navigator.userAgent || '';
|
||||
const isMobileSafari = /iPad|iPhone|iPod/.test(userAgent) && /Safari/i.test(userAgent) && !/Chrome/i.test(userAgent);
|
||||
|
||||
if (!isMobileSafari) return;
|
||||
|
||||
console.log('[reload-prevention] Applying mobile Safari specific fixes');
|
||||
|
||||
// Prevent Safari's aggressive caching that can cause reload loops
|
||||
window.addEventListener('pageshow', (event) => {
|
||||
if (event.persisted) {
|
||||
console.log('[reload-prevention] Page restored from bfcache');
|
||||
// Force a small delay to prevent immediate re-execution
|
||||
setTimeout(() => {
|
||||
console.log('[reload-prevention] bfcache restore handled');
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Safari's navigation timing issues
|
||||
window.addEventListener('beforeunload', () => {
|
||||
// Mark navigation in progress to prevent reload loop detection
|
||||
const state = getReloadLoopState();
|
||||
state.enabled = false;
|
||||
saveReloadLoopState(state);
|
||||
});
|
||||
|
||||
// Re-enable after navigation completes
|
||||
window.addEventListener('load', () => {
|
||||
setTimeout(() => {
|
||||
const state = getReloadLoopState();
|
||||
state.enabled = true;
|
||||
saveReloadLoopState(state);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-initialize on module load
|
||||
if (typeof window !== 'undefined') {
|
||||
applyMobileSafariReloadLoopFixes();
|
||||
}
|
||||
Reference in New Issue
Block a user