feat: add account reset
This commit is contained in:
+27
-19
@@ -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("--------------------------------------------------")
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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 清理日志 - 已移除,改为任务级别的日志清理配置
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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<TaskStatsItem[]>(`/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<SiteSettings>('/settings/site'),
|
||||
getPublicSite: () => request<{ title: string; subtitle: string; icon: string; demo_mode: boolean }>('/settings/public'),
|
||||
|
||||
@@ -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)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
|
||||
|
||||
<div v-if="demoMode" class="flex items-center gap-2 p-3 rounded-md bg-destructive/10 text-destructive text-sm">
|
||||
<AlertTriangle class="h-4 w-4 shrink-0" />
|
||||
<span>演示模式下不能修改密码</span>
|
||||
<span>演示模式下禁止修改任何敏感信息</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right">原密码</Label>
|
||||
<Input v-model="oldPassword" type="password" placeholder="请输入原密码" class="sm:col-span-3" :disabled="demoMode" />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right">新密码</Label>
|
||||
<Input v-model="newPassword" type="password" placeholder="请输入新密码(至少6位)" class="sm:col-span-3" :disabled="demoMode" />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right">确认密码</Label>
|
||||
<Input v-model="confirmPassword" type="password" placeholder="请再次输入新密码" class="sm:col-span-3" :disabled="demoMode" />
|
||||
</div>
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button @click="changePassword" :disabled="demoMode">修改密码</Button>
|
||||
|
||||
<div class="space-y-6">
|
||||
<!-- 基础设置展示 -->
|
||||
<div class="grid gap-4 p-4 rounded-lg border border-border/60 bg-card/50">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right font-medium">新账户名</Label>
|
||||
<Input v-model="username" placeholder="输入新的登录账号" class="sm:col-span-3" :disabled="demoMode" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right font-medium">新密码</Label>
|
||||
<Input v-model="newPassword" type="password" placeholder="若不修改密码请留空" class="sm:col-span-3" :disabled="demoMode" autocomplete="new-password" />
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 sm:grid-cols-4 items-center gap-2 sm:gap-4">
|
||||
<Label class="sm:text-right font-medium">确认新密码</Label>
|
||||
<Input v-model="confirmPassword" type="password" placeholder="请再次输入新密码" class="sm:col-span-3" :disabled="demoMode" autocomplete="new-password" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button @click="prepareUpdate" :disabled="demoMode" size="lg" class="px-8 shadow-md">
|
||||
提交修改
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 身份验证弹窗 -->
|
||||
<Dialog v-model:open="showVerifyDialog">
|
||||
<DialogContent class="sm:max-w-[425px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<ShieldCheck class="w-5 h-5 text-primary" />
|
||||
身份验证
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
为了您的账户安全,修改账号或密码需要验证您当前的凭据。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="old-user" class="text-right">原账号</Label>
|
||||
<Input id="old-user" v-model="verifyOldUsername" placeholder="请输入当前使用的账号" class="col-span-3" />
|
||||
</div>
|
||||
<div class="grid grid-cols-4 items-center gap-4">
|
||||
<Label for="old-pwd" class="text-right">原密码</Label>
|
||||
<Input id="old-pwd" v-model="verifyOldPassword" type="password" placeholder="请输入当前账号的密码" class="col-span-3" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" @click="showVerifyDialog = false" :disabled="isSubmitting">取消</Button>
|
||||
<Button @click="handleFinalUpdate" :disabled="isSubmitting">
|
||||
<span v-if="isSubmitting">验证中...</span>
|
||||
<span v-else>确认修改</span>
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 确保对话框在自动填充时颜色正常 */
|
||||
:deep(input:-webkit-autofill) {
|
||||
-webkit-box-shadow: 0 0 0 1000px hsl(var(--input)) inset !important;
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user