Blog Post

Developer Blog
13 MIN READ

How We Built It: Private Member Info component

SuzieH's avatar
SuzieH
Khoros Alumni (Retired)
4 years ago

We all know that protecting Community users' personal information is vital. That's why the most sensitive user profile data is visible only to users with the Administrator role. That said, a few profile fields enable Khoros staff members to troubleshoot questions better and help customers more effectively.

On Atlas, we allow a select set of Khoros employees to see a user's email, company, first name, last name, and roles in a  Private Member Info component. We display this component on the View Profile Page. Clearly, we don't want every Khoros employee to see this protected data. Therefore, we display the component only to users with a specific role.

Today, we're going to walk through how our Atlas engineers built the Private Member Info component.

Component workflow

The logical flow of our Private Member Info components looks like this:

APIs used

Our Private Member Info component uses FreeMarker context objects to make requests to the Community REST API. These are the FreeMarker and Community APIs we use:

Freemarker

  • restadmin - to make a request to the Community REST API as an administrator
  • restBuilder - to build and send a LiQL query as a request to the Community REST API
  • page.context.user - to get information about the user associated with the user profile page being viewed
  • user.id - to get the ID of a user

Community REST API v1

Code walkthrough

I'm going to walk through our Private Member Info component section by section. You can check out the full component code with annotations and our custom styles at the end of the article.

Determine whether a user meets the requirements

For this component to render, the user viewing the page must be registered and must have the Staff role assigned. The goal of this snippet is to verify that the user viewing the page meets these requirements.

 

 

1 <#if user.registered>
2 <#assign isStaff = false>
3 <#assign roles = restadmin("/users/id/" + user.id?c + "/roles").roles.role>
4 <#assign roleSize = roles?size>
5 <#if (roleSize > 0) >
6    <#list roles as role>
7        <#if role.name?trim == "Staff">
8             <#assign isStaff = true>         
9        </#if>
10   </#list>
11 </#if>

 

 

Line 1: We call the user.registered FreeMarker method to determine the registration status. This method returns a boolean (true or false).

Line 2: We create a variable named isStaff and set it to false until we determine whether the user has the required role.

Line 3: We create a variable named roles. This will hold the roles of the user viewing the page. To set the value, we use the restadmin FreeMarker method to call the Community API v1 /users/id/<id>/roles endpoint.

Pro Tip: Notice how we build the endpoint path passed to restadmin. We add the correct user ID to the endpoint path with the user.id FreeMarker method. That ?c you see in user.id?c is the FreeMarker c built-in. It is a convenience method that converts the ID to a proper format needed for processing the code.

Lines 4-10: Finally, if the user has roles assigned, we loop through them with the list directive. If the role name matches "Staff", we set the isStaff variable to true. The ?trim you see in <#if role.name?trim == "Staff"> is another builtin. It removes any whitespace before and after the role name. This ensures that only the role "Staff" triggers setting isStaff to true.

Retrieve data for the profile being viewed

We've established whether the user viewing the profile page is both registered and has the Staff role. Now, we're going to get the email, first name, and last name of the user whose profile page is being viewed from the database.

The goal of this snippet is to define a LiQL query and send it in a request to Community API v2. We use Community API v2 so that we can retrieve multiple fields with one request.

 

 

1 <#if isStaff>
2    <#assign userQry = "SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}'" />
3    <#assign pageUser = (restBuilder().admin(true).liql(userQry).data.items)![] />
4    <#if pageUser?size gt 0>
5     <#assign pageUser = pageUser[0] />

 

 

Line 1: If isStaff is true, then proceed. The remainder of the component code is contained in this if statement. If isStaff is not true, the component is not rendered on the page.

Line 2: We assign our LiQL query to a variable called userQry. This is the query:
SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}'

