feat: Add user password reset and username modification features

This commit is contained in:
dqzboy
2025-12-31 12:29:38 +08:00
parent da0950b0c4
commit da2111e998
7 changed files with 620 additions and 7 deletions

View File

@@ -73,7 +73,12 @@ app.use('/api', (req, res, next) => {
'/api/config',
'/api/monitoring-config',
'/api/documentation',
'/api/documentation/file'
'/api/documentation/file',
'/api/captcha',
'/api/auth/captcha',
'/api/auth/request-reset-token',
'/api/auth/reset-password',
'/api/auth/validate-reset-token'
];
// 如果是公共API或用户已登录则继续

View File

@@ -82,6 +82,31 @@ router.post('/change-password', requireLogin, async (req, res) => {
}
});
// 修改用户名
router.post('/change-username', requireLogin, async (req, res) => {
const { newUsername, password } = req.body;
// 用户名格式校验
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
if (!usernameRegex.test(newUsername)) {
return res.status(400).json({ error: '用户名格式不正确3-20位只能包含字母、数字和下划线' });
}
try {
const currentUsername = req.session.user.username;
const result = await userServiceDB.changeUsername(currentUsername, newUsername, password);
// 更新session中的用户名
req.session.user.username = newUsername;
logger.info(`用户 ${currentUsername} 已修改用户名为 ${newUsername}`);
res.json({ success: true, newUsername });
} catch (error) {
logger.error('修改用户名失败:', error);
res.status(400).json({ error: error.message || '修改用户名失败' });
}
});
// 获取用户信息
router.get('/user-info', requireLogin, async (req, res) => {
try {
@@ -138,6 +163,79 @@ router.get('/check-session', (req, res) => {
});
});
// 请求密码重置令牌(需要用户名验证)
router.post('/request-reset-token', async (req, res) => {
const { username, captcha } = req.body;
// 验证码检查
if (req.session.captcha !== parseInt(captcha)) {
logger.warn(`重置密码验证码验证失败: ${username}`);
return res.status(401).json({ error: '验证码错误' });
}
try {
// 验证用户是否存在
const user = await userServiceDB.getUserByUsername(username);
if (!user) {
logger.warn(`密码重置请求失败,用户不存在: ${username}`);
return res.status(404).json({ error: '用户不存在' });
}
// 生成重置令牌
const token = userServiceDB.generateResetToken(username);
logger.info(`用户 ${username} 请求了密码重置令牌`);
// 返回令牌(在生产环境中,这应该通过邮件发送)
res.json({
success: true,
token,
message: '重置令牌已生成有效期10分钟',
expiresIn: '10分钟'
});
} catch (error) {
logger.error('生成重置令牌失败:', error);
res.status(500).json({ error: '生成重置令牌失败', details: error.message });
}
});
// 使用令牌重置密码
router.post('/reset-password', async (req, res) => {
const { token, newPassword, confirmPassword } = req.body;
// 验证密码是否匹配
if (newPassword !== confirmPassword) {
return res.status(400).json({ error: '两次输入的密码不一致' });
}
// 密码复杂度校验
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
if (!passwordRegex.test(newPassword)) {
return res.status(400).json({ error: '密码需要8-16位包含至少一个字母、一个数字和一个特殊字符' });
}
try {
const result = await userServiceDB.resetPasswordWithToken(token, newPassword);
logger.info(`用户 ${result.username} 通过重置令牌成功修改了密码`);
res.json({ success: true, message: '密码重置成功,请使用新密码登录' });
} catch (error) {
logger.error('重置密码失败:', error);
res.status(400).json({ error: error.message || '重置密码失败' });
}
});
// 验证重置令牌是否有效
router.post('/validate-reset-token', (req, res) => {
const { token } = req.body;
const username = userServiceDB.validateResetToken(token);
if (username) {
res.json({ valid: true, username });
} else {
res.status(400).json({ valid: false, error: '令牌无效或已过期' });
}
});
logger.success('✓ 认证路由已加载');
// 导出路由

View File

@@ -155,6 +155,57 @@ class UserServiceDB {
}
}
/**
* 修改用户名
*/
async changeUsername(currentUsername, newUsername, password) {
try {
const user = await this.getUserByUsername(currentUsername);
if (!user) {
throw new Error('用户不存在');
}
// 验证密码
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) {
throw new Error('密码不正确');
}
// 验证新用户名格式
if (!this.isUsernameValid(newUsername)) {
throw new Error('用户名格式不正确3-20位只能包含字母、数字和下划线');
}
// 检查新用户名是否已存在
const existingUser = await this.getUserByUsername(newUsername);
if (existingUser) {
throw new Error('该用户名已被使用');
}
// 更新用户名
await database.run(
'UPDATE users SET username = ?, updated_at = ? WHERE username = ?',
[newUsername, new Date().toISOString(), currentUsername]
);
logger.info(`用户 ${currentUsername} 已成功修改用户名为 ${newUsername}`);
return { success: true, newUsername };
} catch (error) {
logger.error('修改用户名失败:', error);
throw error;
}
}
/**
* 验证用户名格式
*/
isUsernameValid(username) {
// 3-20位只能包含字母、数字和下划线
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
return usernameRegex.test(username);
}
/**
* 验证密码复杂度
*/
@@ -185,6 +236,132 @@ class UserServiceDB {
throw error;
}
}
/**
* 生成密码重置令牌
* 返回一个临时令牌存储在内存中有效期10分钟
*/
generateResetToken(username) {
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
const expiry = Date.now() + 10 * 60 * 1000; // 10分钟有效期
// 存储在内存中(简单实现)
if (!this.resetTokens) {
this.resetTokens = {};
}
this.resetTokens[token] = {
username,
expiry
};
// 清理过期令牌
this.cleanExpiredTokens();
logger.info(`为用户 ${username} 生成了密码重置令牌`);
return token;
}
/**
* 清理过期的重置令牌
*/
cleanExpiredTokens() {
if (!this.resetTokens) return;
const now = Date.now();
for (const token in this.resetTokens) {
if (this.resetTokens[token].expiry < now) {
delete this.resetTokens[token];
}
}
}
/**
* 验证重置令牌
*/
validateResetToken(token) {
if (!this.resetTokens || !this.resetTokens[token]) {
return null;
}
const tokenData = this.resetTokens[token];
if (tokenData.expiry < Date.now()) {
delete this.resetTokens[token];
return null;
}
return tokenData.username;
}
/**
* 使用令牌重置密码
*/
async resetPasswordWithToken(token, newPassword) {
try {
const username = this.validateResetToken(token);
if (!username) {
throw new Error('重置令牌无效或已过期');
}
// 验证新密码复杂度
if (!this.isPasswordComplex(newPassword)) {
throw new Error('新密码不符合复杂度要求需要8-16位包含字母、数字和特殊字符');
}
const user = await this.getUserByUsername(username);
if (!user) {
throw new Error('用户不存在');
}
// 更新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
await database.run(
'UPDATE users SET password = ?, updated_at = ? WHERE username = ?',
[hashedNewPassword, new Date().toISOString(), username]
);
// 删除使用过的令牌
delete this.resetTokens[token];
logger.info(`用户 ${username} 密码已通过重置令牌成功修改`);
return { success: true, username };
} catch (error) {
logger.error('重置密码失败:', error);
throw error;
}
}
/**
* 直接重置用户密码(管理员功能,无需旧密码)
* 用于忘记密码时,通过验证用户名来重置
*/
async forceResetPassword(username, newPassword) {
try {
const user = await this.getUserByUsername(username);
if (!user) {
throw new Error('用户不存在');
}
// 验证新密码复杂度
if (!this.isPasswordComplex(newPassword)) {
throw new Error('新密码不符合复杂度要求需要8-16位包含字母、数字和特殊字符');
}
// 更新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
await database.run(
'UPDATE users SET password = ?, updated_at = ? WHERE username = ?',
[hashedNewPassword, new Date().toISOString(), username]
);
logger.info(`用户 ${username} 密码已被强制重置`);
return { success: true };
} catch (error) {
logger.error('强制重置密码失败:', error);
throw error;
}
}
}
module.exports = new UserServiceDB();

