Anchor for Header

Kentico Xperience with React

by Brett Andrew
10/08/2020

As a Kentico partner for 6 years now and having just completed some major investments in React for our own projects, we have now started putting the two together.

The combination of the two provide for a real thrilling experience for content editors and end-users.

Kentico Xperience is the award-winning digital experience platform that combines content management, digital marketing, and commerce, on-premises or in the cloud.   

Anchor for Text


When we looked at the ability to use React as a front end for Xperience, we had some genuine real excitement.

Firstly, what this means is you have to build everything on the front end yourself, you get nothing out of the box from Xperience, but what you do get is a million out of the box components with React.

Like React-Bootstrap for example. Just using this single plugin alone gives you industry leading responsive design and a good selection of controls.

But how do you connect to Kentico itself? Well this is where this page is designed to help. You don't need to use a 2nd site license like the RAZOR MVC site does, instead you extend the CMS and create API's on the CMS side.

This first step, which is well documented by Kentico is the starting point to unleashing all the power of Kentico, after following these instructions  you create the starting point for an entire catalogue of web api's that your react application can connect to.

https://docs.kentico.com/k12/integrating-3rd-party-systems/using-asp-net-web-api-with-kentico 

Once you have your custom API working, you can then call the Kentico API from your API like this hello world example

Anchor for DisplayCode
  [HttpGet]
        public HttpResponseMessage HelloWorld()
        {
            string hello = "Hello from " + CMS.SiteProvider.SiteContext.CurrentSite.SiteName;
            return Request.CreateResponse(HttpStatusCode.OK, hello );
        }
Anchor for Text

That small example uses the inbuilt Xperience API to get the site name, just like that you can now use all the other API's available in your custom Web Api. Here is a link to some more examples from Xperience:

 https://docs.kentico.com/api12 

For REACT, I created a brand new React project that works well in Visual Studio using this link

https://code.visualstudio.com/docs/nodejs/reactjs-tutorial

I'm not going to dive into details on how to setup Kentico and React, but I will show you how to call that API from React.



Anchor for DisplayCode
import axios from 'axios';

export default class Home extends Component {

    constructor() {
       super();
       this.state = {
           loading: true
       };
        this.refresh();
   }

refresh =async()=> {
   axios.defaults.withCredentials = true;
  axios.defaults.crossDomain = true;     
        var x = await axios.get(`http://localhost:51872/customapi/CustomWebAPI/Login`);
        console.log('HelloWorld: ' +  x);
}
Anchor for Text

CORS

So in the above react call, we use WithCredentials=True; using this will allow your cookies to be saved on the users browser as well as be accessible when subsequent calls to your API are made, which is where the CMS puts authentication and other things, so we need it!

Now, learn from the master here (of bashing his head against a wall for hours), one thing I learnt during this journey is that you can't run away from CORS:

CORS is designed to keep cookies secret between a websites and the browser, preventing other sites from reading your cookies and probably trying to hack your user accounts. 

Building a CMS and a REACT site on two different domains (which in my example is what I did, but not that you have to do it too), means cookies are going to be on a different URLs and CORS won't like that. If you can work out a way for your REACT site to be deployed under your CMS site URL (you could host your React public files in any folder under the CMS site really), you can do that too, it depends on your change management strategy and architecture.

But this can also be solved in two other ways that I know about; firstly for local development we put the CORS details for our react site in the web.config:


By putting the below Access-Control-Allow-Origin and these other Access-Control-Allow-* settings in the Xperience CMS web.config. In this example the React website was hosted at http://localhost:3000 - so adjust it to your react url, you can use https if your React site uses ssl.


Quick fact: did you know you can run react in ssl by running:   npm HTTPS=true npm start

Anchor for DisplayCode
 <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="http://localhost:3000" />
        <add name="Access-Control-Allow-Credentials" value="true" />        
        <add name="Access-Control-Allow-Headers" value="Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization" />
        <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE, OPTIONS" />
        
