521 lines
15 KiB
JavaScript
521 lines
15 KiB
JavaScript
const express = require('express');
|
|
const bcrypt = require('bcryptjs');
|
|
const jwt = require('jsonwebtoken');
|
|
const { getDB } = require('../database');
|
|
|
|
const router = express.Router();
|
|
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
|
|
|
|
|
/**
|
|
* @swagger
|
|
* tags:
|
|
* name: Authentication
|
|
* description: 用户认证API
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* components:
|
|
* schemas:
|
|
* LoginCredentials:
|
|
* type: object
|
|
* required:
|
|
* - username
|
|
* - password
|
|
* properties:
|
|
* username:
|
|
* type: string
|
|
* description: 用户名或手机号
|
|
* password:
|
|
* type: string
|
|
* description: 密码
|
|
* RegisterRequest:
|
|
* type: object
|
|
* required:
|
|
* - username
|
|
* - phone
|
|
* - password
|
|
* - registrationCode
|
|
* - city
|
|
* - district_id
|
|
* - captchaId
|
|
* - captchaText
|
|
* - smsCode
|
|
* properties:
|
|
* username:
|
|
* type: string
|
|
* description: 用户名
|
|
* phone:
|
|
* type: string
|
|
* description: 手机号
|
|
* password:
|
|
* type: string
|
|
* description: 密码
|
|
* registrationCode:
|
|
* type: string
|
|
* description: 注册激活码
|
|
* city:
|
|
* type: string
|
|
* description: 城市
|
|
* district_id:
|
|
* type: string
|
|
* description: 区域ID
|
|
* captchaId:
|
|
* type: string
|
|
* description: 图形验证码ID
|
|
* captchaText:
|
|
* type: string
|
|
* description: 图形验证码文本
|
|
* smsCode:
|
|
* type: string
|
|
* description: 短信验证码
|
|
* role:
|
|
* type: string
|
|
* description: 用户角色
|
|
* default: user
|
|
*/
|
|
|
|
/**
|
|
* @swagger
|
|
* /auth/register:
|
|
* post:
|
|
* summary: 用户注册
|
|
* description: 需要提供有效的激活码才能注册
|
|
* tags: [Authentication]
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/RegisterRequest'
|
|
* responses:
|
|
* 201:
|
|
* description: 用户注册成功
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* message:
|
|
* type: string
|
|
* token:
|
|
* type: string
|
|
* description: JWT认证令牌
|
|
* user:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: integer
|
|
* username:
|
|
* type: string
|
|
* role:
|
|
* type: string
|
|
* 400:
|
|
* description: 请求参数错误
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.post('/register', async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
await db.query('START TRANSACTION');
|
|
|
|
const {
|
|
username,
|
|
phone,
|
|
password,
|
|
registrationCode,
|
|
city,
|
|
district_id: district,
|
|
captchaId,
|
|
captchaText,
|
|
smsCode, // 短信验证码
|
|
role = 'user'
|
|
} = req.body;
|
|
|
|
if (!username || !phone || !password || !registrationCode || !city || !district) {
|
|
return res.status(400).json({ success: false, message: '用户名、手机号、密码、激活码、城市和区域不能为空' });
|
|
}
|
|
|
|
if (!captchaId || !captchaText) {
|
|
return res.status(400).json({ success: false, message: '图形验证码不能为空' });
|
|
}
|
|
|
|
if (!smsCode) {
|
|
return res.status(400).json({ success: false, message: '短信验证码不能为空' });
|
|
}
|
|
|
|
// 注意:图形验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
|
|
|
// 验证短信验证码
|
|
const smsAPI = require('./sms');
|
|
const smsValid = smsAPI.verifySMSCode(phone, smsCode);
|
|
if (!smsValid) {
|
|
return res.status(400).json({ success: false, message: '短信验证码错误或已过期' });
|
|
}
|
|
|
|
// 验证手机号格式
|
|
const phoneRegex = /^1[3-9]\d{9}$/;
|
|
if (!phoneRegex.test(phone)) {
|
|
return res.status(400).json({ success: false, message: '手机号格式不正确' });
|
|
}
|
|
|
|
// 验证激活码
|
|
const [registrationCodes] = await db.execute(
|
|
'SELECT id, is_used, expires_at, agent_id FROM registration_codes WHERE code = ?',
|
|
[registrationCode]
|
|
);
|
|
|
|
if (registrationCodes.length === 0) {
|
|
return res.status(400).json({ success: false, message: '激活码不存在' });
|
|
}
|
|
|
|
const regCode = registrationCodes[0];
|
|
|
|
// 检查激活码是否已使用
|
|
if (regCode.is_used) {
|
|
return res.status(400).json({ success: false, message: '激活码已被使用' });
|
|
}
|
|
|
|
// 检查激活码是否过期
|
|
if (new Date() > new Date(regCode.expires_at)) {
|
|
return res.status(400).json({ success: false, message: '激活码已过期' });
|
|
}
|
|
|
|
// 检查用户是否已存在
|
|
const [existingUsers] = await db.execute(
|
|
'SELECT id FROM users WHERE username = ? OR phone = ?',
|
|
[username, phone]
|
|
);
|
|
|
|
if (existingUsers.length > 0) {
|
|
return res.status(400).json({ success: false, message: '用户名或手机号已存在' });
|
|
}
|
|
|
|
// 加密密码
|
|
const hashedPassword = await bcrypt.hash(password, 10);
|
|
|
|
// 创建用户(待审核状态,可以进入系统但匹配需审核)
|
|
const [result] = await db.execute(
|
|
'INSERT INTO users (username, phone, password, role, points, audit_status, city, district_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
|
[username, phone, hashedPassword, role, 0, 'pending', city, district]
|
|
);
|
|
|
|
const userId = result.insertId;
|
|
|
|
// 用户余额已在创建用户时设置为默认值0.00,无需额外操作
|
|
|
|
// 标记激活码为已使用
|
|
await db.execute(
|
|
'UPDATE registration_codes SET is_used = TRUE, used_at = NOW(), used_by_user_id = ? WHERE id = ?',
|
|
[userId, regCode.id]
|
|
);
|
|
|
|
// 如果是代理邀请码,建立代理关系
|
|
if (regCode.agent_id) {
|
|
// 验证agent_id是否存在于regional_agents表中
|
|
const [agentExists] = await db.execute(
|
|
'SELECT id FROM regional_agents WHERE id = ?',
|
|
[regCode.agent_id]
|
|
);
|
|
|
|
if (agentExists.length > 0) {
|
|
await db.execute(
|
|
'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
|
|
[regCode.agent_id, userId]
|
|
);
|
|
}
|
|
} else {
|
|
// 如果不是代理邀请码,根据地区自动关联代理
|
|
const [agents] = await db.execute(
|
|
'SELECT ra.id FROM users u INNER JOIN regional_agents ra ON u.id = ra.user_id WHERE ra.region_id = ? AND ra.status = "active" ORDER BY ra.created_at ASC LIMIT 1',
|
|
[district]
|
|
);
|
|
|
|
if (agents.length > 0) {
|
|
await db.execute(
|
|
'INSERT INTO agent_merchants (agent_id, merchant_id, created_at) VALUES (?, ?, NOW())',
|
|
[agents[0].id, userId]
|
|
);
|
|
}
|
|
}
|
|
|
|
await db.query('COMMIT');
|
|
|
|
// 生成JWT token
|
|
const token = jwt.sign(
|
|
{ userId: userId, username, role },
|
|
JWT_SECRET,
|
|
{ expiresIn: '24h' }
|
|
);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
message: '注册成功',
|
|
token,
|
|
user: {
|
|
id: userId,
|
|
username,
|
|
phone,
|
|
role,
|
|
points: 0,
|
|
audit_status: 'pending',
|
|
city,
|
|
district
|
|
}
|
|
});
|
|
} catch (error) {
|
|
try {
|
|
await getDB().query('ROLLBACK');
|
|
} catch (rollbackError) {
|
|
console.error('回滚错误:', rollbackError);
|
|
}
|
|
console.error('注册错误详情:', error);
|
|
console.error('错误堆栈:', error.stack);
|
|
res.status(500).json({
|
|
success: false,
|
|
message: '注册失败',
|
|
error: process.env.NODE_ENV === 'development' ? error.message : undefined
|
|
});
|
|
}
|
|
});
|
|
|
|
/**
|
|
* @swagger
|
|
* /auth/login:
|
|
* post:
|
|
* summary: 用户登录
|
|
* tags: [Authentication]
|
|
* requestBody:
|
|
* required: true
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* $ref: '#/components/schemas/LoginCredentials'
|
|
* responses:
|
|
* 200:
|
|
* description: 登录成功
|
|
* content:
|
|
* application/json:
|
|
* schema:
|
|
* type: object
|
|
* properties:
|
|
* success:
|
|
* type: boolean
|
|
* message:
|
|
* type: string
|
|
* token:
|
|
* type: string
|
|
* description: JWT认证令牌
|
|
* user:
|
|
* type: object
|
|
* properties:
|
|
* id:
|
|
* type: integer
|
|
* username:
|
|
* type: string
|
|
* role:
|
|
* type: string
|
|
* avatar:
|
|
* type: string
|
|
* points:
|
|
* type: integer
|
|
* 400:
|
|
* description: 请求参数错误
|
|
* 401:
|
|
* description: 用户名或密码错误
|
|
* 403:
|
|
* description: 账户审核未通过
|
|
* 500:
|
|
* description: 服务器错误
|
|
*/
|
|
router.post('/login', async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
const { username, password, captchaId, captchaText } = req.body;
|
|
|
|
if (!username || !password) {
|
|
return res.status(400).json({ success: false, message: '用户名和密码不能为空' });
|
|
}
|
|
|
|
if (!captchaId || !captchaText) {
|
|
return res.status(400).json({ success: false, message: '验证码不能为空' });
|
|
}
|
|
// 获取存储的验证码
|
|
const storedCaptcha = global.captchaStore.get(captchaId);
|
|
console.log(storedCaptcha);
|
|
|
|
if (!storedCaptcha) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: '验证码不存在或已过期'
|
|
});
|
|
}
|
|
|
|
// 检查是否过期
|
|
if (Date.now() > storedCaptcha.expires) {
|
|
global.captchaStore.delete(captchaId);
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: '验证码已过期'
|
|
});
|
|
}
|
|
|
|
// 验证验证码(不区分大小写)
|
|
const isValid = storedCaptcha.text === captchaText.toLowerCase();
|
|
|
|
// 删除已验证的验证码
|
|
global.captchaStore.delete(captchaId);
|
|
|
|
if (!isValid) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: '验证码错误'
|
|
});
|
|
}
|
|
|
|
// 注意:验证码已在前端通过 /captcha/verify 接口验证过,这里不再重复验证
|
|
|
|
// 查找用户
|
|
console.log('登录尝试 - 用户名:', username);
|
|
const [users] = await db.execute(
|
|
'SELECT * FROM users WHERE username = ?',
|
|
[username]
|
|
);
|
|
|
|
console.log('查找到的用户数量:', users.length);
|
|
if (users.length === 0) {
|
|
console.log('用户不存在:', username);
|
|
return res.status(401).json({ success: false, message: '用户名或密码错误' });
|
|
}
|
|
|
|
const user = users[0];
|
|
console.log('找到用户:', user.username, '密码长度:', user.password ? user.password.length : 'null');
|
|
|
|
// 验证密码
|
|
console.log('验证密码 - 输入密码:', password, '数据库密码前10位:', user.password ? user.password.substring(0, 10) : 'null');
|
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
console.log('密码验证结果:', isValidPassword);
|
|
|
|
if (!isValidPassword) {
|
|
console.log('密码验证失败');
|
|
return res.status(401).json({ success: false, message: '用户名或密码错误' });
|
|
}
|
|
|
|
// 检查用户审核状态(管理员除外,只阻止被拒绝的用户)
|
|
if (user.role !== 'admin' && user.audit_status === 'rejected') {
|
|
return res.status(403).json({ success: false, message: '您的账户审核未通过,请联系管理员' });
|
|
}
|
|
// 待审核用户可以正常登录使用系统,但匹配功能会有限制
|
|
|
|
// 生成JWT token
|
|
const token = jwt.sign(
|
|
{ userId: user.id, username: user.username, role: user.role },
|
|
JWT_SECRET,
|
|
{ expiresIn: '24h' }
|
|
);
|
|
|
|
res.json({
|
|
success: true,
|
|
message: '登录成功',
|
|
token,
|
|
user: {
|
|
id: user.id,
|
|
username: user.username,
|
|
role: user.role,
|
|
avatar: user.avatar,
|
|
points: user.points
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('登录错误:', error);
|
|
res.status(500).json({ success: false, message: '登录失败' });
|
|
}
|
|
});
|
|
|
|
// 验证token中间件
|
|
const authenticateToken = (req, res, next) => {
|
|
const authHeader = req.headers['authorization'];
|
|
const token = authHeader && authHeader.split(' ')[1];
|
|
|
|
if (!token) {
|
|
return res.status(401).json({ success: false, message: '访问令牌缺失' });
|
|
}
|
|
|
|
jwt.verify(token, JWT_SECRET, (err, user) => {
|
|
if (err) {
|
|
return res.status(403).json({ success: false, message: '访问令牌无效' });
|
|
}
|
|
req.user = user;
|
|
next();
|
|
});
|
|
};
|
|
|
|
// 获取当前用户信息
|
|
router.get('/me', authenticateToken, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
const [users] = await db.execute(
|
|
'SELECT id, username, role, avatar, points, created_at FROM users WHERE id = ?',
|
|
[req.user.userId]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
|
}
|
|
|
|
res.json({ success: true, user: users[0] });
|
|
} catch (error) {
|
|
console.error('获取用户信息错误:', error);
|
|
res.status(500).json({ success: false, message: '获取用户信息失败' });
|
|
}
|
|
});
|
|
|
|
// 修改密码
|
|
router.put('/change-password', authenticateToken, async (req, res) => {
|
|
try {
|
|
const db = getDB();
|
|
const { currentPassword, newPassword } = req.body;
|
|
|
|
if (!currentPassword || !newPassword) {
|
|
return res.status(400).json({ success: false, message: '旧密码和新密码不能为空' });
|
|
}
|
|
|
|
// 获取用户当前密码
|
|
const [users] = await db.execute(
|
|
'SELECT password FROM users WHERE id = ?',
|
|
[req.user.userId]
|
|
);
|
|
|
|
if (users.length === 0) {
|
|
return res.status(404).json({ success: false, message: '用户不存在' });
|
|
}
|
|
|
|
// 验证旧密码
|
|
const isValidPassword = await bcrypt.compare(currentPassword, users[0].password);
|
|
|
|
if (!isValidPassword) {
|
|
return res.status(400).json({ success: false, message: '旧密码错误' });
|
|
}
|
|
|
|
// 加密新密码
|
|
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
|
|
|
// 更新密码
|
|
await db.execute(
|
|
'UPDATE users SET password = ? WHERE id = ?',
|
|
[hashedNewPassword, req.user.userId]
|
|
);
|
|
|
|
res.json({ success: true, message: '密码修改成功' });
|
|
} catch (error) {
|
|
console.error('修改密码错误:', error);
|
|
res.status(500).json({ success: false, message: '修改密码失败' });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
module.exports.authenticateToken = authenticateToken; |