Pro Tip: See how we use a FreeMarker interpolation (${page.context.user.id}) to define the value of the WHERE clause in our LiQL query? The page.context.user FreeMarker method lets us get the user object associated with the current page. Once we have that user object, we can use any of the methods on the FreeMarker user context object -- in this case, the id method. When the LiQL query is processed server-side, the query will return results based on the ID of the user whose profile page is being viewed. (If we had used ${user.id}, the query would return the ID of the user viewing the page, and we would receive and display the wrong user's data.

Line 3: We make a request to Community API v2 to assign the response as the value of a variable called pageUser. We're going to use the pageUser variable later when we display the name and email address in the component. The request in this line uses the restBuilder FreeMarker context object. The methods on restBuilder include a handy way to pass a LiQL query and make the GET request as an administrator.

Component styling

This next snippet adds styling for the component. (We've actually reused classes from other components.) One thing to note is that we have hard-coded the component title:

 

 

class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub>

 

 


Consider creating a custom text key to hold the title text. You can use the text FreeMarker context object to retrieve and display the value of the text key. For example:

 

 

class="lia-panel-heading-bar-title">${text.format("private-member-info-title")}</span><sub>${text.format("private-member-info-subtitle")}</sub>

 

 

Here is our styling. See the CSS  section for our custom style definitions.

 

 

<div class="lia-panel lia-panel-standard PrivateStatisticsTaplet Chrome lia-component-users-widget-my-private-statistics">
<div class="lia-decoration-border">
<div class="lia-decoration-border-top">
   <div> </div>
</div>
<div class="lia-decoration-border-content">
<div>
<div class="lia-panel-heading-bar-wrapper">
   <div class="lia-panel-heading-bar"><span class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub></div>
</div>
<div class="lia-panel-content-wrapper">
<div class="lia-panel-content">
<div id="myPrivateStatisticsTaplet" class="MyStatisticsTaplet">
<div class="MyStatisticsBeanDisplay">

 

 

Display the email and verification indicator

Let's use that pageUser variable that holds the email address and first/last name of the profile being viewed. In this snippet, we display the email address, if one exists, and an icon indicating whether the email address is verified or not.

Pro Tip: Check out the attempt/recover blocks used to catch and handle errors. For example, we call restadmin to retrieve the value of the email_verified setting in an attempt block. If that call fails, we set the value to false manually in a recover block. We talk about error handling more in the FreeMarker section of Community customization and performance best practices.

 

 

1           <#assign showEmail = (pageUser.email)!'' />
2           <p><strong style="font-weight: bold">Email:</strong> <a href="mailto:${showEmail}">${showEmail}</a>
3                   <#attempt>
4                 <#assign email_verification = restadmin("/users/id/${page.context.user.id?url}/settings/name/user.email_verified").value!'false' />
5               <#recover>
6                 <#assign email_verification = false />
7               </#attempt>
8               <#attempt>
9                 <#assign email_verification = email_verification?boolean />
10               <#recover>
11                 <#assign email_verification = false />
12               </#attempt>
13               <#if email_verification>
14                 <span class="profile_email_verified" title="Email verified"></span>
15               <#else>
16                 <span class="profile_email_not_verified" title="Email NOT verified"></span>
17               </#if>
18               </p>

 

 

Lines 1-2: We create the showEmail variable and set it to the value of the email address ((pageUser.email)!''). We add the HTML to render and style the email address. If there is no email present, then we display an empty string.

Lines 4-7: We determine whether or not the email address is verified. The email_verified setting contains this information, so we make a request to Community API v1 using the restadmin context object to retrieve the value. (API v2 does not support retrieving settings by name.) We send the request to the /users/id/<id>/settings/name/{name} endpoint and we store the response in a variable called email_verification.

Lines 8-12: We convert the value of email_verification to a boolean.

Lines 13-17: We set a "verified" icon next to the email address if verified; otherwise, we set an "unverified" icon. These are Font Awesome icons defined in our custom CSS.

Display first and last name

This section is pretty straightforward. All we're doing here is retrieving the first name and last name from the pageUser element and displaying them. If there is no value for the name, we display an empty string.

 

 

<#attempt>
    <#assign nameFirst = (pageUser.first_name)!'' />
    <#assign nameLast = (pageUser.last_name)!'' />
    <p><strong style="font-weight: bold">First + Last Name:</strong> ${nameFirst!""} ${nameLast!""}</p>
    <#recover>
</#attempt>

 

 

Display company name

Our Private Member Info component displays the company associated with the profile being viewed. Company is not a default field on the user profile. It is a customer profile field that was set up for Atlas. Your company might have custom profile fields as well. These are generally set up during launch.

We retrieve the "company" profile field with a request to the Community API v1 /users/id/{id}/profiles/name/{profile_name} endpoint using the restadmin FreeMarker context object. After we retrieve the value, we display it with an interpolation (${showCompany}).

 

 

<#assign showCompany = (restadmin("/users/id/${page.context.user.id?url}/profiles/name/company").value)!'N/A'>
    <p><strong style="font-weight: bold">Company:</strong> ${showCompany}</p>

 

 

Display roles

In this final snippet, we list the roles associated with the user profile being viewed. We retrieve the roles with a request to the Community API v1 /users/id/{id}/roles endpoint. From there, we loop through the roles returned and present them in a list.

 

 

<#assign userroles = restadmin("/users/id/${page.context.user.id?url}/roles").roles.role>
           <strong style="font-weight: bold">Roles:</strong>
           <ul style="margin-left: 15px">
           <#list userroles as userrole>
           <#assign userRoleName = userrole.name?trim />
                   <li style="list-style: inside">${userRoleName}</li>
           </#list>            
           </ul>
           
</div></div></div></div></div></div><div class="lia-decoration-border-bottom"><div> </div></div></div>
</div>
</#if>
</#if>
</#if>

 

 

How I tested it

To build and test this component in my test environment, I made sure that I had a login to a staging environment that enables you to assign a role to a test user and to access:

  • Community Admin > Display > Skins
  • Community Admin > Content > Custom Pages
  • Studio > Community Style > CSS
  • Studio > Components
  • Studio > Page

I followed these steps:

  1. Choose a role in Community Admin to use as the required role (e.g., Staff).
  2. Assign the role to at least one test user (preferably not one with the Administrator role).
  3. In Studio > Components, create a custom component called custom.profile.staff-visible-details.
  4. Copy/paste the annotated code into the component text area.
  5. Search for the following lines of code and replace "Staff" with the name of the role you want to test with.
    <#list roles as role>
        <#if role.name?trim == "Staff">
            <#assign isStaff = true>
        </#if>
    </#list>
  6. Go to Studio > Page and place the custom.profile.staff-visible-details component on the View Profile Page used for your community.
    Pro Tip: Check to see whether your community uses a customer version of the View Profile Page in Admin > Content > Custom Pages. If your community uses a custom version of the View Profile Page, be sure to place the component on that quilt.
  7. Add the custom CSS for the component in the _style.scss file for my community skin in Studio > Community Style > CSS. Look in Community Admin > Display > Skins to view which skin your community uses.
  8. Test on your stage site. Log in as your test user and navigate to the View Profile Page for any community member. You should see the Private member Info component on the page in the location where you placed it on the quilt in step 6.

Custom CSS

These are the custom styles that we use for the component.

Atlas uses the Support Theme. My test community does not, so when I tested this in my local environment, I replaced the values for the color variable with HEX values. I added these styles to the _style.scss file for my community skin in Studio > Community Style > Community Skins > CSS.

 

 

#lia-body.ViewProfilePage {
  	.lia-component-users-widget-my-private-statistics {
      	.profile_email_verified:before {
            color: $theme-color-matcha;
            content: "\f058";
            font: normal normal normal 16px/1 FontAwesome;
      	}
        .profile_email_not_verified:before {  
            color: $theme-color-cerise;           
            content: "\f071";          
            font: normal normal normal 16px/1 FontAwesome;     
           }
      	#profile_sfdc_search_link:after {
            color: $theme-color-blue;
            content: "\f0c1";
            font: normal normal normal 16px/1 FontAwesome;
            margin-left: 3px;
      	}
      }
}

 

 

