|
@@ -895,194 +895,217 @@ app.get('/api/auto-login/:siteId', async (req, res) => {
|
|
|
|
|
|
|
|
console.log(`[${requestId}] 登录成功!`);
|
|
console.log(`[${requestId}] 登录成功!`);
|
|
|
|
|
|
|
|
- // OAuth2 跨端口增强方案
|
|
|
|
|
|
|
+ // OAuth2 跨端口:调试页面方案
|
|
|
if (config.loginMethod === 'home-assistant' && loginResult.useEnhancedRedirect) {
|
|
if (config.loginMethod === 'home-assistant' && loginResult.useEnhancedRedirect) {
|
|
|
- console.log(`[${requestId}] 🚀 Home Assistant OAuth2 跨端口方案`);
|
|
|
|
|
- console.log(`[${requestId}] 已获取 Token(用于验证)`);
|
|
|
|
|
- console.log(`[${requestId}] 使用 iframe 预加载方案,防止前端路由抢跑`);
|
|
|
|
|
|
|
+ console.log(`[${requestId}] 🚀 Home Assistant OAuth2 - 调试重定向方案`);
|
|
|
|
|
+ console.log(`[${requestId}] Token 已获取: ${loginResult.tokens.access_token.substring(0, 30)}...`);
|
|
|
|
|
+ console.log(`[${requestId}] Authorization Code: ${loginResult.redirectUrl.match(/code=([^&]+)/)?.[1]}`);
|
|
|
|
|
+ console.log(`[${requestId}] 重定向 URL: ${loginResult.redirectUrl}`);
|
|
|
|
|
|
|
|
const magicLink = loginResult.redirectUrl;
|
|
const magicLink = loginResult.redirectUrl;
|
|
|
- const targetBaseUrl = loginResult.targetBaseUrl;
|
|
|
|
|
|
|
+ const authCode = magicLink.match(/code=([^&]+)/)?.[1] || 'unknown';
|
|
|
|
|
|
|
|
- // 生成增强的 OAuth2 中间页面
|
|
|
|
|
- // 核心策略:使用隐藏 iframe 先让 HA 处理 code,然后再显示跳转
|
|
|
|
|
- const oauth2EnhancedHtml = `
|
|
|
|
|
|
|
+ // 生成调试页面
|
|
|
|
|
+ const debugHtml = `
|
|
|
<!DOCTYPE html>
|
|
<!DOCTYPE html>
|
|
|
<html lang="zh-CN">
|
|
<html lang="zh-CN">
|
|
|
<head>
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
- <title>正在登录 Home Assistant...</title>
|
|
|
|
|
|
|
+ <title>Home Assistant OAuth2 调试</title>
|
|
|
<style>
|
|
<style>
|
|
|
body {
|
|
body {
|
|
|
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
|
|
|
- color: white;
|
|
|
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
- justify-content: center;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- height: 100vh;
|
|
|
|
|
|
|
+ background: #1e1e1e;
|
|
|
|
|
+ color: #d4d4d4;
|
|
|
|
|
+ padding: 20px;
|
|
|
margin: 0;
|
|
margin: 0;
|
|
|
}
|
|
}
|
|
|
- .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: 60px;
|
|
|
|
|
- height: 60px;
|
|
|
|
|
- animation: spin 1s linear infinite;
|
|
|
|
|
- margin: 0 auto 30px;
|
|
|
|
|
|
|
+ .container {
|
|
|
|
|
+ max-width: 1000px;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
}
|
|
}
|
|
|
- @keyframes spin {
|
|
|
|
|
- 0% { transform: rotate(0deg); }
|
|
|
|
|
- 100% { transform: rotate(360deg); }
|
|
|
|
|
|
|
+ h1 {
|
|
|
|
|
+ color: #4ec9b0;
|
|
|
|
|
+ border-bottom: 2px solid #4ec9b0;
|
|
|
|
|
+ padding-bottom: 10px;
|
|
|
}
|
|
}
|
|
|
- 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;
|
|
|
|
|
|
|
+ .section {
|
|
|
|
|
+ background: #252526;
|
|
|
|
|
+ border: 1px solid #3e3e42;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ padding: 20px;
|
|
|
margin: 20px 0;
|
|
margin: 20px 0;
|
|
|
- text-shadow: 0 2px 10px rgba(0,0,0,0.3);
|
|
|
|
|
}
|
|
}
|
|
|
- .info {
|
|
|
|
|
- margin-top: 20px;
|
|
|
|
|
|
|
+ .section h2 {
|
|
|
|
|
+ color: #569cd6;
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+ }
|
|
|
|
|
+ pre {
|
|
|
|
|
+ background: #1e1e1e;
|
|
|
|
|
+ border: 1px solid #3e3e42;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ overflow-x: auto;
|
|
|
|
|
+ }
|
|
|
|
|
+ .success { color: #4ec9b0; }
|
|
|
|
|
+ .warning { color: #ce9178; }
|
|
|
|
|
+ .error { color: #f48771; }
|
|
|
|
|
+ .button {
|
|
|
|
|
+ background: #0e639c;
|
|
|
|
|
+ color: white;
|
|
|
|
|
+ border: none;
|
|
|
|
|
+ padding: 12px 24px;
|
|
|
|
|
+ border-radius: 6px;
|
|
|
|
|
+ font-size: 16px;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ margin: 10px 5px;
|
|
|
|
|
+ }
|
|
|
|
|
+ .button:hover { background: #1177bb; }
|
|
|
|
|
+ .button.secondary {
|
|
|
|
|
+ background: #3e3e42;
|
|
|
|
|
+ }
|
|
|
|
|
+ .button.secondary:hover { background: #505050; }
|
|
|
|
|
+ #log {
|
|
|
|
|
+ background: #1e1e1e;
|
|
|
|
|
+ border: 1px solid #3e3e42;
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ padding: 15px;
|
|
|
|
|
+ max-height: 300px;
|
|
|
|
|
+ overflow-y: auto;
|
|
|
|
|
+ font-family: 'Consolas', 'Monaco', monospace;
|
|
|
font-size: 12px;
|
|
font-size: 12px;
|
|
|
- opacity: 0.7;
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ .log-entry {
|
|
|
|
|
+ margin: 5px 0;
|
|
|
|
|
+ padding: 5px;
|
|
|
|
|
+ border-left: 3px solid #569cd6;
|
|
|
|
|
+ padding-left: 10px;
|
|
|
}
|
|
}
|
|
|
</style>
|
|
</style>
|
|
|
</head>
|
|
</head>
|
|
|
<body>
|
|
<body>
|
|
|
<div class="container">
|
|
<div class="container">
|
|
|
- <div class="loader"></div>
|
|
|
|
|
- <h2>✓ OAuth2 认证成功</h2>
|
|
|
|
|
- <div class="status">正在完成跨端口登录流程...</div>
|
|
|
|
|
|
|
+ <h1>🔍 Home Assistant OAuth2 登录调试</h1>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section">
|
|
|
|
|
+ <h2>✅ 后端登录成功</h2>
|
|
|
|
|
+ <p>Authorization Code 已获取,Token 已验证。</p>
|
|
|
|
|
+ <p><strong class="success">Authorization Code:</strong> <code>${authCode.substring(0, 20)}...</code></p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section">
|
|
|
|
|
+ <h2>📋 OAuth2 跨端口问题分析</h2>
|
|
|
|
|
+ <p class="warning">⚠️ 检测到跨端口场景:</p>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li>Node.js 后端:<code>222.243.138.146:8889</code></li>
|
|
|
|
|
+ <li>Home Assistant:<code>222.243.138.146:8123</code></li>
|
|
|
|
|
+ <li>localStorage 隔离:不同端口无法共享 Token</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="section">
|
|
|
|
|
+ <h2>🎯 手动测试步骤</h2>
|
|
|
|
|
+ <p>请按以下步骤测试,帮助我们诊断问题:</p>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>测试 1:直接访问魔术链接</h3>
|
|
|
|
|
+ <p>复制下面的 URL 到新标签页,看是否能登录:</p>
|
|
|
|
|
+ <pre>${magicLink}</pre>
|
|
|
|
|
+ <button class="button" onclick="window.open('${magicLink}', '_blank')">
|
|
|
|
|
+ 🔗 在新标签页打开
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>测试 2:在当前标签页跳转</h3>
|
|
|
|
|
+ <p>让当前页面跳转过去(可能有更好的效果):</p>
|
|
|
|
|
+ <button class="button secondary" onclick="window.location.href='${magicLink}'">
|
|
|
|
|
+ ➡️ 当前标签页跳转
|
|
|
|
|
+ </button>
|
|
|
|
|
+
|
|
|
|
|
+ <h3>测试 3:iframe 预加载然后跳转</h3>
|
|
|
|
|
+ <p>使用 iframe 预加载,5秒后跳转:</p>
|
|
|
|
|
+ <button class="button secondary" onclick="testIframeMethod()">
|
|
|
|
|
+ 🔄 使用 iframe 方案
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </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 class="section">
|
|
|
|
|
+ <h2>📊 实时日志</h2>
|
|
|
|
|
+ <div id="log"></div>
|
|
|
|
|
+ </div>
|
|
|
|
|
|
|
|
- <div id="countdown">5</div>
|
|
|
|
|
- <div class="info">请勿关闭此页面</div>
|
|
|
|
|
|
|
+ <div class="section">
|
|
|
|
|
+ <h2>💡 建议</h2>
|
|
|
|
|
+ <p>如果以上测试都失败,强烈建议使用 <strong class="success">Trusted Networks</strong> 方案:</p>
|
|
|
|
|
+ <ul>
|
|
|
|
|
+ <li>✅ 官方支持,100% 可靠</li>
|
|
|
|
|
+ <li>✅ 无需复杂的 OAuth2 流程</li>
|
|
|
|
|
+ <li>✅ 零延迟,直接登录</li>
|
|
|
|
|
+ </ul>
|
|
|
|
|
+ <button class="button" onclick="alert('请在 Home Assistant 的 configuration.yaml 中配置:\\n\\nhomeassistant:\\n auth_providers:\\n - type: trusted_networks\\n trusted_networks:\\n - 118.251.191.88/32\\n trusted_users:\\n 118.251.191.88/32: YOUR_USER_ID\\n allow_bypass_login: true\\n - type: homeassistant')">
|
|
|
|
|
+ 📖 查看 Trusted Networks 配置
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 隐藏 iframe 用于预加载 HA 并触发 code -> token 交换 -->
|
|
|
|
|
- <iframe id="authFrame" style="position:absolute;width:0;height:0;border:0;"></iframe>
|
|
|
|
|
|
|
+ <iframe id="testFrame" style="display:none;"></iframe>
|
|
|
|
|
|
|
|
<script>
|
|
<script>
|
|
|
- (function() {
|
|
|
|
|
- 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');
|
|
|
|
|
-
|
|
|
|
|
- const magicLink = "${magicLink}";
|
|
|
|
|
- const targetUrl = "${targetBaseUrl}";
|
|
|
|
|
-
|
|
|
|
|
- console.log('========================================');
|
|
|
|
|
- console.log('[OAuth2 跨端口] 开始处理');
|
|
|
|
|
- console.log('[OAuth2 跨端口] 魔术链接:', magicLink);
|
|
|
|
|
- console.log('[OAuth2 跨端口] 目标地址:', targetUrl);
|
|
|
|
|
- console.log('========================================');
|
|
|
|
|
-
|
|
|
|
|
- let seconds = 5;
|
|
|
|
|
- let iframeLoaded = false;
|
|
|
|
|
-
|
|
|
|
|
- // 步骤 1: 已完成(后端已获取 code)
|
|
|
|
|
- step1.classList.add('done');
|
|
|
|
|
-
|
|
|
|
|
- // 步骤 2: 使用 iframe 预加载带有 code 的 URL
|
|
|
|
|
- console.log('[步骤 2/4] 使用 iframe 预加载 HA...');
|
|
|
|
|
|
|
+ const logDiv = document.getElementById('log');
|
|
|
|
|
+ const iframe = document.getElementById('testFrame');
|
|
|
|
|
+
|
|
|
|
|
+ function addLog(msg, type = 'info') {
|
|
|
|
|
+ const entry = document.createElement('div');
|
|
|
|
|
+ entry.className = 'log-entry';
|
|
|
|
|
+ entry.textContent = new Date().toLocaleTimeString() + ' - ' + msg;
|
|
|
|
|
+ logDiv.appendChild(entry);
|
|
|
|
|
+ logDiv.scrollTop = logDiv.scrollHeight;
|
|
|
|
|
+ console.log('[调试] ' + msg);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function testIframeMethod() {
|
|
|
|
|
+ addLog('开始 iframe 测试...');
|
|
|
|
|
+ addLog('加载 URL: ${magicLink}');
|
|
|
|
|
|
|
|
|
|
+ let loaded = false;
|
|
|
iframe.onload = function() {
|
|
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');
|
|
|
|
|
|
|
+ if (!loaded) {
|
|
|
|
|
+ loaded = true;
|
|
|
|
|
+ addLog('✓ iframe 加载完成');
|
|
|
|
|
+ addLog('等待 5 秒后跳转...');
|
|
|
|
|
+
|
|
|
|
|
+ let countdown = 5;
|
|
|
|
|
+ const timer = setInterval(function() {
|
|
|
|
|
+ countdown--;
|
|
|
|
|
+ addLog('倒计时: ' + countdown + '秒');
|
|
|
|
|
+ if (countdown <= 0) {
|
|
|
|
|
+ clearInterval(timer);
|
|
|
|
|
+ addLog('正在跳转到 Home Assistant...');
|
|
|
|
|
+ window.location.href = '${targetBaseUrl}';
|
|
|
|
|
+ }
|
|
|
|
|
+ }, 1000);
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
iframe.onerror = function(e) {
|
|
iframe.onerror = function(e) {
|
|
|
- console.error('[步骤 2/4] ✗ iframe 加载失败:', e);
|
|
|
|
|
- console.log('[步骤 2/4] 将继续倒计时后跳转...');
|
|
|
|
|
|
|
+ addLog('✗ iframe 加载失败: ' + e, 'error');
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
- // 加载带有 authorization code 的 URL
|
|
|
|
|
- // HA 前端会在 iframe 中处理这个 code 并换取 token
|
|
|
|
|
- console.log('[步骤 2/4] 加载 URL:', magicLink);
|
|
|
|
|
- iframe.src = magicLink;
|
|
|
|
|
-
|
|
|
|
|
- // 步骤 3 & 4: 倒计时后跳转
|
|
|
|
|
- const countdown = setInterval(function() {
|
|
|
|
|
- seconds--;
|
|
|
|
|
- countdownEl.textContent = seconds;
|
|
|
|
|
-
|
|
|
|
|
- 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);
|
|
|
|
|
-
|
|
|
|
|
- // 允许用户点击立即跳转(如果等不及)
|
|
|
|
|
- document.body.style.cursor = 'pointer';
|
|
|
|
|
- document.body.onclick = function() {
|
|
|
|
|
- if (seconds <= 3) { // 至少等待 2 秒
|
|
|
|
|
- console.log('[用户操作] 提前跳转');
|
|
|
|
|
- clearInterval(countdown);
|
|
|
|
|
- window.location.href = targetUrl;
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
-
|
|
|
|
|
- // 保险机制:10秒后强制跳转
|
|
|
|
|
- setTimeout(function() {
|
|
|
|
|
- console.log('[超时保护] 10秒后强制跳转');
|
|
|
|
|
- clearInterval(countdown);
|
|
|
|
|
- window.location.href = targetUrl;
|
|
|
|
|
- }, 10000);
|
|
|
|
|
-
|
|
|
|
|
- })();
|
|
|
|
|
|
|
+ iframe.src = '${magicLink}';
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ addLog('后端 OAuth2 登录成功');
|
|
|
|
|
+ addLog('Authorization Code: ${authCode.substring(0, 20)}...');
|
|
|
|
|
+ addLog('请选择测试方法');
|
|
|
</script>
|
|
</script>
|
|
|
</body>
|
|
</body>
|
|
|
</html>
|
|
</html>
|
|
|
`;
|
|
`;
|
|
|
|
|
|
|
|
|
|
+ console.log(`[${requestId}] 返回调试页面,供手动测试`);
|
|
|
console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
|
|
console.log(`[${requestId}] 总耗时: ${Date.now() - startTime}ms`);
|
|
|
console.log('='.repeat(80) + '\n');
|
|
console.log('='.repeat(80) + '\n');
|
|
|
|
|
|
|
|
- return res.send(oauth2EnhancedHtml);
|
|
|
|
|
|
|
+ return res.send(debugHtml);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 对于 Home Assistant,如果使用传统 redirect 方式(降级方案)
|
|
// 对于 Home Assistant,如果使用传统 redirect 方式(降级方案)
|
|
@@ -1364,3 +1387,4 @@ app.listen(PORT, '0.0.0.0', () => {
|
|
|
console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
|
|
console.log(`\n💡 提示: 确保防火墙已配置端口映射 (前端:8888, 后端:8889 -> 外网)`);
|
|
|
console.log('='.repeat(80) + '\n');
|
|
console.log('='.repeat(80) + '\n');
|
|
|
});
|
|
});
|
|
|
|
|
+
|