File size: 4,607 Bytes
0994949 6cdc5e2 0994949 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 |
import axios from 'axios';
import crypto from 'crypto';
import log from '../utils/logger.js';
import config from '../config/config.js';
import { generateProjectId } from '../utils/idGenerator.js';
import tokenManager from './token_manager.js';
import { OAUTH_CONFIG, OAUTH_SCOPES } from '../constants/oauth.js';
import { buildAxiosRequestConfig } from '../utils/httpClient.js';
class OAuthManager {
constructor() {
this.state = crypto.randomUUID();
}
/**
* 生成授权URL
*/
generateAuthUrl(port) {
const params = new URLSearchParams({
access_type: 'offline',
client_id: OAUTH_CONFIG.CLIENT_ID,
prompt: 'consent',
redirect_uri: `http://localhost:${port}/oauth-callback`,
response_type: 'code',
scope: OAUTH_SCOPES.join(' '),
state: this.state
});
return `${OAUTH_CONFIG.AUTH_URL}?${params.toString()}`;
}
/**
* 交换授权码获取Token
*/
async exchangeCodeForToken(code, port) {
const postData = new URLSearchParams({
code,
client_id: OAUTH_CONFIG.CLIENT_ID,
client_secret: OAUTH_CONFIG.CLIENT_SECRET,
redirect_uri: `http://localhost:${port}/oauth-callback`,
grant_type: 'authorization_code'
});
const response = await axios(buildAxiosRequestConfig({
method: 'POST',
url: OAUTH_CONFIG.TOKEN_URL,
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
data: postData.toString(),
timeout: config.timeout
}));
return response.data;
}
/**
* 获取用户邮箱
*/
async fetchUserEmail(accessToken) {
try {
const response = await axios(buildAxiosRequestConfig({
method: 'GET',
url: 'https://www.googleapis.com/oauth2/v2/userinfo',
headers: {
'Host': 'www.googleapis.com',
'User-Agent': 'Go-http-client/1.1',
'Authorization': `Bearer ${accessToken}`,
'Accept-Encoding': 'gzip'
},
timeout: config.timeout
}));
return response.data?.email;
} catch (err) {
log.warn('获取用户邮箱失败:', err.message);
return null;
}
}
/**
* 资格校验:尝试获取projectId,失败则自动回退到随机projectId
*/
async validateAndGetProjectId(accessToken) {
// 如果配置跳过API验证,直接返回随机projectId
if (config.skipProjectIdFetch) {
const projectId = generateProjectId();
log.info('已跳过API验证,使用随机生成的projectId: ' + projectId);
return { projectId, hasQuota: true };
}
// 尝试从API获取projectId
try {
log.info('正在验证账号资格...');
const projectId = await tokenManager.fetchProjectId({ access_token: accessToken });
if (projectId === undefined) {
// 无资格,自动回退到随机projectId
const randomProjectId = generateProjectId();
log.warn('该账号无资格使用,已自动退回无资格模式,使用随机projectId: ' + randomProjectId);
return { projectId: randomProjectId, hasQuota: false };
}
log.info('账号验证通过,projectId: ' + projectId);
return { projectId, hasQuota: true };
} catch (err) {
// 获取失败时也退回到随机projectId
const randomProjectId = generateProjectId();
log.warn('验证账号资格失败: ' + err.message + ',已自动退回无资格模式');
log.info('使用随机生成的projectId: ' + randomProjectId);
return { projectId: randomProjectId, hasQuota: false };
}
}
/**
* 完整的OAuth认证流程:交换Token -> 获取邮箱 -> 资格校验
*/
async authenticate(code, port) {
// 1. 交换授权码获取Token
const tokenData = await this.exchangeCodeForToken(code, port);
if (!tokenData.access_token) {
throw new Error('Token交换失败:未获取到access_token');
}
const account = {
access_token: tokenData.access_token,
refresh_token: tokenData.refresh_token,
expires_in: tokenData.expires_in,
timestamp: Date.now()
};
// 2. 获取用户邮箱
const email = await this.fetchUserEmail(account.access_token);
if (email) {
account.email = email;
log.info('获取到用户邮箱: ' + email);
}
// 3. 资格校验并获取projectId
const { projectId, hasQuota } = await this.validateAndGetProjectId(account.access_token);
account.projectId = projectId;
account.hasQuota = hasQuota;
account.enable = true;
return account;
}
}
export default new OAuthManager();
|