From d91188149988eae91ff3517cd550d0d72943973e Mon Sep 17 00:00:00 2001 From: Sakurasan <26715255+Sakurasan@users.noreply.github.com> Date: Mon, 7 Sep 2026 02:33:11 +0800 Subject: [PATCH] feat: add reset-password CLI command for any user --- Makefile | 5 +- server/internal/cli/reset_password.go | 176 ++++++++++++++++++++++++++ server/internal/cli/root.go | 3 +- 3 files changed, 182 insertions(+), 2 deletions(-) create mode 100644 server/internal/cli/reset_password.go diff --git a/Makefile b/Makefile index 6a57187..2c257ce 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: run build test tidy mock-upstream web-dev web-build reset-admin-password +.PHONY: run build test tidy mock-upstream web-dev web-build reset-admin-password reset-password run: cd server && go run ./cmd/server @@ -6,6 +6,9 @@ run: reset-admin-password: cd server && go run ./cmd/server reset-admin-password +reset-password: + cd server && go run ./cmd/server reset-password + build: cd server && go build -o bin/openteam ./cmd/server diff --git a/server/internal/cli/reset_password.go b/server/internal/cli/reset_password.go new file mode 100644 index 0000000..27f7b05 --- /dev/null +++ b/server/internal/cli/reset_password.go @@ -0,0 +1,176 @@ +// Package cli 命令行入口。 +package cli + +import ( + "fmt" + "log" + "os" + "strconv" + "strings" + + "github.com/openteam/server/internal/config" + "github.com/openteam/server/internal/pkg/crypto" + "github.com/openteam/server/internal/store" + "github.com/spf13/cobra" + "golang.org/x/term" + "gorm.io/gorm" +) + +var ( + resetPwUser string + resetPwPassword string + resetPwUserID string +) + +var resetPasswordCmd = &cobra.Command{ + Use: "reset-password", + Short: "重置用户密码", + Long: `重置任意用户密码(管理员可重置任何用户,需在可加载配置的目录下执行)。 + +交互式引导(默认): + go run ./cmd/server reset-password + +非交互式(用于脚本): + go run ./cmd/server reset-password --user admin --password "新密码" + go run ./cmd/server reset-password --user-id 123 --password "新密码"`, + Run: func(cmd *cobra.Command, args []string) { + runResetPassword() + }, +} + +func init() { + rootCmd.AddCommand(resetPasswordCmd) + resetPasswordCmd.Flags().StringVar(&resetPwUser, "user", "", "目标用户名(留空则交互选择)") + resetPasswordCmd.Flags().StringVar(&resetPwUserID, "user-id", "", "目标用户 ID(优先于 --user)") + resetPasswordCmd.Flags().StringVar(&resetPwPassword, "password", "", "新密码,至少 8 位(留空则交互输入)") +} + +func runResetPassword() { + // --- 阶段 1:加载配置与数据库 --- + cfg, err := config.Load() + if err != nil { + log.Fatalf("加载配置失败: %v", err) + } + db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN) + if err != nil { + log.Fatalf("连接数据库失败: %v", err) + } + + // --- 阶段 2:选择目标用户 --- + target := targetUser(db) + + // --- 阶段 3:获取新密码 --- + var pw string + if resetPwPassword != "" { + pw = resetPwPassword + } else { + pw = promptNewPassword() + } + if pw == "" { + log.Fatalf("密码不能为空") + } + if len(pw) < 8 || len(pw) > 72 { + log.Fatalf("密码长度必须在 8 到 72 位之间") + } + + // --- 阶段 4:加密并更新 --- + hasher := crypto.NewPasswordHasher(cfg.Auth.Argon2Time, cfg.Auth.Argon2Memory, cfg.Auth.Argon2Threads, cfg.Auth.Argon2KeyLen, cfg.Auth.SaltLen) + hash, err := hasher.HashPassword(pw) + if err != nil { + log.Fatalf("加密密码失败: %v", err) + } + if err := db.Model(&store.User{}).Where("id = ?", target.ID).Update("password_hash", hash).Error; err != nil { + log.Fatalf("更新密码失败: %v", err) + } + + fmt.Printf("✅ 已重置用户 %q 的密码!\n", target.Username) +} + +// targetUser 根据 --user-id 或 --user 解析目标用户,否则交互选择。 +func targetUser(db *gorm.DB) *store.User { + // 优先使用 --user-id + if resetPwUserID != "" { + id, err := strconv.ParseUint(resetPwUserID, 10, 64) + if err != nil { + log.Fatalf("无效的用户 ID: %v", err) + } + var u store.User + if err := db.First(&u, id).Error; err != nil { + log.Fatalf("用户 ID %d 不存在", id) + } + return &u + } + + // 使用 --user + if resetPwUser != "" { + var u store.User + if err := db.Where("username = ?", resetPwUser).First(&u).Error; err != nil { + log.Fatalf("用户 %q 不存在", resetPwUser) + } + return &u + } + + // 交互选择 + var users []struct { + ID uint64 + Username string + Role string + } + db.Model(&store.User{}).Select("id, username, role").Order("id ASC").Find(&users) + if len(users) == 0 { + log.Fatalf("系统中没有用户") + } + if len(users) == 1 { + return &store.User{ID: users[0].ID, Username: users[0].Username} + } + return selectUser(users) +} + +// selectUser 交互式选择用户。 +func selectUser(users []struct { + ID uint64 + Username string + Role string +}) *store.User { + fmt.Println("请选择要重置密码的用户:") + for i, u := range users { + fmt.Printf(" [%d] %s (%s)\n", i+1, u.Username, u.Role) + } + fmt.Printf("请输入编号 (1-%d): ", len(users)) + var n int + if _, err := fmt.Scanf("%d", &n); err != nil || n < 1 || n > len(users) { + log.Fatalf("无效输入") + } + return &store.User{ID: users[n-1].ID, Username: users[n-1].Username} +} + +// promptNewPassword 交互式输入密码。 +func promptNewPassword() string { + fmt.Println("请选择密码设置方式:") + fmt.Println(" [1] 手动输入密码") + fmt.Println(" [2] 自动生成随机密码(16 位,不含易混淆字符)") + fmt.Printf("请输入编号 (1-2): ") + + var n int + if _, err := fmt.Scanf("%d", &n); err != nil || n < 1 || n > 2 { + log.Fatalf("无效输入,请输入 1 或 2") + } + if n == 2 { + pw, err := randomPassword(16) + if err != nil { + log.Fatalf("生成随机密码失败: %v", err) + } + fmt.Printf("生成的随机密码:%s\n", pw) + fmt.Println("(请立即保存,不会再次显示)") + return pw + } + + // 手动输入:静默输入(不回显) + fmt.Print("请输入新密码(至少 8 位):") + raw, err := term.ReadPassword(int(os.Stdin.Fd())) + fmt.Println() + if err != nil { + log.Fatalf("读取密码失败: %v", err) + } + return strings.TrimSpace(string(raw)) +} diff --git a/server/internal/cli/root.go b/server/internal/cli/root.go index 58cd679..633f1e3 100644 --- a/server/internal/cli/root.go +++ b/server/internal/cli/root.go @@ -19,7 +19,8 @@ var rootCmd = &cobra.Command{ go run ./cmd/server 子命令: - reset-admin-password 重置管理员密码(交互式,无参数时自动提示)`, + reset-admin-password 重置管理员密码(交互式,无参数时自动提示) + reset-password 重置任意用户密码`, RunE: func(c *cobra.Command, args []string) error { return serve() },