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;