| | |
| | | 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(); |
| | | } |
| | | |
| | | if (this.isAuthenticating) return; |
| | | this.isAuthenticating = true; |
| | | |
| | | try { |
| | | const cached = this.getCachedAuth(); |
| | | if (cached) { |
| | | this.setAuthData(cached); |
| | | // 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: true }); |
| | | this.notify('auth-loaded', { fromCache: false }); |
| | | return; |
| | | } |
| | | |
| | | // Fallback: REST fetch (Redis-backed, fast server-side) |
| | | await this.fetchAuth(); |
| | | |
| | | } catch (error) { |
| | | console.error('Failed to initialize auth:', error); |
| | | this.clearAuthData(); |
| | | this.initialized = true; |
| | | this.isAuthenticating = false; |
| | |
| | | */ |
| | | async fetch(url, options = {}) { |
| | | const attempt = async (retryCount = 0) => { |
| | | const isFormData = options.body instanceof FormData; |
| | | |
| | | const headers = { |
| | | 'Content-Type': 'application/json', |
| | | ...(!isFormData && { 'Content-Type': 'application/json' }), |
| | | ...options.headers, |
| | | 'X-WP-Nonce': this.getNonce() |
| | | }; |
| | |
| | | headers |
| | | }); |
| | | |
| | | // If auth failed and we haven't retried yet, refresh and try once more |
| | | if ((response.status === 403 || response.status === 401) && retryCount === 0) { |
| | | const result = await response.clone().json(); |
| | | if (resconsole.logult.code === 'rest_cookie_invalid_nonce' || result.message?.includes('Cookie check')) { |
| | | ('Nonce invalid, refreshing auth...'); |
| | | if (result.code === 'rest_cookie_invalid_nonce' || result.message?.includes('Cookie check')) { |
| | | console.log('Nonce invalid, refreshing auth...'); |
| | | await this.refresh(); |
| | | return attempt(1); // Retry once |
| | | return attempt(retryCount + 1); |
| | | } |
| | | } |
| | | |
| | |
| | | const response = await fetch(`${jvbSettings.api}auth/status`, { |
| | | method: 'GET', |
| | | credentials: 'same-origin', |
| | | headers: { |
| | | 'Content-Type': 'application/json' |
| | | } |
| | | headers: { 'Content-Type': 'application/json' } |
| | | }); |
| | | |
| | | if (!response.ok) { |
| | | throw new Error('Auth check failed'); |
| | | } |
| | | 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) { |
| | | const wasAuthenticated = this.initialized && this.authenticated; |
| | | |
| | | this.authenticated = authData.authenticated || false; |
| | | this.user = authData.user || false; |
| | | this.nonces = authData.nonces || {}; |
| | | |
| | | // Session expired — was logged in, now isn't |
| | | if (wasAuthenticated && !this.authenticated) { |
| | | window.location.href = `/login?redirect_to=${encodeURIComponent(window.location.href)}`; |
| | | } |
| | | } |
| | | |
| | | /** |
| | |
| | | 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) |
| | |
| | | * 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(); |
| | | } |
| | | |
| | |
| | | }); |
| | | } |
| | | |
| | | /** |
| | | * 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 |