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 @@
+
+
+
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,