main.go 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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/db"
  26. )
  27. // bcryptCost 固定 12,见 docs/backend/auth.md 8.1 与 architecture.md 第四节常量表。
  28. const bcryptCost = 12
  29. // minPasswordLen 见 docs/api.md 2.7:新密码 ≥ 12 位。
  30. const minPasswordLen = 12
  31. func main() {
  32. if err := run(); err != nil {
  33. fmt.Fprintf(os.Stderr, "创建用户失败: %v\n", err)
  34. os.Exit(1)
  35. }
  36. }
  37. func run() error {
  38. username := flag.String("username", "", "登录名(必填)")
  39. flag.Parse()
  40. name := strings.TrimSpace(*username)
  41. if name == "" {
  42. return errors.New("必须通过 -username 指定登录名")
  43. }
  44. dsn := strings.TrimSpace(os.Getenv("HAVEN_DB_DSN"))
  45. if dsn == "" {
  46. return errors.New("未设置 HAVEN_DB_DSN")
  47. }
  48. password, err := readPassword()
  49. if err != nil {
  50. return err
  51. }
  52. if len([]rune(password)) < minPasswordLen {
  53. return fmt.Errorf("密码至少 %d 位", minPasswordLen)
  54. }
  55. hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
  56. if err != nil {
  57. return fmt.Errorf("生成密码哈希失败: %w", err)
  58. }
  59. ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
  60. defer cancel()
  61. conn, err := db.Open(ctx, dsn)
  62. if err != nil {
  63. return err
  64. }
  65. defer conn.Close()
  66. // 不区分大小写判重,与 idx_users_username_lower 一致。
  67. var exists bool
  68. if err := conn.QueryRowContext(ctx,
  69. `SELECT EXISTS (SELECT 1 FROM users WHERE lower(username) = lower($1))`, name,
  70. ).Scan(&exists); err != nil {
  71. return fmt.Errorf("检查用户是否存在失败: %w", err)
  72. }
  73. if exists {
  74. return fmt.Errorf("用户 %q 已存在,本期只允许一个账号", name)
  75. }
  76. var id string
  77. if err := conn.QueryRowContext(ctx,
  78. `INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id`,
  79. name, string(hash),
  80. ).Scan(&id); err != nil {
  81. return fmt.Errorf("写入用户失败: %w", err)
  82. }
  83. fmt.Printf("已创建用户 %s(id=%s,totp_enabled=false)\n", name, id)
  84. return nil
  85. }
  86. // readPassword 优先读 HAVEN_INIT_PASSWORD,否则从终端读取一次不回显的输入。
  87. func readPassword() (string, error) {
  88. if pw := os.Getenv("HAVEN_INIT_PASSWORD"); pw != "" {
  89. return pw, nil
  90. }
  91. fmt.Fprint(os.Stderr, "请输入初始密码: ")
  92. raw, err := term.ReadPassword(int(os.Stdin.Fd()))
  93. fmt.Fprintln(os.Stderr)
  94. if err != nil {
  95. return "", fmt.Errorf("读取密码失败: %w", err)
  96. }
  97. return string(raw), nil
  98. }
苏ICP备2026082406号
苏公网安备 待审核