package bootstrap import ( "baron-sso-backend/internal/domain" "baron-sso-backend/internal/repository" "context" "errors" "fmt" "net/mail" "strings" "time" "gorm.io/gorm" ) type SuperAdminIdentityAdmin interface { FindIdentityIDByIdentifier(ctx context.Context, identifier string) (string, error) CreateUser(ctx context.Context, user *domain.BrokerUser, password string) (string, error) UpdateIdentityPassword(ctx context.Context, identityID, newPassword string) error } type SuperAdminStore interface { FindUserByEmail(ctx context.Context, email string) (*domain.User, error) CreateUser(ctx context.Context, user *domain.User) error UpdateUserSuperAdmin(ctx context.Context, userID string, name string) (*domain.User, error) EnqueueSuperAdminRelation(ctx context.Context, userID string) error } type EnsureSuperAdminOptions struct { Email string Password string Name string Source string UpdatePassword bool } type EnsureSuperAdminResult struct { Email string IdentityID string LocalUserID string IdentityCreated bool PasswordUpdated bool LocalUserCreated bool LocalUserUpdated bool KetoRelationQueued bool } func EnsureSuperAdmin(ctx context.Context, identityAdmin SuperAdminIdentityAdmin, store SuperAdminStore, opts EnsureSuperAdminOptions) (EnsureSuperAdminResult, error) { email := strings.ToLower(strings.TrimSpace(opts.Email)) name := strings.TrimSpace(opts.Name) if name == "" { name = "System Admin" } source := strings.TrimSpace(opts.Source) if source == "" { source = "admin_cli" } result := EnsureSuperAdminResult{Email: email} if _, err := mail.ParseAddress(email); err != nil { return result, fmt.Errorf("invalid admin email: %w", err) } if identityAdmin == nil { return result, errors.New("identity admin is required") } if store == nil { return result, errors.New("super admin store is required") } identityID, err := identityAdmin.FindIdentityIDByIdentifier(ctx, email) if err != nil { return result, fmt.Errorf("find admin identity: %w", err) } if identityID == "" { if strings.TrimSpace(opts.Password) == "" { return result, errors.New("admin password is required to create identity") } identityID, err = identityAdmin.CreateUser(ctx, buildSuperAdminBrokerUser(email, name), opts.Password) if err != nil { return result, fmt.Errorf("create admin identity: %w", err) } result.IdentityCreated = true } else if opts.UpdatePassword { if strings.TrimSpace(opts.Password) == "" { return result, errors.New("admin password is required to update identity password") } if err := identityAdmin.UpdateIdentityPassword(ctx, identityID, opts.Password); err != nil { return result, fmt.Errorf("update admin identity password: %w", err) } result.PasswordUpdated = true } result.IdentityID = identityID user, err := store.FindUserByEmail(ctx, email) if err != nil { return result, fmt.Errorf("find local admin user: %w", err) } if user == nil { if identityID == "" { return result, errors.New("identity id is required to create local admin user") } user = &domain.User{ ID: identityID, Email: email, Name: name, Role: domain.RoleSuperAdmin, Status: domain.UserStatusActive, CreatedAt: time.Now(), UpdatedAt: time.Now(), Metadata: domain.JSONMap{ "source": source, }, } if err := store.CreateUser(ctx, user); err != nil { return result, fmt.Errorf("create local admin user: %w", err) } result.LocalUserCreated = true } else if domain.NormalizeRole(user.Role) != domain.RoleSuperAdmin || user.Status != domain.UserStatusActive || (name != "" && user.Name != name) { user, err = store.UpdateUserSuperAdmin(ctx, user.ID, name) if err != nil { return result, fmt.Errorf("update local admin user: %w", err) } result.LocalUserUpdated = true } result.LocalUserID = user.ID if err := store.EnqueueSuperAdminRelation(ctx, user.ID); err != nil { return result, fmt.Errorf("enqueue super admin keto relation: %w", err) } result.KetoRelationQueued = true return result, nil } func buildSuperAdminBrokerUser(email, name string) *domain.BrokerUser { return &domain.BrokerUser{ Email: email, Name: name, PhoneNumber: "", Attributes: map[string]interface{}{ "department": "Admin", "affiliationType": "internal", "grade": "", "role": domain.RoleSuperAdmin, }, } } type gormSuperAdminStore struct { db *gorm.DB outbox repository.KetoOutboxRepository } func NewGormSuperAdminStore(db *gorm.DB, outbox repository.KetoOutboxRepository) SuperAdminStore { return &gormSuperAdminStore{db: db, outbox: outbox} } func (s *gormSuperAdminStore) FindUserByEmail(ctx context.Context, email string) (*domain.User, error) { var user domain.User if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } return nil, err } return &user, nil } func (s *gormSuperAdminStore) CreateUser(ctx context.Context, user *domain.User) error { return s.db.WithContext(ctx).Create(user).Error } func (s *gormSuperAdminStore) UpdateUserSuperAdmin(ctx context.Context, userID string, name string) (*domain.User, error) { updates := map[string]interface{}{ "role": domain.RoleSuperAdmin, "status": domain.UserStatusActive, "updated_at": time.Now(), } if strings.TrimSpace(name) != "" { updates["name"] = strings.TrimSpace(name) } if err := s.db.WithContext(ctx).Model(&domain.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil { return nil, err } var user domain.User if err := s.db.WithContext(ctx).Where("id = ?", userID).First(&user).Error; err != nil { return nil, err } return &user, nil } func (s *gormSuperAdminStore) EnqueueSuperAdminRelation(ctx context.Context, userID string) error { if s.outbox == nil { return nil } return s.outbox.Create(ctx, &domain.KetoOutbox{ Namespace: "System", Object: "global", Relation: "super_admins", Subject: "User:" + userID, Action: domain.KetoOutboxActionCreate, }) }