/** * Secure Authentication Manager for KSTools License Manager * 實施安全情境:15分鐘無活動自動登出 + 關閉瀏覽器自動登出 */ class AuthManager { constructor() { this.supabase = null; this.currentUser = null; this.initialized = false; this.activityTimer = null; this.warningTimer = null; // 檢查是否為本地開發環境 this.isDevMode = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; if (this.isDevMode) { console.log('🔧 開發模式 - 跳過認證'); this.currentUser = { id: "dev-user", email: "admin@kstools.dev", user_metadata: { name: "開發管理員" } }; this.initialized = true; // 直接初始化應用 setTimeout(() => this.initializeApp(), 500); } else { // 安全情境:15分鐘無活動自動登出 this.ACTIVITY_TIMEOUT = 15 * 60 * 1000; // 15分鐘 this.WARNING_TIME = 2 * 60 * 1000; // 最後2分鐘顯示警告 console.log('🔧 AuthManager 創建 - 安全模式(15分鐘無活動+關閉瀏覽器自動登出)'); // 等待 Supabase 和配置載入後初始化 this.waitForDependenciesAndInit(); } } initializeApp() { console.log('✅ 開發模式 - 應用初始化'); // 隱藏載入畫面 const loadingOverlay = document.getElementById('loading-overlay'); if (loadingOverlay) { loadingOverlay.classList.add('hidden'); } // 更新用戶資訊顯示 this.updateAuthUI(); // 初始化應用 if (window.app) { window.app.init().catch(console.error); } // 更新UI顯示用戶資訊 this.updateUIForLoggedInUser(); } updateUIForLoggedInUser() { const userNameElements = document.querySelectorAll('.user-name, #userName'); userNameElements.forEach(el => { if (el) el.textContent = this.currentUser.user_metadata?.name || this.currentUser.email; }); const userEmailElements = document.querySelectorAll('.user-email, #userEmail'); userEmailElements.forEach(el => { if (el) el.textContent = this.currentUser.email; }); } async waitForDependenciesAndInit() { console.log('🔄 等待依賴載入...'); // 等待 Supabase 載入 if (!window.supabase?.createClient) { await new Promise(resolve => { const checkSupabase = setInterval(() => { if (window.supabase?.createClient) { clearInterval(checkSupabase); resolve(); } }, 50); // 最多等 3 秒 setTimeout(() => { clearInterval(checkSupabase); resolve(); }, 3000); }); } // 等待配置載入 if (!window.APP_CONFIG?.CONFIG_LOADED) { await new Promise(resolve => { const checkConfig = setInterval(() => { if (window.APP_CONFIG?.CONFIG_LOADED) { clearInterval(checkConfig); resolve(); } }, 50); // 最多等 3 秒 setTimeout(() => { clearInterval(checkConfig); resolve(); }, 3000); }); } // 依賴就緒後初始化 await this.init(); } async init() { try { console.log('🚀 AuthManager 初始化'); // 再次檢查依賴(防程式) if (!window.supabase?.createClient || !window.APP_CONFIG?.CONFIG_LOADED) { console.log('⚠️ 依賴仍未準備好,重定向到登入'); // 如果是登入頁面,不重定向 if (!window.location.pathname.includes('login.html')) { return this.redirectToLogin(); } return; } // 創建 Supabase 客戶端 const config = this.getSupabaseConfig(); if (!config.url || !config.anonKey) { console.log('⚠️ 配置缺失,重定向到登入'); return this.redirectToLogin(); } // 強制使用 sessionStorage,完全禁用 localStorage this.supabase = window.supabase.createClient(config.url, config.anonKey, { auth: { storage: window.sessionStorage, storageKey: 'kstools-auth-session', persistSession: true, detectSessionInUrl: false, autoRefreshToken: true, flowType: 'implicit' } }); console.log('✅ Supabase 客戶端創建成功(強制使用 sessionStorage)'); // 檢查當前會話 const { data: { session } } = await this.supabase.auth.getSession(); if (session?.user) { // 用戶已登入 this.currentUser = session.user; console.log('✅ 用戶已登入:', session.user.email); // 更新使用者介面 this.updateAuthUI(); // 啟動活動監控 this.startActivityMonitoring(); // 如果在登入頁面,重定向到後台 if (window.location.pathname.includes('login.html')) { this.redirectToDashboard(); } } else { // 用戶未登入 console.log('ℹ️ 用戶未登入'); // 如果不在登入頁面,重定向到登入 if (!window.location.pathname.includes('login.html')) { this.redirectToLogin(); } } // 監聽認證狀態變化 this.supabase.auth.onAuthStateChange((event, session) => { console.log('🔄 認證狀態變化:', event); if (event === 'SIGNED_IN' && session) { this.currentUser = session.user; this.updateAuthUI(); this.startActivityMonitoring(); // 只在登入頁面時跳轉,避免頁籤切換時的誤跳轉 if (window.location.pathname.includes('login.html')) { this.redirectToDashboard(); } } else if (event === 'SIGNED_OUT') { this.currentUser = null; this.updateAuthUI(); this.stopActivityMonitoring(); this.redirectToLogin(); } }); this.initialized = true; console.log('✅ AuthManager 初始化完成'); } catch (error) { console.error('❌ AuthManager 初始化失敗:', error); this.redirectToLogin(); } } getSupabaseConfig() { // Try sessionStorage first (不再使用 localStorage) const savedConfig = Utils.getStorage('supabaseConfig', {}); if (savedConfig.url && savedConfig.anonKey) { return savedConfig; } // Try global config if (window.APP_CONFIG) { return { url: window.APP_CONFIG.SUPABASE_LICENSE_URL, anonKey: window.APP_CONFIG.SUPABASE_LICENSE_ANON_KEY }; } console.warn('⚠️ Supabase configuration not available'); return { url: null, anonKey: null }; } async login(email, password) { if (!this.supabase) { throw new Error('Supabase not initialized'); } try { const { data, error } = await this.supabase.auth.signInWithPassword({ email: email, password: password }); if (error) throw error; if (data.user) { this.currentUser = data.user; // 只使用 sessionStorage(關閉瀏覽器即失效) sessionStorage.setItem('auth_session', 'active'); sessionStorage.setItem('auth_timestamp', Date.now().toString()); return { success: true, user: data.user }; } throw new Error('Login failed'); } catch (error) { let errorMessage = '登入失敗'; if (error.message.includes('Invalid login credentials')) { errorMessage = '電子郵件或密碼不正確'; } else if (error.message.includes('Email not confirmed')) { errorMessage = '請先確認您的電子郵件'; } return { success: false, error: errorMessage }; } } async logout() { if (!this.supabase) return { success: false }; try { this.stopActivityMonitoring(); await this.supabase.auth.signOut(); this.currentUser = null; // 清除所有認證相關的儲存資料 this.clearAllAuthData(); return { success: true }; } catch (error) { return { success: false, error: error.message }; } } clearAllAuthData() { console.log('🗑️ 清除所有認證相關資料'); try { // 清除自定義的 sessionStorage sessionStorage.removeItem('auth_session'); sessionStorage.removeItem('auth_timestamp'); // 清除 Supabase 相關的 sessionStorage sessionStorage.removeItem('kstools-auth-session'); const sessionKeys = Object.keys(sessionStorage); sessionKeys.forEach(key => { if (key.startsWith('sb-') || key.includes('supabase') || key.includes('kstools')) { sessionStorage.removeItem(key); } }); // 強制清除任何可能遺留在 localStorage 的認證資料 const localKeys = Object.keys(localStorage); localKeys.forEach(key => { if (key.startsWith('sb-') || key.includes('supabase') || key.includes('auth') || key.includes('kstools') || key.includes('rememberLogin')) { console.log(`🗑️ 清除 localStorage: ${key}`); localStorage.removeItem(key); } }); console.log('✅ 所有認證資料已清除(僅使用 sessionStorage)'); } catch (error) { console.warn('⚠️ 清除儲存資料時出錯:', error); } } // 活動監控方法 startActivityMonitoring() { const timeoutMinutes = Math.floor(this.ACTIVITY_TIMEOUT / 60000); const timeoutSeconds = Math.floor((this.ACTIVITY_TIMEOUT % 60000) / 1000); console.log(`🔍 啟動活動監控 (${timeoutMinutes > 0 ? timeoutMinutes + '分鐘' : timeoutSeconds + '秒'}無活動自動登出)`); // 清除現有定時器 this.stopActivityMonitoring(); // 重置活動定時器 this.resetActivityTimer(); // 監聽用戶活動事件 const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']; events.forEach(event => { document.addEventListener(event, this.onUserActivity.bind(this), true); }); } stopActivityMonitoring() { console.log('⏹️ 停止活動監控'); if (this.activityTimer) { clearTimeout(this.activityTimer); this.activityTimer = null; } if (this.warningTimer) { clearTimeout(this.warningTimer); this.warningTimer = null; } // 移除事件監聽器 const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click']; events.forEach(event => { document.removeEventListener(event, this.onUserActivity.bind(this), true); }); } onUserActivity() { // 重置活動定時器 this.resetActivityTimer(); } resetActivityTimer() { if (this.activityTimer) { clearTimeout(this.activityTimer); } this.activityTimer = setTimeout(() => { console.log('⏰ 15分鐘無活動,自動登出'); Utils.showWarning('會話超時', '由於15分鐘無活動,您將被自動登出'); setTimeout(() => { this.logout(); }, 2000); // 2秒後登出,讓用戶看到提示 }, this.ACTIVITY_TIMEOUT); } isAuthenticated() { return this.currentUser !== null; } requireAuth() { if (!this.isAuthenticated()) { this.redirectToLogin(); return false; } return true; } redirectToLogin() { console.log('🔄 重定向到登入頁面'); if (!window.location.pathname.includes('login.html')) { window.location.href = '/login.html'; } } redirectToDashboard() { console.log('🔄 重定向到儀表板頁面'); // 統一導向到 dashboard.html window.location.href = '/dashboard.html'; } updateAuthUI() { const userInfo = document.getElementById('userInfo'); if (!userInfo) return; if (this.currentUser) { userInfo.innerHTML = `
${this.currentUser.email}
`; } else { userInfo.innerHTML = ''; } } // Login page methods initLoginPage() { this.setupLoginForm(); this.setupPasswordToggle(); } setupLoginForm() { const loginForm = document.getElementById('loginForm'); if (!loginForm) return; loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(loginForm); const email = formData.get('email')?.trim(); const password = formData.get('password'); const remember = formData.get('rememberMe') === 'on'; if (!email || !password) { Utils.showError('請填寫所有必填欄位'); return; } const loginBtn = document.getElementById('loginBtn'); const originalContent = loginBtn.innerHTML; try { loginBtn.disabled = true; loginBtn.innerHTML = ' 登入中...'; const result = await this.login(email, password, remember); if (result.success) { Utils.showSuccess('登入成功', '正在跳轉...'); // 立即跳轉,不等待 this.redirectToDashboard(); } else { throw new Error(result.error); } } catch (error) { Utils.showError(error.message || '登入失敗'); loginBtn.disabled = false; loginBtn.innerHTML = originalContent; } }); } setupPasswordToggle() { const toggleBtn = document.getElementById('togglePassword'); const passwordInput = document.getElementById('password'); if (!toggleBtn || !passwordInput) return; toggleBtn.addEventListener('click', () => { const isPassword = passwordInput.type === 'password'; passwordInput.type = isPassword ? 'text' : 'password'; const icon = toggleBtn.querySelector('i'); if (icon) { icon.className = isPassword ? 'fas fa-eye-slash' : 'fas fa-eye'; } }); } // 統一的頁面認證檢查方法 async checkPageAuth(pageName = 'protected') { console.log(`🔐 檢查頁面認證: ${pageName}`); // 開發模式直接通過 if (this.isDevMode) { console.log('🔧 開發模式 - 跳過認證檢查'); this.updateAuthUI(); return true; } // 快速檢查初始化狀態 if (!this.initialized) { console.log('⏳ 快速初始化中...'); // 如果已有認證資訊,直接使用 const sessionAuth = sessionStorage.getItem('auth_session'); if (sessionAuth === 'active') { // 有認證資訊,立即通過 return true; } // 等待初始化,但時間極短 await new Promise(resolve => { const checkInit = setInterval(() => { if (this.initialized) { clearInterval(checkInit); resolve(); } }, 10); // 10ms 檢查一次 // 最多等待 500ms setTimeout(() => { clearInterval(checkInit); resolve(); }, 500); }); } // 檢查認證狀態 if (!this.isAuthenticated()) { console.log('❌ 用戶未登入,重定向到登入頁'); this.redirectToLogin(); return false; } console.log('✅ 認證檢查通過'); this.updateAuthUI(); this.startActivityMonitoring(); return true; } // 初始化受保護頁面(零延遲優化) async initProtectedPage(pageName, callback) { console.log(`🚀 初始化受保護頁面: ${pageName}`); const loadingOverlay = document.getElementById('loading-overlay'); // 立即隱藏載入畫面(零延遲) if (loadingOverlay) { loadingOverlay.style.display = 'none'; } try { // 快速檢查認證狀態 if (this.isDevMode || sessionStorage.getItem('auth_session') === 'active') { // 開發模式或已認證,立即執行回調 if (callback && typeof callback === 'function') { callback(); // 不用 await,讓頁面立即顯示 } // 背景執行完整認證檢查 this.checkPageAuth(pageName).then(isAuth => { if (!isAuth && !this.isDevMode) { this.redirectToLogin(); } }); } else { // 沒有認證,檢查後決定 const isAuth = await this.checkPageAuth(pageName); if (isAuth && callback && typeof callback === 'function') { await callback(); } } } catch (error) { console.error('❌ 頁面初始化失敗:', error); Utils.showError('頁面載入失敗,請重新整理'); } } // 統一的登出方法(增加清理和重定向) async handleLogout() { try { const result = await this.logout(); if (result.success) { Utils.showSuccess('登出成功'); this.redirectToLogin(); } else { Utils.showError('登出失敗'); } } catch (error) { console.error('登出錯誤:', error); // 即使出錯也重定向到登入頁 this.redirectToLogin(); } } // Development helper setSupabaseConfig(url, anonKey) { Utils.setStorage('supabaseConfig', { url, anonKey }); this.init(); } } // Create global instance - prevent duplicates if (!window.authManager) { console.log('🔧 Creating AuthManager instance...'); window.authManager = new AuthManager(); } else { console.log('⚠️ AuthManager already exists, skipping creation'); }