View File

@@ -2963,6 +2963,24 @@
</div>
</div>
<!-- 修改用户名卡片 -->
<div class="user-center-card">
<div class="user-center-section">
<h2 class="user-center-section-title">修改用户名</h2>
<form id="changeUsernameForm">
<label for="ucNewUsername">新用户名</label>
<span class="password-hint">用户名需要3-20位只能包含字母、数字和下划线</span>
<input type="text" id="ucNewUsername" name="newUsername" placeholder="请输入新用户名">
<label for="ucUsernamePassword">当前密码</label>
<span class="password-hint">修改用户名需要验证当前密码</span>
<input type="password" id="ucUsernamePassword" name="password" placeholder="请输入当前密码">
<div style="display: flex; align-items: center; margin-top: 10px;">
<button type="submit" class="btn btn-primary">修改用户名</button>
</div>
</form>
</div>
</div>
<!-- 密码修改卡片 -->
<div class="user-center-card">
<div class="user-center-section">
@@ -2992,9 +3010,10 @@
<div class="login-modal" id="loginModal" style="display: none;">
<div class="login-content">
<div class="login-header">
<h2>管理员登录</h2>
<h2 id="loginTitle">管理员登录</h2>
</div>
<!-- 登录表单 -->
<form id="loginForm" class="login-form">
<input type="text" id="username" name="username" placeholder="用户名" required>
<input type="password" id="password" name="password" placeholder="密码" required>
@@ -3004,6 +3023,44 @@
<span id="captchaText" onclick="auth.refreshCaptcha()">点击刷新验证码</span>
</div>
<button type="submit" id="loginButton">登录</button>
<div class="forgot-password-link" style="text-align: center; margin-top: 15px;">
<a href="javascript:void(0)" onclick="auth.showForgotPassword()" style="color: #3d7cf4; text-decoration: none; font-size: 14px;">忘记密码?</a>
</div>
</form>
<!-- 忘记密码表单 - 步骤1: 验证用户名 -->
<form id="forgotPasswordForm" class="login-form" style="display: none;">
<div class="form-description" style="text-align: center; margin-bottom: 15px; color: #666; font-size: 14px;">
请输入您的用户名来获取密码重置令牌
</div>
<input type="text" id="resetUsername" name="resetUsername" placeholder="用户名" required>
<div class="captcha-container">
<input type="text" id="resetCaptcha" name="resetCaptcha" placeholder="验证码" required>
<span id="resetCaptchaText" onclick="auth.refreshCaptcha()">点击刷新验证码</span>
</div>
<button type="submit" id="getTokenButton">获取重置令牌</button>
<div style="text-align: center; margin-top: 15px;">
<a href="javascript:void(0)" onclick="auth.showLoginForm()" style="color: #3d7cf4; text-decoration: none; font-size: 14px;">← 返回登录</a>
</div>
</form>
<!-- 重置密码表单 - 步骤2: 设置新密码 -->
<form id="resetPasswordForm" class="login-form" style="display: none;">
<div class="form-description" style="text-align: center; margin-bottom: 15px; color: #666; font-size: 14px;">
请设置您的新密码
</div>
<div id="resetTokenDisplay" style="background: #f0f8ff; padding: 10px; border-radius: 5px; margin-bottom: 15px; word-break: break-all; font-size: 12px; display: none;">
<strong>重置令牌:</strong><span id="tokenValue"></span>
</div>
<input type="password" id="resetNewPassword" name="resetNewPassword" placeholder="新密码8-16位含字母、数字和特殊字符" required>
<input type="password" id="resetConfirmPassword" name="resetConfirmPassword" placeholder="确认新密码" required>
<div class="password-requirements" style="font-size: 12px; color: #888; margin-bottom: 15px;">
密码要求8-16位包含字母、数字和特殊字符
</div>
<button type="submit" id="resetPasswordButton">重置密码</button>
<div style="text-align: center; margin-top: 15px;">
<a href="javascript:void(0)" onclick="auth.showLoginForm()" style="color: #3d7cf4; text-decoration: none; font-size: 14px;">← 返回登录</a>
</div>
</form>
</div>
</div>

