feat: add reset-password CLI command for any user
This commit is contained in:
@@ -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:
|
run:
|
||||||
cd server && go run ./cmd/server
|
cd server && go run ./cmd/server
|
||||||
@@ -6,6 +6,9 @@ run:
|
|||||||
reset-admin-password:
|
reset-admin-password:
|
||||||
cd server && go run ./cmd/server reset-admin-password
|
cd server && go run ./cmd/server reset-admin-password
|
||||||
|
|
||||||
|
reset-password:
|
||||||
|
cd server && go run ./cmd/server reset-password
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cd server && go build -o bin/openteam ./cmd/server
|
cd server && go build -o bin/openteam ./cmd/server
|
||||||
|
|
||||||
|
|||||||
@@ -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))
|
||||||
|
}
|
||||||
@@ -19,7 +19,8 @@ var rootCmd = &cobra.Command{
|
|||||||
go run ./cmd/server
|
go run ./cmd/server
|
||||||
|
|
||||||
子命令:
|
子命令:
|
||||||
reset-admin-password 重置管理员密码(交互式,无参数时自动提示)`,
|
reset-admin-password 重置管理员密码(交互式,无参数时自动提示)
|
||||||
|
reset-password 重置任意用户密码`,
|
||||||
RunE: func(c *cobra.Command, args []string) error {
|
RunE: func(c *cobra.Command, args []string) error {
|
||||||
return serve()
|
return serve()
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user