package admin import ( "context" "fmt" "galaxy/backend/internal/config" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" ) // Bootstrap inserts the seed admin row when the env-driven // `BACKEND_ADMIN_BOOTSTRAP_USER` / `BACKEND_ADMIN_BOOTSTRAP_PASSWORD` // values are supplied and no row with that username exists yet. The // insert is idempotent across restarts so operators can leave the env // vars set after the first deploy without re-creating the row on // every boot. // // Bootstrap runs *before* `Cache.Warm` so the warm read picks up the // seed row. Errors are returned to the caller; the boot path in // `cmd/backend/main.go` aborts startup if Bootstrap fails (a missing // admin would lock the surface out anyway, so failing fast is the // safer default). // // When both env vars are empty the function logs "skipped" and // returns nil. `config.Validate()` already enforces that the username // and password are set together, so by the time Bootstrap runs the // remaining "user set without password" combination is impossible. func Bootstrap(ctx context.Context, store *Store, cfg config.AdminBootstrapConfig, logger *zap.Logger) error { if logger == nil { logger = zap.NewNop() } logger = logger.Named("admin.bootstrap") if cfg.User == "" { logger.Info("skipped (no env vars)") return nil } hash, err := bcrypt.GenerateFromPassword([]byte(cfg.Password), bootstrapBcryptCost) if err != nil { return fmt.Errorf("admin bootstrap: hash password: %w", err) } inserted, err := store.BootstrapInsert(ctx, cfg.User, hash) if err != nil { return fmt.Errorf("admin bootstrap: %w", err) } if inserted { logger.Info("inserted seed admin", zap.String("admin_username", cfg.User)) } else { logger.Info("skipped (admin exists)", zap.String("admin_username", cfg.User)) } return nil }