|
|
import express from 'express'; |
|
|
import { generateToken, authMiddleware } from '../auth/jwt.js'; |
|
|
import tokenManager from '../auth/token_manager.js'; |
|
|
import quotaManager from '../auth/quota_manager.js'; |
|
|
import oauthManager from '../auth/oauth_manager.js'; |
|
|
import config, { getConfigJson, saveConfigJson } from '../config/config.js'; |
|
|
import logger from '../utils/logger.js'; |
|
|
import { parseEnvFile, updateEnvFile } from '../utils/envParser.js'; |
|
|
import { reloadConfig } from '../utils/configReloader.js'; |
|
|
import { deepMerge } from '../utils/deepMerge.js'; |
|
|
import { getModelsWithQuotas } from '../api/client.js'; |
|
|
import { getEnvPath } from '../utils/paths.js'; |
|
|
import dotenv from 'dotenv'; |
|
|
|
|
|
const envPath = getEnvPath(); |
|
|
|
|
|
const router = express.Router(); |
|
|
|
|
|
|
|
|
const loginAttempts = new Map(); |
|
|
const MAX_LOGIN_ATTEMPTS = 5; |
|
|
const BLOCK_DURATION = 5 * 60 * 1000; |
|
|
const ATTEMPT_WINDOW = 15 * 60 * 1000; |
|
|
|
|
|
function getClientIP(req) { |
|
|
return req.headers['x-forwarded-for']?.split(',')[0]?.trim() || |
|
|
req.headers['x-real-ip'] || |
|
|
req.connection?.remoteAddress || |
|
|
req.ip || |
|
|
'unknown'; |
|
|
} |
|
|
|
|
|
function checkLoginRateLimit(ip) { |
|
|
const now = Date.now(); |
|
|
const attempt = loginAttempts.get(ip); |
|
|
|
|
|
if (!attempt) return { allowed: true }; |
|
|
|
|
|
|
|
|
if (attempt.blockedUntil && now < attempt.blockedUntil) { |
|
|
const remainingSeconds = Math.ceil((attempt.blockedUntil - now) / 1000); |
|
|
return { |
|
|
allowed: false, |
|
|
message: `登录尝试过多,请 ${remainingSeconds} 秒后重试`, |
|
|
remainingSeconds |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
if (now - attempt.lastAttempt > ATTEMPT_WINDOW) { |
|
|
loginAttempts.delete(ip); |
|
|
return { allowed: true }; |
|
|
} |
|
|
|
|
|
return { allowed: true }; |
|
|
} |
|
|
|
|
|
function recordLoginAttempt(ip, success) { |
|
|
const now = Date.now(); |
|
|
|
|
|
if (success) { |
|
|
|
|
|
loginAttempts.delete(ip); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const attempt = loginAttempts.get(ip) || { count: 0, lastAttempt: now }; |
|
|
attempt.count++; |
|
|
attempt.lastAttempt = now; |
|
|
|
|
|
|
|
|
if (attempt.count >= MAX_LOGIN_ATTEMPTS) { |
|
|
attempt.blockedUntil = now + BLOCK_DURATION; |
|
|
logger.warn(`IP ${ip} 因登录失败次数过多被暂时封禁`); |
|
|
} |
|
|
|
|
|
loginAttempts.set(ip, attempt); |
|
|
} |
|
|
|
|
|
|
|
|
router.post('/login', (req, res) => { |
|
|
const clientIP = getClientIP(req); |
|
|
|
|
|
|
|
|
const rateCheck = checkLoginRateLimit(clientIP); |
|
|
if (!rateCheck.allowed) { |
|
|
return res.status(429).json({ |
|
|
success: false, |
|
|
message: rateCheck.message, |
|
|
retryAfter: rateCheck.remainingSeconds |
|
|
}); |
|
|
} |
|
|
|
|
|
const { username, password } = req.body; |
|
|
|
|
|
|
|
|
if (!username || !password || typeof username !== 'string' || typeof password !== 'string') { |
|
|
return res.status(400).json({ success: false, message: '用户名和密码必填' }); |
|
|
} |
|
|
|
|
|
|
|
|
if (username.length > 100 || password.length > 100) { |
|
|
return res.status(400).json({ success: false, message: '输入过长' }); |
|
|
} |
|
|
|
|
|
if (username === config.admin.username && password === config.admin.password) { |
|
|
recordLoginAttempt(clientIP, true); |
|
|
const token = generateToken({ username, role: 'admin' }); |
|
|
res.json({ success: true, token }); |
|
|
} else { |
|
|
recordLoginAttempt(clientIP, false); |
|
|
res.status(401).json({ success: false, message: '用户名或密码错误' }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.get('/tokens', authMiddleware, async (req, res) => { |
|
|
try { |
|
|
const tokens = await tokenManager.getTokenList(); |
|
|
res.json({ success: true, data: tokens }); |
|
|
} catch (error) { |
|
|
logger.error('获取Token列表失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
router.post('/tokens', authMiddleware, async (req, res) => { |
|
|
const { access_token, refresh_token, expires_in, timestamp, enable, projectId, email } = req.body; |
|
|
if (!access_token || !refresh_token) { |
|
|
return res.status(400).json({ success: false, message: 'access_token和refresh_token必填' }); |
|
|
} |
|
|
const tokenData = { access_token, refresh_token, expires_in }; |
|
|
if (timestamp) tokenData.timestamp = timestamp; |
|
|
if (enable !== undefined) tokenData.enable = enable; |
|
|
if (projectId) tokenData.projectId = projectId; |
|
|
if (email) tokenData.email = email; |
|
|
|
|
|
try { |
|
|
const result = await tokenManager.addToken(tokenData); |
|
|
res.json(result); |
|
|
} catch (error) { |
|
|
logger.error('添加Token失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
router.put('/tokens/:refreshToken', authMiddleware, async (req, res) => { |
|
|
const { refreshToken } = req.params; |
|
|
const updates = req.body; |
|
|
try { |
|
|
const result = await tokenManager.updateToken(refreshToken, updates); |
|
|
res.json(result); |
|
|
} catch (error) { |
|
|
logger.error('更新Token失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
router.delete('/tokens/:refreshToken', authMiddleware, async (req, res) => { |
|
|
const { refreshToken } = req.params; |
|
|
try { |
|
|
const result = await tokenManager.deleteToken(refreshToken); |
|
|
res.json(result); |
|
|
} catch (error) { |
|
|
logger.error('删除Token失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
router.post('/tokens/reload', authMiddleware, async (req, res) => { |
|
|
try { |
|
|
await tokenManager.reload(); |
|
|
res.json({ success: true, message: 'Token已热重载' }); |
|
|
} catch (error) { |
|
|
logger.error('热重载失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.post('/tokens/:refreshToken/refresh', authMiddleware, async (req, res) => { |
|
|
const { refreshToken } = req.params; |
|
|
try { |
|
|
logger.info('正在刷新token...'); |
|
|
const tokens = await tokenManager.getTokenList(); |
|
|
const tokenData = tokens.find(t => t.refresh_token === refreshToken); |
|
|
|
|
|
if (!tokenData) { |
|
|
return res.status(404).json({ success: false, message: 'Token不存在' }); |
|
|
} |
|
|
|
|
|
|
|
|
const refreshedToken = await tokenManager.refreshToken(tokenData); |
|
|
res.json({ success: true, message: 'Token刷新成功', data: { expires_in: refreshedToken.expires_in, timestamp: refreshedToken.timestamp } }); |
|
|
} catch (error) { |
|
|
logger.error('刷新Token失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
router.post('/oauth/exchange', authMiddleware, async (req, res) => { |
|
|
const { code, port } = req.body; |
|
|
if (!code || !port) { |
|
|
return res.status(400).json({ success: false, message: 'code和port必填' }); |
|
|
} |
|
|
|
|
|
try { |
|
|
const account = await oauthManager.authenticate(code, port); |
|
|
const message = account.hasQuota |
|
|
? 'Token添加成功' |
|
|
: 'Token添加成功(该账号无资格,已自动使用随机ProjectId)'; |
|
|
res.json({ success: true, data: account, message, fallbackMode: !account.hasQuota }); |
|
|
} catch (error) { |
|
|
logger.error('认证失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.get('/config', authMiddleware, (req, res) => { |
|
|
try { |
|
|
const envData = parseEnvFile(envPath); |
|
|
const jsonData = getConfigJson(); |
|
|
res.json({ success: true, data: { env: envData, json: jsonData } }); |
|
|
} catch (error) { |
|
|
logger.error('读取配置失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.put('/config', authMiddleware, (req, res) => { |
|
|
try { |
|
|
const { env: envUpdates, json: jsonUpdates } = req.body; |
|
|
|
|
|
if (envUpdates) updateEnvFile(envPath, envUpdates); |
|
|
if (jsonUpdates) saveConfigJson(deepMerge(getConfigJson(), jsonUpdates)); |
|
|
|
|
|
dotenv.config({ override: true }); |
|
|
reloadConfig(); |
|
|
|
|
|
logger.info('配置已更新并热重载'); |
|
|
res.json({ success: true, message: '配置已保存并生效(端口/HOST修改需重启)' }); |
|
|
} catch (error) { |
|
|
logger.error('更新配置失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.get('/rotation', authMiddleware, (req, res) => { |
|
|
try { |
|
|
const rotationConfig = tokenManager.getRotationConfig(); |
|
|
res.json({ success: true, data: rotationConfig }); |
|
|
} catch (error) { |
|
|
logger.error('获取轮询配置失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.put('/rotation', authMiddleware, (req, res) => { |
|
|
try { |
|
|
const { strategy, requestCount } = req.body; |
|
|
|
|
|
|
|
|
const validStrategies = ['round_robin', 'quota_exhausted', 'request_count']; |
|
|
if (strategy && !validStrategies.includes(strategy)) { |
|
|
return res.status(400).json({ |
|
|
success: false, |
|
|
message: `无效的策略,可选值: ${validStrategies.join(', ')}` |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
tokenManager.updateRotationConfig(strategy, requestCount); |
|
|
|
|
|
|
|
|
const currentConfig = getConfigJson(); |
|
|
if (!currentConfig.rotation) currentConfig.rotation = {}; |
|
|
if (strategy) currentConfig.rotation.strategy = strategy; |
|
|
if (requestCount) currentConfig.rotation.requestCount = requestCount; |
|
|
saveConfigJson(currentConfig); |
|
|
|
|
|
|
|
|
reloadConfig(); |
|
|
|
|
|
logger.info(`轮询策略已更新: ${strategy || '未变'}, 请求次数: ${requestCount || '未变'}`); |
|
|
res.json({ success: true, message: '轮询策略已更新', data: tokenManager.getRotationConfig() }); |
|
|
} catch (error) { |
|
|
logger.error('更新轮询配置失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
router.get('/tokens/:refreshToken/quotas', authMiddleware, async (req, res) => { |
|
|
try { |
|
|
const { refreshToken } = req.params; |
|
|
const forceRefresh = req.query.refresh === 'true'; |
|
|
const tokens = await tokenManager.getTokenList(); |
|
|
let tokenData = tokens.find(t => t.refresh_token === refreshToken); |
|
|
|
|
|
if (!tokenData) { |
|
|
return res.status(404).json({ success: false, message: 'Token不存在' }); |
|
|
} |
|
|
|
|
|
|
|
|
if (tokenManager.isExpired(tokenData)) { |
|
|
try { |
|
|
tokenData = await tokenManager.refreshToken(tokenData); |
|
|
} catch (error) { |
|
|
logger.error('刷新token失败:', error.message); |
|
|
|
|
|
return res.status(400).json({ success: false, message: 'Google Token已过期且刷新失败,请重新登录Google账号' }); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
let quotaData = forceRefresh ? null : quotaManager.getQuota(refreshToken); |
|
|
|
|
|
if (!quotaData) { |
|
|
|
|
|
const token = { access_token: tokenData.access_token, refresh_token: refreshToken }; |
|
|
const quotas = await getModelsWithQuotas(token); |
|
|
quotaManager.updateQuota(refreshToken, quotas); |
|
|
quotaData = { lastUpdated: Date.now(), models: quotas }; |
|
|
} |
|
|
|
|
|
|
|
|
const modelsWithBeijingTime = {}; |
|
|
Object.entries(quotaData.models).forEach(([modelId, quota]) => { |
|
|
modelsWithBeijingTime[modelId] = { |
|
|
remaining: quota.r, |
|
|
resetTime: quotaManager.convertToBeijingTime(quota.t), |
|
|
resetTimeRaw: quota.t |
|
|
}; |
|
|
}); |
|
|
|
|
|
res.json({ |
|
|
success: true, |
|
|
data: { |
|
|
lastUpdated: quotaData.lastUpdated, |
|
|
models: modelsWithBeijingTime |
|
|
} |
|
|
}); |
|
|
} catch (error) { |
|
|
logger.error('获取额度失败:', error.message); |
|
|
res.status(500).json({ success: false, message: error.message }); |
|
|
} |
|
|
}); |
|
|
|
|
|
export default router; |