cc / src /routes /web.js
hequ's picture
Upload 221 files
69b897d verified
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