/** * quiXzoom SSO Client v2.0 * Universal cross-domain silent login * * Usage: * * */ (function(global) { 'use strict'; const DEFAULT_CONFIG = { ssoUrl: 'https://auth.quixzoom.com', checkInterval: 60000, // Check auth every 60 seconds silentTimeout: 5000, }; class QZAuthClient { constructor() { this.config = { ...DEFAULT_CONFIG }; this.user = null; this.isAuthenticated = false; this.callbacks = { onLogin: null, onLogout: null, onError: null, }; this.checkIntervalId = null; } init(config = {}) { this.config = { ...this.config, ...config }; if (config.onLogin) this.callbacks.onLogin = config.onLogin; if (config.onLogout) this.callbacks.onLogout = config.onLogout; if (config.onError) this.callbacks.onError = config.onError; // Check auth immediately this.checkAuth(); // Set up periodic checks this.checkIntervalId = setInterval(() => { this.checkAuth(); }, this.config.checkInterval); // Listen for storage events (login from other tabs) window.addEventListener('storage', (e) => { if (e.key === 'qz_auth_event') { const event = JSON.parse(e.newValue || '{}'); if (event.type === 'login') { this.checkAuth(); } else if (event.type === 'logout') { this.logout(); } } }); // Listen for messages from SSO iframe window.addEventListener('message', (e) => { if (e.origin !== this.config.ssoUrl) return; if (e.data.type === 'qz_auth_silent_response') { this.handleSilentResponse(e.data); } }); console.log('[QZAuth] Initialized'); } async checkAuth() { try { // Try silent login first (uses refresh token cookie) const response = await fetch(`${this.config.ssoUrl}/auth/silent`, { method: 'GET', credentials: 'include', }); if (response.ok) { const data = await response.json(); if (data.authenticated) { this.setUser(data.user); return; } } // Silent login failed, check if we have a token in URL (OAuth redirect) const urlParams = new URLSearchParams(window.location.search); const accessToken = urlParams.get('access_token'); if (accessToken) { // Verify token with SSO const verifyResponse = await fetch(`${this.config.ssoUrl}/auth/me`, { headers: { 'Authorization': `Bearer ${accessToken}`, }, }); if (verifyResponse.ok) { const user = await verifyResponse.json(); this.setUser(user); // Clean URL window.history.replaceState({}, document.title, window.location.pathname); return; } } // Not authenticated this.clearUser(); } catch (error) { console.error('[QZAuth] Auth check failed:', error); if (this.callbacks.onError) { this.callbacks.onError(error); } } } handleSilentResponse(data) { if (data.authenticated) { this.setUser(data.user); } else { this.clearUser(); } } setUser(user) { const wasAuthenticated = this.isAuthenticated; this.user = user; this.isAuthenticated = true; // Store in localStorage for other tabs localStorage.setItem('qz_user', JSON.stringify(user)); localStorage.setItem('qz_auth_event', JSON.stringify({ type: 'login', timestamp: Date.now(), })); // Update UI this.updateUI(); // Trigger callback if newly logged in if (!wasAuthenticated && this.callbacks.onLogin) { this.callbacks.onLogin(user); } } clearUser() { const wasAuthenticated = this.isAuthenticated; this.user = null; this.isAuthenticated = false; localStorage.removeItem('qz_user'); localStorage.setItem('qz_auth_event', JSON.stringify({ type: 'logout', timestamp: Date.now(), })); this.updateUI(); if (wasAuthenticated && this.callbacks.onLogout) { this.callbacks.onLogout(); } } updateUI() { // Update all elements with data-qz-auth attribute document.querySelectorAll('[data-qz-auth]').forEach(el => { const action = el.getAttribute('data-qz-auth'); if (action === 'show-when-authenticated') { el.style.display = this.isAuthenticated ? '' : 'none'; } else if (action === 'show-when-guest') { el.style.display = !this.isAuthenticated ? '' : 'none'; } else if (action === 'user-name') { el.textContent = this.user?.name || ''; } else if (action === 'user-email') { el.textContent = this.user?.email || ''; } else if (action === 'user-avatar') { el.src = this.user?.avatar || '/default-avatar.png'; } }); } async login(email, password) { try { const response = await fetch(`${this.config.ssoUrl}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ email, password }), }); if (!response.ok) { throw new Error('Login failed'); } const data = await response.json(); this.setUser(data.user); return data; } catch (error) { console.error('[QZAuth] Login failed:', error); throw error; } } async logout() { try { await fetch(`${this.config.ssoUrl}/auth/logout`, { method: 'POST', credentials: 'include', }); } catch (error) { console.error('[QZAuth] Logout error:', error); } finally { this.clearUser(); } } getUser() { return this.user; } isLoggedIn() { return this.isAuthenticated; } destroy() { if (this.checkIntervalId) { clearInterval(this.checkIntervalId); } } } // Create global instance global.QZAuth = new QZAuthClient(); })(window);