File size: 6,586 Bytes
15bfd5e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
import { Router } from 'express';
import https from 'https';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import logger from '../utils/logger.js';

const router = Router();

const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
const CLIENT_SECRET = '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';
const ACCOUNTS_FILE = path.join(process.cwd(), 'data', 'accounts.json');

const SCOPES = [
  'https://www.googleapis.com/auth/cloud-platform',
  'https://www.googleapis.com/auth/userinfo.email',
  'https://www.googleapis.com/auth/userinfo.profile',
  'https://www.googleapis.com/auth/cclog',
  'https://www.googleapis.com/auth/experimentsandconfigs'
];

// 存储 state 用于验证
const pendingStates = new Map();

// 获取回调基础 URL
function getBaseUrl(req) {
  // 优先使用环境变量
  if (process.env.OAUTH_CALLBACK_URL) {
    return process.env.OAUTH_CALLBACK_URL;
  }
  // 从请求中推断
  const protocol = req.headers['x-forwarded-proto'] || req.protocol || 'http';
  const host = req.headers['x-forwarded-host'] || req.headers.host;
  return `${protocol}://${host}`;
}

// 生成授权 URL
router.get('/login', (req, res) => {
  const state = crypto.randomUUID();
  const baseUrl = getBaseUrl(req);
  const redirectUri = `${baseUrl}/oauth-callback`;
  
  // 保存 state,5分钟过期
  pendingStates.set(state, { redirectUri, timestamp: Date.now() });
  setTimeout(() => pendingStates.delete(state), 5 * 60 * 1000);
  
  const params = new URLSearchParams({
    access_type: 'offline',
    client_id: CLIENT_ID,
    prompt: 'consent',
    redirect_uri: redirectUri,
    response_type: 'code',
    scope: SCOPES.join(' '),
    state: state
  });
  
  const authUrl = `https://accounts.google.com/o/oauth2/v2/auth?${params.toString()}`;
  
  // 返回 HTML 页面,用户点击后跳转
  res.send(`
    <!DOCTYPE html>
    <html>
    <head>
      <meta charset="utf-8">
      <title>Google 账号登录</title>
      <style>
        body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
        .container { text-align: center; background: white; padding: 40px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
        h1 { color: #333; margin-bottom: 20px; }
        p { color: #666; margin-bottom: 30px; }
        a { display: inline-block; background: #4285f4; color: white; padding: 12px 32px; border-radius: 6px; text-decoration: none; font-weight: 500; }
        a:hover { background: #3367d6; }
        .info { font-size: 12px; color: #999; margin-top: 20px; }
      </style>
    </head>
    <body>
      <div class="container">
        <h1>🚀 添加 Google 账号</h1>
        <p>点击下方按钮使用 Google 账号授权</p>
        <a href="${authUrl}">使用 Google 登录</a>
        <p class="info">回调地址: ${redirectUri}</p>
      </div>
    </body>
    </html>
  `);
});

// OAuth 回调处理
router.get('/oauth-callback', async (req, res) => {
  const { code, state, error } = req.query;
  
  if (error) {
    logger.error('OAuth 授权失败:', error);
    return res.send('<h1>授权失败</h1><p>' + error + '</p>');
  }
  
  if (!code) {
    return res.send('<h1>授权失败</h1><p>未收到授权码</p>');
  }
  
  // 验证 state
  const stateData = pendingStates.get(state);
  if (!stateData) {
    logger.warn('无效的 state 参数,可能是过期或重复使用');
    // 尝试从当前请求推断 redirect_uri
  }
  
  const redirectUri = stateData?.redirectUri || `${getBaseUrl(req)}/oauth-callback`;
  pendingStates.delete(state);
  
  try {
    logger.info('收到授权码,正在交换 Token...');
    const tokenData = await exchangeCodeForToken(code, redirectUri);
    
    const account = {
      access_token: tokenData.access_token,
      refresh_token: tokenData.refresh_token,
      expires_in: tokenData.expires_in,
      timestamp: Date.now()
    };
    
    // 保存到 accounts.json
    let accounts = [];
    try {
      if (fs.existsSync(ACCOUNTS_FILE)) {
        accounts = JSON.parse(fs.readFileSync(ACCOUNTS_FILE, 'utf-8'));
      }
    } catch (err) {
      logger.warn('读取 accounts.json 失败,将创建新文件');
    }
    
    accounts.push(account);
    
    const dir = path.dirname(ACCOUNTS_FILE);
    if (!fs.existsSync(dir)) {
      fs.mkdirSync(dir, { recursive: true });
    }
    
    fs.writeFileSync(ACCOUNTS_FILE, JSON.stringify(accounts, null, 2));
    logger.info(`Token 已保存,当前共 ${accounts.length} 个账号`);
    
    res.send(`
      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <title>授权成功</title>
        <style>
          body { font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f5f5f5; }
          .container { text-align: center; background: white; padding: 40px; border-radius: 12px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); }
          h1 { color: #4caf50; }
          p { color: #666; }
        </style>
      </head>
      <body>
        <div class="container">
          <h1>✅ 授权成功!</h1>
          <p>账号已添加,当前共 ${accounts.length} 个账号</p>
          <p><a href="/">返回首页</a></p>
        </div>
      </body>
      </html>
    `);
  } catch (err) {
    logger.error('Token 交换失败:', err.message);
    res.send(`<h1>Token 获取失败</h1><p>${err.message}</p>`);
  }
});

// 交换授权码获取 Token
function exchangeCodeForToken(code, redirectUri) {
  return new Promise((resolve, reject) => {
    const postData = new URLSearchParams({
      code: code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: redirectUri,
      grant_type: 'authorization_code'
    }).toString();
    
    const options = {
      hostname: 'oauth2.googleapis.com',
      path: '/token',
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        'Content-Length': Buffer.byteLength(postData)
      }
    };
    
    const req = https.request(options, (res) => {
      let body = '';
      res.on('data', chunk => body += chunk);
      res.on('end', () => {
        if (res.statusCode === 200) {
          resolve(JSON.parse(body));
        } else {
          reject(new Error(`HTTP ${res.statusCode}: ${body}`));
        }
      });
    });
    
    req.on('error', reject);
    req.write(postData);
    req.end();
  });
}

export default router;