| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117 |
- // haven-init-user 创建 Haven 的初始用户(本期唯一账号)。
- //
- // 依据 docs/backend/auth.md 第九节:本期只有一条 users 记录,由部署时的
- // 初始化脚本创建;不提供注册接口,账号创建只能走命令行。
- //
- // 用法:
- //
- // HAVEN_DB_DSN='postgres://haven@127.0.0.1:5432/haven?sslmode=disable' \
- // go run ./cmd/haven-init-user -username kairo
- //
- // 密码从标准输入读取一次(不回显到命令行历史),或通过环境变量
- // HAVEN_INIT_PASSWORD 提供(便于部署脚本)。密码要求见 docs/api.md 2.7:
- // 至少 12 位。bcrypt cost 固定为 12(auth.md 8.1)。
- package main
- import (
- "context"
- "errors"
- "flag"
- "fmt"
- "os"
- "strings"
- "time"
- "golang.org/x/crypto/bcrypt"
- "golang.org/x/term"
- "github.com/kairo/haven/internal/db"
- )
- // bcryptCost 固定 12,见 docs/backend/auth.md 8.1 与 architecture.md 第四节常量表。
- const bcryptCost = 12
- // minPasswordLen 见 docs/api.md 2.7:新密码 ≥ 12 位。
- const minPasswordLen = 12
- func main() {
- if err := run(); err != nil {
- fmt.Fprintf(os.Stderr, "创建用户失败: %v\n", err)
- os.Exit(1)
- }
- }
- func run() error {
- username := flag.String("username", "", "登录名(必填)")
- flag.Parse()
- name := strings.TrimSpace(*username)
- if name == "" {
- return errors.New("必须通过 -username 指定登录名")
- }
- dsn := strings.TrimSpace(os.Getenv("HAVEN_DB_DSN"))
- if dsn == "" {
- return errors.New("未设置 HAVEN_DB_DSN")
- }
- password, err := readPassword()
- if err != nil {
- return err
- }
- if len([]rune(password)) < minPasswordLen {
- return fmt.Errorf("密码至少 %d 位", minPasswordLen)
- }
- hash, err := bcrypt.GenerateFromPassword([]byte(password), bcryptCost)
- if err != nil {
- return fmt.Errorf("生成密码哈希失败: %w", err)
- }
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- conn, err := db.Open(ctx, dsn)
- if err != nil {
- return err
- }
- defer conn.Close()
- // 不区分大小写判重,与 idx_users_username_lower 一致。
- var exists bool
- if err := conn.QueryRowContext(ctx,
- `SELECT EXISTS (SELECT 1 FROM users WHERE lower(username) = lower($1))`, name,
- ).Scan(&exists); err != nil {
- return fmt.Errorf("检查用户是否存在失败: %w", err)
- }
- if exists {
- return fmt.Errorf("用户 %q 已存在,本期只允许一个账号", name)
- }
- var id string
- if err := conn.QueryRowContext(ctx,
- `INSERT INTO users (username, password_hash) VALUES ($1, $2) RETURNING id`,
- name, string(hash),
- ).Scan(&id); err != nil {
- return fmt.Errorf("写入用户失败: %w", err)
- }
- fmt.Printf("已创建用户 %s(id=%s,totp_enabled=false)\n", name, id)
- return nil
- }
- // readPassword 优先读 HAVEN_INIT_PASSWORD,否则从终端读取一次不回显的输入。
- func readPassword() (string, error) {
- if pw := os.Getenv("HAVEN_INIT_PASSWORD"); pw != "" {
- return pw, nil
- }
- fmt.Fprint(os.Stderr, "请输入初始密码: ")
- raw, err := term.ReadPassword(int(os.Stdin.Fd()))
- fmt.Fprintln(os.Stderr)
- if err != nil {
- return "", fmt.Errorf("读取密码失败: %w", err)
- }
- return string(raw), nil
- }
|