package app import ( "context" "errors" "fmt" "strings" "go.uber.org/zap" "golang.org/x/crypto/bcrypt" "github.com/geo-platform/tenant-api/internal/ops/domain" "github.com/geo-platform/tenant-api/internal/ops/repository" ) type DefaultAdminSeed struct { Username string Password string DisplayName string Email string } // SeedDefaultAdmin inserts a single admin account when the operator_accounts // table is empty. It is safe to call on every startup — a non-empty table is // a no-op. Returns an error only when the table is empty AND the seed config // is incomplete (so a fresh deployment cannot silently boot without any // way to log in). func SeedDefaultAdmin(ctx context.Context, accounts *repository.AccountRepository, seed DefaultAdminSeed, logger *zap.Logger) error { count, err := accounts.Count(ctx) if err != nil { return fmt.Errorf("count operator accounts: %w", err) } if count > 0 { return nil } username := strings.TrimSpace(seed.Username) password := seed.Password if username == "" || password == "" { return errors.New("operator_accounts is empty but default_admin.username/password are not configured (set OPS_DEFAULT_ADMIN_USERNAME / OPS_DEFAULT_ADMIN_PASSWORD)") } displayName := strings.TrimSpace(seed.DisplayName) if displayName == "" { displayName = username } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return fmt.Errorf("hash default admin password: %w", err) } var emailPtr *string if email := strings.TrimSpace(seed.Email); email != "" { emailPtr = &email } _, err = accounts.Create(ctx, repository.CreateAccountInput{ Username: username, Email: emailPtr, PasswordHash: string(hash), DisplayName: displayName, Role: domain.RoleAdmin, }) if err != nil { return fmt.Errorf("create default admin: %w", err) } logger.Sugar().Infof("ops default admin seeded: username=%s", username) return nil }