View File

@@ -1,5 +1,8 @@
// 用户认证相关功能
// 存储重置令牌
let currentResetToken = null;
// 登录函数
async function login() {
const username = document.getElementById('username').value;
@@ -82,10 +85,28 @@ async function refreshCaptcha() {
throw new Error(`验证码获取失败: ${response.status}`);
}
const data = await response.json();
document.getElementById('captchaText').textContent = data.captcha;
// 更新登录表单验证码
const captchaText = document.getElementById('captchaText');
if (captchaText) {
captchaText.textContent = data.captcha;
}
// 更新忘记密码表单验证码
const resetCaptchaText = document.getElementById('resetCaptchaText');
if (resetCaptchaText) {
resetCaptchaText.textContent = data.captcha;
}
} catch (error) {
// console.error('刷新验证码失败:', error);
document.getElementById('captchaText').textContent = '验证码加载失败,点击重试';
const captchaText = document.getElementById('captchaText');
if (captchaText) {
captchaText.textContent = '验证码加载失败,点击重试';
}
const resetCaptchaText = document.getElementById('resetCaptchaText');
if (resetCaptchaText) {
resetCaptchaText.textContent = '验证码加载失败,点击重试';
}
}
}
@@ -104,20 +125,171 @@ function showLoginModal() {
}
document.getElementById('loginModal').style.display = 'flex';
showLoginForm(); // 确保显示登录表单
refreshCaptcha();
}
// 显示登录表单
function showLoginForm() {
document.getElementById('loginTitle').textContent = '管理员登录';
document.getElementById('loginForm').style.display = 'block';
document.getElementById('forgotPasswordForm').style.display = 'none';
document.getElementById('resetPasswordForm').style.display = 'none';
currentResetToken = null;
refreshCaptcha();
}
// 显示忘记密码表单
function showForgotPassword() {
document.getElementById('loginTitle').textContent = '忘记密码';
document.getElementById('loginForm').style.display = 'none';
document.getElementById('forgotPasswordForm').style.display = 'block';
document.getElementById('resetPasswordForm').style.display = 'none';
refreshCaptcha();
}
// 显示重置密码表单
function showResetPasswordForm(token) {
document.getElementById('loginTitle').textContent = '重置密码';
document.getElementById('loginForm').style.display = 'none';
document.getElementById('forgotPasswordForm').style.display = 'none';
document.getElementById('resetPasswordForm').style.display = 'block';
if (token) {
currentResetToken = token;
document.getElementById('tokenValue').textContent = token;
document.getElementById('resetTokenDisplay').style.display = 'block';
}
}
// 请求重置令牌
async function requestResetToken() {
const username = document.getElementById('resetUsername').value;
const captcha = document.getElementById('resetCaptcha').value;
if (!username) {
core.showAlert('请输入用户名', 'error');
return;
}
if (!captcha) {
core.showAlert('请输入验证码', 'error');
return;
}
try {
core.showLoading();
const response = await fetch('/api/auth/request-reset-token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, captcha })
});
const data = await response.json();
if (response.ok) {
core.showAlert('重置令牌已生成有效期10分钟', 'success');
showResetPasswordForm(data.token);
} else {
core.showAlert(data.error || '获取重置令牌失败', 'error');
refreshCaptcha();
}
} catch (error) {
core.showAlert('获取重置令牌失败: ' + error.message, 'error');
refreshCaptcha();
} finally {
core.hideLoading();
}
}
// 重置密码
async function resetPassword() {
const newPassword = document.getElementById('resetNewPassword').value;
const confirmPassword = document.getElementById('resetConfirmPassword').value;
if (!newPassword || !confirmPassword) {
core.showAlert('请填写所有密码字段', 'error');
return;
}
if (newPassword !== confirmPassword) {
core.showAlert('两次输入的密码不一致', 'error');
return;
}
// 密码复杂度验证
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
if (!passwordRegex.test(newPassword)) {
core.showAlert('密码需要8-16位包含至少一个字母、一个数字和一个特殊字符', 'error');
return;
}
if (!currentResetToken) {
core.showAlert('重置令牌无效,请重新获取', 'error');
showForgotPassword();
return;
}
try {
core.showLoading();
const response = await fetch('/api/auth/reset-password', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
token: currentResetToken,
newPassword,
confirmPassword
})
});
const data = await response.json();
if (response.ok) {
core.showAlert('密码重置成功!请使用新密码登录', 'success');
currentResetToken = null;
showLoginForm();
} else {
core.showAlert(data.error || '重置密码失败', 'error');
}
} catch (error) {
core.showAlert('重置密码失败: ' + error.message, 'error');
} finally {
core.hideLoading();
}
}
// 导出模块
const auth = {
init: function() {
// console.log('初始化认证模块...');
// 在这里可以添加认证模块初始化的相关代码
// 初始化忘记密码表单事件
const forgotPasswordForm = document.getElementById('forgotPasswordForm');
if (forgotPasswordForm) {
forgotPasswordForm.addEventListener('submit', function(e) {
e.preventDefault();
requestResetToken();
});
}
const resetPasswordForm = document.getElementById('resetPasswordForm');
if (resetPasswordForm) {
resetPasswordForm.addEventListener('submit', function(e) {
e.preventDefault();
resetPassword();
});
}
return Promise.resolve(); // 返回一个已解决的 Promise保持与其他模块一致
},
login,
logout,
refreshCaptcha,
showLoginModal
showLoginModal,
showLoginForm,
showForgotPassword,
showResetPasswordForm,
requestResetToken,
resetPassword
};
// 全局公开认证模块

