/**
|
* AuthManager - Handles user authentication state
|
*
|
* Responsibilities:
|
* - Fetch and cache authentication state from /auth/status
|
* - Store auth data in sessionStorage to reduce API requests
|
* - Invalidate cache when WordPress cookie changes
|
* - Provide auth data through class properties
|
* - Emit events for auth state changes
|
*/
|
class AuthManager {
|
constructor() {
|
this.initialized = false;
|
this.isAuthenticating = false;
|
this.authenticated = false;
|
this.user = false;
|
this.nonces = {};
|
|
this.subscribers = new Set();
|
this.storageKey = 'jvb_auth_state';
|
this.cacheMetaKey = 'jvb_auth_meta';
|
this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
|
|
this.init();
|
}
|
|
/**
|
* Initialize authentication
|
*/
|
async init() {
|
if (this.isAuthenticating) {
|
return this.ready();
|
}
|
|
this.isAuthenticating = true;
|
|
try {
|
const cached = this.getCachedAuth();
|
if (cached) {
|
this.setAuthData(cached);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-loaded', { fromCache: true });
|
return;
|
}
|
|
await this.fetchAuth();
|
|
} catch (error) {
|
console.error('Failed to initialize auth:', error);
|
this.clearAuthData();
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-error', { error });
|
}
|
}
|
|
/**
|
* Refresh nonce if authentication fails
|
*/
|
async refreshNonce(action = 'wp_rest') {
|
try {
|
await this.fetchAuth();
|
return this.getNonce(action);
|
} catch (error) {
|
console.error('Failed to refresh nonce:', error);
|
return null;
|
}
|
}
|
|
/**
|
* Fetch with automatic nonce refresh on auth failure
|
* Use this for all authenticated API requests
|
*/
|
async fetch(url, options = {}) {
|
const attempt = async (retryCount = 0) => {
|
const headers = {
|
'Content-Type': 'application/json',
|
...options.headers,
|
'X-WP-Nonce': this.getNonce()
|
};
|
|
const response = await fetch(url, {
|
...options,
|
credentials: 'same-origin',
|
headers
|
});
|
|
if ((response.status === 403 || response.status === 401) && retryCount === 0) {
|
const result = await response.clone().json();
|
if (result.code === 'rest_cookie_invalid_nonce' || result.message?.includes('Cookie check')) {
|
console.log('Nonce invalid, refreshing auth...');
|
await this.refresh();
|
return attempt(1);
|
}
|
}
|
|
return response;
|
};
|
|
return attempt();
|
}
|
|
/**
|
* Fetch authentication status from API
|
*/
|
async fetchAuth() {
|
const response = await fetch(`${jvbSettings.api}auth/status`, {
|
method: 'GET',
|
credentials: 'same-origin',
|
headers: {
|
'Content-Type': 'application/json'
|
}
|
});
|
|
if (!response.ok) {
|
throw new Error('Auth check failed');
|
}
|
|
const authData = await response.json();
|
|
// Check if session changed (e.g., logout in another tab)
|
const cachedMeta = sessionStorage.getItem(this.cacheMetaKey);
|
if (cachedMeta) {
|
const meta = JSON.parse(cachedMeta);
|
if (meta.session_id && meta.session_id !== authData.session_id) {
|
this.clearCachedAuth();
|
this.notify('session-changed', {});
|
}
|
}
|
|
this.cacheAuth(authData);
|
this.setAuthData(authData);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-loaded', { fromCache: false });
|
}
|
|
/**
|
* Set authentication data
|
*/
|
setAuthData(authData) {
|
this.authenticated = authData.authenticated || false;
|
this.user = authData.user || false;
|
this.nonces = authData.nonces || {};
|
}
|
|
/**
|
* Clear authentication data
|
*/
|
clearAuthData() {
|
this.authenticated = false;
|
this.user = null;
|
this.nonces = {};
|
|
sessionStorage.removeItem(this.storageKey);
|
sessionStorage.removeItem(this.cacheMetaKey );
|
}
|
|
/**
|
* Get cached auth data (only if cookie matches)
|
*/
|
getCachedAuth() {
|
try {
|
const cachedAuth = sessionStorage.getItem(this.storageKey);
|
const cacheMeta = sessionStorage.getItem(this.cacheMetaKey);
|
|
if (!cachedAuth || !cacheMeta) {
|
return null;
|
}
|
|
const meta = JSON.parse(cacheMeta);
|
const authData = JSON.parse(cachedAuth);
|
|
// Time-based expiry (nonce freshness)
|
if (Date.now() - meta.timestamp > this.cacheExpiry) {
|
this.clearCachedAuth();
|
return null;
|
}
|
|
// Session changed (login/logout in another tab/window)
|
// We'll verify this on next fetch and clear if mismatched
|
|
return authData;
|
|
} catch (error) {
|
console.error('Error reading cached auth:', error);
|
return null;
|
}
|
}
|
|
/**
|
* Cache auth data in sessionStorage
|
*/
|
cacheAuth(authData) {
|
try {
|
sessionStorage.setItem(this.storageKey, JSON.stringify(authData));
|
sessionStorage.setItem(this.cacheMetaKey, JSON.stringify({
|
session_id: authData.session_id || null,
|
timestamp: Date.now()
|
}));
|
} catch (error) {
|
console.error('Error caching auth:', error);
|
}
|
}
|
|
clearCachedAuth() {
|
sessionStorage.removeItem(this.storageKey);
|
sessionStorage.removeItem(this.cacheMetaKey);
|
}
|
|
/**
|
* Refresh authentication (force new fetch)
|
*/
|
async refresh() {
|
this.isAuthenticating = true;
|
this.initialized = false;
|
|
try {
|
await this.fetchAuth();
|
this.notify('auth-refreshed', {});
|
} catch (error) {
|
console.error('Failed to refresh auth:', error);
|
this.clearAuthData();
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-error', { error });
|
}
|
}
|
|
/**
|
* Get nonce for a specific action
|
*/
|
getNonce(action = 'wp_rest') {
|
return this.nonces[action] || '';
|
}
|
|
getUser() {
|
return this.user;
|
}
|
|
isAuthenticated() {
|
return this.authenticated;
|
}
|
|
/**
|
* Handle successful login (call after login completes)
|
*/
|
async handleLogin(authData = null) {
|
// Clear old cache
|
sessionStorage.removeItem(this.storageKey);
|
sessionStorage.removeItem(this.cacheMetaKey);
|
|
// If auth data provided, use it directly
|
if (authData) {
|
this.cacheAuth(authData);
|
this.setAuthData(authData);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-loaded', { fromCache: false, fromLogin: true });
|
return;
|
}
|
|
// Otherwise fetch fresh (for backward compatibility)
|
await this.refresh();
|
}
|
|
/**
|
* Handle logout
|
*/
|
handleLogout() {
|
this.clearAuthData();
|
this.notify('logged-out', {});
|
}
|
|
/**
|
* Subscribe to auth events
|
*/
|
subscribe(callback) {
|
this.subscribers.add(callback);
|
|
// If already initialized, immediately notify
|
if (this.initialized) {
|
callback('auth-loaded', {
|
fromCache: false,
|
immediate: true
|
});
|
}
|
|
return () => this.subscribers.delete(callback);
|
}
|
|
/**
|
* Notify subscribers of events
|
*/
|
notify(event, data) {
|
this.subscribers.forEach(callback => {
|
try {
|
callback(event, data);
|
} catch (error) {
|
console.error('Subscriber error:', error);
|
}
|
});
|
}
|
|
/**
|
* Wait for auth to be ready
|
*/
|
ready() {
|
if (this.initialized) {
|
return Promise.resolve();
|
}
|
|
return new Promise(resolve => {
|
const unsubscribe = this.subscribe((event) => {
|
if (event === 'auth-loaded' || event === 'auth-error') {
|
unsubscribe();
|
resolve();
|
}
|
});
|
});
|
}
|
}
|
|
// Initialize global instance
|
window.auth = new AuthManager();
|