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();