Spaces:
Sleeping
Sleeping
| 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; | |