diff --git a/.gitignore b/.gitignore index 59fb9cc..95ea24f 100644 --- a/.gitignore +++ b/.gitignore @@ -161,3 +161,4 @@ cython_debug/ node_modules .DS_Store hubcmdui/package-lock.json +hubcmdui/data/app.db diff --git a/hubcmdui/app.js b/hubcmdui/app.js index 17a7dff..8119722 100644 --- a/hubcmdui/app.js +++ b/hubcmdui/app.js @@ -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或用户已登录,则继续 diff --git a/hubcmdui/routes/auth.js b/hubcmdui/routes/auth.js index 8d013b4..71d42fc 100644 --- a/hubcmdui/routes/auth.js +++ b/hubcmdui/routes/auth.js @@ -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('✓ 认证路由已加载'); // 导出路由 diff --git a/hubcmdui/services/userServiceDB.js b/hubcmdui/services/userServiceDB.js index e5b1f95..ec5956e 100644 --- a/hubcmdui/services/userServiceDB.js +++ b/hubcmdui/services/userServiceDB.js @@ -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(); diff --git a/hubcmdui/web/admin.html b/hubcmdui/web/admin.html index e1e7b51..6ff22ab 100644 --- a/hubcmdui/web/admin.html +++ b/hubcmdui/web/admin.html @@ -2963,6 +2963,24 @@ + +
+
+

修改用户名

+
+ + 用户名需要3-20位,只能包含字母、数字和下划线 + + + 修改用户名需要验证当前密码 + +
+ +
+
+
+
+
@@ -2992,9 +3010,10 @@
diff --git a/hubcmdui/web/js/auth.js b/hubcmdui/web/js/auth.js index 4b44e27..186ad30 100644 --- a/hubcmdui/web/js/auth.js +++ b/hubcmdui/web/js/auth.js @@ -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 }; // 全局公开认证模块 diff --git a/hubcmdui/web/js/userCenter.js b/hubcmdui/web/js/userCenter.js index 8a8d18a..d7443a9 100644 --- a/hubcmdui/web/js/userCenter.js +++ b/hubcmdui/web/js/userCenter.js @@ -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 = ' 提交中...'; + + 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: `您的用户名已成功修改为 ${data.newUsername},系统将在 ${countDown} 秒后自动退出,请使用新用户名重新登录。`, + 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,