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