新增系统配置页(运营管理系统后台)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
# 服务器配置
|
||||
BACKEND_HOST=127.0.0.1
|
||||
PORT=3000
|
||||
NODE_ENV=development
|
||||
|
||||
@@ -15,6 +16,10 @@ JWT_EXPIRES_IN=2h
|
||||
|
||||
# 游戏服务端代理地址
|
||||
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!1
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
|
||||
# CORS配置
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
@@ -15,6 +15,10 @@ JWT_EXPIRES_IN=2h
|
||||
|
||||
# 游戏服务端代理地址
|
||||
GAME_SERVER_PROXY_URL=http://127.0.0.1:8080/tool/http
|
||||
GAME_PSK=THIS_IS_A_32_BYTE_FIXED_PSK!!!!!
|
||||
|
||||
# 日志配置
|
||||
LOG_LEVEL=info
|
||||
|
||||
# CORS配置(生产环境请修改为实际域名)
|
||||
CORS_ORIGIN=http://your-frontend-domain.com
|
||||
|
||||
79
backend/database/config_init.sql
Normal file
79
backend/database/config_init.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- ============================================
|
||||
-- 梦幻西游一站式运营管理平台 - 系统配置表初始化脚本
|
||||
-- MySQL 8.4 兼容版本
|
||||
-- 创建日期: 2026-01-05
|
||||
-- ============================================
|
||||
|
||||
-- 使用数据库
|
||||
USE mhxy_web_vue;
|
||||
|
||||
-- 设置字符集
|
||||
SET NAMES utf8mb4;
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ============================================
|
||||
-- 表: configs (系统配置表)
|
||||
-- ============================================
|
||||
DROP TABLE IF EXISTS `configs`;
|
||||
CREATE TABLE `configs` (
|
||||
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID',
|
||||
`config_key` VARCHAR(100) NOT NULL COMMENT '配置键',
|
||||
`config_value` TEXT NOT NULL COMMENT '配置值',
|
||||
`config_type` VARCHAR(50) NOT NULL COMMENT '配置类型 (basic:基础配置, security:安全配置, game:游戏配置, payment:充值配置, email:邮件配置)',
|
||||
`description` VARCHAR(255) NULL COMMENT '配置描述',
|
||||
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE INDEX `idx_config_key` (`config_key`),
|
||||
INDEX `idx_config_type` (`config_type`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='系统配置表';
|
||||
|
||||
-- ============================================
|
||||
-- 插入默认配置数据
|
||||
-- ============================================
|
||||
|
||||
-- 基础配置
|
||||
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||
('backend_host', '127.0.0.1', 'basic', '后端IP或域名地址'),
|
||||
('backend_port', '3000', 'basic', '后端端口'),
|
||||
('log_level', 'info', 'basic', '日志级别 (info/debug/error)'),
|
||||
('login_captcha_enabled', 'true', 'basic', '登录验证码开关 (true:开启, false:关闭)'),
|
||||
('player_service_enabled', 'true', 'basic', '玩家服务中心开关 (true:开启, false:关闭)'),
|
||||
('player_service_close_msg', '玩家服务中心系统维护中', 'basic', '玩家服务中心关闭提示文本');
|
||||
|
||||
-- 安全配置
|
||||
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||
('cors_origin', 'http://localhost:5173', 'security', '跨域地址'),
|
||||
('jwt_secret', 'your-secret-key-change-in-production', 'security', 'JWT密钥'),
|
||||
('jwt_expires_in', '2h', 'security', 'JWT有效期 (如: 2h, 7d, 30m)');
|
||||
|
||||
-- 游戏配置
|
||||
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||
('game_server_proxy_url', 'http://127.0.0.1:8080/tool/http', 'game', '游戏服务端代理地址'),
|
||||
('game_server_psk', 'THIS_IS_A_32_BYTE_FIXED_PSK!!!!', 'game', '游戏服务端PSK密钥');
|
||||
|
||||
-- 邮件配置
|
||||
INSERT INTO `configs` (`config_key`, `config_value`, `config_type`, `description`) VALUES
|
||||
('mail_from', '', 'email', '发件人邮箱'),
|
||||
('mail_smtp_host', '', 'email', 'SMTP服务器地址'),
|
||||
('mail_smtp_port', '587', 'email', 'SMTP服务器端口'),
|
||||
('mail_smtp_user', '', 'email', 'SMTP用户名'),
|
||||
('mail_smtp_password', '', 'email', 'SMTP密码');
|
||||
|
||||
-- ============================================
|
||||
-- 验证数据
|
||||
-- ============================================
|
||||
SELECT
|
||||
id,
|
||||
config_key,
|
||||
config_value,
|
||||
config_type,
|
||||
description,
|
||||
created_at,
|
||||
updated_at
|
||||
FROM configs
|
||||
ORDER BY config_type, id;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- 初始化完成
|
||||
1377
backend/package-lock.json
generated
1377
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@types/nodemailer": "^7.0.4",
|
||||
"axios": "^1.13.2",
|
||||
"bcrypt": "^6.0.0",
|
||||
"cors": "^2.8.5",
|
||||
@@ -19,6 +20,8 @@
|
||||
"express": "^5.2.1",
|
||||
"jsonwebtoken": "^9.0.3",
|
||||
"mysql2": "^3.16.0",
|
||||
"nodemailer": "^7.0.12",
|
||||
"svg-captcha": "^1.4.0",
|
||||
"typeorm": "^0.3.28"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -8,6 +8,8 @@ import jwt from 'jsonwebtoken';
|
||||
import { AppDataSource } from '../config/database';
|
||||
// 导入管理员用户模型
|
||||
import { AdminUser } from '../models/AdminUser';
|
||||
// 导入验证码服务
|
||||
import { CaptchaService } from '../services/captchaService';
|
||||
|
||||
/**
|
||||
* 管理员认证控制器
|
||||
@@ -22,8 +24,8 @@ export class AdminAuthController {
|
||||
*/
|
||||
async login(req: Request, res: Response) {
|
||||
try {
|
||||
// 从请求体中获取用户名和密码
|
||||
const { username, password } = req.body;
|
||||
// 从请求体中获取用户名、密码和验证码信息
|
||||
const { username, password, captchaId, captchaCode } = req.body;
|
||||
|
||||
// 验证用户名和密码是否为空
|
||||
if (!username || !password) {
|
||||
@@ -33,6 +35,33 @@ export class AdminAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
// 创建验证码服务实例
|
||||
const captchaService = new CaptchaService();
|
||||
|
||||
// 检查是否启用了验证码功能
|
||||
const captchaEnabled = await captchaService.checkCaptchaEnabled();
|
||||
|
||||
// 如果启用了验证码,则验证验证码
|
||||
if (captchaEnabled) {
|
||||
// 验证验证码参数是否完整
|
||||
if (!captchaId || !captchaCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入验证码'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码是否正确
|
||||
const captchaVerify = captchaService.verifyCaptcha(captchaId, captchaCode);
|
||||
|
||||
if (!captchaVerify.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: captchaVerify.message || '验证码错误或已过期'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 获取管理员用户仓库
|
||||
const adminUserRepository = AppDataSource.getRepository(AdminUser);
|
||||
// 根据用户名查找管理员用户
|
||||
|
||||
89
backend/src/controllers/captchaController.ts
Normal file
89
backend/src/controllers/captchaController.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { CaptchaService } from '../services/captchaService';
|
||||
|
||||
/**
|
||||
* 验证码控制器
|
||||
* 处理验证码的生成和验证
|
||||
*/
|
||||
export class CaptchaController {
|
||||
private captchaService: CaptchaService;
|
||||
|
||||
constructor() {
|
||||
this.captchaService = new CaptchaService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async generateCaptcha(req: Request, res: Response) {
|
||||
try {
|
||||
const { captchaId, svg } = this.captchaService.generateCaptcha();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
captchaId,
|
||||
svg
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('生成验证码失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '生成验证码失败'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async verifyCaptcha(req: Request, res: Response) {
|
||||
try {
|
||||
const { captchaId, code } = req.body;
|
||||
|
||||
const result = this.captchaService.verifyCaptcha(captchaId, code);
|
||||
|
||||
if (!result.success) {
|
||||
return res.status(400).json(result);
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '验证码正确'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('验证验证码失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查验证码是否启用
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async checkCaptchaEnabled(req: Request, res: Response) {
|
||||
try {
|
||||
const enabled = await this.captchaService.checkCaptchaEnabled();
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: { enabled }
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查验证码状态失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
261
backend/src/controllers/configController.ts
Normal file
261
backend/src/controllers/configController.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { AppDataSource } from '../config/database';
|
||||
import { Config } from '../models/Config';
|
||||
import { EnvHelper } from '../utils/envHelper';
|
||||
import nodemailer from 'nodemailer';
|
||||
|
||||
/**
|
||||
* 系统配置控制器
|
||||
* 处理系统配置相关的操作,包括获取配置、更新配置、测试邮件等
|
||||
*/
|
||||
export class ConfigController {
|
||||
/**
|
||||
* 获取所有配置
|
||||
* 按配置类型分组返回
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async getAllConfigs(req: Request, res: Response) {
|
||||
try {
|
||||
const configRepository = AppDataSource.getRepository(Config);
|
||||
|
||||
const configs = await configRepository.find({
|
||||
order: { configType: 'ASC', id: 'ASC' }
|
||||
});
|
||||
|
||||
const groupedConfigs: Record<string, any[]> = {
|
||||
basic: [],
|
||||
security: [],
|
||||
game: [],
|
||||
payment: [],
|
||||
email: []
|
||||
};
|
||||
|
||||
configs.forEach(config => {
|
||||
if (groupedConfigs[config.configType]) {
|
||||
groupedConfigs[config.configType].push({
|
||||
id: config.id,
|
||||
configKey: config.configKey,
|
||||
configValue: config.configValue,
|
||||
description: config.description
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: groupedConfigs
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取配置失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
* 批量更新配置,同时更新数据库和.env文件
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async updateConfigs(req: Request, res: Response) {
|
||||
try {
|
||||
const { configs } = req.body;
|
||||
|
||||
if (!configs || !Array.isArray(configs)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '配置数据格式错误'
|
||||
});
|
||||
}
|
||||
|
||||
const configRepository = AppDataSource.getRepository(Config);
|
||||
const envUpdates: Record<string, string> = {};
|
||||
|
||||
for (const config of configs) {
|
||||
if (!config.configKey || config.configValue === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const existingConfig = await configRepository.findOne({
|
||||
where: { configKey: config.configKey }
|
||||
});
|
||||
|
||||
if (existingConfig) {
|
||||
existingConfig.configValue = String(config.configValue);
|
||||
await configRepository.save(existingConfig);
|
||||
}
|
||||
|
||||
const envKey = this.mapConfigKeyToEnvKey(config.configKey);
|
||||
if (envKey) {
|
||||
envUpdates[envKey] = String(config.configValue);
|
||||
}
|
||||
}
|
||||
|
||||
const envFilePath = EnvHelper.getEnvFilePath(process.env.NODE_ENV || 'development');
|
||||
const updateSuccess = EnvHelper.updateEnvFile(envFilePath, envUpdates);
|
||||
|
||||
if (!updateSuccess) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '更新.env文件失败'
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '配置更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新配置失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试邮件发送
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async testEmail(req: Request, res: Response) {
|
||||
try {
|
||||
const { to } = req.body;
|
||||
|
||||
if (!to || !this.isValidEmail(to)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入有效的邮箱地址'
|
||||
});
|
||||
}
|
||||
|
||||
const configRepository = AppDataSource.getRepository(Config);
|
||||
|
||||
const mailConfigs = await configRepository.find({
|
||||
where: [
|
||||
{ configKey: 'mail_from' },
|
||||
{ configKey: 'mail_smtp_host' },
|
||||
{ configKey: 'mail_smtp_port' },
|
||||
{ configKey: 'mail_smtp_user' },
|
||||
{ configKey: 'mail_smtp_password' }
|
||||
]
|
||||
});
|
||||
|
||||
const configMap = new Map(mailConfigs.map(c => [c.configKey, c.configValue]));
|
||||
|
||||
const mailFrom = configMap.get('mail_from');
|
||||
const smtpHost = configMap.get('mail_smtp_host');
|
||||
const smtpPort = configMap.get('mail_smtp_port');
|
||||
const smtpUser = configMap.get('mail_smtp_user');
|
||||
const smtpPassword = configMap.get('mail_smtp_password');
|
||||
|
||||
if (!mailFrom || !smtpHost || !smtpPort || !smtpUser || !smtpPassword) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '邮件配置不完整,请先配置邮件信息'
|
||||
});
|
||||
}
|
||||
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: smtpHost,
|
||||
port: Number(smtpPort),
|
||||
secure: Number(smtpPort) === 465,
|
||||
auth: {
|
||||
user: smtpUser,
|
||||
pass: smtpPassword
|
||||
}
|
||||
});
|
||||
|
||||
const mailOptions = {
|
||||
from: mailFrom,
|
||||
to: to,
|
||||
subject: '梦幻西游运营管理系统 - 邮件测试',
|
||||
text: '这是一封测试邮件,如果您收到此邮件,说明邮件配置正确。'
|
||||
};
|
||||
|
||||
await transporter.sendMail(mailOptions);
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: '邮件发送成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('测试邮件发送失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '邮件发送失败,请检查配置'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将配置键映射到.env文件中的环境变量键
|
||||
* @param configKey - 配置键
|
||||
* @returns 环境变量键,如果不映射则返回null
|
||||
*/
|
||||
private mapConfigKeyToEnvKey(configKey: string): string | null {
|
||||
const mapping: Record<string, string> = {
|
||||
'backend_host': 'BACKEND_HOST',
|
||||
'backend_port': 'PORT',
|
||||
'log_level': 'LOG_LEVEL',
|
||||
'cors_origin': 'CORS_ORIGIN',
|
||||
'jwt_secret': 'JWT_SECRET',
|
||||
'jwt_expires_in': 'JWT_EXPIRES_IN',
|
||||
'game_server_proxy_url': 'GAME_SERVER_PROXY_URL',
|
||||
'game_server_psk': 'GAME_PSK'
|
||||
};
|
||||
|
||||
return mapping[configKey] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
* @param email - 邮箱地址
|
||||
* @returns 是否有效
|
||||
*/
|
||||
private isValidEmail(email: string): boolean {
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
return emailRegex.test(email);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查玩家服务中心状态
|
||||
* @param req - Express请求对象
|
||||
* @param res - Express响应对象
|
||||
*/
|
||||
async checkPlayerServiceStatus(req: Request, res: Response) {
|
||||
try {
|
||||
const configRepository = AppDataSource.getRepository(Config);
|
||||
|
||||
const playerServiceEnabledConfig = await configRepository.findOne({
|
||||
where: { configKey: 'player_service_enabled' }
|
||||
});
|
||||
|
||||
const playerServiceCloseMsgConfig = await configRepository.findOne({
|
||||
where: { configKey: 'player_service_close_msg' }
|
||||
});
|
||||
|
||||
const enabled = playerServiceEnabledConfig?.configValue === 'true';
|
||||
const closeMsg = playerServiceCloseMsgConfig?.configValue || '玩家服务中心系统维护中';
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
enabled,
|
||||
closeMsg
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('检查玩家服务中心状态失败:', error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: '服务器内部错误'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import axios from 'axios';
|
||||
import { CaptchaService } from '../services/captchaService';
|
||||
|
||||
export class PlayerAuthController {
|
||||
private readonly gameServerUrl: string;
|
||||
@@ -10,7 +11,7 @@ export class PlayerAuthController {
|
||||
|
||||
async login(req: Request, res: Response) {
|
||||
try {
|
||||
const { username, password } = req.body;
|
||||
const { username, password, captchaId, captchaCode } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return res.status(400).json({
|
||||
@@ -19,6 +20,33 @@ export class PlayerAuthController {
|
||||
});
|
||||
}
|
||||
|
||||
// 创建验证码服务实例
|
||||
const captchaService = new CaptchaService();
|
||||
|
||||
// 检查是否启用了验证码功能
|
||||
const captchaEnabled = await captchaService.checkCaptchaEnabled();
|
||||
|
||||
// 如果启用了验证码,则验证验证码
|
||||
if (captchaEnabled) {
|
||||
// 验证验证码参数是否完整
|
||||
if (!captchaId || !captchaCode) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请输入验证码'
|
||||
});
|
||||
}
|
||||
|
||||
// 验证验证码是否正确
|
||||
const captchaVerify = captchaService.verifyCaptcha(captchaId, captchaCode);
|
||||
|
||||
if (!captchaVerify.success) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: captchaVerify.message || '验证码错误或已过期'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.gameServerUrl}?code=auth/login`,
|
||||
{
|
||||
|
||||
25
backend/src/models/Config.ts
Normal file
25
backend/src/models/Config.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||
|
||||
@Entity('configs')
|
||||
export class Config {
|
||||
@PrimaryGeneratedColumn('increment')
|
||||
id: number;
|
||||
|
||||
@Column({ type: 'varchar', length: 100, unique: true, name: 'config_key' })
|
||||
configKey: string;
|
||||
|
||||
@Column({ type: 'text', name: 'config_value' })
|
||||
configValue: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 50, name: 'config_type' })
|
||||
configType: string;
|
||||
|
||||
@Column({ type: 'varchar', length: 255, nullable: true })
|
||||
description: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -2,12 +2,16 @@ import { Router } from 'express';
|
||||
import { AdminAuthController } from '../controllers/adminAuthController';
|
||||
import { AdminUserController } from '../controllers/adminUserController';
|
||||
import { AdminRoleController } from '../controllers/adminRoleController';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { CaptchaController } from '../controllers/captchaController';
|
||||
import { adminAuthMiddleware, AuthRequest } from '../middleware/adminAuth';
|
||||
|
||||
const router = Router();
|
||||
const adminAuthController = new AdminAuthController();
|
||||
const adminUserController = new AdminUserController();
|
||||
const adminRoleController = new AdminRoleController();
|
||||
const configController = new ConfigController();
|
||||
const captchaController = new CaptchaController();
|
||||
|
||||
// 认证相关路由
|
||||
router.post('/login', (req, res) => adminAuthController.login(req, res));
|
||||
@@ -27,4 +31,14 @@ router.post('/roles', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleC
|
||||
router.put('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.updateRole(req, res));
|
||||
router.delete('/roles/:id', adminAuthMiddleware, (req: AuthRequest, res) => adminRoleController.deleteRole(req, res));
|
||||
|
||||
// 系统配置路由(需要认证)
|
||||
router.get('/configs', adminAuthMiddleware, (req: AuthRequest, res) => configController.getAllConfigs(req, res));
|
||||
router.put('/configs', adminAuthMiddleware, (req: AuthRequest, res) => configController.updateConfigs(req, res));
|
||||
router.post('/configs/test-email', adminAuthMiddleware, (req: AuthRequest, res) => configController.testEmail(req, res));
|
||||
|
||||
// 验证码路由
|
||||
router.get('/captcha', (req, res) => captchaController.generateCaptcha(req, res));
|
||||
router.post('/captcha/verify', (req, res) => captchaController.verifyCaptcha(req, res));
|
||||
router.get('/captcha/enabled', (req, res) => captchaController.checkCaptchaEnabled(req, res));
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import { Router } from 'express';
|
||||
import { PlayerAuthController } from '../controllers/playerAuthController';
|
||||
import { CaptchaController } from '../controllers/captchaController';
|
||||
import { ConfigController } from '../controllers/configController';
|
||||
import { playerAuthMiddleware, PlayerAuthRequest } from '../middleware/playerAuth';
|
||||
|
||||
const router = Router();
|
||||
const playerAuthController = new PlayerAuthController();
|
||||
const captchaController = new CaptchaController();
|
||||
const configController = new ConfigController();
|
||||
|
||||
router.post('/login', (req, res) => playerAuthController.login(req, res));
|
||||
|
||||
@@ -11,4 +15,12 @@ router.post('/logout', (req, res) => playerAuthController.logout(req, res));
|
||||
|
||||
router.get('/account', playerAuthMiddleware, (req: PlayerAuthRequest, res) => playerAuthController.getAccountInfo(req, res));
|
||||
|
||||
// 验证码路由
|
||||
router.get('/captcha', (req, res) => captchaController.generateCaptcha(req, res));
|
||||
router.post('/captcha/verify', (req, res) => captchaController.verifyCaptcha(req, res));
|
||||
router.get('/captcha/enabled', (req, res) => captchaController.checkCaptchaEnabled(req, res));
|
||||
|
||||
// 玩家服务中心状态路由
|
||||
router.get('/service-status', (req, res) => configController.checkPlayerServiceStatus(req, res));
|
||||
|
||||
export default router;
|
||||
|
||||
130
backend/src/services/captchaService.ts
Normal file
130
backend/src/services/captchaService.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import svgCaptcha from 'svg-captcha';
|
||||
|
||||
/**
|
||||
* 验证码存储
|
||||
* 使用内存存储验证码,生产环境建议使用Redis
|
||||
*/
|
||||
const captchaStore = new Map<string, { code: string; expireTime: number }>();
|
||||
|
||||
/**
|
||||
* 清理过期的验证码
|
||||
*/
|
||||
setInterval(() => {
|
||||
const now = Date.now();
|
||||
for (const [key, value] of captchaStore.entries()) {
|
||||
if (value.expireTime < now) {
|
||||
captchaStore.delete(key);
|
||||
}
|
||||
}
|
||||
}, 60000);
|
||||
|
||||
/**
|
||||
* 验证码服务类
|
||||
* 提供验证码的生成和验证功能
|
||||
*/
|
||||
export class CaptchaService {
|
||||
/**
|
||||
* 生成验证码
|
||||
* @returns 验证码ID和SVG图片
|
||||
*/
|
||||
generateCaptcha(): { captchaId: string; svg: string } {
|
||||
const captcha = svgCaptcha.create({
|
||||
size: 4,
|
||||
ignoreChars: '0o1iIl',
|
||||
noise: 2,
|
||||
color: true,
|
||||
background: '#f5f5f5',
|
||||
width: 120,
|
||||
height: 40,
|
||||
fontSize: 36
|
||||
});
|
||||
|
||||
const captchaId = this.generateCaptchaId();
|
||||
const expireTime = Date.now() + 5 * 60 * 1000;
|
||||
|
||||
captchaStore.set(captchaId, {
|
||||
code: captcha.text.toLowerCase(),
|
||||
expireTime
|
||||
});
|
||||
|
||||
return {
|
||||
captchaId,
|
||||
svg: captcha.data
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证验证码
|
||||
* @param captchaId - 验证码ID
|
||||
* @param code - 用户输入的验证码
|
||||
* @returns 验证结果
|
||||
*/
|
||||
verifyCaptcha(captchaId: string, code: string): { success: boolean; message?: string } {
|
||||
if (!captchaId || !code) {
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码参数不完整'
|
||||
};
|
||||
}
|
||||
|
||||
const captchaData = captchaStore.get(captchaId);
|
||||
|
||||
if (!captchaData) {
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码已过期或不存在'
|
||||
};
|
||||
}
|
||||
|
||||
if (captchaData.expireTime < Date.now()) {
|
||||
captchaStore.delete(captchaId);
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码已过期'
|
||||
};
|
||||
}
|
||||
|
||||
if (captchaData.code !== code.toLowerCase()) {
|
||||
captchaStore.delete(captchaId);
|
||||
return {
|
||||
success: false,
|
||||
message: '验证码错误'
|
||||
};
|
||||
}
|
||||
|
||||
captchaStore.delete(captchaId);
|
||||
|
||||
return {
|
||||
success: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查验证码是否启用
|
||||
* @returns 是否启用验证码
|
||||
*/
|
||||
async checkCaptchaEnabled(): Promise<boolean> {
|
||||
try {
|
||||
const { AppDataSource } = await import('../config/database');
|
||||
const { Config } = await import('../models/Config');
|
||||
|
||||
const configRepository = AppDataSource.getRepository(Config);
|
||||
const config = await configRepository.findOne({
|
||||
where: { configKey: 'login_captcha_enabled' }
|
||||
});
|
||||
|
||||
return config ? config.configValue === 'true' : true;
|
||||
} catch (error) {
|
||||
console.error('检查验证码状态失败:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码ID
|
||||
* @returns 验证码ID
|
||||
*/
|
||||
private generateCaptchaId(): string {
|
||||
return `captcha_${Date.now()}_${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
}
|
||||
90
backend/src/utils/envHelper.ts
Normal file
90
backend/src/utils/envHelper.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
/**
|
||||
* .env文件更新工具
|
||||
* 用于读取、更新和保存.env文件
|
||||
*/
|
||||
export class EnvHelper {
|
||||
/**
|
||||
* 读取.env文件内容
|
||||
* @param envFilePath .env文件路径
|
||||
* @returns 环境变量键值对
|
||||
*/
|
||||
static readEnvFile(envFilePath: string): Record<string, string> {
|
||||
try {
|
||||
const content = fs.readFileSync(envFilePath, 'utf-8');
|
||||
const envVars: Record<string, string> = {};
|
||||
|
||||
const lines = content.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||
const equalIndex = trimmedLine.indexOf('=');
|
||||
if (equalIndex > 0) {
|
||||
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||
const value = trimmedLine.substring(equalIndex + 1).trim();
|
||||
envVars[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return envVars;
|
||||
} catch (error) {
|
||||
console.error('读取.env文件失败:', error);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新.env文件中的指定变量
|
||||
* @param envFilePath .env文件路径
|
||||
* @param updates 要更新的环境变量键值对
|
||||
* @returns 是否更新成功
|
||||
*/
|
||||
static updateEnvFile(envFilePath: string, updates: Record<string, string>): boolean {
|
||||
try {
|
||||
const content = fs.readFileSync(envFilePath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
const updatedLines: string[] = [];
|
||||
const updatedKeys = new Set<string>();
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmedLine = line.trim();
|
||||
if (trimmedLine && !trimmedLine.startsWith('#')) {
|
||||
const equalIndex = trimmedLine.indexOf('=');
|
||||
if (equalIndex > 0) {
|
||||
const key = trimmedLine.substring(0, equalIndex).trim();
|
||||
if (updates.hasOwnProperty(key)) {
|
||||
updatedLines.push(`${key}=${updates[key]}`);
|
||||
updatedKeys.add(key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
updatedLines.push(line);
|
||||
}
|
||||
|
||||
for (const key in updates) {
|
||||
if (!updatedKeys.has(key)) {
|
||||
updatedLines.push(`${key}=${updates[key]}`);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(envFilePath, updatedLines.join('\n'), 'utf-8');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('更新.env文件失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取.env文件路径
|
||||
* @param env 环境名称 (development/production)
|
||||
* @returns .env文件路径
|
||||
*/
|
||||
static getEnvFilePath(env: string = 'development'): string {
|
||||
return path.join(process.cwd(), '.env');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user