liuq il y a 4 mois
Parent
commit
7c62dbb41b
1 fichiers modifiés avec 193 ajouts et 24 suppressions
  1. 193 24
      server.js

+ 193 - 24
server.js

@@ -545,7 +545,7 @@ async function handleHomeAssistantLogin(config, credentials) {
     console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
     
     // ==========================================
-    // 步骤3: 构造浏览器跳转 URL(OAuth2 核心
+    // 步骤3: 换取 Token(全托管方案
     // ==========================================
     const responseData = loginResponse.data || {};
     const responseType = responseData.type;
@@ -555,28 +555,61 @@ async function handleHomeAssistantLogin(config, credentials) {
     // 如果登录成功,type 为 'create_entry',result 字段包含 Authorization Code
     if (responseData.result && responseType === 'create_entry') {
       const authCode = responseData.result;
-      console.log('[3/3] 登录成功!获取到 Authorization Code:', authCode);
+      console.log('[3/4] 登录成功!获取到 Authorization Code:', authCode);
+      console.log('[3/4] Node.js 将代替浏览器换取 Token...');
       
-      // 【核心修正点】
-      // 拼接的 URL 必须是 REDIRECT_URI + &code=...
-      // ⚠️ 绝对不能改成 /lovelace,否则会导致 redirect_uri_mismatch 错误
-      // HA 前端会自动完成:提取 code → 换取 Token → 跳转到 /lovelace
-      const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
-      
-      console.log('✅ 生成魔术链接:', magicLink);
-      console.log('📝 说明: 浏览器将访问根路径,HA 前端 JS 会自动:');
-      console.log('   1. 识别 URL 中的 code 参数');
-      console.log('   2. 向后端换取 access_token');
-      console.log('   3. 存储 token 到 localStorage');
-      console.log('   4. 自动跳转到 /lovelace 仪表盘');
-      
-      // 返回跳转 URL
-      return {
-        success: true,
-        redirectUrl: magicLink,
-        cookies: [], // HA 不依赖 Cookie,使用 Token 认证
-        response: loginResponse.data
-      };
+      try {
+        // ==========================================
+        // Node.js 直接换取 Token(避免前端路由抢跑问题)
+        // ==========================================
+        const tokenResponse = await axios.post(
+          `${targetBaseUrl}/auth/token`,
+          new URLSearchParams({
+            grant_type: 'authorization_code',
+            code: authCode,
+            client_id: CLIENT_ID
+          }).toString(),
+          {
+            headers: {
+              'Content-Type': 'application/x-www-form-urlencoded'
+            }
+          }
+        );
+        
+        const tokens = tokenResponse.data;
+        console.log('[4/4] ✅ Token 换取成功!');
+        console.log(`Access Token: ${tokens.access_token.substring(0, 20)}...`);
+        console.log(`Token 类型: ${tokens.token_type}`);
+        console.log(`过期时间: ${tokens.expires_in}秒`);
+        
+        // 返回 Token 和特殊标记
+        return {
+          success: true,
+          useTokenInjection: true,  // 标记使用 Token 注入方式
+          tokens: tokens,
+          targetBaseUrl: targetBaseUrl,
+          clientId: CLIENT_ID,
+          cookies: [],
+          response: loginResponse.data
+        };
+        
+      } catch (tokenError) {
+        console.error('❌ Token 换取失败:', tokenError.message);
+        if (tokenError.response) {
+          console.error('Token 响应:', JSON.stringify(tokenError.response.data, null, 2));
+        }
+        
+        // 如果 Token 换取失败,降级到传统方式
+        console.log('⚠️ 降级到传统 redirect 方式...');
+        const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
+        
+        return {
+          success: true,
+          redirectUrl: magicLink,
+          cookies: [],
+          response: loginResponse.data
+        };
+      }
     } else {
       console.error('❌ 登录失败!未返回 Authorization Code');
       console.error('响应数据:', responseData);
@@ -858,9 +891,145 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
     
     console.log(`[${requestId}] 登录成功!`);
     
-    // 对于 Home Assistant,如果返回了 redirectUrl,直接重定向
+    // 对于 Home Assistant,使用 Token 注入方式
+    if (config.loginMethod === 'home-assistant' && loginResult.useTokenInjection) {
+      console.log(`[${requestId}] Home Assistant 登录成功,使用 Token 注入方式`);
+      console.log(`[${requestId}] 生成注入页面...`);
+      
+      const tokens = loginResult.tokens;
+      const targetBaseUrl = loginResult.targetBaseUrl;
+      const clientId = loginResult.clientId;
+      
+      // 生成 Token 注入 HTML 页面
+      const tokenInjectionHtml = `
+<!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; }
+        #status { font-size: 14px; margin-top: 20px; }
+    </style>
+</head>
+<body>
+    <div class="container">
+        <div class="loader"></div>
+        <h2>正在安全登录...</h2>
+        <p>请稍候,正在完成身份验证</p>
+        <div id="status"></div>
+    </div>
+    
+    <!-- 使用隐藏 iframe 来完成 OAuth2 流程 -->
+    <iframe id="authFrame" style="display:none;"></iframe>
+    
+    <script>
+        (function() {
+            const statusDiv = document.getElementById('status');
+            
+            function updateStatus(msg) {
+                console.log('[Token注入] ' + msg);
+                statusDiv.textContent = msg;
+            }
+            
+            // Token 数据
+            const tokens = ${JSON.stringify(tokens)};
+            const targetBaseUrl = "${targetBaseUrl}";
+            const clientId = "${clientId}";
+            
+            updateStatus('步骤 1/3: 准备 Token...');
+            
+            // 构造 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)
+            };
+            
+            console.log('[Token注入] Token 数据已准备:', hassTokens);
+            
+            // 方案:使用 iframe + URL hash 传递 Token
+            // 因为 localStorage 跨端口不共享,我们使用 URL hash 作为中转
+            updateStatus('步骤 2/3: 加载 Home Assistant...');
+            
+            // 将 Token 编码到 URL hash 中
+            const tokenData = btoa(JSON.stringify(hassTokens));
+            const iframe = document.getElementById('authFrame');
+            
+            // 先加载一个带有 Token 的特殊 URL
+            // HA 前端可以从 hash 中提取 Token
+            iframe.onload = function() {
+                updateStatus('步骤 3/3: 正在跳转...');
+                console.log('[Token注入] iframe 加载完成,准备跳转');
+                
+                // 延迟跳转,确保 Token 已经被处理
+                setTimeout(function() {
+                    // 直接跳转到 HA,希望 Token 已经生效
+                    // 如果 HA 支持从 hash 读取 Token,这里会成功
+                    // 否则我们需要其他方案
+                    window.location.href = targetBaseUrl + '/lovelace';
+                }, 1000);
+            };
+            
+            // 尝试通过 hash 传递 Token(某些版本的 HA 可能支持)
+            iframe.src = targetBaseUrl + '/#token=' + encodeURIComponent(tokenData);
+            
+            // 备用方案:如果 5 秒后还没跳转,直接跳转到登录页
+            setTimeout(function() {
+                if (window.location.href.indexOf(targetBaseUrl) === -1) {
+                    console.log('[Token注入] 超时,直接跳转');
+                    window.location.href = targetBaseUrl;
+                }
+            }, 5000);
+            
+        })();
+    </script>
+</body>
+</html>
+      `;
+      
+      console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
+      console.log('='.repeat(80) + '\n');
+      
+      return res.send(tokenInjectionHtml);
+    }
+    
+    // 对于 Home Assistant,如果使用传统 redirect 方式
     if (config.loginMethod === 'home-assistant' && loginResult.redirectUrl) {
-      console.log(`[${requestId}] Home Assistant 登录成功,直接重定向到: ${loginResult.redirectUrl}`);
+      console.log(`[${requestId}] Home Assistant 登录成功,使用传统 redirect 方式`);
+      console.log(`[${requestId}] 重定向到: ${loginResult.redirectUrl}`);
       console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
       console.log('='.repeat(80) + '\n');