main.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. // haven-init-user 创建 Haven 的初始用户(本期唯一账号)。
  2. //
  3. // 依据 docs/backend/auth.md 第九节:本期只有一条 users 记录,由部署时的
  4. // 初始化脚本创建;不提供注册接口,账号创建只能走命令行。
  5. //
  6. // 用法:
  7. //
  8. // HAVEN_DB_DSN='postgres://haven@127.0.0.1:5432/haven?sslmode=disable' \
  9. // go run ./cmd/haven-init-user -username kairo
  10. //
  11. // 密码从标准输入读取一次(不回显到命令行历史),或通过环境变量
  12. // HAVEN_INIT_PASSWORD 提供(便于部署脚本)。密码要求见 docs/api.md 2.7:
  13. // 至少 12 位。bcrypt cost 固定为 12(auth.md 8.1)。
  14. package main
  15. import (
  16. "context"
  17. "errors"
  18. "flag"
  19. "fmt"
  20. "os"
  21. "strings"
  22. "time"
  23. "golang.org/x/crypto/bcrypt"
  24. "golang.org/x/term"
  25. "github.com/kairo/haven/internal/auth"
  26. "github.com/kairo/haven/internal/db"
  27. )
  28. // 与认证包保持一致:bcrypt cost 与密码最小长度只在一个地方定义。
  29. const (
  30. bcryptCost = auth.BcryptCost
  31. minPasswordLen = auth.MinPasswordLen
  32. )
  33. func main() {
  34. if err := run(); err != nil {
  35. fmt.Fprintf(os.Stderr, "创建用户失败: %v\n", err)
  36. os.Exit(1)
  37. }
  38. }
  39. func run() error {
  40. username := flag.String("username", "", "登录名(必填)")
  41. flag.Parse()
  42. name := strings.TrimSpace(*username)
  43. if name == "" {
  44. return errors.New("必须通过 -username 指定登录名")
  45. }
  46. dsn := strings.TrimSpace(os.Getenv("HAVEN_DB_DSN"))
  47. if dsn == "" {
  48. return errors.New("未设置 HAVEN_DB_DSN")
  49. }
  50. password, err := readPassword()
  51. if err != nil {
  52. return err
  53. }
  54. if len([]rune(password)) < minPasswordLen {
  55. return fmt.Errorf("密码至少 %d 位", minPasswordLen)
  56. }
  57. hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
  58. if err != nil {
  59. return fmt.Errorf("生成密码哈希失败: %w", err)
  60. }
  61. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  62. defer cancel()
  63. conn, err := db.Open(ctx, dsn)
  64. if err != nil {
  65. return err
  66. }
  67. defer conn.Close()
  68. // 不区分大小写判重,与 idx_users_username_lower 一致。
  69. var exists bool
  70. if err := conn.QueryRowContext(ctx,
  71. `SELECT EXISTS (SELECT 1 FROM users WHERE lower(username) = lower($1))`, name,
  72. ).Scan(&exists); err != nil {
  73. return fmt.Errorf("检查用户是否存在失败: %w", err)
  74. }
  75. if exists {
  76. return fmt.Errorf("用户 %q 已存在,本期只允许一个账号", name)
  77. }
  78. var id string
  79. if err := conn.QueryRowContext(ctx,
  80. `INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id`,
  81. name, string(hash),
  82. ).Scan(&id); err != nil {
  83. return fmt.Errorf("写入用户失败: %w", err)
  84. }
  85. fmt.Printf("已创建用户 %s(id=%s,totp_enabled=false)\n", name, id)
  86. return nil
  87. }
  88. // readPassword 优先读 HAVEN_INIT_PASSWORD,否则从终端读取一次不回显的输入。
  89. func readPassword() (string, error) {
  90. if pw := os.Getenv("HAVEN_INIT_PASSWORD"); pw != "" {
  91. return pw, nil
  92. }
  93. fmt.Fprint(os.Stderr, "请输入初始密码: ")
  94. raw, err := term.ReadPassword(int(os.Stdin.Fd()))
  95. fmt.Fprintln(os.Stderr)
  96. if err != nil {
  97. return "", fmt.Errorf("读取密码失败: %w", err)
  98. }
  99. return string(raw), nil
  100. }
苏ICP备2026082406号
苏公网安备 待审核