feat: 邮箱验证码注册+登录协议勾选
- 后端: VerificationCode模型+send-code API+register增加verifyCode校验 - 前端: 注册表单增加验证码输入框+60s倒计时获取按钮 - 前端: 登录表单增加用户协议/隐私政策勾选 - 前端: 登录按钮增加disabled状态(未勾选时不可点击) - 后端: nodemailer邮件发送工具(可配置SMTP,未配置时返回devCode)
This commit is contained in:
+119
-16
@@ -1,10 +1,11 @@
|
||||
const express = require('express')
|
||||
const router = express.Router()
|
||||
const jwt = require('jsonwebtoken')
|
||||
const { User, Admin, AdminLog, LoginLog } = require('../models')
|
||||
const { User, Admin, AdminLog, LoginLog, VerificationCode } = require('../models')
|
||||
const ApiResponse = require('../utils/response')
|
||||
const weChatService = require('../utils/weixin')
|
||||
const logger = require('../utils/logger')
|
||||
const { sendVerificationCode } = require('../utils/mail')
|
||||
const axios = require('axios')
|
||||
|
||||
/**
|
||||
@@ -312,10 +313,17 @@ router.post('/admin/login', async (req, res) => {
|
||||
*/
|
||||
router.post('/user/login', async (req, res) => {
|
||||
try {
|
||||
const { username, password } = req.body
|
||||
const { username, email, password } = req.body
|
||||
// 支持邮箱或用户名登录
|
||||
const identifier = email || username
|
||||
|
||||
// 查找用户(显式包含密码字段)
|
||||
const user = await User.findOne({ username }).select('+password')
|
||||
const user = await User.findOne({
|
||||
$or: [
|
||||
{ email: identifier },
|
||||
{ username: identifier }
|
||||
]
|
||||
}).select('+password')
|
||||
if (!user) {
|
||||
return ApiResponse.error(res, '用户名或密码错误', 401)
|
||||
}
|
||||
@@ -349,28 +357,123 @@ router.post('/user/login', async (req, res) => {
|
||||
}, '登录成功')
|
||||
|
||||
} catch (error) {
|
||||
logger.error('用户登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
logger.error('用户登录失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* H5环境用户注册
|
||||
* POST /api/auth/user/register
|
||||
*/
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
* POST /api/auth/user/send-code
|
||||
*/
|
||||
router.post('/user/send-code', async (req, res) => {
|
||||
try {
|
||||
const { email } = req.body
|
||||
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
|
||||
return ApiResponse.error(res, '邮箱格式不正确', 400)
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被注册
|
||||
const existing = await User.findOne({ email })
|
||||
if (existing) {
|
||||
return ApiResponse.error(res, '该邮箱已被注册', 400)
|
||||
}
|
||||
|
||||
// 检查发送频率(1分钟内只能发一次)
|
||||
const recent = await VerificationCode.findOne({
|
||||
email,
|
||||
createdAt: { $gte: new Date(Date.now() - 60000) }
|
||||
})
|
||||
if (recent) {
|
||||
return ApiResponse.error(res, '发送过于频繁,请 1 分钟后再试', 429)
|
||||
}
|
||||
|
||||
// 生成6位随机验证码
|
||||
const code = String(Math.floor(100000 + Math.random() * 900000))
|
||||
|
||||
// 10分钟过期
|
||||
const expiresAt = new Date(Date.now() + 10 * 60 * 1000)
|
||||
|
||||
// 使之前的验证码失效
|
||||
await VerificationCode.updateMany({ email, used: false }, { $set: { used: true } })
|
||||
|
||||
// 保存新验证码
|
||||
await VerificationCode.create({ email, code, expiresAt })
|
||||
|
||||
// 发送邮件
|
||||
const result = await sendVerificationCode(email, code)
|
||||
|
||||
if (result.success) {
|
||||
return ApiResponse.success(res, {
|
||||
devCode: result.devCode || undefined
|
||||
}, '验证码已发送')
|
||||
} else {
|
||||
return ApiResponse.error(res, '验证码发送失败,请稍后重试', 500)
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('发送验证码失败:', error)
|
||||
return ApiResponse.serverError(res, error.message)
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* H5环境用户注册(需邮箱验证码)
|
||||
* POST /api/auth/user/register
|
||||
*/
|
||||
router.post('/user/register', async (req, res) => {
|
||||
try {
|
||||
const { username, password, nickName, avatarUrl } = req.body
|
||||
const { username, email, password, nickName, avatarUrl, verifyCode } = req.body
|
||||
const identifier = email || username
|
||||
|
||||
if (!identifier) {
|
||||
return ApiResponse.error(res, '请输入邮箱或用户名', 400)
|
||||
}
|
||||
|
||||
if (!email) {
|
||||
return ApiResponse.error(res, '邮箱为必填项', 400)
|
||||
}
|
||||
|
||||
// 验证邮箱验证码
|
||||
if (!verifyCode) {
|
||||
return ApiResponse.error(res, '请输入邮箱验证码', 400)
|
||||
}
|
||||
const validCode = await VerificationCode.findOne({
|
||||
email,
|
||||
code: verifyCode,
|
||||
used: false,
|
||||
expiresAt: { $gt: new Date() }
|
||||
})
|
||||
if (!validCode) {
|
||||
return ApiResponse.error(res, '验证码无效或已过期', 400)
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
if (email) {
|
||||
const existingEmail = await User.findOne({ email })
|
||||
if (existingEmail) {
|
||||
return ApiResponse.error(res, '该邮箱已被注册', 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 检查用户名是否已存在
|
||||
const existingUser = await User.findOne({ username })
|
||||
if (existingUser) {
|
||||
return ApiResponse.error(res, '用户名已存在', 400)
|
||||
if (username) {
|
||||
const existingUser = await User.findOne({ username })
|
||||
if (existingUser) {
|
||||
return ApiResponse.error(res, '用户名已存在', 400)
|
||||
}
|
||||
}
|
||||
|
||||
// 标记验证码为已使用
|
||||
validCode.used = true
|
||||
await validCode.save()
|
||||
|
||||
// 自动生成用户名(若未提供)
|
||||
const finalUsername = username || email.split('@')[0]
|
||||
|
||||
// 创建新用户
|
||||
const user = new User({
|
||||
username,
|
||||
username: finalUsername,
|
||||
email: email || '',
|
||||
password,
|
||||
nickName: nickName || '星辰旅行者',
|
||||
avatarUrl: avatarUrl || '',
|
||||
|
||||
Reference in New Issue
Block a user