const express = require('express'); const router = express.Router(); const bcrypt = require('bcryptjs'); const jwt = require('jsonwebtoken'); const db = require('../db'); const SmsClient = require('../utils/sms'); const logger = require('../utils/logger'); require('dotenv').config(); // 内存存储验证码 (手机号 -> {code, expire, lastSentTime, ip}) const verifyCodes = new Map(); // 定时清理过期验证码 (每5分钟执行一次) setInterval(() => { const now = Date.now(); for (const [phone, data] of verifyCodes.entries()) { if (now > data.expire) { verifyCodes.delete(phone); } } }, 5 * 60 * 1000); // 发送验证码 router.post('/send-code', async (req, res) => { const { phoneNumber, type } = req.body; // type: 'register' | 'reset' const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; if (!/^1[3-9]\d{9}$/.test(phoneNumber)) { return res.status(400).json({ message: '手机号格式不正确' }); } // 检查发送频率 (60s 冷却) const existingRecord = verifyCodes.get(phoneNumber); if (existingRecord) { const timeSinceLastSent = Date.now() - existingRecord.lastSentTime; if (timeSinceLastSent < 60 * 1000) { const remainingSeconds = Math.ceil((60000 - timeSinceLastSent) / 1000); return res.status(429).json({ message: `请等待 ${remainingSeconds} 秒后再试` }); } } try { // 如果是重置密码,检查用户是否存在 if (type === 'reset') { const userCheck = await db.query('SELECT id FROM users WHERE username = $1', [phoneNumber]); if (userCheck.rows.length === 0) { return res.status(404).json({ message: '该手机号未注册' }); } } // 如果是注册,检查用户是否已存在 else if (type === 'register') { const userCheck = await db.query('SELECT id FROM users WHERE username = $1', [phoneNumber]); if (userCheck.rows.length > 0) { return res.status(400).json({ message: '该手机号已注册' }); } } const code = Math.floor(100000 + Math.random() * 900000).toString(); await SmsClient.sendVerifyCode(phoneNumber, code); verifyCodes.set(phoneNumber, { code, expire: Date.now() + 2 * 60 * 1000, // 2分钟有效 lastSentTime: Date.now(), ip: clientIp }); logger.info(`验证码已发送至 ${phoneNumber} (类型: ${type}, IP: ${clientIp})`); res.json({ message: '验证码已发送' }); } catch (error) { logger.error(`发送验证码失败: ${error.message}`); res.status(500).json({ message: '发送失败,请稍后再试' }); } }); // 重置密码 router.post('/reset-password', async (req, res) => { const { phoneNumber, code, newPassword } = req.body; try { // 1. 校验验证码 const record = verifyCodes.get(phoneNumber); if (!record || record.code !== code || Date.now() > record.expire) { return res.status(400).json({ message: '验证码无效或已过期' }); } verifyCodes.delete(phoneNumber); // 2. 更新密码 const salt = await bcrypt.genSalt(10); const passwordHash = await bcrypt.hash(newPassword, salt); // 更新密码同时增加 token_version,使所有旧设备下线 (增加溢出保护) await db.query( 'UPDATE users SET password_hash = $1, token_version = CASE WHEN token_version >= 2000000000 THEN 1 ELSE token_version + 1 END WHERE username = $2', [passwordHash, phoneNumber] ); logger.info(`用户 ${phoneNumber} 重置密码成功`); res.json({ message: '密码重置成功' }); } catch (err) { logger.error(`重置密码失败: ${err.message}`); res.status(500).json({ message: '服务器错误' }); } }); // 登录 router.post('/login', async (req, res) => { const { username, password } = req.body; const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; if (!username || !password) { return res.status(400).json({ message: '用户名和密码不能为空' }); } try { const result = await db.query('SELECT * FROM users WHERE username = $1', [username]); const user = result.rows[0]; if (!user) { logger.warn(`登录失败: 用户名 ${username} 不存在 (IP: ${clientIp})`); return res.status(401).json({ message: '用户名或密码错误' }); } const isMatch = await bcrypt.compare(password, user.password_hash); if (!isMatch) { logger.warn(`登录失败: 用户 ${username} 密码错误 (IP: ${clientIp})`); return res.status(401).json({ message: '用户名或密码错误' }); } // 登录成功,更新 token_version 实现单点登录 (增加溢出保护) const updateRes = await db.query( 'UPDATE users SET token_version = CASE WHEN token_version >= 2000000000 THEN 1 ELSE token_version + 1 END WHERE id = $1 RETURNING token_version', [user.id] ); const tokenVersion = updateRes.rows[0].token_version; logger.info(`用户 ${username} 登录成功 (IP: ${clientIp}, Version: ${tokenVersion})`); const token = jwt.sign( { id: user.id, username: user.username, role: user.role, real_name: user.real_name, token_version: tokenVersion // 将版本号放入 Token }, process.env.JWT_SECRET, { expiresIn: '24h' } ); res.json({ token, user: { id: user.id, username: user.username, role: user.role, real_name: user.real_name } }); } catch (err) { console.error(err); res.status(500).json({ message: '服务器错误' }); } }); // 注册 router.post('/register', async (req, res) => { const { username, password, role: requestedRole, real_name, department, code } = req.body; try { // 1. 校验验证码 const record = verifyCodes.get(username); if (!record || record.code !== code || Date.now() > record.expire) { return res.status(400).json({ message: '验证码无效或已过期' }); } verifyCodes.delete(username); // 校验成功后删除 // 2. 手机号格式校验 if (!/^1[3-9]\d{9}$/.test(username)) { return res.status(400).json({ message: '手机号格式不正确' }); } const userExists = await db.query('SELECT * FROM users WHERE username = $1', [username]); if (userExists.rows.length > 0) { return res.status(400).json({ message: '该手机号已注册' }); } // 校验部门 const deptCheck = await db.query('SELECT * FROM departments WHERE name = $1', [department]); let finalDepartment = department; let deptStatus = 'verified'; if (deptCheck.rows.length === 0) { finalDepartment = '待定'; deptStatus = 'pending'; } const salt = await bcrypt.genSalt(10); const passwordHash = await bcrypt.hash(password, salt); if (requestedRole && requestedRole !== 'user') { const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; logger.warn(`娉ㄥ唽 role 瓒婃潈灏濊瘯: username=${username}, requestedRole=${requestedRole}, IP=${clientIp}`); } const result = await db.query( 'INSERT INTO users (username, password_hash, role, real_name, department, dept_status) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, username, role, real_name, department, dept_status', [username, passwordHash, 'user', real_name, finalDepartment, deptStatus] ); const clientIp = req.headers['x-forwarded-for'] || req.socket.remoteAddress; logger.info(`新用户注册成功: ${username} (${real_name}) (IP: ${clientIp})`); res.status(201).json(result.rows[0]); } catch (err) { console.error(err); res.status(500).json({ message: '服务器错误' }); } }); module.exports = router;