|
|
@@ -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');
|
|
|
|