sec commit

This commit is contained in:
2026-04-02 00:59:42 +08:00
parent ab7dabc6a8
commit 881cca3fe6
1835 changed files with 207016 additions and 0 deletions

View File

@@ -0,0 +1,306 @@
const express = require('express');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const { v4: uuidv4 } = require('uuid');
const path = require('path');
const fs = require('fs');
const { authMiddleware, JWT_SECRET, JWT_EXPIRES_IN } = require('../middleware/auth');
const router = express.Router();
const DATA_DIR = path.join(__dirname, '../data');
const USERS_DIR = path.join(DATA_DIR, 'users');
const DEVICES_DIR = path.join(DATA_DIR, 'devices');
// 确保目录存在
[USERS_DIR, DEVICES_DIR].forEach(dir => {
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
});
// 内存存储验证码captchaKey -> { code, expiresAt }
const captchaStore = new Map();
// 生成6位数字验证码
function generateCode() {
return Math.floor(100000 + Math.random() * 900000).toString();
}
// 清理过期验证码
function cleanupExpiredCodes() {
const now = Date.now();
for (const [key, value] of captchaStore.entries()) {
if (value.expiresAt < now) captchaStore.delete(key);
}
}
setInterval(cleanupExpiredCodes, 60000);
// 辅助:读取用户文件
function getUserFilePath(phone) {
return path.join(USERS_DIR, `${phone}.json`);
}
// 辅助:读取用户
function getUser(phone) {
const filePath = getUserFilePath(phone);
if (!fs.existsSync(filePath)) return null;
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
}
// 辅助:保存用户
function saveUser(user) {
const filePath = getUserFilePath(user.phone);
fs.writeFileSync(filePath, JSON.stringify(user, null, 2), 'utf-8');
}
// 辅助:读取设备列表
function getDevices(userId) {
const files = fs.readdirSync(DEVICES_DIR);
return files
.filter(f => f.endsWith('.json'))
.map(f => JSON.parse(fs.readFileSync(path.join(DEVICES_DIR, f), 'utf-8')))
.filter(d => d.userId === userId);
}
// 辅助:保存设备
function saveDevice(device) {
const filePath = path.join(DEVICES_DIR, `${device.id}.json`);
fs.writeFileSync(filePath, JSON.stringify(device, null, 2), 'utf-8');
}
// 辅助:删除设备
function deleteDevice(deviceId) {
const filePath = path.join(DEVICES_DIR, `${deviceId}.json`);
if (fs.existsSync(filePath)) fs.unlinkSync(filePath);
}
// 辅助:删除用户所有设备
function deleteUserDevices(userId) {
getDevices(userId).forEach(d => deleteDevice(d.id));
}
// 辅助:生成 JWT
function generateToken(user) {
return jwt.sign(
{ id: user.id, phone: user.phone },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
}
// F1: POST /api/auth/send-code — 发送验证码
router.post('/send-code', (req, res) => {
const { phone, captchaKey, captchaCode } = req.body;
if (!phone || !captchaKey || !captchaCode) {
return res.status(400).json({ code: 400, message: '参数不完整' });
}
// 模拟图形验证码校验captchaCode 忽略大小写)
// 实际生产中应查询 Redis 或存储的验证码
const stored = captchaStore.get(captchaKey);
if (!stored) {
return res.status(400).json({ code: 400, message: '图形验证码已过期,请刷新' });
}
if (stored.code.toLowerCase() !== captchaCode.toLowerCase()) {
return res.status(400).json({ code: 400, message: '图形验证码错误' });
}
// 生成6位短信验证码5分钟有效
const code = generateCode();
const expiresAt = Date.now() + 5 * 60 * 1000;
// 存储短信验证码(用手机号作为子键方便后续验证)
captchaStore.set(`sms:${phone}`, { code, expiresAt });
console.log(`[send-code] 手机号 ${phone} 的验证码是: ${code}`);
// DEBUG: 开发环境返回验证码方便测试
const debug = process.env.NODE_ENV === 'production' ? {} : { debugCode: code };
return res.json({ code: 0, message: '发送成功', ...debug });
});
// F1: POST /api/auth/register — 注册
router.post('/register', (req, res) => {
const { phone, code, password } = req.body;
if (!phone || !code || !password) {
return res.status(400).json({ code: 400, message: '参数不完整' });
}
// 密码长度校验
if (password.length < 6) {
return res.status(400).json({ code: 400, message: '密码长度不能少于6位' });
}
// 验证短信验证码
const stored = captchaStore.get(`sms:${phone}`);
if (!stored || stored.expiresAt < Date.now()) {
return res.status(400).json({ code: 400, message: '验证码已过期,请重新获取' });
}
if (stored.code !== code) {
return res.status(400).json({ code: 400, message: '验证码错误' });
}
// 检查是否已注册
if (getUser(phone)) {
return res.status(400).json({ code: 400, message: '该手机号已注册' });
}
// 创建用户
const user = {
id: uuidv4(),
phone,
passwordHash: bcrypt.hashSync(password, 10),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
saveUser(user);
// 清除验证码
captchaStore.delete(`sms:${phone}`);
const token = generateToken(user);
return res.json({
code: 0,
token,
user: { id: user.id, phone: user.phone, createdAt: user.createdAt }
});
});
// F2: POST /api/auth/login — 登录
router.post('/login', (req, res) => {
const { phone, password } = req.body;
if (!phone || !password) {
return res.status(400).json({ code: 400, message: '参数不完整' });
}
const user = getUser(phone);
if (!user) {
return res.status(401).json({ code: 401, message: '手机号或密码错误' });
}
if (!bcrypt.compareSync(password, user.passwordHash)) {
return res.status(401).json({ code: 401, message: '手机号或密码错误' });
}
// 获取当前用户设备数
let devices = getDevices(user.id);
const MAX_DEVICES = 3;
// 超过3个设备踢出最早的
if (devices.length >= MAX_DEVICES) {
devices.sort((a, b) => new Date(a.lastActiveAt) - new Date(b.lastActiveAt));
const oldestDevice = devices[0];
deleteDevice(oldestDevice.id);
console.log(`[login] 设备数超限,踢出设备: ${oldestDevice.id}`);
}
// 创建新设备记录
const device = {
id: uuidv4(),
userId: user.id,
deviceName: req.headers['x-device-name'] || '未知设备',
deviceToken: req.headers['x-device-token'] || '',
lastActiveAt: new Date().toISOString(),
createdAt: new Date().toISOString()
};
saveDevice(device);
// 更新用户最后登录时间
user.updatedAt = new Date().toISOString();
saveUser(user);
const token = generateToken(user);
return res.json({
code: 0,
token,
user: { id: user.id, phone: user.phone, createdAt: user.createdAt }
});
});
// F3: POST /api/auth/change-password — 修改密码
router.post('/change-password', (req, res) => {
const { phone, code, newPassword } = req.body;
if (!phone || !code || !newPassword) {
return res.status(400).json({ code: 400, message: '参数不完整' });
}
if (newPassword.length < 6) {
return res.status(400).json({ code: 400, message: '密码长度不能少于6位' });
}
// 验证短信验证码
const stored = captchaStore.get(`sms:${phone}`);
if (!stored || stored.expiresAt < Date.now()) {
return res.status(400).json({ code: 400, message: '验证码已过期,请重新获取' });
}
if (stored.code !== code) {
return res.status(400).json({ code: 400, message: '验证码错误' });
}
const user = getUser(phone);
if (!user) {
return res.status(404).json({ code: 404, message: '用户不存在' });
}
user.passwordHash = bcrypt.hashSync(newPassword, 10);
user.updatedAt = new Date().toISOString();
saveUser(user);
// 清除验证码 & 清除该用户所有设备(强制重新登录)
captchaStore.delete(`sms:${phone}`);
deleteUserDevices(user.id);
return res.json({ code: 0, message: '修改成功' });
});
// F4: GET /api/auth/devices — 获取登录设备列表
router.get('/devices', authMiddleware, (req, res) => {
const devices = getDevices(req.user.id).map(d => ({
id: d.id,
deviceName: d.deviceName,
deviceToken: d.deviceToken,
lastActiveAt: d.lastActiveAt,
createdAt: d.createdAt
}));
return res.json({ code: 0, devices });
});
// F4: DELETE /api/auth/devices/:id — 删除设备
router.delete('/devices/:id', authMiddleware, (req, res) => {
const deviceId = req.params.id;
const devices = getDevices(req.user.id);
const device = devices.find(d => d.id === deviceId);
if (!device) {
return res.status(404).json({ code: 404, message: '设备不存在' });
}
deleteDevice(deviceId);
return res.json({ code: 0, message: '已下线' });
});
// 模拟图形验证码GET /api/auth/captcha 获取图形验证码
router.get('/captcha', (req, res) => {
const captchaKey = uuidv4();
const captchaCode = Math.random().toString(36).slice(2, 8).toUpperCase();
// 存储5分钟
captchaStore.set(captchaKey, {
code: captchaCode,
expiresAt: Date.now() + 5 * 60 * 1000
});
// 返回 key 和一个简单的 SVG 验证码图片(文本形式,实际生产用真图片)
// 这里返回 key 让前端存储,后续 /send-code 验证
return res.json({
code: 0,
captchaKey,
// DEBUG: 实际验证码(仅开发环境)
captchaCode: process.env.NODE_ENV === 'production' ? undefined : captchaCode
});
});
module.exports = router;