初次提交
This commit is contained in:
110
middleware/auth.js
Normal file
110
middleware/auth.js
Normal file
@@ -0,0 +1,110 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { getDB } = require('../database');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key'; // 在生产环境中应该使用环境变量
|
||||
|
||||
/**
|
||||
* 用户认证中间件
|
||||
* 验证JWT令牌并检查用户状态(包括是否被拉黑)
|
||||
*/
|
||||
const auth = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: '未提供认证令牌' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const db = getDB();
|
||||
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(401).json({ success: false, message: '用户不存在' });
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 检查用户是否被拉黑
|
||||
if (user.is_blacklisted) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被拉黑,请联系管理员',
|
||||
code: 'USER_BLACKLISTED'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查支付状态(管理员除外)
|
||||
if (user.role !== 'admin' && user.payment_status === 'unpaid') {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '您的账户尚未激活,请完成支付后再使用',
|
||||
code: 'PAYMENT_REQUIRED',
|
||||
needPayment: true,
|
||||
userId: user.id
|
||||
});
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(401).json({ success: false, message: '无效的认证令牌' });
|
||||
}
|
||||
};
|
||||
|
||||
// 管理员认证中间件
|
||||
const adminAuth = (req, res, next) => {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ success: false, message: '需要管理员权限' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
/**
|
||||
* 支付认证中间件
|
||||
* 只验证JWT令牌和用户状态,不检查支付状态
|
||||
* 用于支付相关接口,允许未支付用户创建支付订单
|
||||
*/
|
||||
const paymentAuth = async (req, res, next) => {
|
||||
try {
|
||||
const token = req.header('Authorization')?.replace('Bearer ', '');
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ success: false, message: '未提供认证令牌' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
const db = getDB();
|
||||
const [users] = await db.execute('SELECT * FROM users WHERE id = ?', [decoded.userId]);
|
||||
|
||||
if (users.length === 0) {
|
||||
return res.status(401).json({ success: false, message: '用户不存在' });
|
||||
}
|
||||
|
||||
const user = users[0];
|
||||
|
||||
// 检查用户是否被拉黑
|
||||
if (user.is_blacklisted) {
|
||||
return res.status(403).json({
|
||||
success: false,
|
||||
message: '账户已被拉黑,请联系管理员',
|
||||
code: 'USER_BLACKLISTED'
|
||||
});
|
||||
}
|
||||
|
||||
// 注意:这里不检查支付状态,允许未支付用户创建支付订单
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('支付认证失败:', error);
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(401).json({ success: false, message: '无效的认证令牌' });
|
||||
}
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(401).json({ success: false, message: '认证令牌已过期' });
|
||||
}
|
||||
return res.status(500).json({ success: false, message: '认证失败' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = { auth, adminAuth, paymentAuth, JWT_SECRET };
|
||||
129
middleware/errorHandler.js
Normal file
129
middleware/errorHandler.js
Normal file
@@ -0,0 +1,129 @@
|
||||
const { logger } = require('../config/logger');
|
||||
const { ERROR_CODES, HTTP_STATUS } = require('../config/constants');
|
||||
|
||||
// 全局错误处理中间件
|
||||
const errorHandler = (err, req, res, next) => {
|
||||
let error = { ...err };
|
||||
error.message = err.message;
|
||||
|
||||
// 记录错误日志
|
||||
logger.error('Error occurred:', {
|
||||
message: err.message,
|
||||
stack: err.stack,
|
||||
url: req.originalUrl,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
userAgent: req.get('User-Agent'),
|
||||
userId: req.user?.id
|
||||
});
|
||||
|
||||
// MySQL错误处理
|
||||
if (err.code) {
|
||||
switch (err.code) {
|
||||
case 'ER_DUP_ENTRY':
|
||||
error.message = '数据已存在';
|
||||
error.statusCode = HTTP_STATUS.CONFLICT;
|
||||
error.errorCode = ERROR_CODES.DUPLICATE_ENTRY;
|
||||
break;
|
||||
case 'ER_NO_REFERENCED_ROW_2':
|
||||
error.message = '关联数据不存在';
|
||||
error.statusCode = HTTP_STATUS.BAD_REQUEST;
|
||||
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
|
||||
break;
|
||||
case 'ER_ROW_IS_REFERENCED_2':
|
||||
error.message = '数据正在被使用,无法删除';
|
||||
error.statusCode = HTTP_STATUS.CONFLICT;
|
||||
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
|
||||
break;
|
||||
case 'ECONNREFUSED':
|
||||
error.message = '数据库连接失败';
|
||||
error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
|
||||
error.errorCode = ERROR_CODES.DATABASE_ERROR;
|
||||
break;
|
||||
default:
|
||||
error.message = '数据库操作失败';
|
||||
error.statusCode = HTTP_STATUS.INTERNAL_SERVER_ERROR;
|
||||
error.errorCode = ERROR_CODES.DATABASE_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// JWT错误处理
|
||||
if (err.name === 'JsonWebTokenError') {
|
||||
error.message = '无效的访问令牌';
|
||||
error.statusCode = HTTP_STATUS.UNAUTHORIZED;
|
||||
error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR;
|
||||
}
|
||||
|
||||
if (err.name === 'TokenExpiredError') {
|
||||
error.message = '访问令牌已过期';
|
||||
error.statusCode = HTTP_STATUS.UNAUTHORIZED;
|
||||
error.errorCode = ERROR_CODES.AUTHENTICATION_ERROR;
|
||||
}
|
||||
|
||||
// 参数验证错误
|
||||
if (err.name === 'ValidationError' || err.isJoi) {
|
||||
const message = err.details ? err.details.map(detail => detail.message).join(', ') : err.message;
|
||||
error.message = `参数验证失败: ${message}`;
|
||||
error.statusCode = HTTP_STATUS.BAD_REQUEST;
|
||||
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
|
||||
}
|
||||
|
||||
// 业务逻辑错误处理
|
||||
if (err.message === '余额不足') {
|
||||
error.message = '用户积分余额不足,无法完成转账操作。请先为用户充值积分或选择其他用户。';
|
||||
error.statusCode = HTTP_STATUS.BAD_REQUEST;
|
||||
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
|
||||
}
|
||||
|
||||
if (err.message === '用户不存在') {
|
||||
error.message = '指定的用户不存在,请检查用户信息后重试。';
|
||||
error.statusCode = HTTP_STATUS.BAD_REQUEST;
|
||||
error.errorCode = ERROR_CODES.VALIDATION_ERROR;
|
||||
}
|
||||
|
||||
// 自定义错误
|
||||
if (err.statusCode) {
|
||||
error.statusCode = err.statusCode;
|
||||
error.errorCode = err.errorCode || ERROR_CODES.INTERNAL_ERROR;
|
||||
}
|
||||
|
||||
// 默认错误
|
||||
const statusCode = error.statusCode || HTTP_STATUS.INTERNAL_SERVER_ERROR;
|
||||
const errorCode = error.errorCode || ERROR_CODES.INTERNAL_ERROR;
|
||||
const message = error.message || '服务器内部错误';
|
||||
|
||||
res.status(statusCode).json({
|
||||
success: false,
|
||||
error: {
|
||||
code: errorCode,
|
||||
message: message
|
||||
},
|
||||
...(process.env.NODE_ENV === 'development' && { stack: err.stack })
|
||||
});
|
||||
};
|
||||
|
||||
// 404错误处理
|
||||
const notFound = (req, res, next) => {
|
||||
const error = new Error(`路径 ${req.originalUrl} 未找到`);
|
||||
error.statusCode = HTTP_STATUS.NOT_FOUND;
|
||||
error.errorCode = ERROR_CODES.NOT_FOUND;
|
||||
next(error);
|
||||
};
|
||||
|
||||
// 自定义错误类
|
||||
class AppError extends Error {
|
||||
constructor(message, statusCode, errorCode) {
|
||||
super(message);
|
||||
this.statusCode = statusCode;
|
||||
this.errorCode = errorCode;
|
||||
this.isOperational = true;
|
||||
|
||||
Error.captureStackTrace(this, this.constructor);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
errorHandler,
|
||||
notFound,
|
||||
AppError
|
||||
};
|
||||
230
middleware/validation.js
Normal file
230
middleware/validation.js
Normal file
@@ -0,0 +1,230 @@
|
||||
const Joi = require('joi');
|
||||
const { AppError } = require('./errorHandler');
|
||||
const { ERROR_CODES, HTTP_STATUS } = require('../config/constants');
|
||||
|
||||
// 验证中间件工厂函数
|
||||
const validate = (schema) => {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.body, { abortEarly: false });
|
||||
if (error) {
|
||||
const errorMessage = error.details.map(detail => detail.message).join(', ');
|
||||
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// 查询参数验证中间件
|
||||
const validateQuery = (schema) => {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.query, { abortEarly: false });
|
||||
if (error) {
|
||||
const errorMessage = error.details.map(detail => detail.message).join(', ');
|
||||
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// 路径参数验证中间件
|
||||
const validateParams = (schema) => {
|
||||
return (req, res, next) => {
|
||||
const { error } = schema.validate(req.params, { abortEarly: false });
|
||||
if (error) {
|
||||
const errorMessage = error.details.map(detail => detail.message).join(', ');
|
||||
return next(new AppError(errorMessage, HTTP_STATUS.BAD_REQUEST, ERROR_CODES.VALIDATION_ERROR));
|
||||
}
|
||||
next();
|
||||
};
|
||||
};
|
||||
|
||||
// 通用验证规则
|
||||
const commonSchemas = {
|
||||
// ID验证
|
||||
id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': 'ID必须是数字',
|
||||
'number.integer': 'ID必须是整数',
|
||||
'number.positive': 'ID必须是正数',
|
||||
'any.required': 'ID是必需的'
|
||||
}),
|
||||
|
||||
// 分页验证
|
||||
pagination: Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1).messages({
|
||||
'number.base': '页码必须是数字',
|
||||
'number.integer': '页码必须是整数',
|
||||
'number.min': '页码必须大于0'
|
||||
}),
|
||||
limit: Joi.number().integer().min(1).max(100).default(10).messages({
|
||||
'number.base': '每页数量必须是数字',
|
||||
'number.integer': '每页数量必须是整数',
|
||||
'number.min': '每页数量必须大于0',
|
||||
'number.max': '每页数量不能超过100'
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// 用户相关验证规则
|
||||
const userSchemas = {
|
||||
// 用户注册
|
||||
register: Joi.object({
|
||||
username: Joi.string().alphanum().min(3).max(30).required().messages({
|
||||
'string.base': '用户名必须是字符串',
|
||||
'string.alphanum': '用户名只能包含字母和数字',
|
||||
'string.min': '用户名至少3个字符',
|
||||
'string.max': '用户名最多30个字符',
|
||||
'any.required': '用户名是必需的'
|
||||
}),
|
||||
password: Joi.string().min(6).max(128).required().messages({
|
||||
'string.base': '密码必须是字符串',
|
||||
'string.min': '密码至少6个字符',
|
||||
'string.max': '密码最多128个字符',
|
||||
'any.required': '密码是必需的'
|
||||
}),
|
||||
phone: Joi.string().pattern(/^1[3-9]\d{9}$/).required().messages({
|
||||
'string.pattern.base': '手机号格式不正确',
|
||||
'any.required': '手机号是必需的'
|
||||
}),
|
||||
// 可选字段,注册时不需要填写
|
||||
real_name: Joi.string().max(50).allow('').optional().messages({
|
||||
'string.max': '真实姓名最多50个字符'
|
||||
}),
|
||||
role: Joi.string().valid('admin', 'user').default('user').messages({
|
||||
'any.only': '角色只能是admin或user'
|
||||
})
|
||||
}),
|
||||
|
||||
// 用户登录
|
||||
login: Joi.object({
|
||||
username: Joi.string().required().messages({
|
||||
'any.required': '用户名是必需的'
|
||||
}),
|
||||
password: Joi.string().required().messages({
|
||||
'any.required': '密码是必需的'
|
||||
})
|
||||
})
|
||||
};
|
||||
|
||||
// 转账相关验证规则
|
||||
const transferSchemas = {
|
||||
// 转账查询参数
|
||||
query: Joi.object({
|
||||
page: Joi.number().integer().min(1).default(1).messages({
|
||||
'number.base': '页码必须是数字',
|
||||
'number.integer': '页码必须是整数',
|
||||
'number.min': '页码必须大于0'
|
||||
}),
|
||||
limit: Joi.number().integer().min(1).max(100).default(10).messages({
|
||||
'number.base': '每页数量必须是数字',
|
||||
'number.integer': '每页数量必须是整数',
|
||||
'number.min': '每页数量必须大于0',
|
||||
'number.max': '每页数量不能超过100'
|
||||
}),
|
||||
status: Joi.string().valid('pending', 'confirmed', 'rejected', 'cancelled').allow('').messages({
|
||||
'any.only': '状态值无效'
|
||||
}),
|
||||
type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({
|
||||
'any.only': '转账类型无效'
|
||||
}),
|
||||
search: Joi.string().allow('').max(100).messages({
|
||||
'string.max': '搜索关键词最多100个字符'
|
||||
}),
|
||||
transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').allow('').messages({
|
||||
'any.only': '转账类型无效'
|
||||
}),
|
||||
start_date: Joi.date().iso().allow('').messages({
|
||||
'date.format': '开始日期格式不正确'
|
||||
}),
|
||||
end_date: Joi.date().iso().allow('').messages({
|
||||
'date.format': '结束日期格式不正确'
|
||||
}),
|
||||
sort: Joi.string().valid('id', 'amount', 'created_at', 'updated_at', 'status').allow('').messages({
|
||||
'any.only': '排序字段无效,只支持: id, amount, created_at, updated_at, status'
|
||||
}),
|
||||
order: Joi.string().valid('asc', 'desc').allow('').messages({
|
||||
'any.only': '排序方向无效,只支持: asc, desc'
|
||||
}),
|
||||
// 优先显示待处理转账参数
|
||||
show_pending: Joi.alternatives().try(
|
||||
Joi.boolean(),
|
||||
Joi.string().valid('true', 'false', '')
|
||||
).allow('').messages({
|
||||
'alternatives.match': 'show_pending参数只能是布尔值或字符串true/false'
|
||||
})
|
||||
}),
|
||||
|
||||
// 创建转账
|
||||
create: Joi.object({
|
||||
to_user_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '收款用户ID必须是数字',
|
||||
'number.integer': '收款用户ID必须是整数',
|
||||
'number.positive': '收款用户ID必须是正数',
|
||||
'any.required': '收款用户ID是必需的'
|
||||
}),
|
||||
amount: Joi.number().positive().precision(2).required().messages({
|
||||
'number.base': '金额必须是数字',
|
||||
'number.positive': '金额必须是正数',
|
||||
'any.required': '金额是必需的'
|
||||
}),
|
||||
transfer_type: Joi.string().valid('user_to_user', 'system_to_user', 'user_to_system').required().messages({
|
||||
'any.only': '转账类型无效',
|
||||
'any.required': '转账类型是必需的'
|
||||
}),
|
||||
description: Joi.string().max(500).allow('').messages({
|
||||
'string.max': '描述最多500个字符'
|
||||
}),
|
||||
voucher_url: Joi.string().uri().allow('').messages({
|
||||
'string.uri': '凭证URL格式不正确'
|
||||
})
|
||||
}),
|
||||
|
||||
// 确认转账
|
||||
confirm: Joi.object({
|
||||
transfer_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '转账ID必须是数字',
|
||||
'number.integer': '转账ID必须是整数',
|
||||
'number.positive': '转账ID必须是正数',
|
||||
'any.required': '转账ID是必需的'
|
||||
}),
|
||||
note: Joi.string().max(500).allow('').messages({
|
||||
'string.max': '备注最多500个字符'
|
||||
})
|
||||
}),
|
||||
|
||||
// 拒绝转账
|
||||
reject: Joi.object({
|
||||
transfer_id: Joi.number().integer().positive().required().messages({
|
||||
'number.base': '转账ID必须是数字',
|
||||
'number.integer': '转账ID必须是整数',
|
||||
'number.positive': '转账ID必须是正数',
|
||||
'any.required': '转账ID是必需的'
|
||||
}),
|
||||
note: Joi.string().max(500).allow('').messages({
|
||||
'string.max': '备注最多500个字符'
|
||||
})
|
||||
})
|
||||
};
|
||||
// 系统设置相关验证规则
|
||||
const systemSchemas = {
|
||||
updateSettings: Joi.object({
|
||||
site_name: Joi.string().max(100).optional(),
|
||||
site_description: Joi.string().max(500).optional(),
|
||||
|
||||
contact_phone: Joi.string().max(20).optional(),
|
||||
maintenance_mode: Joi.boolean().optional(),
|
||||
max_transfer_amount: Joi.number().positive().optional(),
|
||||
min_transfer_amount: Joi.number().positive().optional(),
|
||||
transfer_fee_rate: Joi.number().min(0).max(1).optional()
|
||||
})
|
||||
};
|
||||
|
||||
// 导出所有验证规则
|
||||
module.exports = {
|
||||
validate,
|
||||
validateQuery,
|
||||
validateParams,
|
||||
commonSchemas,
|
||||
userSchemas,
|
||||
transferSchemas,
|
||||
systemSchemas
|
||||
};
|
||||
Reference in New Issue
Block a user