mirror of
https://github.com/dqzboy/Docker-Proxy.git
synced 2026-01-12 16:25:42 +08:00
feat: Add user password reset and username modification features
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -161,3 +161,4 @@ cython_debug/
|
|||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
hubcmdui/package-lock.json
|
hubcmdui/package-lock.json
|
||||||
|
hubcmdui/data/app.db
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ app.use('/api', (req, res, next) => {
|
|||||||
'/api/config',
|
'/api/config',
|
||||||
'/api/monitoring-config',
|
'/api/monitoring-config',
|
||||||
'/api/documentation',
|
'/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或用户已登录,则继续
|
// 如果是公共API或用户已登录,则继续
|
||||||
|
|||||||
@@ -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) => {
|
router.get('/user-info', requireLogin, async (req, res) => {
|
||||||
try {
|
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('✓ 认证路由已加载');
|
logger.success('✓ 认证路由已加载');
|
||||||
|
|
||||||
// 导出路由
|
// 导出路由
|
||||||
|
|||||||
@@ -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;
|
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();
|
module.exports = new UserServiceDB();
|
||||||
|
|||||||
@@ -2963,6 +2963,24 @@
|
|||||||
</div>
|
</div>
|
||||||
</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-card">
|
||||||
<div class="user-center-section">
|
<div class="user-center-section">
|
||||||
@@ -2992,9 +3010,10 @@
|
|||||||
<div class="login-modal" id="loginModal" style="display: none;">
|
<div class="login-modal" id="loginModal" style="display: none;">
|
||||||
<div class="login-content">
|
<div class="login-content">
|
||||||
<div class="login-header">
|
<div class="login-header">
|
||||||
<h2>管理员登录</h2>
|
<h2 id="loginTitle">管理员登录</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 登录表单 -->
|
||||||
<form id="loginForm" class="login-form">
|
<form id="loginForm" class="login-form">
|
||||||
<input type="text" id="username" name="username" placeholder="用户名" required>
|
<input type="text" id="username" name="username" placeholder="用户名" required>
|
||||||
<input type="password" id="password" name="password" placeholder="密码" required>
|
<input type="password" id="password" name="password" placeholder="密码" required>
|
||||||
@@ -3004,6 +3023,44 @@
|
|||||||
<span id="captchaText" onclick="auth.refreshCaptcha()">点击刷新验证码</span>
|
<span id="captchaText" onclick="auth.refreshCaptcha()">点击刷新验证码</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" id="loginButton">登录</button>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
// 用户认证相关功能
|
// 用户认证相关功能
|
||||||
|
|
||||||
|
// 存储重置令牌
|
||||||
|
let currentResetToken = null;
|
||||||
|
|
||||||
// 登录函数
|
// 登录函数
|
||||||
async function login() {
|
async function login() {
|
||||||
const username = document.getElementById('username').value;
|
const username = document.getElementById('username').value;
|
||||||
@@ -82,10 +85,28 @@ async function refreshCaptcha() {
|
|||||||
throw new Error(`验证码获取失败: ${response.status}`);
|
throw new Error(`验证码获取失败: ${response.status}`);
|
||||||
}
|
}
|
||||||
const data = await response.json();
|
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) {
|
} catch (error) {
|
||||||
// console.error('刷新验证码失败:', 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';
|
document.getElementById('loginModal').style.display = 'flex';
|
||||||
|
showLoginForm(); // 确保显示登录表单
|
||||||
refreshCaptcha();
|
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 = {
|
const auth = {
|
||||||
init: function() {
|
init: function() {
|
||||||
// console.log('初始化认证模块...');
|
// 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,保持与其他模块一致
|
return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致
|
||||||
},
|
},
|
||||||
login,
|
login,
|
||||||
logout,
|
logout,
|
||||||
refreshCaptcha,
|
refreshCaptcha,
|
||||||
showLoginModal
|
showLoginModal,
|
||||||
|
showLoginForm,
|
||||||
|
showForgotPassword,
|
||||||
|
showResetPasswordForm,
|
||||||
|
requestResetToken,
|
||||||
|
resetPassword
|
||||||
};
|
};
|
||||||
|
|
||||||
// 全局公开认证模块
|
// 全局公开认证模块
|
||||||
|
|||||||
@@ -163,6 +163,100 @@ function isPasswordComplex(password) {
|
|||||||
return passwordRegex.test(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() {
|
function checkUcPasswordStrength() {
|
||||||
const password = document.getElementById('ucNewPassword').value;
|
const password = document.getElementById('ucNewPassword').value;
|
||||||
@@ -312,11 +406,20 @@ function loadUserStats() {
|
|||||||
const userCenter = {
|
const userCenter = {
|
||||||
init: function() {
|
init: function() {
|
||||||
// console.log('初始化用户中心模块...');
|
// console.log('初始化用户中心模块...');
|
||||||
// 可以在这里调用初始化逻辑,也可以延迟到需要时调用
|
// 初始化修改用户名表单事件
|
||||||
|
const changeUsernameForm = document.getElementById('changeUsernameForm');
|
||||||
|
if (changeUsernameForm) {
|
||||||
|
changeUsernameForm.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
changeUsername();
|
||||||
|
});
|
||||||
|
}
|
||||||
return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致
|
return Promise.resolve(); // 返回一个已解决的 Promise,保持与其他模块一致
|
||||||
},
|
},
|
||||||
getUserInfo,
|
getUserInfo,
|
||||||
changePassword,
|
changePassword,
|
||||||
|
changeUsername,
|
||||||
|
isUsernameValid,
|
||||||
checkUcPasswordStrength,
|
checkUcPasswordStrength,
|
||||||
initUserCenter,
|
initUserCenter,
|
||||||
loadUserStats,
|
loadUserStats,
|
||||||
|
|||||||
Reference in New Issue
Block a user