liuq 4 months ago
parent
commit
f8f6387090
1 changed files with 221 additions and 79 deletions
  1. 221 79
      server.js

+ 221 - 79
server.js

@@ -582,13 +582,17 @@ async function handleHomeAssistantLogin(config, credentials) {
         console.log(`Token 类型: ${tokens.token_type}`);
         console.log(`过期时间: ${tokens.expires_in}秒`);
         
-        // 返回 Token 和特殊标记
+        // OAuth2 跨端口方案:返回带有 code 的 URL,但使用增强的中间页面
+        // 虽然获取了 Token,但由于跨端口限制,我们仍然使用 code 方式
+        // 只是添加更好的处理逻辑
+        const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
+        
         return {
           success: true,
-          useTokenInjection: true,  // 标记使用 Token 注入方式
-          tokens: tokens,
+          useEnhancedRedirect: true,  // 使用增强的重定向方案
+          redirectUrl: magicLink,
+          tokens: tokens,  // 保留 Token 信息用于日志
           targetBaseUrl: targetBaseUrl,
-          clientId: CLIENT_ID,
           cookies: [],
           response: loginResponse.data
         };
@@ -891,17 +895,18 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
     
     console.log(`[${requestId}] 登录成功!`);
     
-    // 对于 Home Assistant,使用 Token 注入方式
-    if (config.loginMethod === 'home-assistant' && loginResult.useTokenInjection) {
-      console.log(`[${requestId}] Home Assistant 登录成功,使用 Token 注入方式`);
-      console.log(`[${requestId}] 生成注入页面...`);
+    // OAuth2 跨端口增强方案
+    if (config.loginMethod === 'home-assistant' && loginResult.useEnhancedRedirect) {
+      console.log(`[${requestId}] 🚀 Home Assistant OAuth2 跨端口方案`);
+      console.log(`[${requestId}] 已获取 Token(用于验证)`);
+      console.log(`[${requestId}] 使用 iframe 预加载方案,防止前端路由抢跑`);
       
-      const tokens = loginResult.tokens;
+      const magicLink = loginResult.redirectUrl;
       const targetBaseUrl = loginResult.targetBaseUrl;
-      const clientId = loginResult.clientId;
       
-      // 生成 Token 注入 HTML 页面
-      const tokenInjectionHtml = `
+      // 生成增强的 OAuth2 中间页面
+      // 核心策略:使用隐藏 iframe 先让 HA 处理 code,然后再显示跳转
+      const oauth2EnhancedHtml = `
 <!DOCTYPE html>
 <html lang="zh-CN">
 <head>
@@ -914,105 +919,159 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
             color: white;
             font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
             display: flex;
+            flex-direction: column;
             justify-content: center;
             align-items: center;
             height: 100vh;
             margin: 0;
         }
-        .container {
-            text-align: center;
-        }
+        .container { text-align: center; max-width: 500px; padding: 20px; }
         .loader {
             border: 4px solid rgba(255, 255, 255, 0.3);
             border-top: 4px solid white;
             border-radius: 50%;
-            width: 50px;
-            height: 50px;
+            width: 60px;
+            height: 60px;
             animation: spin 1s linear infinite;
-            margin: 0 auto 20px;
+            margin: 0 auto 30px;
         }
         @keyframes spin {
             0% { transform: rotate(0deg); }
             100% { transform: rotate(360deg); }
         }
-        h2 { margin: 0 0 10px 0; }
-        p { margin: 5px 0; opacity: 0.9; }
-        #status { font-size: 14px; margin-top: 20px; }
+        h2 { margin: 0 0 15px 0; font-size: 28px; }
+        .status { margin: 10px 0; opacity: 0.9; font-size: 16px; }
+        .step { 
+            background: rgba(255,255,255,0.1); 
+            padding: 10px 20px; 
+            border-radius: 8px; 
+            margin: 5px 0;
+            font-size: 14px;
+        }
+        .step.active { background: rgba(255,255,255,0.3); }
+        .step.done { background: rgba(76,175,80,0.5); }
+        #countdown { 
+            font-size: 48px; 
+            font-weight: bold; 
+            margin: 20px 0;
+            text-shadow: 0 2px 10px rgba(0,0,0,0.3);
+        }
+        .info {
+            margin-top: 20px;
+            font-size: 12px;
+            opacity: 0.7;
+        }
     </style>
 </head>
 <body>
     <div class="container">
         <div class="loader"></div>
-        <h2>正在安全登录...</h2>
-        <p>请稍候,正在完成身份验证</p>
-        <div id="status"></div>
+        <h2>✓ OAuth2 认证成功</h2>
+        <div class="status">正在完成跨端口登录流程...</div>
+        
+        <div class="step" id="step1">步骤 1: 获取授权码 ✓</div>
+        <div class="step active" id="step2">步骤 2: 预加载 Home Assistant</div>
+        <div class="step" id="step3">步骤 3: 等待 Token 交换</div>
+        <div class="step" id="step4">步骤 4: 跳转到仪表盘</div>
+        
+        <div id="countdown">5</div>
+        <div class="info">请勿关闭此页面</div>
     </div>
     
-    <!-- 使用隐藏 iframe 来完成 OAuth2 流程 -->
-    <iframe id="authFrame" style="display:none;"></iframe>
+    <!-- 隐藏 iframe 用于预加载 HA 并触发 code -> token 交换 -->
+    <iframe id="authFrame" style="position:absolute;width:0;height:0;border:0;"></iframe>
     
     <script>
         (function() {
-            const statusDiv = document.getElementById('status');
+            const iframe = document.getElementById('authFrame');
+            const step1 = document.getElementById('step1');
+            const step2 = document.getElementById('step2');
+            const step3 = document.getElementById('step3');
+            const step4 = document.getElementById('step4');
+            const countdownEl = document.getElementById('countdown');
             
-            function updateStatus(msg) {
-                console.log('[Token注入] ' + msg);
-                statusDiv.textContent = msg;
-            }
+            const magicLink = "${magicLink}";
+            const targetUrl = "${targetBaseUrl}";
             
-            // Token 数据
-            const tokens = ${JSON.stringify(tokens)};
-            const targetBaseUrl = "${targetBaseUrl}";
-            const clientId = "${clientId}";
+            console.log('========================================');
+            console.log('[OAuth2 跨端口] 开始处理');
+            console.log('[OAuth2 跨端口] 魔术链接:', magicLink);
+            console.log('[OAuth2 跨端口] 目标地址:', targetUrl);
+            console.log('========================================');
             
-            updateStatus('步骤 1/3: 准备 Token...');
+            let seconds = 5;
+            let iframeLoaded = false;
             
-            // 构造 HA 需要的 Token 格式
-            const hassTokens = {
-                access_token: tokens.access_token,
-                refresh_token: tokens.refresh_token,
-                expires_in: tokens.expires_in,
-                token_type: tokens.token_type,
-                clientId: clientId,
-                hassUrl: targetBaseUrl,
-                expires: Date.now() + (tokens.expires_in * 1000)
-            };
+            // 步骤 1: 已完成(后端已获取 code)
+            step1.classList.add('done');
+            
+            // 步骤 2: 使用 iframe 预加载带有 code 的 URL
+            console.log('[步骤 2/4] 使用 iframe 预加载 HA...');
             
-            console.log('[Token注入] Token 数据已准备:', hassTokens);
+            iframe.onload = function() {
+                if (!iframeLoaded) {
+                    iframeLoaded = true;
+                    console.log('[步骤 2/4] ✓ iframe 加载完成');
+                    step2.classList.remove('active');
+                    step2.classList.add('done');
+                    step3.classList.add('active');
+                    console.log('[步骤 3/4] 等待 HA 前端完成 Token 交换...');
+                    console.log('[步骤 3/4] HA 前端正在后台处理 OAuth2 code');
+                }
+            };
             
-            // 方案:使用 iframe + URL hash 传递 Token
-            // 因为 localStorage 跨端口不共享,我们使用 URL hash 作为中转
-            updateStatus('步骤 2/3: 加载 Home Assistant...');
+            iframe.onerror = function(e) {
+                console.error('[步骤 2/4] ✗ iframe 加载失败:', e);
+                console.log('[步骤 2/4] 将继续倒计时后跳转...');
+            };
             
-            // 将 Token 编码到 URL hash 中
-            const tokenData = btoa(JSON.stringify(hassTokens));
-            const iframe = document.getElementById('authFrame');
+            // 加载带有 authorization code 的 URL
+            // HA 前端会在 iframe 中处理这个 code 并换取 token
+            console.log('[步骤 2/4] 加载 URL:', magicLink);
+            iframe.src = magicLink;
             
-            // 先加载一个带有 Token 的特殊 URL
-            // HA 前端可以从 hash 中提取 Token
-            iframe.onload = function() {
-                updateStatus('步骤 3/3: 正在跳转...');
-                console.log('[Token注入] iframe 加载完成,准备跳转');
+            // 步骤 3 & 4: 倒计时后跳转
+            const countdown = setInterval(function() {
+                seconds--;
+                countdownEl.textContent = seconds;
                 
-                // 延迟跳转,确保 Token 已经被处理
-                setTimeout(function() {
-                    // 直接跳转到 HA,希望 Token 已经生效
-                    // 如果 HA 支持从 hash 读取 Token,这里会成功
-                    // 否则我们需要其他方案
-                    window.location.href = targetBaseUrl + '/lovelace';
-                }, 1000);
-            };
+                if (seconds === 2 && iframeLoaded) {
+                    console.log('[步骤 3/4] ✓ 假设 Token 交换已完成');
+                    step3.classList.remove('active');
+                    step3.classList.add('done');
+                    step4.classList.add('active');
+                    console.log('[步骤 4/4] 准备跳转到主界面...');
+                }
+                
+                if (seconds <= 0) {
+                    clearInterval(countdown);
+                    step4.classList.remove('active');
+                    step4.classList.add('done');
+                    console.log('[步骤 4/4] ✓ 跳转中...');
+                    console.log('========================================');
+                    
+                    // 最终跳转到 HA 主页
+                    // 由于 iframe 已经处理了 code,HA 应该已经有了 token
+                    window.location.href = targetUrl;
+                }
+            }, 1000);
             
-            // 尝试通过 hash 传递 Token(某些版本的 HA 可能支持)
-            iframe.src = targetBaseUrl + '/#token=' + encodeURIComponent(tokenData);
+            // 允许用户点击立即跳转(如果等不及)
+            document.body.style.cursor = 'pointer';
+            document.body.onclick = function() {
+                if (seconds <= 3) {  // 至少等待 2 秒
+                    console.log('[用户操作] 提前跳转');
+                    clearInterval(countdown);
+                    window.location.href = targetUrl;
+                }
+            };
             
-            // 备用方案:如果 5 秒后还没跳转,直接跳转到登录页
+            // 保险机制:10秒后强制跳转
             setTimeout(function() {
-                if (window.location.href.indexOf(targetBaseUrl) === -1) {
-                    console.log('[Token注入] 超时,直接跳转');
-                    window.location.href = targetBaseUrl;
-                }
-            }, 5000);
+                console.log('[超时保护] 10秒后强制跳转');
+                clearInterval(countdown);
+                window.location.href = targetUrl;
+            }, 10000);
             
         })();
     </script>
