chore: initial import
This commit is contained in:
@@ -0,0 +1,220 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user