feat: 新增 reset-admin-password 命令行重置管理员密码

- cmd/server 子命令分发:reset-admin-password 不启动服务
- -password 留空随机生成 16 位(去除易混淆字符),打印一次
- -user 指定目标管理员,仅匹配 role=admin
- 密码长度校验 8-72 与 API 一致,argon2id 参数取自配置
- Makefile 增加 make reset-admin-password 入口
This commit is contained in:
Sakurasan
2026-08-26 15:57:04 +08:00
parent a45a6e16bd
commit 2983c89b42
3 changed files with 91 additions and 1 deletions
+4 -1
View File
@@ -1,8 +1,11 @@
.PHONY: run build test tidy mock-upstream web-dev web-build
.PHONY: run build test tidy mock-upstream web-dev web-build reset-admin-password
run:
cd server && go run ./cmd/server
reset-admin-password:
cd server && go run ./cmd/server reset-admin-password
build:
cd server && go build -o bin/openteam ./cmd/server
+6
View File
@@ -20,6 +20,12 @@ import (
)
func main() {
// 子命令分发:`openteam reset-admin-password ...`(无子命令时启动服务)
if len(os.Args) > 1 && os.Args[1] == "reset-admin-password" {
runResetAdminPassword(os.Args[2:])
return
}
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
+81
View File
@@ -0,0 +1,81 @@
// 子命令 reset-admin-password:重置管理员密码(忘记密码时用,需在 server/ 目录或配置可加载处执行)。
// 用法:go run ./cmd/server reset-admin-password [-user admin] [-password 新密码]
package main
import (
"crypto/rand"
"flag"
"fmt"
"log"
"math/big"
"github.com/openteam/server/internal/config"
"github.com/openteam/server/internal/pkg/crypto"
"github.com/openteam/server/internal/store"
)
// pwAlphabet 密码随机串字符集(去掉 0/O/1/l/I 等易混淆字符)。
const pwAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
func runResetAdminPassword(args []string) {
fs := flag.NewFlagSet("reset-admin-password", flag.ExitOnError)
user := fs.String("user", "admin", "目标管理员用户名")
password := fs.String("password", "", "新密码(至少 8 位;留空则随机生成并打印一次)")
_ = fs.Parse(args)
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN)
if err != nil {
log.Fatalf("db: %v", err)
}
var u store.User
if err := db.Where("username = ? AND role = ?", *user, store.RoleAdmin).First(&u).Error; err != nil {
log.Fatalf("admin user %q not found", *user)
}
pw := *password
generated := false
if pw == "" {
pw, err = randomPassword(16)
if err != nil {
log.Fatalf("generate password: %v", err)
}
generated = true
}
if len(pw) < 8 || len(pw) > 72 {
log.Fatal("password length must be between 8 and 72")
}
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("hash password: %v", err)
}
if err := db.Model(&store.User{}).Where("id = ?", u.ID).Update("password_hash", hash).Error; err != nil {
log.Fatalf("update password: %v", err)
}
if generated {
fmt.Printf("已重置 %q 的密码:%s\n", u.Username, pw)
fmt.Println("(请立即保存,不会再次显示)")
} else {
fmt.Printf("已重置 %q 的密码。\n", u.Username)
}
}
// randomPassword 用 crypto/rand 从 pwAlphabet 生成 n 位随机密码。
func randomPassword(n int) (string, error) {
buf := make([]byte, n)
for i := range buf {
k, err := rand.Int(rand.Reader, big.NewInt(int64(len(pwAlphabet))))
if err != nil {
return "", err
}
buf[i] = pwAlphabet[k.Int64()]
}
return string(buf), nil
}