✉️ 添加完整的邮件系统功能
后端新增功能: - 集成 nodemailer 支持 SMTP 邮件发送 - 新增邮箱验证系统(VerificationDB) - 新增密码重置令牌系统(PasswordResetTokenDB) - 实现邮件发送限流(30分钟3次,全天10次) - 添加 SMTP 配置管理接口 - 支持邮箱激活和密码重置邮件发送 前端新增功能: - 注册时邮箱必填并需验证 - 邮箱验证激活流程 - 重发激活邮件功能 - 基于邮箱的密码重置流程(替代管理员审核) - 管理后台 SMTP 配置界面 - SMTP 测试邮件发送功能 安全改进: - 邮件发送防刷限流保护 - 验证令牌随机生成(48字节) - 重置链接有效期限制 - 支持 SSL/TLS 加密传输 支持的邮箱服务:QQ邮箱、163邮箱、企业邮箱等主流SMTP服务 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
184
frontend/app.js
184
frontend/app.js
@@ -127,13 +127,28 @@ createApp({
|
||||
// 忘记密码
|
||||
showForgotPasswordModal: false,
|
||||
forgotPasswordForm: {
|
||||
username: '',
|
||||
email: ''
|
||||
},
|
||||
showResetPasswordModal: false,
|
||||
resetPasswordForm: {
|
||||
token: '',
|
||||
new_password: ''
|
||||
},
|
||||
showResendVerify: false,
|
||||
resendVerifyEmail: '',
|
||||
|
||||
// 系统设置
|
||||
systemSettings: {
|
||||
maxUploadSizeMB: 100
|
||||
maxUploadSizeMB: 100,
|
||||
smtp: {
|
||||
host: '',
|
||||
port: 465,
|
||||
secure: true,
|
||||
user: '',
|
||||
from: '',
|
||||
password: '',
|
||||
has_password: false
|
||||
}
|
||||
},
|
||||
|
||||
// Toast通知
|
||||
@@ -325,6 +340,8 @@ handleDragLeave(e) {
|
||||
this.token = response.data.token;
|
||||
this.user = response.data.user;
|
||||
this.isLoggedIn = true;
|
||||
this.showResendVerify = false;
|
||||
this.resendVerifyEmail = '';
|
||||
|
||||
// 登录成功后隐藏验证码并清空验证码输入
|
||||
this.showCaptcha = false;
|
||||
@@ -393,6 +410,15 @@ handleDragLeave(e) {
|
||||
this.showCaptcha = true;
|
||||
this.refreshCaptcha();
|
||||
}
|
||||
|
||||
// 邮箱未验证提示
|
||||
if (error.response?.data?.needVerify) {
|
||||
this.showResendVerify = true;
|
||||
this.resendVerifyEmail = error.response?.data?.email || this.loginForm.username || '';
|
||||
} else {
|
||||
this.showResendVerify = false;
|
||||
this.resendVerifyEmail = '';
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -401,6 +427,44 @@ handleDragLeave(e) {
|
||||
this.captchaUrl = `${this.apiBase}/api/captcha?t=${Date.now()}`;
|
||||
},
|
||||
|
||||
async resendVerification() {
|
||||
if (!this.resendVerifyEmail) {
|
||||
this.showToast('error', '错误', '请输入邮箱或用户名后再重试');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = {};
|
||||
if (this.resendVerifyEmail.includes('@')) {
|
||||
payload.email = this.resendVerifyEmail;
|
||||
} else {
|
||||
payload.username = this.resendVerifyEmail;
|
||||
}
|
||||
const response = await axios.post(`${this.apiBase}/api/resend-verification`, payload);
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '验证邮件已发送,请查收');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重发验证邮件失败:', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '发送失败');
|
||||
}
|
||||
},
|
||||
|
||||
async handleVerifyToken(token) {
|
||||
try {
|
||||
const response = await axios.get(`${this.apiBase}/api/verify-email`, { params: { token } });
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '邮箱验证成功,请登录');
|
||||
// 清理URL
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('verifyToken');
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('邮箱验证失败:', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '验证失败');
|
||||
}
|
||||
},
|
||||
|
||||
async handleRegister() {
|
||||
this.errorMessage = '';
|
||||
this.successMessage = '';
|
||||
@@ -409,7 +473,7 @@ handleDragLeave(e) {
|
||||
const response = await axios.post(`${this.apiBase}/api/register`, this.registerForm);
|
||||
|
||||
if (response.data.success) {
|
||||
this.successMessage = '注册成功!请登录';
|
||||
this.successMessage = '注册成功!请查收邮箱完成验证后再登录';
|
||||
this.isLogin = true;
|
||||
|
||||
// 清空表单
|
||||
@@ -696,6 +760,8 @@ handleDragLeave(e) {
|
||||
this.token = null;
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
this.showResendVerify = false;
|
||||
this.resendVerifyEmail = '';
|
||||
|
||||
// 停止定期检查
|
||||
this.stopProfileSync();
|
||||
@@ -1579,25 +1645,21 @@ handleDragLeave(e) {
|
||||
// ===== 忘记密码功能 =====
|
||||
|
||||
async requestPasswordReset() {
|
||||
if (!this.forgotPasswordForm.username) {
|
||||
this.showToast('error', '错误', '请输入用户名');
|
||||
return;
|
||||
}
|
||||
if (!this.forgotPasswordForm.new_password || this.forgotPasswordForm.new_password.length < 6) {
|
||||
this.showToast('error', '错误', '新密码至少6个字符');
|
||||
if (!this.forgotPasswordForm.email) {
|
||||
this.showToast('error', '错误', '请输入注册邮箱');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/password-reset/request`,
|
||||
`${this.apiBase}/api/password/forgot`,
|
||||
this.forgotPasswordForm
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '密码重置请求已提交,请等待管理员审核');
|
||||
this.showToast('success', '成功', '如果邮箱存在,将收到重置邮件');
|
||||
this.showForgotPasswordModal = false;
|
||||
this.forgotPasswordForm = { username: '', new_password: '' };
|
||||
this.forgotPasswordForm = { email: '' };
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('提交密码重置请求失败:', error);
|
||||
@@ -1605,6 +1667,28 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async submitResetPassword() {
|
||||
if (!this.resetPasswordForm.token || !this.resetPasswordForm.new_password || this.resetPasswordForm.new_password.length < 6) {
|
||||
this.showToast('error', '错误', '请输入有效的重置链接和新密码(至少6位)');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await axios.post(`${this.apiBase}/api/password/reset`, this.resetPasswordForm);
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '密码已重置,请登录');
|
||||
this.showResetPasswordModal = false;
|
||||
this.resetPasswordForm = { token: '', new_password: '' };
|
||||
// 清理URL中的token
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete('resetToken');
|
||||
window.history.replaceState({}, document.title, url.toString());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('密码重置失败:', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '重置失败');
|
||||
}
|
||||
},
|
||||
|
||||
// ===== 管理员:密码重置审核 =====
|
||||
|
||||
async loadPasswordResetRequests() {
|
||||
@@ -1978,6 +2062,15 @@ handleDragLeave(e) {
|
||||
if (response.data.success) {
|
||||
const settings = response.data.settings;
|
||||
this.systemSettings.maxUploadSizeMB = Math.round(settings.max_upload_size / (1024 * 1024));
|
||||
if (settings.smtp) {
|
||||
this.systemSettings.smtp.host = settings.smtp.host || '';
|
||||
this.systemSettings.smtp.port = settings.smtp.port || 465;
|
||||
this.systemSettings.smtp.secure = !!settings.smtp.secure;
|
||||
this.systemSettings.smtp.user = settings.smtp.user || '';
|
||||
this.systemSettings.smtp.from = settings.smtp.from || settings.smtp.user || '';
|
||||
this.systemSettings.smtp.has_password = !!settings.smtp.has_password;
|
||||
this.systemSettings.smtp.password = '';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载系统设置失败:', error);
|
||||
@@ -2000,25 +2093,54 @@ handleDragLeave(e) {
|
||||
}
|
||||
},
|
||||
|
||||
async updateSystemSettings() {
|
||||
try {
|
||||
const maxUploadSize = parseInt(this.systemSettings.maxUploadSizeMB) * 1024 * 1024;
|
||||
async updateSystemSettings() {
|
||||
try {
|
||||
const maxUploadSize = parseInt(this.systemSettings.maxUploadSizeMB) * 1024 * 1024;
|
||||
|
||||
const payload = {
|
||||
max_upload_size: maxUploadSize,
|
||||
smtp: {
|
||||
host: this.systemSettings.smtp.host,
|
||||
port: this.systemSettings.smtp.port,
|
||||
secure: this.systemSettings.smtp.secure,
|
||||
user: this.systemSettings.smtp.user,
|
||||
from: this.systemSettings.smtp.from || this.systemSettings.smtp.user
|
||||
}
|
||||
};
|
||||
if (this.systemSettings.smtp.password) {
|
||||
payload.smtp.password = this.systemSettings.smtp.password;
|
||||
}
|
||||
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/admin/settings`,
|
||||
payload,
|
||||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '系统设置已更新');
|
||||
this.systemSettings.smtp.password = '';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新系统设置失败:', error);
|
||||
this.showToast('error', '错误', '更新系统设置失败');
|
||||
}
|
||||
}
|
||||
,
|
||||
|
||||
async testSmtp() {
|
||||
try {
|
||||
const response = await axios.post(
|
||||
`${this.apiBase}/api/admin/settings`,
|
||||
{ max_upload_size: maxUploadSize },
|
||||
`${this.apiBase}/api/admin/settings/test-smtp`,
|
||||
{ to: this.systemSettings.smtp.user },
|
||||
{ headers: { Authorization: `Bearer ${this.token}` } }
|
||||
);
|
||||
|
||||
if (response.data.success) {
|
||||
this.showToast('success', '成功', '系统设置已更新');
|
||||
}
|
||||
this.showToast('success', '成功', response.data.message || '测试邮件已发送');
|
||||
} catch (error) {
|
||||
console.error('更新系统设置失败:', error);
|
||||
this.showToast('error', '错误', '更新系统设置失败');
|
||||
console.error('测试SMTP失败:', error);
|
||||
this.showToast('error', '错误', error.response?.data?.message || '测试失败');
|
||||
}
|
||||
}
|
||||
,
|
||||
},
|
||||
|
||||
// ===== 上传工具管理 =====
|
||||
|
||||
@@ -2137,6 +2259,18 @@ handleDragLeave(e) {
|
||||
// 初始化调试模式状态
|
||||
this.debugMode = localStorage.getItem('debugMode') === 'true';
|
||||
|
||||
// 处理URL中的验证/重置token
|
||||
const url = new URL(window.location.href);
|
||||
const verifyToken = url.searchParams.get('verifyToken');
|
||||
const resetToken = url.searchParams.get('resetToken');
|
||||
if (verifyToken) {
|
||||
this.handleVerifyToken(verifyToken);
|
||||
}
|
||||
if (resetToken) {
|
||||
this.resetPasswordForm.token = resetToken;
|
||||
this.showResetPasswordModal = true;
|
||||
}
|
||||
|
||||
// 阻止全局拖拽默认行为(防止拖到区域外打开新页面)
|
||||
window.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
Reference in New Issue
Block a user