Files
2026-04-02 00:59:42 +08:00

307 lines
9.1 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;