Annotated component code

 

 

<#--
Display a user's name, company, email, and roles to Khoros employees with the Staff role.
-->

<#-- Verify whether the user in context meets requirements to see the component. User must be registered and have Staff role. --> 

<#if user.registered>
<#assign isStaff = false>
<#assign roles = restadmin("/users/id/" + user.id?c + "/roles").roles.role>
<#assign roleSize = roles?size>
<#if (roleSize > 0) >
   <#list roles as role>
       <#if role.name?trim == "Staff">
            <#assign isStaff = true>         
       </#if>
   </#list>
</#if>

<#-- If the user has the Staff role, retrieve the email, first/last name, ID for the user profile being viewed. First we build the LiQL query to retrieve the data. Then, we make a request to the Community REST API passing our query -->
 
<#if isStaff>
<#assign userQry = "SELECT id, email, first_name, last_name FROM users WHERE id='${page.context.user.id}'" />
<#assign pageUser = (restBuilder().admin(true).liql(userQry).data.items)![] />
<#if pageUser?size gt 0>
<#assign pageUser = pageUser[0] />

<#-- Some styling to make this pretty. We're reusing the same styling as the out-of-the-box Private Statistics component. -->

<div class="lia-panel lia-panel-standard PrivateStatisticsTaplet Chrome lia-component-users-widget-my-private-statistics"><div class="lia-decoration-border"><div class="lia-decoration-border-top"><div> </div></div><div class="lia-decoration-border-content"><div><div class="lia-panel-heading-bar-wrapper"><div class="lia-panel-heading-bar"><span class="lia-panel-heading-bar-title">Private member info</span><sub>Staff only</sub></div></div><div class="lia-panel-content-wrapper"><div class="lia-panel-content"><div id="myPrivateStatisticsTaplet" class="MyStatisticsTaplet">
        <div class="MyStatisticsBeanDisplay">
        
		<#-- Display the hyperlinked email address if one exists. If email isn't verified, display an indicator. -->

           <#assign showEmail = (pageUser.email)!'' />
           <p><strong style="font-weight: bold">Email:</strong> <a href="mailto:${showEmail}">${showEmail}</a>
                   <#attempt>
                 <#assign email_verification = restadmin("/users/id/${page.context.user.id?url}/settings/name/user.email_verified").value!'false' />
               <#recover>
                 <#assign email_verification = false />
               </#attempt>
               <#attempt>
                 <#assign email_verification = email_verification?boolean />
               <#recover>
                 <#assign email_verification = false />
               </#attempt>
               <#if email_verification>
                 <span class="profile_email_verified" title="Email verified"></span>
               <#else>
                 <span class="profile_email_not_verified" title="Email NOT verified"></span>
               </#if>
               </p>
 
