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.
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
[HttpGet]
public HttpResponseMessage HelloWorld()
{
string hello = "Hello from " + CMS.SiteProvider.SiteContext.CurrentSite.SiteName;
return Request.CreateResponse(HttpStatusCode.OK, hello );
}
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.
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);
}
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
<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>
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.
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.
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;
}
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
Powered by mition