/** * quiXzoom SSO Client * Universal auth client for all quixzoom properties * * Usage: * import { quixzoomAuth } from './sso-client.js'; * * // Check if logged in * const user = await quixzoomAuth.getUser(); * * // Login * await quixzoomAuth.login(email, password); * * // Logout * await quixzoomAuth.logout(); */ const AUTH_BASE_URL = 'https://auth.quixzoom.com'; const COOKIE_NAMES = ['qz_access_token', 'qz_refresh_token']; class QuixzoomAuth { constructor() { this.user = null; this.tokenRefreshTimer = null; } /** * Initialize auth - call on page load */ async init() { // Check if we have a valid session const user = await this.getUser(); if (user) { this.startTokenRefresh(); } return user; } /** * Get current user (checks cookie automatically) */ async getUser() { try { const response = await fetch(`${AUTH_BASE_URL}/auth/me`, { method: 'GET', credentials: 'include', // Important: sends cookies headers: { 'Accept': 'application/json', } }); if (response.ok) { const data = await response.json(); this.user = data.user; return data.user; } // Try to refresh if access token expired if (response.status === 401) { const refreshed = await this.refreshToken(); if (refreshed) { return this.getUser(); } } return null; } catch (error) { console.error('Auth check failed:', error); return null; } } /** * Login with email/password */ async login(email, password, deviceInfo = {}) { const response = await fetch(`${AUTH_BASE_URL}/auth/login`, { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ email, password, device_info: deviceInfo, redirect_url: window.location.href, }) }); if (!response.ok) { const error = await response.json(); throw new Error(error.error || 'Login failed'); } const data = await response.json(); this.user = data.user; this.startTokenRefresh(); return data; } /** * Refresh access token */ async refreshToken() { try { const response = await fetch(`${AUTH_BASE_URL}/auth/refresh`, { method: 'POST', credentials: 'include', headers: { 'Accept': 'application/json', } }); if (response.ok) { const data = await response.json(); this.user = data.user; return true; } return false; } catch (error) { console.error('Token refresh failed:', error); return false; } } /** * Logout */ async logout() { try { await fetch(`${AUTH_BASE_URL}/auth/logout`, { method: 'POST', credentials: 'include', }); } catch (error) { console.error('Logout error:', error); } this.user = null; this.stopTokenRefresh(); // Clear any local storage localStorage.removeItem('quixzoom_user'); return true; } /** * Check if user is authenticated */ isAuthenticated() { return this.user !== null; } /** * Redirect to login if not authenticated */ async requireAuth(redirectUrl = window.location.href) { const user = await this.getUser(); if (!user) { // Store intended URL sessionStorage.setItem('auth_redirect', redirectUrl); // Redirect to login page const loginUrl = `https://auth.quixzoom.com/login?redirect=${encodeURIComponent(redirectUrl)}`; window.location.href = loginUrl; return null; } return user; } /** * Start automatic token refresh */ startTokenRefresh() { // Refresh 2 minutes before expiry const refreshInterval = (15 - 2) * 60 * 1000; // 13 minutes this.stopTokenRefresh(); this.tokenRefreshTimer = setInterval(() => { this.refreshToken(); }, refreshInterval); } /** * Stop automatic token refresh */ stopTokenRefresh() { if (this.tokenRefreshTimer) { clearInterval(this.tokenRefreshTimer); this.tokenRefreshTimer = null; } } /** * Get auth headers for API requests */ getAuthHeaders() { // Cookies are sent automatically, but we can add Bearer if needed return { 'Accept': 'application/json', }; } } // Singleton instance export const quixzoomAuth = new QuixzoomAuth(); // Auto-init on import quixzoomAuth.init(); // Also expose for non-module usage if (typeof window !== 'undefined') { window.quixzoomAuth = quixzoomAuth; }