@@ -1023,18 +1082,101 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
       console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
       console.log('='.repeat(80) + '\n');
       
-      return res.send(tokenInjectionHtml);
+      return res.send(oauth2EnhancedHtml);
     }
     
-    // 对于 Home Assistant,如果使用传统 redirect 方式
+    // 对于 Home Assistant,如果使用传统 redirect 方式(降级方案)
     if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
-      console.log(`[${requestId}] Home Assistant 登录成功,使用传统 redirect 方式`);
+      console.log(`[${requestId}] Home Assistant 登录成功,使用传统 redirect 方式(降级)`);
       console.log(`[${requestId}] 重定向到: ${loginResult.redirectUrl}`);
+      
+      // 使用中间页面而不是直接 redirect
+      // 这样可以添加延迟,让 HA 前端有时间处理 code
+      const intermediateHtml = `
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>正在登录 Home Assistant...</title>
+    <style>
+        body {
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+            color: white;
+            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            height: 100vh;
+            margin: 0;
+        }
+        .container { text-align: center; }
+        .loader {
+            border: 4px solid rgba(255, 255, 255, 0.3);
+            border-top: 4px solid white;
+            border-radius: 50%;
+            width: 50px;
+            height: 50px;
+            animation: spin 1s linear infinite;
+            margin: 0 auto 20px;
+        }
+        @keyframes spin {
+            0% { transform: rotate(0deg); }
+            100% { transform: rotate(360deg); }
+        }
+        h2 { margin: 0 0 10px 0; }
+        p { margin: 5px 0; opacity: 0.9; font-size: 14px; }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="loader"></div>
+        <h2>正在登录...</h2>
+        <p>准备进入 Home Assistant</p>
+    </div>
+    <iframe id="authFrame" style="display:none;"></iframe>
+    <script>
+        // 使用 iframe 预加载带有 code 的 URL
+        // 让 HA 前端在后台完成 code → token 的交换
+        const authUrl = "${loginResult.redirectUrl}";
+        const targetUrl = "${config.targetBaseUrl}";
+        const iframe = document.getElementById('authFrame');
+        
+        console.log('[降级方案] 使用 iframe 预加载:', authUrl);
+        
+        // 设置 iframe 超时
+        let loaded = false;
+        iframe.onload = function() {
+            if (!loaded) {
+                loaded = true;
+                console.log('[降级方案] iframe 加载完成,等待 HA 处理 code...');
+                // 给 HA 足够时间处理 code
+                setTimeout(function() {
+                    console.log('[降级方案] 跳转到主页');
+                    window.location.href = targetUrl;
+                }, 2000);
+            }
+        };
+        
+        // 加载带有 code 的 URL
+        iframe.src = authUrl;
+        
+        // 保险起见,10秒后强制跳转
+        setTimeout(function() {
+            if (!loaded) {
+                console.log('[降级方案] 超时,强制跳转');
+                window.location.href = targetUrl;
+            }
+        }, 10000);
+    </script>
+</body>
+</html>
+      `;
+      
       console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
       console.log('='.repeat(80) + '\n');
       
-      // 直接重定向到带有 auth code 的 URL
-      return res.redirect(loginResult.redirectUrl);
+      return res.send(intermediateHtml);
     }
     
     // 解析 Cookie