Introduction
Hi friends, in this post we are going to learn on how we can restrict pages, lists or libraries for both internal & external users using SPFx extensions. There are many ways in hiding or restricting the content inside SharePoint but this method will be more flexible and dynamic for the end users.
Pre-requisites
For this extension to work, we are going to have a SharePoint list to maintain certain properties and based on the values of the properties, we will design the extension. I had created a list named Settings and below are the fields that are required
- Title – Default title field that comes with the list.
- ConfigValue – Multiline text with plain text to store the values.
Below are the 3 items that are required. The value of the title should not be changed but the values can be based on your requirement
- HomePage – Page to be redirected when the user visits the page that is restricted.
- RedirectPages – List of page names or keywords that will be present in the URL to check. If page names or keyword present in the URL, then they will be redirected to the HomePage
- RPAllowedUsers – Users that are allowed to access the RedirectPages, rest of the users are blocked from accessing the pages.
Focus on the Code
You can refer Part 1 post on the creation of the SPFx project and installing all the dependencies. I had used SPFx generator version 1.18.0 along with @pnp/sp & @pnp/logging. For this article we have to create a SPFx extension and choose the type as Application Customizer.
Below is the code from ApplicationCustomizer.ts file
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Log } from '@microsoft/sp-core-library';
import {
BaseApplicationCustomizer, PlaceholderContent, PlaceholderName
} from '@microsoft/sp-application-base';
import { SPFI } from "@pnp/sp";
import * as strings from 'RestrictPagesApplicationCustomizerStrings';
import RedirectToHome from './RedirectToHome';
import { getSP } from './pnp.config';
const LOG_SOURCE: string = 'RestrictPagesApplicationCustomizer';
/**
* If your command set uses the ClientSideComponentProperties JSON input,
* it will be deserialized into the BaseExtension.properties object.
* You can define an interface to describe it.
*/
export interface IRestrictPagesApplicationCustomizerProperties {
// This is an example; replace with your own property
testMessage: string;
}
/** A Custom Action which can be run during execution of a Client Side Application */
export default class RestrictPagesApplicationCustomizer
extends BaseApplicationCustomizer<IRestrictPagesApplicationCustomizerProperties> {
private sp: SPFI;
private static headerPlaceholder: PlaceholderContent | undefined;
private render() {
if (this.context.placeholderProvider.placeholderNames.indexOf(PlaceholderName.Top) !== -1) {
if (!RestrictPagesApplicationCustomizer.headerPlaceholder || !RestrictPagesApplicationCustomizer.headerPlaceholder.domElement) {
RestrictPagesApplicationCustomizer.headerPlaceholder = this.context.placeholderProvider.tryCreateContent(PlaceholderName.Top, {
onDispose: this.onDispose
});
}
this.startReactRender();
} else {
console.log(`The following placeholder names are available`, this.context.placeholderProvider.placeholderNames);
}
}
private startReactRender() {
if (RestrictPagesApplicationCustomizer.headerPlaceholder && RestrictPagesApplicationCustomizer.headerPlaceholder.domElement) {
const elem: React.ReactElement<{}> = React.createElement(RedirectToHome, {
sp: this.sp,
currentUser: this.context.pageContext.user.loginName
});
ReactDOM.render(elem, RestrictPagesApplicationCustomizer.headerPlaceholder.domElement);
} else {
console.log('DOM element of the header is undefined. Start to re-render.');
this.render();
}
}
public onInit(): Promise<void> {
Log.info(LOG_SOURCE, `Initialized ${strings.Title}`);
this.sp = getSP(this.context);
console.log(this.sp);
this.context.placeholderProvider.changedEvent.add(this, this.startReactRender);
this.render();
return Promise.resolve();
}
public onDispose() {
if (RestrictPagesApplicationCustomizer.headerPlaceholder && RestrictPagesApplicationCustomizer.headerPlaceholder.domElement) {
ReactDOM.unmountComponentAtNode(RestrictPagesApplicationCustomizer.headerPlaceholder.domElement);
}
}
}
There are few major changes done to the file.
- Created SPFI object for accessing the SharePoint lists.
- There is a separate component which is rendered for checking the access and redircting to the configured page.
- I had also used a separate ts file for getting the SP context and imported all the modules that are required using the selective import.
Below is the file named pnp.config.ts
/* eslint-disable no-var */
import { ApplicationCustomizerContext } from "@microsoft/sp-application-base";
// import pnp and pnp logging system
import { spfi, SPFI, SPFx as spSPFx } from "@pnp/sp";
import { LogLevel, PnPLogging } from "@pnp/logging";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";
// eslint-disable-next-line no-var
var _sp: SPFI;
export const getSP = (context?: ApplicationCustomizerContext): SPFI => {
if (context != null) {
//You must add the @pnp/logging package to include the PnPLogging behavior it is no longer a peer dependency
// The LogLevel set's at what level a message will be written to the console
_sp = spfi().using(spSPFx(context)).using(PnPLogging(LogLevel.Warning));
}
return _sp;
};
Below is the helper.ts file which has all the functions that are required.
import { SPFI } from '@pnp/sp';
import { useCallback } from 'react';
import { filter } from 'lodash';
import { Navigation } from 'spfx-navigation';
export const useHelper = (sp: SPFI) => {
const getSettings = useCallback(async (useCache?: boolean): Promise<any[]> => {
let retItems: any[] = [];
try {
retItems = await sp.web.lists.getByTitle('Settings').items
.select('Title', 'ConfigValue')();
} catch (err) {
throw err;
}
return retItems;
}, [sp]);
const getValueFromArray = (arr: any[], key: string, valToCheck: string, returnKey: string): any => {
if (arr && arr.length > 0) {
let fil: any[] = filter(arr, (o: any) => { return o[key].toLowerCase() == valToCheck.toLowerCase(); });
if (fil && fil.length > 0) {
return fil[0][returnKey];
}
}
return '';
};
const openURL = (url: string, newTab: boolean): void => {
if (newTab) window.open(url, newTab ? '_blank' : '');
else Navigation.navigate(url, false);
};
return {
getSettings,
getValueFromArray,
openURL
}
}
Below are the methods that are defined
- getSettings – Get the configured values from the settings list
- getValueFromArray – Get the value for the key from the array collection
- openURL – Redirect to the configured page if there is no access
Below is the action component that checks and does the redirection. I had named the component as RedirectToHome.tsx
import Dialog, { DialogType, IDialogContentProps } from '@fluentui/react/lib/Dialog';
import { Label } from '@fluentui/react/lib/Label';
import { Spinner, SpinnerSize } from '@fluentui/react/lib/Spinner';
import { SPFI } from '@pnp/sp';
import * as React from 'react';
import { useEffect, useState } from 'react';
import { useHelper } from './helper';
export interface IRedirectToHomeProps {
sp: SPFI;
currentUser: string;
}
const modelProps = {
isBlocking: true
};
const dialogContentProps: IDialogContentProps = {
type: DialogType.normal,
title: '',
subText: '',
showCloseButton: false,
styles: {
content: {
width: 'auto',
height: 'auto',
overflow: 'hidden'
}
}
};
const RedirectToHome: React.FC<IRedirectToHomeProps> = (props: IRedirectToHomeProps) => {
const { getSettings, getValueFromArray, openURL } = useHelper(props.sp);
const [hideDialog, setHideDialog] = useState<boolean>(false);
const _closeDialog = () => setHideDialog(true);
const checkForRedirect = async () => {
const settings = await getSettings();
console.log("Current User: ", props.currentUser);
let pagesToRestrict: string = getValueFromArray(settings, "Title", "RedirectPages", "ConfigValue");
let homePage: string = getValueFromArray(settings, "Title", "HomePage", "ConfigValue");
let allowedUsers: string = getValueFromArray(settings, "Title", "RPAllowedUsers", "ConfigValue");
pagesToRestrict.split(',').forEach((page) => {
if (window.location.pathname.toLowerCase().indexOf(page.toLowerCase()) > -1) {
if (allowedUsers.indexOf(props.currentUser) < 0) {
_closeDialog();
openURL(homePage, false);
}
} else _closeDialog();
});
};
useEffect(() => {
(async () => {
try {
await checkForRedirect();
} catch (err) {
console.log(err);
}
})();
}, []);
return (
<div>
<Dialog
hidden={hideDialog}
onDismiss={_closeDialog}
dialogContentProps={dialogContentProps}
modalProps={modelProps}
closeButtonAriaLabel={'Close'}
minWidth="200px"
maxWidth="200px">
<div>
<Label>Checking for access...</Label>
<Spinner size={SpinnerSize.large} label="Please wait..." />
</div>
</Dialog>
</div>
);
};
export default RedirectToHome;
In the above code, we had used the dialog component which will be displayed for all the users in all the pages in the site where the extension is deployed. Since this is a Application customizer, it will be loaded for all the pages. It will display a dialog showing Checking for access and then if the page is allowed for the user, the dialog will hide automatically if not it will redirect to the configured page.
Note: This extension will work only for the modern pages and not work for classic pages.
Reference URL
Conclusion
I hope the above solution will fix some issue few of the customers are facing. You can also improve the code by add the settings values to the cache since the settings will not change frequently and at the same time you can increase the performance so that users will not see much difference when the page is loaded.
Please like and comment if you like the article and also subscribe to my blog and my youtube channel.