mirror of
https://github.com/Wei-Shaw/claude-relay-service.git
synced 2026-01-23 18:09:15 +00:00
- Replace .eslintrc.js with .eslintrc.cjs for better ES module compatibility - Add .prettierrc configuration for consistent code formatting - Update package.json with new lint and format scripts - Add nodemon.json for development hot reloading configuration - Standardize code formatting across all JavaScript and Vue files - Update web admin SPA with improved linting rules and formatting - Add prettier configuration to web admin SPA 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
345 lines
10 KiB
JavaScript
345 lines
10 KiB
JavaScript
const express = require('express')
|
||
const bcrypt = require('bcryptjs')
|
||
const crypto = require('crypto')
|
||
const path = require('path')
|
||
const fs = require('fs')
|
||
const redis = require('../models/redis')
|
||
const logger = require('../utils/logger')
|
||
const config = require('../../config/config')
|
||
|
||
const router = express.Router()
|
||
|
||
// 🏠 服务静态文件
|
||
router.use('/assets', express.static(path.join(__dirname, '../../web/assets')))
|
||
|
||
// 🌐 页面路由重定向到新版 admin-spa
|
||
router.get('/', (req, res) => {
|
||
res.redirect(301, '/admin-next/api-stats')
|
||
})
|
||
|
||
// 🔐 管理员登录
|
||
router.post('/auth/login', async (req, res) => {
|
||
try {
|
||
const { username, password } = req.body
|
||
|
||
if (!username || !password) {
|
||
return res.status(400).json({
|
||
error: 'Missing credentials',
|
||
message: 'Username and password are required'
|
||
})
|
||
}
|
||
|
||
// 从Redis获取管理员信息
|
||
let adminData = await redis.getSession('admin_credentials')
|
||
|
||
// 如果Redis中没有管理员凭据,尝试从init.json重新加载
|
||
if (!adminData || Object.keys(adminData).length === 0) {
|
||
const initFilePath = path.join(__dirname, '../../data/init.json')
|
||
|
||
if (fs.existsSync(initFilePath)) {
|
||
try {
|
||
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8'))
|
||
const saltRounds = 10
|
||
const passwordHash = await bcrypt.hash(initData.adminPassword, saltRounds)
|
||
|
||
adminData = {
|
||
username: initData.adminUsername,
|
||
passwordHash,
|
||
createdAt: initData.initializedAt || new Date().toISOString(),
|
||
lastLogin: null,
|
||
updatedAt: initData.updatedAt || null
|
||
}
|
||
|
||
// 重新存储到Redis,不设置过期时间
|
||
await redis.getClient().hset('session:admin_credentials', adminData)
|
||
|
||
logger.info('✅ Admin credentials reloaded from init.json')
|
||
} catch (error) {
|
||
logger.error('❌ Failed to reload admin credentials:', error)
|
||
return res.status(401).json({
|
||
error: 'Invalid credentials',
|
||
message: 'Invalid username or password'
|
||
})
|
||
}
|
||
} else {
|
||
return res.status(401).json({
|
||
error: 'Invalid credentials',
|
||
message: 'Invalid username or password'
|
||
})
|
||
}
|
||
}
|
||
|
||
// 验证用户名和密码
|
||
const isValidUsername = adminData.username === username
|
||
const isValidPassword = await bcrypt.compare(password, adminData.passwordHash)
|
||
|
||
if (!isValidUsername || !isValidPassword) {
|
||
logger.security(`🔒 Failed login attempt for username: ${username}`)
|
||
return res.status(401).json({
|
||
error: 'Invalid credentials',
|
||
message: 'Invalid username or password'
|
||
})
|
||
}
|
||
|
||
// 生成会话token
|
||
const sessionId = crypto.randomBytes(32).toString('hex')
|
||
|
||
// 存储会话
|
||
const sessionData = {
|
||
username: adminData.username,
|
||
loginTime: new Date().toISOString(),
|
||
lastActivity: new Date().toISOString()
|
||
}
|
||
|
||
await redis.setSession(sessionId, sessionData, config.security.adminSessionTimeout)
|
||
|
||
// 不再更新 Redis 中的最后登录时间,因为 Redis 只是缓存
|
||
// init.json 是唯一真实数据源
|
||
|
||
logger.success(`🔐 Admin login successful: ${username}`)
|
||
|
||
return res.json({
|
||
success: true,
|
||
token: sessionId,
|
||
expiresIn: config.security.adminSessionTimeout,
|
||
username: adminData.username // 返回真实用户名
|
||
})
|
||
} catch (error) {
|
||
logger.error('❌ Login error:', error)
|
||
return res.status(500).json({
|
||
error: 'Login failed',
|
||
message: 'Internal server error'
|
||
})
|
||
}
|
||
})
|
||
|
||
// 🚪 管理员登出
|
||
router.post('/auth/logout', async (req, res) => {
|
||
try {
|
||
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken
|
||
|
||
if (token) {
|
||
await redis.deleteSession(token)
|
||
logger.success('🚪 Admin logout successful')
|
||
}
|
||
|
||
return res.json({ success: true, message: 'Logout successful' })
|
||
} catch (error) {
|
||
logger.error('❌ Logout error:', error)
|
||
return res.status(500).json({
|
||
error: 'Logout failed',
|
||
message: 'Internal server error'
|
||
})
|
||
}
|
||
})
|
||
|
||
// 🔑 修改账户信息
|
||
router.post('/auth/change-password', async (req, res) => {
|
||
try {
|
||
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
error: 'No token provided',
|
||
message: 'Authentication required'
|
||
})
|
||
}
|
||
|
||
const { newUsername, currentPassword, newPassword } = req.body
|
||
|
||
if (!currentPassword || !newPassword) {
|
||
return res.status(400).json({
|
||
error: 'Missing required fields',
|
||
message: 'Current password and new password are required'
|
||
})
|
||
}
|
||
|
||
// 验证新密码长度
|
||
if (newPassword.length < 8) {
|
||
return res.status(400).json({
|
||
error: 'Password too short',
|
||
message: 'New password must be at least 8 characters long'
|
||
})
|
||
}
|
||
|
||
// 获取当前会话
|
||
const sessionData = await redis.getSession(token)
|
||
if (!sessionData) {
|
||
return res.status(401).json({
|
||
error: 'Invalid token',
|
||
message: 'Session expired or invalid'
|
||
})
|
||
}
|
||
|
||
// 获取当前管理员信息
|
||
const adminData = await redis.getSession('admin_credentials')
|
||
if (!adminData) {
|
||
return res.status(500).json({
|
||
error: 'Admin data not found',
|
||
message: 'Administrator credentials not found'
|
||
})
|
||
}
|
||
|
||
// 验证当前密码
|
||
const isValidPassword = await bcrypt.compare(currentPassword, adminData.passwordHash)
|
||
if (!isValidPassword) {
|
||
logger.security(`🔒 Invalid current password attempt for user: ${sessionData.username}`)
|
||
return res.status(401).json({
|
||
error: 'Invalid current password',
|
||
message: 'Current password is incorrect'
|
||
})
|
||
}
|
||
|
||
// 准备更新的数据
|
||
const updatedUsername =
|
||
newUsername && newUsername.trim() ? newUsername.trim() : adminData.username
|
||
|
||
// 先更新 init.json(唯一真实数据源)
|
||
const initFilePath = path.join(__dirname, '../../data/init.json')
|
||
if (!fs.existsSync(initFilePath)) {
|
||
return res.status(500).json({
|
||
error: 'Configuration file not found',
|
||
message: 'init.json file is missing'
|
||
})
|
||
}
|
||
|
||
try {
|
||
const initData = JSON.parse(fs.readFileSync(initFilePath, 'utf8'))
|
||
// const oldData = { ...initData }; // 备份旧数据
|
||
|
||
// 更新 init.json
|
||
initData.adminUsername = updatedUsername
|
||
initData.adminPassword = newPassword // 保存明文密码到init.json
|
||
initData.updatedAt = new Date().toISOString()
|
||
|
||
// 先写入文件(如果失败则不会影响 Redis)
|
||
fs.writeFileSync(initFilePath, JSON.stringify(initData, null, 2))
|
||
|
||
// 文件写入成功后,更新 Redis 缓存
|
||
const saltRounds = 10
|
||
const newPasswordHash = await bcrypt.hash(newPassword, saltRounds)
|
||
|
||
const updatedAdminData = {
|
||
username: updatedUsername,
|
||
passwordHash: newPasswordHash,
|
||
createdAt: adminData.createdAt,
|
||
lastLogin: adminData.lastLogin,
|
||
updatedAt: new Date().toISOString()
|
||
}
|
||
|
||
await redis.setSession('admin_credentials', updatedAdminData)
|
||
} catch (fileError) {
|
||
logger.error('❌ Failed to update init.json:', fileError)
|
||
return res.status(500).json({
|
||
error: 'Update failed',
|
||
message: 'Failed to update configuration file'
|
||
})
|
||
}
|
||
|
||
// 清除当前会话(强制用户重新登录)
|
||
await redis.deleteSession(token)
|
||
|
||
logger.success(`🔐 Admin password changed successfully for user: ${updatedUsername}`)
|
||
|
||
return res.json({
|
||
success: true,
|
||
message: 'Password changed successfully. Please login again.',
|
||
newUsername: updatedUsername
|
||
})
|
||
} catch (error) {
|
||
logger.error('❌ Change password error:', error)
|
||
return res.status(500).json({
|
||
error: 'Change password failed',
|
||
message: 'Internal server error'
|
||
})
|
||
}
|
||
})
|
||
|
||
// 👤 获取当前用户信息
|
||
router.get('/auth/user', async (req, res) => {
|
||
try {
|
||
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
error: 'No token provided',
|
||
message: 'Authentication required'
|
||
})
|
||
}
|
||
|
||
// 获取当前会话
|
||
const sessionData = await redis.getSession(token)
|
||
if (!sessionData) {
|
||
return res.status(401).json({
|
||
error: 'Invalid token',
|
||
message: 'Session expired or invalid'
|
||
})
|
||
}
|
||
|
||
// 获取管理员信息
|
||
const adminData = await redis.getSession('admin_credentials')
|
||
if (!adminData) {
|
||
return res.status(500).json({
|
||
error: 'Admin data not found',
|
||
message: 'Administrator credentials not found'
|
||
})
|
||
}
|
||
|
||
return res.json({
|
||
success: true,
|
||
user: {
|
||
username: adminData.username,
|
||
loginTime: sessionData.loginTime,
|
||
lastActivity: sessionData.lastActivity
|
||
}
|
||
})
|
||
} catch (error) {
|
||
logger.error('❌ Get user info error:', error)
|
||
return res.status(500).json({
|
||
error: 'Get user info failed',
|
||
message: 'Internal server error'
|
||
})
|
||
}
|
||
})
|
||
|
||
// 🔄 刷新token
|
||
router.post('/auth/refresh', async (req, res) => {
|
||
try {
|
||
const token = req.headers['authorization']?.replace('Bearer ', '') || req.cookies?.adminToken
|
||
|
||
if (!token) {
|
||
return res.status(401).json({
|
||
error: 'No token provided',
|
||
message: 'Authentication required'
|
||
})
|
||
}
|
||
|
||
const sessionData = await redis.getSession(token)
|
||
|
||
if (!sessionData) {
|
||
return res.status(401).json({
|
||
error: 'Invalid token',
|
||
message: 'Session expired or invalid'
|
||
})
|
||
}
|
||
|
||
// 更新最后活动时间
|
||
sessionData.lastActivity = new Date().toISOString()
|
||
await redis.setSession(token, sessionData, config.security.adminSessionTimeout)
|
||
|
||
return res.json({
|
||
success: true,
|
||
token,
|
||
expiresIn: config.security.adminSessionTimeout
|
||
})
|
||
} catch (error) {
|
||
logger.error('❌ Token refresh error:', error)
|
||
return res.status(500).json({
|
||
error: 'Token refresh failed',
|
||
message: 'Internal server error'
|
||
})
|
||
}
|
||
})
|
||
|
||
module.exports = router
|