Files
claude-relay-service/src/routes/web.js
千羽 8a74bf5afe refactor: standardize code formatting and linting configuration
- 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>
2025-08-07 18:19:31 +09:00

345 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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