package admin import ( "context" "errors" "fmt" "strings" platformusers "github.com/ultisuite/ulti-backend/internal/users" ) type bulkUsersRequest struct { UserIDs []string `json:"user_ids"` Action string `json:"action"` Role string `json:"role"` GroupID string `json:"group_id"` } type bulkUsersResult struct { SuccessCount int `json:"success_count"` Failed []bulkUserFailure `json:"failed,omitempty"` } type bulkUserFailure struct { UserID string `json:"user_id"` Message string `json:"message"` } func (s *Service) BulkUsersAction(ctx context.Context, actorSub string, req bulkUsersRequest) (bulkUsersResult, error) { action := strings.ToLower(strings.TrimSpace(req.Action)) userIDs := dedupeNonEmpty(req.UserIDs) if len(userIDs) == 0 { return bulkUsersResult{}, fmt.Errorf("no user ids") } result := bulkUsersResult{} switch action { case "disable": for _, userID := range userIDs { if err := s.DisableUser(ctx, actorSub, userID); err != nil { result.Failed = append(result.Failed, bulkUserFailure{UserID: userID, Message: bulkErrorMessage(err)}) continue } result.SuccessCount++ } case "reactivate": for _, userID := range userIDs { if err := s.ReactivateUser(ctx, actorSub, userID); err != nil { result.Failed = append(result.Failed, bulkUserFailure{UserID: userID, Message: bulkErrorMessage(err)}) continue } result.SuccessCount++ } case "delete": for _, userID := range userIDs { if err := s.DeleteUser(ctx, actorSub, userID); err != nil { result.Failed = append(result.Failed, bulkUserFailure{UserID: userID, Message: bulkErrorMessage(err)}) continue } result.SuccessCount++ } case "set_role": role := strings.TrimSpace(req.Role) for _, userID := range userIDs { if _, err := s.SetUserRole(ctx, actorSub, userID, role); err != nil { result.Failed = append(result.Failed, bulkUserFailure{UserID: userID, Message: bulkErrorMessage(err)}) continue } result.SuccessCount++ } case "add_to_group": groupID := strings.TrimSpace(req.GroupID) if groupID == "" { return bulkUsersResult{}, fmt.Errorf("group_id required") } if err := s.AddUsersToGroup(ctx, actorSub, groupID, userIDs); err != nil { return bulkUsersResult{}, err } result.SuccessCount = len(userIDs) case "remove_from_group": groupID := strings.TrimSpace(req.GroupID) if groupID == "" { return bulkUsersResult{}, fmt.Errorf("group_id required") } if err := s.RemoveUsersFromGroup(ctx, actorSub, groupID, userIDs); err != nil { return bulkUsersResult{}, err } result.SuccessCount = len(userIDs) default: return bulkUsersResult{}, fmt.Errorf("invalid action") } s.logAudit(ctx, actorSub, "bulk_users_action", map[string]any{ "action": action, "success_count": result.SuccessCount, "failed_count": len(result.Failed), "group_id": strings.TrimSpace(req.GroupID), "role": strings.TrimSpace(req.Role), }) return result, nil } func bulkErrorMessage(err error) string { if errors.Is(err, ErrNotFound) { return "not found" } if errors.Is(err, platformusers.ErrLastPlatformAdmin) { return "cannot remove the last platform admin" } if err == nil { return "" } return err.Error() } func dedupeNonEmpty(ids []string) []string { seen := make(map[string]struct{}, len(ids)) out := make([]string, 0, len(ids)) for _, id := range ids { id = strings.TrimSpace(id) if id == "" { continue } if _, ok := seen[id]; ok { continue } seen[id] = struct{}{} out = append(out, id) } return out }