View File

@@ -163,6 +163,100 @@ function isPasswordComplex(password) {
return passwordRegex.test(password);
}
// 验证用户名格式
function isUsernameValid(username) {
// 3-20位只能包含字母、数字和下划线
const usernameRegex = /^[a-zA-Z0-9_]{3,20}$/;
return usernameRegex.test(username);
}
// 修改用户名
async function changeUsername(event) {
if (event) {
event.preventDefault();
}
const form = document.getElementById('changeUsernameForm');
const newUsername = form.querySelector('#ucNewUsername').value;
const password = form.querySelector('#ucUsernamePassword').value;
// 验证表单
if (!newUsername || !password) {
return core.showAlert('所有字段都不能为空', 'error');
}
// 用户名格式检查
if (!isUsernameValid(newUsername)) {
return core.showAlert('用户名格式不正确3-20位只能包含字母、数字和下划线', 'error');
}
// 显示加载状态
const submitButton = form.querySelector('button[type="submit"]');
const originalButtonText = submitButton.innerHTML;
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin"></i> 提交中...';
try {
const response = await fetch('/api/auth/change-username', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
newUsername,
password
})
});
// 无论成功与否,去除加载状态
submitButton.disabled = false;
submitButton.innerHTML = originalButtonText;
if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || '修改用户名失败');
}
const data = await response.json();
// 清空表单
form.reset();
// 设置倒计时并显示
let countDown = 5;
Swal.fire({
title: '用户名修改成功',
html: `您的用户名已成功修改为 <b>${data.newUsername}</b>,系统将在 <b>${countDown}</b> 秒后自动退出,请使用新用户名重新登录。`,
icon: 'success',
timer: countDown * 1000,
timerProgressBar: true,
didOpen: () => {
const content = Swal.getHtmlContainer();
const timerInterval = setInterval(() => {
countDown--;
if (content) {
const b = content.querySelectorAll('b')[1]; // 获取第二个b标签倒计时
if (b) {
b.textContent = countDown > 0 ? countDown : 0;
}
}
if (countDown <= 0) clearInterval(timerInterval);
}, 1000);
},
allowOutsideClick: false,
showConfirmButton: true,
confirmButtonText: '确定'
}).then((result) => {
if (result.dismiss === Swal.DismissReason.timer || result.isConfirmed) {
auth.logout();
}
});
} catch (error) {
core.showAlert('修改用户名失败: ' + error.message, 'error');
}
}
// 检查密码强度
function checkUcPasswordStrength() {
const password = document.getElementById('ucNewPassword').value;
@@ -312,11 +406,20 @@ function loadUserStats() {
const userCenter = {
init: function() {
// console.log('初始化用户中心模块...');
// 可以在这里调用初始化逻辑,也可以延迟到需要时调用
// 初始化修改用户名表单事件
const changeUsernameForm = document.getElementById('changeUsernameForm');
if (changeUsernameForm) {
changeUsernameForm.addEventListener('submit', function(e) {
e.preventDefault();
changeUsername();
});
}
return Promise.resolve(); // 返回一个已解决的 Promise保持与其他模块一致
},
getUserInfo,
changePassword,
changeUsername,
isUsernameValid,
checkUcPasswordStrength,
initUserCenter,
loadUserStats,