From 22018856cc860d624f7c3ff5e9dc57ae80201a57 Mon Sep 17 00:00:00 2001 From: engigu Date: Thu, 12 Mar 2026 10:40:25 +0800 Subject: [PATCH] feat: add account reset --- cmd/resetpwd/resetpwd.go | 46 +++-- internal/controllers/auth_controller.go | 10 +- internal/controllers/settings_controller.go | 55 +++++- internal/services/user_service.go | 27 +++ web/src/api/index.ts | 4 +- web/src/views/settings/PasswordSettings.vue | 191 ++++++++++++++++---- 6 files changed, 266 insertions(+), 67 deletions(-) diff --git a/cmd/resetpwd/resetpwd.go b/cmd/resetpwd/resetpwd.go index 6d147db..d16230e 100644 --- a/cmd/resetpwd/resetpwd.go +++ b/cmd/resetpwd/resetpwd.go @@ -12,7 +12,24 @@ import ( ) func Run(args []string) { - fmt.Print("此操作将重置 admin 用户的密码,是否继续? (y/N): ") + // 基础环境初始化 + bootstrap.InitBasic() + settingsService := services.NewSettingsService() + if err := settingsService.InitSettings(); err != nil { + fmt.Printf("初始化系统设置失败: %v\n", err) + return + } + userService := services.NewUserService() + + var username string + + if len(args) >= 1 { + username = args[0] + } else { + username = "admin" + } + + fmt.Printf("此操作将重置用户 [%s] 的密码,是否继续? (y/N): ", username) reader := bufio.NewReader(os.Stdin) answer, _ := reader.ReadString('\n') answer = strings.TrimSpace(strings.ToLower(answer)) @@ -22,22 +39,7 @@ func Run(args []string) { return } - // 必须初始化环境与数据库才能修改密码 - bootstrap.InitBasic() - settingsService := services.NewSettingsService() - if err := settingsService.InitSettings(); err != nil { - fmt.Printf("初始化系统设置失败: %v\n", err) - return - } - - userService := services.NewUserService() - adminUser := userService.GetUserByUsername("admin") - if adminUser == nil { - fmt.Println("找不到 admin 用户。") - return - } - - fmt.Print("请输入 admin 用户的新密码 (留空则自动随机生成): ") + fmt.Printf("请输入用户 [%s] 的新密码 (留空则自动随机生成): ", username) inputPwd, _ := reader.ReadString('\n') newPassword := strings.TrimSpace(inputPwd) if newPassword == "" { @@ -45,14 +47,20 @@ func Run(args []string) { fmt.Println("未输入密码,系统已自动生成。") } - err := userService.UpdatePassword(adminUser.ID, newPassword) + user := userService.GetUserByUsername(username) + if user == nil { + fmt.Printf("找不到用户 [%s]\n", username) + return + } + + err := userService.UpdatePassword(user.ID, newPassword) if err != nil { fmt.Printf("重置密码失败: %v\n", err) return } fmt.Println("--------------------------------------------------") - fmt.Println("admin 用户密码已重置成功:") + fmt.Printf("用户 [%s] 密码已重置成功:\n", username) fmt.Printf("新密码: %s\n", newPassword) fmt.Println("请妥善保管您的新密码,并登录后及时修改。") fmt.Println("--------------------------------------------------") diff --git a/internal/controllers/auth_controller.go b/internal/controllers/auth_controller.go index 096d8c2..55d7d1a 100644 --- a/internal/controllers/auth_controller.go +++ b/internal/controllers/auth_controller.go @@ -168,13 +168,15 @@ func (ac *AuthController) Logout(c *gin.Context) { } func (ac *AuthController) GetCurrentUser(c *gin.Context) { - username, exists := c.Get("username") - if !exists { - utils.Unauthorized(c, "未登录") + userID := c.GetString("userID") + var user models.User + if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { + utils.Unauthorized(c, "会话无效") return } utils.Success(c, gin.H{ - "username": username, + "username": user.Username, + "role": user.Role, }) } diff --git a/internal/controllers/settings_controller.go b/internal/controllers/settings_controller.go index 3a3ee29..c8c023e 100644 --- a/internal/controllers/settings_controller.go +++ b/internal/controllers/settings_controller.go @@ -42,17 +42,19 @@ func NewSettingsController(userService *services.UserService, loginLogService *s } } -// ChangePassword 修改密码 +// ChangePassword 修改密码及账号信息 func (sc *SettingsController) ChangePassword(c *gin.Context) { - // 演示模式下禁止修改密码 + // 演示模式下禁止修改 if constant.DemoMode { - utils.BadRequest(c, "演示模式下不能修改密码") + utils.BadRequest(c, "演示模式下不能修改账号或密码") return } var req struct { + OldUsername string `json:"old_username"` + Username string `json:"username"` OldPassword string `json:"old_password" binding:"required"` - NewPassword string `json:"new_password" binding:"required,min=6"` + NewPassword string `json:"new_password"` } if err := c.ShouldBindJSON(&req); err != nil { @@ -67,13 +69,46 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) { return } - if !sc.userService.ValidatePassword(user, req.OldPassword) { + // 统一校验原账密 + if req.OldUsername != "" && req.OldUsername != user.Username { + utils.BadRequest(c, "原账号不正确") + return + } + + if !sc.userService.AuthenticateUser(user.Username, req.OldPassword) { utils.BadRequest(c, "原密码错误") return } - if err := sc.userService.UpdatePassword(user.ID, req.NewPassword); err != nil { - utils.ServerError(c, "修改密码失败") + var updated bool + var logoutRequired bool + + // 1. 处理用户名修改 + if req.Username != "" && req.Username != user.Username { + if err := sc.userService.UpdateAccount(user.ID, req.Username); err != nil { + utils.BadRequest(c, err.Error()) + return + } + updated = true + logoutRequired = true + } + + // 2. 处理密码修改 + if req.NewPassword != "" { + if len(req.NewPassword) < 6 { + utils.BadRequest(c, "新密码至少6位") + return + } + if err := sc.userService.UpdatePassword(user.ID, req.NewPassword); err != nil { + utils.ServerError(c, "修改密码失败") + return + } + updated = true + logoutRequired = true + } + + if !updated { + utils.SuccessMsg(c, "未检测到变更内容") return } @@ -84,7 +119,11 @@ func (sc *SettingsController) ChangePassword(c *gin.Context) { }, }) - utils.SuccessMsg(c, "密码修改成功") + msg := "保存成功" + if logoutRequired { + msg += ",请重新登录" + } + utils.SuccessMsg(c, msg) } // CleanLogs 清理日志 - 已移除,改为任务级别的日志清理配置 diff --git a/internal/services/user_service.go b/internal/services/user_service.go index a7a8e56..675f42b 100644 --- a/internal/services/user_service.go +++ b/internal/services/user_service.go @@ -3,6 +3,7 @@ package services import ( "crypto/sha256" "encoding/hex" + "fmt" "strings" "golang.org/x/crypto/bcrypt" @@ -110,3 +111,29 @@ func (us *UserService) UpdatePassword(userID string, newPassword string) error { func (us *UserService) InvalidateUserTokens(userID string) error { return database.DB.Model(&models.User{}).Where("id = ?", userID).Update("token_version", gorm.Expr("token_version + 1")).Error } + +func (us *UserService) UpdateAccount(userID string, newUsername string) error { + var user models.User + if err := database.DB.Where("id = ?", userID).First(&user).Error; err != nil { + return err + } + + updates := make(map[string]interface{}) + if newUsername != "" && newUsername != user.Username { + // 检查用户名是否已存在 + var count int64 + database.DB.Model(&models.User{}).Where("username = ? AND id <> ?", newUsername, userID).Count(&count) + if count > 0 { + return fmt.Errorf("用户名 [%s] 已被占用", newUsername) + } + updates["username"] = newUsername + // 用户名变更,必须失效所有 Token,因为 Token 中包含 Username 且中间件会校验 + updates["token_version"] = gorm.Expr("token_version + 1") + } + + if len(updates) == 0 { + return nil + } + + return database.DB.Model(&user).Updates(updates).Error +} diff --git a/web/src/api/index.ts b/web/src/api/index.ts index 45dd918..09900f3 100644 --- a/web/src/api/index.ts +++ b/web/src/api/index.ts @@ -53,7 +53,7 @@ export const api = { login: (data: { username: string; password: string }) => request<{ user: string }>('/auth/login', { method: 'POST', body: JSON.stringify(data) }), logout: () => request('/auth/logout', { method: 'POST' }), - me: () => request<{ username: string }>('/auth/me'), + me: () => request<{ username: string; role: string }>('/auth/me'), register: (data: { username: string; password: string; email: string }) => request('/auth/register', { method: 'POST', body: JSON.stringify(data) }) }, @@ -126,7 +126,7 @@ export const api = { taskStats: (days?: number) => request(`/taskstats${days ? `?days=${days}` : ''}`) }, settings: { - changePassword: (data: { old_password: string; new_password: string }) => + changePassword: (data: { old_username?: string; username?: string; old_password: string; new_password?: string }) => request('/settings/password', { method: 'POST', body: JSON.stringify(data) }), getSite: () => request('/settings/site'), getPublicSite: () => request<{ title: string; subtitle: string; icon: string; demo_mode: boolean }>('/settings/public'), diff --git a/web/src/views/settings/PasswordSettings.vue b/web/src/views/settings/PasswordSettings.vue index c819856..687ccd3 100644 --- a/web/src/views/settings/PasswordSettings.vue +++ b/web/src/views/settings/PasswordSettings.vue @@ -3,75 +3,198 @@ import { ref, onMounted } from 'vue' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Label } from '@/components/ui/label' -import { AlertTriangle } from 'lucide-vue-next' +import { AlertTriangle, ShieldCheck } from 'lucide-vue-next' import { api } from '@/api' import { toast } from 'vue-sonner' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' -const oldPassword = ref('') +const initialUsername = ref('') +const username = ref('') const newPassword = ref('') const confirmPassword = ref('') const demoMode = ref(false) -async function loadDemoMode() { +// 校验弹窗状态 +const showVerifyDialog = ref(false) +const verifyOldUsername = ref('') +const verifyOldPassword = ref('') +const isSubmitting = ref(false) + +async function loadData() { try { - const res = await api.settings.getPublicSite() - demoMode.value = res.demo_mode || false + const [publicSite, me] = await Promise.all([ + api.settings.getPublicSite(), + api.auth.me() + ]) + demoMode.value = publicSite.demo_mode || false + username.value = me.username + initialUsername.value = me.username } catch { // ignore } } -async function changePassword() { +// 点击保存,先进行前置校验 +function prepareUpdate() { if (demoMode.value) { - toast.error('演示模式下不能修改密码') + toast.error('演示模式下不能修改') return } - if (!oldPassword.value || !newPassword.value) { - toast.error('请填写完整') + + // 1. 基础合法性校验 + if (!username.value.trim()) { + toast.error('账户名不能为空') return } - if (newPassword.value.length < 6) { - toast.error('新密码至少6位') + + // 2. 检查是否有实质性变更 + const isUsernameChanged = username.value !== initialUsername.value + const isPasswordChanged = !!newPassword.value + + if (!isUsernameChanged && !isPasswordChanged) { + toast.info('未检测到任何修改内容') return } - if (newPassword.value !== confirmPassword.value) { - toast.error('两次密码不一致') + + // 3. 密码一致性和长度校验(如果尝试修改密码) + if (isPasswordChanged) { + if (newPassword.value.length < 6) { + toast.error('新密码至少6位') + return + } + if (newPassword.value !== confirmPassword.value) { + toast.error('两次输入的新密码不一致') + return + } + } + + // 所有前置校验通过,重置并打开验证弹窗 + verifyOldUsername.value = '' + verifyOldPassword.value = '' + showVerifyDialog.value = true +} + +// 弹窗确认,执行真正的修改流程 +async function handleFinalUpdate() { + if (!verifyOldUsername.value || !verifyOldPassword.value) { + toast.error('请输入原账号和密码进行验证') return } + + isSubmitting.value = true try { - await api.settings.changePassword({ old_password: oldPassword.value, new_password: newPassword.value }) - toast.success('密码修改成功') - oldPassword.value = '' + const res: any = await api.settings.changePassword({ + old_username: verifyOldUsername.value, + username: username.value, + old_password: verifyOldPassword.value, + new_password: newPassword.value || undefined + }) + + showVerifyDialog.value = false + toast.success(res || '修改成功') + + // 如果修改了用户名或密码,后端会返回“请重新登录”的信息 + if (res && res.includes('重新登录')) { + setTimeout(() => { + window.location.reload() + }, 1500) + } + + // 重置密码框 newPassword.value = '' confirmPassword.value = '' } catch (e: any) { - toast.error(e.message || '修改失败') + toast.error(e.message || '验证失败') + } finally { + isSubmitting.value = false } } -onMounted(loadDemoMode) +onMounted(loadData) + +