feat: 注册、重置密码、重发验证邮件添加验证码功能

后端修改:
- 添加通用验证码验证函数 verifyCaptcha()
- /api/register 接口添加验证码验证
- /api/password/forgot 接口添加验证码验证
- /api/resend-verification 接口添加验证码验证

前端修改:
- 注册表单添加验证码输入框和图片
- 忘记密码模态框添加验证码
- 重发验证邮件区域添加验证码输入
- 添加各表单的验证码刷新方法
- 提交失败后自动刷新验证码

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-28 13:11:03 +08:00
parent f8c9f8f739
commit 1d65e97b04
3 changed files with 148 additions and 14 deletions

View File

@@ -1100,6 +1100,42 @@ function checkMailRateLimit(req, type = 'mail') {
}
}
// ===== 验证码验证辅助函数 =====
/**
* 验证验证码
* @param {Object} req - 请求对象
* @param {string} captcha - 用户输入的验证码
* @returns {{valid: boolean, message?: string}} 验证结果
*/
function verifyCaptcha(req, captcha) {
if (!captcha) {
return { valid: false, message: '请输入验证码' };
}
const sessionCaptcha = req.session.captcha;
const captchaTime = req.session.captchaTime;
if (!sessionCaptcha || !captchaTime) {
return { valid: false, message: '验证码已过期,请刷新验证码' };
}
// 验证码有效期5分钟
if (Date.now() - captchaTime > 5 * 60 * 1000) {
return { valid: false, message: '验证码已过期,请刷新验证码' };
}
if (captcha.toLowerCase() !== sessionCaptcha) {
return { valid: false, message: '验证码错误' };
}
// 验证通过后清除session中的验证码
delete req.session.captcha;
delete req.session.captchaTime;
return { valid: true };
}
// ===== 公开API =====
// 健康检查
@@ -1164,7 +1200,8 @@ app.post('/api/register',
.isLength({ min: 3, max: 20 }).withMessage('用户名长度3-20个字符')
.matches(USERNAME_REGEX).withMessage('用户名仅允许中英文、数字、下划线、点和短横线'),
body('email').isEmail().withMessage('邮箱格式不正确'),
body('password').isLength({ min: 6 }).withMessage('密码至少6个字符')
body('password').isLength({ min: 6 }).withMessage('密码至少6个字符'),
body('captcha').notEmpty().withMessage('请输入验证码')
],
async (req, res) => {
const errors = validationResult(req);
@@ -1176,6 +1213,16 @@ app.post('/api/register',
}
try {
// 验证验证码
const { captcha } = req.body;
const captchaResult = verifyCaptcha(req, captcha);
if (!captchaResult.valid) {
return res.status(400).json({
success: false,
message: captchaResult.message
});
}
checkMailRateLimit(req, 'verify');
const { username, email, password } = req.body;
@@ -1263,7 +1310,8 @@ app.post('/api/resend-verification', [
body('username')
.optional({ checkFalsy: true })
.isLength({ min: 3 }).withMessage('用户名格式不正确')
.matches(USERNAME_REGEX).withMessage('用户名仅允许中英文、数字、下划线、点和短横线')
.matches(USERNAME_REGEX).withMessage('用户名仅允许中英文、数字、下划线、点和短横线'),
body('captcha').notEmpty().withMessage('请输入验证码')
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
@@ -1271,6 +1319,16 @@ app.post('/api/resend-verification', [
}
try {
// 验证验证码
const { captcha } = req.body;
const captchaResult = verifyCaptcha(req, captcha);
if (!captchaResult.valid) {
return res.status(400).json({
success: false,
message: captchaResult.message
});
}
checkMailRateLimit(req, 'verify');
const { email, username } = req.body;
@@ -1332,15 +1390,25 @@ app.get('/api/verify-email', async (req, res) => {
// 发起密码重置(邮件)
app.post('/api/password/forgot', [
body('email').isEmail().withMessage('邮箱格式不正确')
body('email').isEmail().withMessage('邮箱格式不正确'),
body('captcha').notEmpty().withMessage('请输入验证码')
], async (req, res) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({ success: false, errors: errors.array() });
}
const { email } = req.body;
const { email, captcha } = req.body;
try {
// 验证验证码
const captchaResult = verifyCaptcha(req, captcha);
if (!captchaResult.valid) {
return res.status(400).json({
success: false,
message: captchaResult.message
});
}
checkMailRateLimit(req, 'pwd_forgot');
const smtpConfig = getSmtpConfig();