      </customHeaders>
    </httpProtocol>
Anchor for Text

When deploying, if you deploy to an AZURE App Service you can add multiple CORS through the CORS configuration on the App Service, it works really well and allows you to have more than one site connecting to your Kentico Xperience APIs / website.

Anchor for Text

So once you have those basics setup, it should take about 3 hours for a novice, here are some cool functions you can have that work great and keep authentication all wrapped up for you and managed by the Xperience CMS! These are tested and work well. Put these in a common class in the new Web API module / controller you created. They wrap the Kentico Authentication functions so you can call them easily from your code.

Anchor for DisplayCode
  public static UserInfo Login(String Username, String Password, string Currentsitename= "")
        {
            UserInfo user = null;

            if(Currentsitename=="")
            {
                Currentsitename = CMS.SiteProvider.SiteContext.CurrentSiteName;
            }

            // Attempts to authenticate user credentials (username and password) against the current site
            user = AuthenticationHelper.AuthenticateUser(Username, Password, Currentsitename);

            if (user != null)
            {
                // Authentication was successful

                // Sets the forms authentication cookie
                //SetCookie("UserID", user.UserID);
                return user;
            }

            return null;
        }



    public static BasicResponse resetPassword(string EmailAddress, string CurrentURL)
        {

            BasicResponse Response = new BasicResponse();
            Response.success = false;
            Response.message = "User not found";

            if (EmailAddress==null || EmailAddress.Length==0)
            {
                return Response;
            }


            List<UserInfo> Search = UserInfoProvider.GetUsers().Where(x => x.Email == EmailAddress).ToList();

            if (Search!=null && Search.Count >0)
            {
                foreach (UserInfo ThisUser in Search)
                {
                    if (ThisUser.Enabled)
                    {
                        //user found, prepare login
                        UserInfo newUser = new UserInfo();


                        string AuthToken = AuthenticationHelper.GetUserAuthenticationUrl(ThisUser, CurrentURL.Replace("/forgottenpassword","") + @"/changepassword").Replace("?authenticationguid=", "/");

                        //send that in an email

                        //get email body
                        string EmailBody = Common.EmailTemplateBuilder("Default", new { Subject = "Reset Email", Firstname = ThisUser.FirstName, Lastname = ThisUser.LastName, Username = ThisUser.UserName, Body= "You recently requested to change your password.Please click the below link to reset it. <br/><br/></br><a target='_blank' href='"+ AuthToken + "'> " + AuthToken + "</a>"});

                        Common.SendEmail(EmailAddress, "", "Password Reset", EmailBody);
                    }
                }

                Response.success = true;
                Response.message = "Reset password email sent!";
            }

            return Response;
            
        }


 public static Int32 CurrrentUser()
        {

            if (MembershipContext.AuthenticatedUser != null)
            {
                //make sure it is not public user
                if (MembershipContext.AuthenticatedUser.UserID != 65)
                {
                    return MembershipContext.AuthenticatedUser.UserID;
                }
            }

            return -1;
        }


 public static bool AddCMSUserRole(string RoleName, int UserID, DateTime ValidTo)
        {
            try
            {

                //if user already in role skip
                if (IsUserInRole(UserID, RoleName))
                    return true;

                if (String.IsNullOrEmpty(RoleName) || UserID <= 0)
                {
                    return false;
                }

                int siteId = CMS.SiteProvider.SiteContext.CurrentSiteID;
                // Find the role

                RoleInfo updateRole = RoleInfoProvider.GetRoleInfo(RoleName, siteId);
                if (updateRole == null)
                {
                    return false;
                }



                UserRoleInfo userRole = UserRoleInfoProvider.GetUserRoleInfo(UserID, updateRole.RoleID);
                if (userRole == null)
                {
                    if (ValidTo != null && ValidTo != DBMinDate)
                    {
                        UserRoleInfoProvider.AddUserToRole(UserID, updateRole.RoleID, ValidTo);
                    }
                    else
                    {
                        UserRoleInfoProvider.AddUserToRole(UserID, updateRole.RoleID);
                    }
                }
                else
                {
                    if (ValidTo != null && ValidTo != DBMinDate)
                    {
                        userRole.ValidTo = ValidTo;
                        UserRoleInfoProvider.SetUserRoleInfo(userRole);
                    }
                    else
                    {

                        userRole.ValidTo = CMS.Helpers.DateTimeHelper.ZERO_TIME;
                        UserRoleInfoProvider.SetUserRoleInfo(userRole);
                    }
                }


                return true;
            }
            catch (Exception ex)
            {



                LogEvent("Common", "CMSRoleUser_Add()", ex.Message.ToString() + " Source:" + ex.StackTrace.ToString());
            }

            return false;
        }
        /// <summary>
        /// Deletes Role from user. (CMS_UserRole table). This table is used for page security purposes.
        /// </summary>
        /// <param name="RoleName">RoleName</param>
        /// <param name="UserID">UserID</param>
        /// <param name="ValidTo">ValidTo</param>
        /// <returns>bool</returns>
        public static bool DeleteUserRole(string RoleName, int UserID)
        {
            try
            {


                //if user not in role, skip            
                if (!IsUserInRole(UserID, RoleName))
                    return true;

                if (String.IsNullOrEmpty(RoleName) || UserID <= 0)
                {
                    return false;
                }
                int siteId = CMS.SiteProvider.SiteContext.CurrentSiteID;
                RoleInfo role = RoleInfoProvider.GetRoleInfo(RoleName, siteId);
                if (role == null)
                {
                    return false;
                }
                UserRoleInfoProvider.AddUserToRole(UserID, role.RoleID);
                return true;
            }
            catch (Exception ex)
            {



                LogEvent("Common", "RoleUser_Delete()", ex.Message.ToString() + " Source:" + ex.StackTrace.ToString());
            }
            return false;

        }