<#-- Display the first and last name of the user profile being viewed -->
           <#attempt>
                          <#assign nameFirst = (pageUser.first_name)!'' />
             <#assign nameLast = (pageUser.last_name)!'' />
             <p><strong style="font-weight: bold">First + Last Name:</strong> ${nameFirst!""} ${nameLast!""}</p>
               <#recover>
               </#attempt>
           
<#-- Display the name of the company associated with the user proview being viewed -->
               <#assign showCompany = (restadmin("/users/id/${page.context.user.id?url}/profiles/name/company").value)!'N/A'>
           <p><strong style="font-weight: bold">Company:</strong> ${showCompany}</p>
 
<#-- Display the roles of the user being viewed -->
           <#assign userroles = restadmin("/users/id/${page.context.user.id?url}/roles").roles.role>
           <strong style="font-weight: bold">Roles:</strong>
           <ul style="margin-left: 15px">
           <#list userroles as userrole>
           <#assign userRoleName = userrole.name?trim />
                   <li style="list-style: inside">${userRoleName}</li>
           </#list>            
           </ul>
           
</div></div></div></div></div></div><div class="lia-decoration-border-bottom"><div> </div></div></div>
</div>
</#if>
</#if>
</#if>

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Updated 4 years ago
Version 5.0
  • SuzieH's avatar
    SuzieH
    Khoros Alumni (Retired)

    Hi alexislbrown . This is what I heard back from the engineer who wrote this customization.

    So for this, I would suggest creating a new role (<special_member>) and assign to all those 40 members. And we will keep a check that members with this role can only see each other's info.

    When a member (out of these 40) wants to see info of any other community member would not be able to see it.

    There could be a separate condition to check whether a community (staff) member can see info of these 40 members as well if you wanted to allow for certain employees of your company to see the private info for the 40 partner members.

  • SuzieH I appreciate you following up on this! So essentially, it is possible to assign the component to only a certain user role and then make only those with the user role have visibility?