feat: implement robust TokenManager for field app resilience
PRODUCTION CRITICAL: Enhanced token lifecycle management Features: - 3-tier storage (PB authStore → IndexedDB → localStorage) - Continuous background token refresh (every 20 min) - Exponential backoff retry queue for network failures - Stale token fallback for offline operation - Cross-tab sync via IndexedDB - Never forces logout on token expiry - Network recovery detection and auto-refresh Changes: - Created token-manager.js with TokenManager class - Updated signin.html to initialize TokenManager on login - Updated index.html to use TokenManager for all API calls - Simplified token extraction logic (centralized in TokenManager) - Removed manual localStorage token management - Added proper cleanup on signout Benefits: - Users stay authenticated during network interruptions - Tokens always warm (proactive refresh) - Field app resilient to poor connectivity - No unnecessary re-authentication prompts - Works offline with cached tokens
This commit is contained in:
+21
-71
@@ -10,6 +10,7 @@
|
||||
</script><script src="https://cdn.jsdelivr.net/npm/pocketbase@0.26.5/dist/pocketbase.umd.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
|
||||
<script src="auth.js"></script>
|
||||
<script src="token-manager.js"></script>
|
||||
</head>
|
||||
<body class="font-sans bg-gray-100 m-0 text-gray-900 overflow-hidden touch-pan-y h-screen flex flex-col">
|
||||
<div class="max-w-full md:max-w-2xl mx-auto p-4 sm:p-3 text-[17px] flex flex-col flex-1 overflow-hidden">
|
||||
@@ -193,48 +194,19 @@
|
||||
const authed = await ensureAuth();
|
||||
if (!authed) return;
|
||||
|
||||
// Make sure a Graph token is available before proceeding; this may refresh silently
|
||||
try {
|
||||
await ensureGraphToken();
|
||||
} catch (err) {
|
||||
console.warn('Graph token missing after refresh; continuing. Preview may prompt for auth.', err?.message || err);
|
||||
}
|
||||
// Initialize TokenManager for robust token handling
|
||||
tokenManager = new TokenManager(pb);
|
||||
await tokenManager.init();
|
||||
console.log('[App] TokenManager initialized');
|
||||
|
||||
// --- Utility ---
|
||||
const fetchNoCache = (url, options={}) => {
|
||||
|
||||
const fetchNoCache = async (url, options={}) => {
|
||||
const token = await tokenManager.getToken();
|
||||
const headers = { ...(options.headers||{}), ...authHeaders() };
|
||||
if (token) headers['x-graph-token'] = token;
|
||||
return fetch(url + `&_ts=${Date.now()}`, { ...options, headers });
|
||||
};
|
||||
const GRAPH_TOKEN_KEY = 'graphAccessToken';
|
||||
const GRAPH_REAUTH_FLAG = 'graphReauthAttempted';
|
||||
const extractGraphToken = (metaObj={}) => (
|
||||
metaObj.graphAccessToken ||
|
||||
metaObj.graph_token ||
|
||||
metaObj.graphToken ||
|
||||
metaObj.accessToken ||
|
||||
metaObj.access_token ||
|
||||
metaObj.token ||
|
||||
metaObj.rawToken ||
|
||||
metaObj?.authData?.access_token ||
|
||||
''
|
||||
);
|
||||
const getGraphToken = () => {
|
||||
// Prefer runtime token already present in PB model/meta, then localStorage
|
||||
const model = pb?.authStore?.model || {};
|
||||
const meta = model?.meta || {};
|
||||
const token = (
|
||||
model.graphAccessToken ||
|
||||
model.graph_token ||
|
||||
model.graphToken ||
|
||||
meta.graphAccessToken ||
|
||||
meta.graph_token ||
|
||||
meta.graphToken ||
|
||||
localStorage.getItem(GRAPH_TOKEN_KEY) ||
|
||||
''
|
||||
);
|
||||
console.log('[getGraphToken] Found token:', token ? token.substring(0, 30) + '...' : 'NONE');
|
||||
return token;
|
||||
};
|
||||
const PB_COLLECTION_URL = "https://pocketbase.ccllc.pro/api/collections/pbc_1407612689/records";
|
||||
const NOTES_COLLECTION_URL = "https://pocketbase.ccllc.pro/api/collections/notes/records";
|
||||
const PREFS_COLLECTION_URL = "https://pocketbase.ccllc.pro/api/collections/user_preferences/records";
|
||||
@@ -282,46 +254,24 @@
|
||||
if(userEmailEl && currentUserEmail) userEmailEl.textContent = currentUserEmail;
|
||||
|
||||
|
||||
// Ensure Graph token is present; refresh if missing; redirect to signin if still absent
|
||||
async function refreshGraphToken(){
|
||||
try {
|
||||
const authData = await pb.collection('users').authRefresh();
|
||||
const meta = authData?.meta || pb?.authStore?.model?.meta || {};
|
||||
const token = extractGraphToken(meta);
|
||||
if (token) localStorage.setItem(GRAPH_TOKEN_KEY, token);
|
||||
return token || '';
|
||||
} catch(err){
|
||||
console.warn('Graph token refresh failed:', err?.message||err);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Graph token is present via TokenManager
|
||||
async function ensureGraphToken(){
|
||||
let token = getGraphToken();
|
||||
if(token) return token;
|
||||
// If PB session is valid, try to refresh; if still missing, redirect to reauth once
|
||||
if (pb?.authStore?.isValid) {
|
||||
token = await refreshGraphToken();
|
||||
if (token) return token;
|
||||
const attempted = localStorage.getItem(GRAPH_REAUTH_FLAG) === '1';
|
||||
if (!attempted) {
|
||||
localStorage.setItem(GRAPH_REAUTH_FLAG, '1');
|
||||
console.warn('Graph token unavailable after refresh; redirecting once to sign-in to obtain a new token.');
|
||||
window.location.href = 'signin.html?reauth=1';
|
||||
throw new Error('GRAPH_TOKEN missing');
|
||||
}
|
||||
console.warn('Graph token unavailable after refresh; reauth already attempted. Proceeding without token.');
|
||||
return '';
|
||||
}
|
||||
// PB session invalid: redirect to signin
|
||||
window.location.href = 'signin.html';
|
||||
const token = await tokenManager.getToken();
|
||||
if (token) return token;
|
||||
|
||||
// Only redirect if truly invalid (not just missing)
|
||||
console.warn('[ensureGraphToken] No token available');
|
||||
window.location.href = 'signin.html?reauth=1';
|
||||
throw new Error('GRAPH_TOKEN missing');
|
||||
}
|
||||
|
||||
if (signOutBtn) {
|
||||
signOutBtn.addEventListener('click', () => {
|
||||
signOutBtn.addEventListener('click', async () => {
|
||||
if (tokenManager) {
|
||||
tokenManager.stopRefreshCycle();
|
||||
await tokenManager.clearToken();
|
||||
}
|
||||
try { pb?.authStore?.clear?.(); } catch {}
|
||||
try { localStorage.removeItem(GRAPH_TOKEN_KEY); } catch {}
|
||||
try { localStorage.removeItem('pocketbase_auth'); } catch {}
|
||||
try { sessionStorage.clear(); } catch {}
|
||||
location.reload();
|
||||
|
||||
Reference in New Issue
Block a user