/**
|
* 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.cacheExpiry = 5 * 60 * 1000; // 5 minutes
|
|
this.init();
|
}
|
|
/**
|
* Initialize authentication
|
*/
|
async init() {
|
if (this.isAuthenticating) return;
|
this.isAuthenticating = true;
|
|
try {
|
// Inlined by wp_localize_script — zero cost
|
if (typeof jvbAuth !== 'undefined') {
|
this.setAuthData(jvbAuth);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-loaded', { fromCache: false });
|
return;
|
}
|
|
// Fallback: REST fetch (Redis-backed, fast server-side)
|
await this.fetchAuth();
|
|
} catch (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 isFormData = options.body instanceof FormData;
|
|
const headers = {
|
...(!isFormData && { '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(retryCount + 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();
|
this.setAuthData(authData);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
|
this.notify('auth-loaded', { fromCache: false });
|
}
|
|
/**
|
* Set authentication data
|
*/
|
setAuthData(authData) {
|
const wasAuthenticated = this.initialized && this.authenticated;
|
|
this.authenticated = authData.authenticated || false;
|
this.user = authData.user || false;
|
this.nonces = authData.nonces || {};
|
console.log(this.nonces);
|
|
// Session expired — was logged in, now isn't
|
if (wasAuthenticated && !this.authenticated) {
|
window.location.href = `/login?redirect_to=${encodeURIComponent(window.location.href)}`;
|
}
|
}
|
|
/**
|
* Clear authentication data
|
*/
|
clearAuthData() {
|
this.authenticated = false;
|
this.user = null;
|
this.nonces = {};
|
}
|
|
|
/**
|
* 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) {
|
if (authData) {
|
this.setAuthData(authData);
|
this.initialized = true;
|
this.isAuthenticating = false;
|
this.notify('auth-loaded', { fromCache: false, fromLogin: true });
|
return;
|
}
|
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);
|
}
|
});
|
}
|
|
}
|
|
// Initialize global instance
|
window.auth = new AuthManager();
|