package auth import ( "context" "fmt" "log/slog" "bountyboard/internal/config" "bountyboard/internal/domain" "bountyboard/internal/store" "bountyboard/internal/ulid" ) // EnsureBootstrapAdmin creates the initial admin from ADMIN_EMAIL / // ADMIN_INITIAL_PASSWORD when the users collection is empty (ยง3). The // password change is forced at first login. func EnsureBootstrapAdmin(ctx context.Context, st *store.Store, cfg *config.Config, log *slog.Logger) error { n, err := st.CountUsers(ctx) if err != nil { return err } if n > 0 { return nil } if cfg.AdminEmail == "" || cfg.AdminInitialPassword == "" { log.Warn("users collection is empty but ADMIN_EMAIL/ADMIN_INITIAL_PASSWORD are not set; no admin bootstrapped") return nil } hash, err := HashPassword(cfg.AdminInitialPassword) if err != nil { return fmt.Errorf("hash bootstrap password: %w", err) } u := &domain.User{ ID: ulid.New(), Email: cfg.AdminEmail, Name: "Administrator", Roles: domain.Roles{Admin: true}, Auth: domain.Auth{ Local: &domain.LocalAuth{PasswordHash: hash, MustChange: true}, }, Settings: domain.UserSettings{ Theme: "light", Notifications: domain.NotificationPrefs{Email: true, InApp: true}, }, } if err := st.CreateUser(ctx, u); err != nil { return fmt.Errorf("bootstrap admin: %w", err) } log.Info("bootstrap admin created; password change is forced at first login", "email", u.Email) return nil }