|
|
@@ -454,33 +454,42 @@ async function handleRSAEncryptedFormLogin(config, credentials) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 处理 Home Assistant 登录(OAuth2 流程)
|
|
|
+// 处理 Home Assistant 登录(OAuth2 流程 - 严格匹配 redirect_uri)
|
|
|
async function handleHomeAssistantLogin(config, credentials) {
|
|
|
const { targetBaseUrl } = config;
|
|
|
|
|
|
- console.log('=== Home Assistant 登录 (OAuth2 模式) ===');
|
|
|
+ console.log('=== Home Assistant 登录 (OAuth2 严格模式) ===');
|
|
|
console.log(`目标URL: ${targetBaseUrl}`);
|
|
|
console.log(`用户名: ${credentials.username}`);
|
|
|
console.log(`密码: ${'*'.repeat(credentials.password.length)}`);
|
|
|
|
|
|
+ // 基础请求头,伪装成浏览器
|
|
|
const baseHeaders = {
|
|
|
'Content-Type': 'application/json',
|
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
|
|
'Accept': 'application/json, text/plain, */*',
|
|
|
- 'Origin': targetBaseUrl
|
|
|
+ 'Origin': targetBaseUrl,
|
|
|
+ 'Referer': `${targetBaseUrl}/`
|
|
|
};
|
|
|
|
|
|
+ // 【关键】:OAuth2 协议要求 client_id 和 redirect_uri 在整个流程中完全一致
|
|
|
+ const CLIENT_ID = `${targetBaseUrl}/`;
|
|
|
+ const REDIRECT_URI = `${targetBaseUrl}/?auth_callback=1`;
|
|
|
+
|
|
|
+ console.log('Client ID:', CLIENT_ID);
|
|
|
+ console.log('Redirect URI:', REDIRECT_URI);
|
|
|
+
|
|
|
try {
|
|
|
// ==========================================
|
|
|
// 步骤1: 创建登录流程 (Init Flow)
|
|
|
// ==========================================
|
|
|
- console.log('步骤1: 创建登录流程...');
|
|
|
+ console.log('[1/3] 初始化登录流程...');
|
|
|
const flowResponse = await axios.post(
|
|
|
`${targetBaseUrl}/auth/login_flow`,
|
|
|
{
|
|
|
- client_id: `${targetBaseUrl}/`, // clientId 必须和浏览器访问的一致
|
|
|
+ client_id: CLIENT_ID,
|
|
|
handler: ['homeassistant', null],
|
|
|
- redirect_uri: `${targetBaseUrl}/?auth_callback=1`
|
|
|
+ redirect_uri: REDIRECT_URI // 【重要】:必须和最后跳转的地址完全一致
|
|
|
},
|
|
|
{
|
|
|
headers: baseHeaders,
|
|
|
@@ -516,13 +525,13 @@ async function handleHomeAssistantLogin(config, credentials) {
|
|
|
// ==========================================
|
|
|
// 步骤2: 提交用户名和密码 (Submit Credentials)
|
|
|
// ==========================================
|
|
|
- console.log('步骤2: 提交用户名和密码...');
|
|
|
+ console.log('[2/3] 提交用户名和密码...');
|
|
|
const loginResponse = await axios.post(
|
|
|
`${targetBaseUrl}/auth/login_flow/${flowId}`,
|
|
|
{
|
|
|
username: credentials.username,
|
|
|
password: credentials.password,
|
|
|
- client_id: `${targetBaseUrl}/` // 保持一致
|
|
|
+ client_id: CLIENT_ID // 【重要】:必须和步骤1的 client_id 完全一致
|
|
|
},
|
|
|
{
|
|
|
headers: baseHeaders,
|
|
|
@@ -536,37 +545,52 @@ async function handleHomeAssistantLogin(config, credentials) {
|
|
|
console.log(`登录响应数据:`, JSON.stringify(loginResponse.data, null, 2));
|
|
|
|
|
|
// ==========================================
|
|
|
- // 步骤3: 构造浏览器跳转 URL (关键修正)
|
|
|
+ // 步骤3: 构造浏览器跳转 URL(OAuth2 核心)
|
|
|
// ==========================================
|
|
|
const responseData = loginResponse.data || {};
|
|
|
const responseType = responseData.type;
|
|
|
|
|
|
console.log(`响应类型: ${responseType}`);
|
|
|
|
|
|
- // 如果登录成功,type 通常是 'create_entry'
|
|
|
- if (responseData.result) {
|
|
|
+ // 如果登录成功,type 为 'create_entry',result 字段包含 Authorization Code
|
|
|
+ if (responseData.result && responseType === 'create_entry') {
|
|
|
const authCode = responseData.result;
|
|
|
- console.log(`登录成功!获取到 Authorization Code: ${authCode}`);
|
|
|
+ console.log('[3/3] 登录成功!获取到 Authorization Code:', authCode);
|
|
|
|
|
|
- // 构造魔术链接,让浏览器去完成 Token 交换
|
|
|
- // 格式: http://ha-url/?auth_callback=1&code=YOUR_CODE
|
|
|
- const redirectUrl = `${targetBaseUrl}/?auth_callback=1&code=${encodeURIComponent(authCode)}`;
|
|
|
+ // 【核心修正点】
|
|
|
+ // 拼接的 URL 必须是 REDIRECT_URI + &code=...
|
|
|
+ // ⚠️ 绝对不能改成 /lovelace,否则会导致 redirect_uri_mismatch 错误
|
|
|
+ // HA 前端会自动完成:提取 code → 换取 Token → 跳转到 /lovelace
|
|
|
+ const magicLink = `${REDIRECT_URI}&code=${encodeURIComponent(authCode)}`;
|
|
|
|
|
|
- console.log('生成自动登录跳转链接:', redirectUrl);
|
|
|
- console.log('Home Assistant 前端将自动识别 URL 中的 code 并完成 Token 交换');
|
|
|
+ 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,让浏览器直接跳转
|
|
|
+ // 返回跳转 URL
|
|
|
return {
|
|
|
success: true,
|
|
|
- redirectUrl: redirectUrl, // 告诉上层直接跳转这个 URL
|
|
|
- cookies: [], // HA 不需要 Cookie
|
|
|
+ redirectUrl: magicLink,
|
|
|
+ cookies: [], // HA 不依赖 Cookie,使用 Token 认证
|
|
|
response: loginResponse.data
|
|
|
};
|
|
|
} else {
|
|
|
- console.error('登录未返回 Authorization Code:', responseData);
|
|
|
+ console.error('❌ 登录失败!未返回 Authorization Code');
|
|
|
+ console.error('响应数据:', responseData);
|
|
|
+
|
|
|
+ // 提取错误信息
|
|
|
+ const errorMessage = responseData.errors?.base?.[0]
|
|
|
+ || responseData.errors?.username?.[0]
|
|
|
+ || responseData.errors?.password?.[0]
|
|
|
+ || responseData.message
|
|
|
+ || `登录失败,响应类型: ${responseType}`;
|
|
|
+
|
|
|
return {
|
|
|
success: false,
|
|
|
- message: '登录失败: 未返回授权码',
|
|
|
+ message: errorMessage,
|
|
|
response: responseData
|
|
|
};
|
|
|
}
|