        /// <summary>
        /// returns user Fullname
        /// </summary>
        /// <param name="UserID">UserID</param>
        /// <returns>string</returns>

        public static string getUserFullName(int UserID)
        {
            string FullName = string.Empty;
            if (UserID <= 0)
            {
                return FullName;
            }
            UserInfo userInfo = UserInfoProvider.GetUserInfo(UserID);
            if (userInfo != null)
            {
                return userInfo.FullName;
            }

            return FullName;
        }

        public static UserInfo getCurrentUserInfo()
        {

            Int32 UserID = CurrrentUser();

            if (UserID <= 0)
            {
                return null;
            }

            return UserInfoProvider.GetUserInfo(UserID);
            
        }


        public static string getUserName(int UserID)
        {
            string Username = string.Empty;
            if (UserID <= 0)
            {
                return Username;
            }
            UserInfo userInfo = UserInfoProvider.GetUserInfo(UserID);
            if (userInfo != null)
            {
                return userInfo.UserName;
            }


            return Username;
        }


        /// <summary>
        /// Checks if user is in role. 
        /// </summary>
        /// <param name="RoleName">RoleName</param>
        /// <param name="UserID">UserID</param>
        /// <returns>bool</returns>
        public static bool IsUserInRole(int userID, string RoleName)
        {
            try
            {
                if (userID <= 0 || RoleName.Length <= 0)
                {
                    return false;
                }
                int siteId = SiteContext.CurrentSiteID;
                RoleInfo role = RoleInfoProvider.GetRoleInfo(RoleName, siteId);
                if (role == null)
                {
                    return false;
                }

                UserRoleInfo userRoleInfo = UserRoleInfoProvider.GetUserRoleInfo(userID, role.RoleID);
                if (userRoleInfo != null && (userRoleInfo.ValidTo == null || userRoleInfo.ValidTo == CMS.Helpers.DateTimeHelper.ZERO_TIME || DateTime.Compare(userRoleInfo.ValidTo, DateTime.Now) > 0))
                {
                    return true;
                }
                else
                {
                    return false;
                }
            }

            catch (Exception ex)
            {


                LogEvent("Common", "CheckUserHasActiveRole()", ex.Message.ToString() + " Source:" + ex.StackTrace.ToString());
            }

            return false;
        }

        public static bool IsUserInRole(string roleName)
        {
            if (MembershipContext.AuthenticatedUser != null)
            {
                return IsUserInRole(MembershipContext.AuthenticatedUser.UserID, roleName);
            }

            return false;
        }

        public static bool IsUserInRoles(params string[] roleNameArray)
        {
            bool retVal = false;

            if (roleNameArray != null && roleNameArray.Count() > 0)
            {
                foreach (var role in roleNameArray)
                {
                    if (IsUserInRole(role))
                    {
                        retVal = true;
                        break;
                    }
                }
            }

            return retVal;
        }

Anchor for Text


I hope that helps! Leave a comment if it helped you or if you have any questions or need a consultant!

Brett Andrew

Enterprise Architect / Lead Developer / Director 

Formition Pty Ltd

Anchor for Contact Us

Contact us