feat(server): 新增 passwd 子命令,可重置用户密码并作废会话

This commit is contained in:
AskaEth committed 2026-09-09 19:23:58 +08:00
1 parent e47251658b
commit ad2322d558
3 files changed
+98

No files matched your search

+33
View File
@@ -169,6 +169,39 @@ func (s *Service) EnsureDefaultAdmin(ctx context.Context) (username, password st
return username, password, true, nil
}
// RandomPassword 生成 n 位随机密码,供首次初始化和 CLI 重置使用。
func RandomPassword(n int) (string, error) { return randomPassword(n) }
// SetPassword 重置用户密码并作废其全部会话;mustChange 表示下次登录后强制改密。
func (s *Service) SetPassword(ctx context.Context, username, password string, mustChange bool) error {
if password == "" {
return fmt.Errorf("密码不能为空")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), s.bcryptCost)
if err != nil {
return fmt.Errorf("生成密码哈希: %w", err)
}
must := 0
if mustChange {
must = 1
}
res, err := s.db.ExecContext(ctx,
`UPDATE users SET password_hash = ?, must_change_password = ?, updated_at = datetime('now') WHERE username = ?`,
string(hash), must, username)
if err != nil {
return fmt.Errorf("更新密码: %w", err)
}
if n, err := res.RowsAffected(); err == nil && n == 0 {
return fmt.Errorf("用户 %s 不存在", username)
}
var id int64
if err := s.db.QueryRowContext(ctx, `SELECT id FROM users WHERE username = ?`, username).Scan(&id); err != nil {
return fmt.Errorf("查询用户: %w", err)
}
return s.RevokeUserSessions(ctx, id)
}
// Authenticate 校验用户名密码并创建会话,返回用户与不落库的明文 token。
func (s *Service) Authenticate(ctx context.Context, username, password, ip, userAgent string) (*User, string, error) {
if !s.AllowLogin(ip) {
+7
View File
@@ -36,6 +36,13 @@ func main() {
}
return
}
if len(os.Args) > 1 && os.Args[1] == "passwd" {
if err := runPasswd(os.Args[2:]); err != nil {
fmt.Fprintf(os.Stderr, "重置密码失败: %v\n", err)
os.Exit(1)
}
return
}
configPath := flag.String("config", "config.yaml", "配置文件路径")
project := flag.String("project", "default", "赛事项目名(data/event_projs/<项目名>/event.db)")
+58
View File
@@ -0,0 +1,58 @@
package main
import (
"context"
"flag"
"fmt"
"git.yeij.top/AskaEth/Scoreboard/server/internal/auth"
"git.yeij.top/AskaEth/Scoreboard/server/internal/config"
"git.yeij.top/AskaEth/Scoreboard/server/internal/store"
)
// runPasswd 实现 `scoreboard passwd` 子命令:重置用户密码并作废其全部会话。
// 初始密码是首次启动随机生成的,只能用日志找回;忘了就用这个命令重置。
func runPasswd(args []string) error {
fs := flag.NewFlagSet("passwd", flag.ExitOnError)
configPath := fs.String("config", "config.yaml", "配置文件路径")
username := fs.String("user", "admin", "用户名")
password := fs.String("password", "", "新密码;留空则随机生成并打印")
if err := fs.Parse(args); err != nil {
return err
}
cfg, err := config.Load(*configPath)
if err != nil {
return err
}
st, err := store.Open(cfg.DataDir)
if err != nil {
return err
}
defer func() { _ = st.Close() }()
if err := st.Migrate(); err != nil {
return err
}
pwd := *password
generated := pwd == ""
if generated {
pwd, err = auth.RandomPassword(16)
if err != nil {
return err
}
}
svc := auth.New(st.DB(), auth.Options{})
if err := svc.SetPassword(context.Background(), *username, pwd, generated); err != nil {
return err
}
if generated {
fmt.Printf("已重置用户 %s 的密码:%s\n请登录后立即修改(已要求下次登录强制改密)。\n", *username, pwd)
} else {
fmt.Printf("已重置用户 %s 的密码。\n", *username)
}
return nil
}