package bootstrap import ( "context" "strings" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/member/domain/entity" domrepo "haixun-backend/internal/model/member/domain/repository" "github.com/google/uuid" "golang.org/x/crypto/bcrypt" ) type AdminOptions struct { TenantID string Email string Password string DisplayName string } func EnsureAdminMember(ctx context.Context, repo domrepo.Repository, opts AdminOptions) (*entity.Member, bool, error) { tenantID := strings.TrimSpace(opts.TenantID) email := normalizeEmail(opts.Email) if tenantID == "" || email == "" || opts.Password == "" { return nil, false, app.For(code.Member).InputMissingRequired("tenant_id, email, and password are required") } if len(opts.Password) < 8 { return nil, false, app.For(code.Member).InputInvalidFormat("password must be at least 8 characters") } existing, err := repo.FindByEmail(ctx, tenantID, email) if err == nil { if err := repo.SetRoles(ctx, tenantID, existing.UID, []string{"admin"}); err != nil { return nil, false, err } existing.Roles = []string{"admin"} return existing, false, nil } if e := app.FromError(err); e == nil || e.Category() != code.ResNotFound { return nil, false, err } hash, err := bcrypt.GenerateFromPassword([]byte(opts.Password), bcrypt.DefaultCost) if err != nil { return nil, false, app.For(code.Member).SysInternal("hash password failed").WithCause(err) } displayName := strings.TrimSpace(opts.DisplayName) if displayName == "" { displayName = "Admin" } member, err := repo.Create(ctx, &entity.Member{ TenantID: tenantID, UID: uuid.NewString(), Email: email, DisplayName: displayName, Language: "zh-TW", Status: entity.StatusOpen, Origin: entity.OriginNative, PasswordHash: string(hash), Roles: []string{"admin"}, }) if err != nil { return nil, false, err } return member, true, nil } func normalizeEmail(email string) string { return strings.ToLower(strings.TrimSpace(email)) }