Nebular is a UI libary from Akveo company for Angular. It can be customized for meeting your needs and has many components and visual themes. It also has authenticate and security modules. NgxAdmin is an admin dashboard built based on Nebular. NgxAdmin is also customizable.
Key tip: I use isGranted()
of NbAccessChecker
from Nebular’s security package, canLoad
and canActivate
directive from Angular framework to achieve features inside this article.
I had been requested to create mechanisms of switching modules' visibility and activation for login-accounts with legacy code of a product from a stakeholder, which were built on Nebular and starter-kit branch of NgxAdmin. After taking trial and efforts of up to couple of months, I have finished implementation of the features and then meet requirements. The product is started to develop from an early version at 2.0.0-rc9 of NgxAdmin and Nebular. Nebular’s security package provides two categories of access control mechanism with role, resource and permission concept. First one is that whole mechanism fully integrated and controlled by backend API, the other one is frontend side offloads part of mechanism from backend-side. Considering rushed schedule of rolling out for products and unsure whether there has successful cases for first one at issue threads of the repository at GitHub.com or not, I chose to the latter.
There are around 4-level permissions ranging from guest, user, manager and admin. Guest can login and it only can see welcome page. Login users and related manager can see content and entries of modules they have purchased and see only entries at side-menu about not purchased modules if admin wants to attract them to buy ones and only set their permission towards modules to be visible and inactivated. In a module, there may exist some content or entries that a manager can access but its user cannot access. For example, we assume there has 2 modules: moduleA and moduleB can be purchased and the extra module is admin-permission-setting for admin, which is not purchased, hidden and prohibit accessed by user and manager-role level account. All modules are lazy-loaded with Angular. Admin role can access all content and entries.
admin | manager | user | guest | |
---|---|---|---|---|
welcome page | O | O | O | O |
module A | ||||
entry A1 | O | O | O | |
entry A2 | O | O | ||
entry A3 | O | |||
Module B | ||||
entry B1 | O | O | O | |
entry B2 | O | O | O | |
entry B3 | O | O | ||
admin-permission-setting module | ||||
entry C1 | O |
table for relationship between modules and roles
Notice: If a manager and its user are set for activated and visible for module A and B, above table describe entries' accessibility for them. It also provide information for admin and guest role.
Following are described and tested at environment of Angular 7.2.4 and Nebular 3.3.0. The structure of files and folders is similar with starter-kit branch of NgxAdmin at GitHub, so you can refer it.
List of files for usage and described at below:
src/app/@core/core.module.ts
-> already exists in ngx-admin’s starter-kit branchsrc/app/services/role-provider.service.ts
-> I create for this featuresrc/app/pages/page-menu.ts
-> already exists in ngx-admin’s starter-kit branchsrc/app/pages/pages-routing.module.ts
-> already exists in ngx-admin starter-kit branchsrc/app/pages/pages.component.ts
-> already exists in ngx-admin starter-kit branchsrc/app/pages/moduleA/moduleA-routing.module.ts
-> already exists in product legacy codesrc/app/services/entry-checking-with-role-permission.service.ts
-> I create for this featuresrc/app/guards/entry-routing-acl.guard.ts
-> I create for this featurecore.module.ts
Under accessControl
of NbSecuritryModule.forRoot({})
, it’s for defining roles, visibility and activation of modules.
Excerpt content of core.module.ts
for roles:
NbSecurityModule.forRoot({
accessControl: {
// ...(skipped content )
guest: {
view: ['welcome'],
load: ['welcome'],
},
user: {
parent: 'guest',
},
manager: {
parent: 'user',
edit: ['moduleA', 'moduleB'],
},
admin: {
parent: 'manager',
admin: ['moduleA'],
view: '*',
load: '*',
},
},
}).providers;
From above content, guest
is a role name, it has two permissions about view
and load
. I define view
permission for a role here that describes an entry of side-menu or a resource is visible. I define load
permission for a role here that describes a resource is enable.
Notice: Correspond value to view/load/admin permission can be string-type or array of string. This information comes from tutorial of Nebular security.
A role can only inherit from single other role. So from above content, inherit chain of roles is guest be inherit to user role, manager inherit from user role, and admin inherit from manager.
Notice: view
, load
, admin
and edit
at permission section of roles and moduleA
, moduleB
and welcome
at corresponded value with permissions, which are defined above example, can be defined by yourself. There is no other constraints for usage of Nebular security package except keyword: parent in attribute’s name and asterisk symbol in attribute’s value.
For section of admin role, values of its view and load permissions are both asterisk symbol. Asterisk symbol means all resources of this web system. It’s convenient for the situation of this web system is adding new modules in the future, and there is no need to change values for correspond permissions. If value here changes to array of string-type, it’s needed to add new value to the array when the web system is adding new modules or resources in the future.
Manager role has edit
permission and corresponded values of resources: moduleA
and moduleB
for presenting it can access entries of A2 and B3 that user role can’t. Similarly, Admin role has admin
permission and corresponded value of resources: moduleA
for presenting it can access the entry of A3 that manager role can’t. You must understand that, mentioned relationship table and content of data
parameter inside page-menu.ts
file together. I will state page-menu.ts
later.
Excerpt content of core.module.ts
for visibility and activation of modules:
// in accessControl section of NbSecurityModule.forRoot()
moduleAShow: {
view: ['moduleA'],
},
moduleAActivate: {
load: ['moduleA'],
},
moduleBShow: {
view: ['moduleB'],
},
moduleBActivate: {
load: ['moduleB'],
},
For achieving goals about visibility and activation of a module, I set two roles with related permission and resource. Again, view
is used for describing an entry of side-menu or a resource, which is at correspond attribute value section, can be saw on UI layout. load
permission is used for a resource can be activated and then be accessible. A resource, which is defined for examples in this article, ranges from a module, a link within a module/routing module, to an element on UI layout. In my principles, I usually take the name of a module as a resource at attribute’s value.
From content of core.module.ts
for visibility and activation of modules, moduleAShow
role has view permission that makes entries of moduleA at sidemenu UI can be saw by accounts with that. moduleAActivate
role has load permission that makes features, links and pages of moduleA can be accessible by accounts with that. Roles of moduleBShow
and moduleBActivate
have similar meanings for moduleB.
role-provider.service.ts
From instructions of Nebular’s security package, you must create it manually if file content of your web system does not have role-provider.service.ts
file. You also need to provide getRole()
and implementation within it. In the content, you can integrate related backend API(s) for your system and getRole()
must provide names of roles that a login account has, which are including module’s visibility and activation. Notice: If backend API(s) cannot directly provide name of roles that for corresponded visibility and activation of a module, you can write convert functions for transforming content of API’s response to meet needs for front-end usage at front-end side.
Excerpt content of role-provider.service.ts
@Injectable()
export class RoleProviderService implements NbRoleProvider {
constructor() {} // skip content
getRole(): Observable<string | string[]> | string | string[] {
/**
* Implement integration with related backend API(s).
* You must return things for fitting above return-types. In my experience, I only return Observable<string | string[]> with role names the login account has, and they must aligned with ones already defined at core.module.ts .
* Notice: If there is a need for converting backend's response to fit front-end needs,
* you can write your own functions for conversion.
*/
}
}
Notice1: getRole()
will be called automatically when you are using isGranted()
of NbAccessChecker
and subscribe result of isGranted()
. So it will send requests to backend API as you subscribing results of isGranted(). Notice2:
getRole()` can return an observable-type of array of strings and single string. It supports multiple roles within an account. An account often has a combination of multiple roles such as roles of manager, moduleAShow and moduleAActivate, or another one of user, moduleBShow, and mobuleBActivate.
page-menu.ts
Excerpt content of page-menu.ts
export const MENU_ITEMS: NbMenuItem[] = [
{
title: 'Welcome',
icon: 'home-outline',
link: 'pages/welcome',
home: true,
key: 'welcome',
data: {
permission: 'view',
resource: 'welcome',
},
},
{
title: 'module A',
icon: 'lock-outline',
key: 'module A',
data: {
permisssion: 'view',
resource: 'moduleA',
},
children: [
{
title: 'entry A1',
link: '/pages/module-A/A1',
key: 'A1',
},
{
title: 'entry A2',
link: '/pages/module-A/A2',
key: 'A2',
data: {
permission: 'edit',
resource: 'moduleA',
},
},
{
title: 'entry A3',
link: '/pages/module-A/A3',
key: 'A3',
data: {
permission: 'admin',
resource: 'moduleA',
},
},
],
},
{
title: 'module B',
icon: 'home-outline',
key: 'module B',
data: {
permission: 'view',
resource: 'moduleB',
},
children: [
{
title: 'entry B1',
link: '/pages/module-B/B1',
key: 'B1',
},
{
title: 'entry B2',
link: '/pages/module-B/B2',
key: 'B2',
},
{
title: 'entry B3',
link: '/pages/module-B/B3',
key: 'B3',
data: {
permission: 'edit',
resource: 'moduleB',
},
},
],
},
{
title: 'Admin permission setting',
icon: 'lock-outline',
key: 'permission-setting',
data: {
permission: 'view',
resource: 'admin-permission-setting',
},
children: [
{
title: 'entry C1',
link: '/pages/admin-permisssion-setting/C1',
key: 'C1',
},
],
},
];
I define a module can be saw(visibility) at side-menu area about U.I. by using parameter of data object, which contains values of permission and resource. It’s recommended for you to read and understand content of this file with the table for relationship between modules and roles and also includes in role section of core.module.ts
.
In principle, I define a module’s visibility at side-menu area at its top-level section, so all of its children have same visibility with same permission and resource without explicitly adding same data parameter to each of them. If a child under a module has extra requirement for visibility such as entry A2 and B3 cannot be saw by user role but can be saw by manager and admin, I will assign a data object to this child with different permission and role comparing to the module’s top-level one. If a child under a module cannot be saw by roles of user and manager but can be saw only by admin such as entry A3, I will add a data object explicitly to it with different permission and resource.
Again, the module of admin-permission-setting is only accessible by admin role, so I add a data object to this module’s top-level with its permission and resource. This module is static and only visible for admin role, so it would not be listed at core.module.ts
with other roles.
pages-routing.module.ts
I define a module’s activation at file pages-routing.module.ts
. In this case, all modules are lazy-loaded, so we assign a data object at each module’s top-level and component and use a directive canLoad
from Angular in this file for dynamically checking a module can be accessed.
I implement canLoad
logic for this feature from requests of a stakeholder in file entry-routing-acl.guard.ts
and I will state its content in the later part of this article.
Excerpt content of pages-routing.module.ts
const routes: Route = [
{
path: '',
component: PagesComponent,
children: [
{
path: 'welcome',
component: WelcomeComponent,
data: {
permission: 'load',
resource: 'welcome',
},
canLoad: [EntryRoutingACLGuard],
},
{
path: 'moduleA',
loadChildren: './moduleA/moduleA.module#moduleA',
data: {
permission: 'load',
resource: 'moduleA',
},
canLoad: [EntryRoutingACLGuard],
},
{
path: 'moduleB',
loadChildren: './moduleB/moduleB.module#moduleB',
data: {
permission: 'load',
resource: 'moduleB',
},
canLoad: [EntryRoutingACLGuard],
},
{
path: 'admin-permission-setting',
loadChildren:
'./adminPermissionSetting/adminPermissionSetting.module#AdminPermissionSetting',
data: {
permission: 'load',
resource: 'admin-permission-setting',
},
canLoad: [EntryRoutingACLGuard],
},
],
},
];
From above content of pages-routing.module.ts
, each module and component all has its data object and same canLoad
directive corresponded Guard: EntryRoutingACLGuard
. Each data object contains same permission: load
, which means its access permission toward a resource, and different resource name, which means the module represents its resource.
pages.component.ts
In starter-kit
branch of ngx-admin, this component load all entries, which are listed at page-menu.ts
to side-menu. For fulfilling the feature of modules' visibility, I add extra mechanism for dynamically setting visibility of an entry regards to different account and its role.
In this file, I call a function setVisibilityOfEntries()
coming from entry-checking-with-role-permission.service.ts
for setting visibility of a module and its children.
Excerpt content of pages.component.ts
export class PagesComponent {
menu = [];
constructor(private _entryChkr: EntryCheckingWithRolePermissionService) {}
ngOnInit() {
this.menu = MENU_ITEMS;
this.menu = this._entryChkr.setVisibilityOfEntries(this.menu);
}
}
moduleA-routing.module.ts
For fulfilling feature of checking activation of a module and its children, I add canActivate
directive with EntryRoutingACLGuard
routing guard to each child explicitly for dynamically checking whether they are accessible by a login account with its own roles or not. This meaning is same as declaration about a routing path needs to be checked.
Warning: If a child does not have canActivate
directive with EntryRoutingACLGuard
, it won’t be checked about role, resource and permission within a login account.
Excerpt content of moduleA-routing.module.ts
const routes: Routes = [
{
path: '',
component: ModuleAComponent,
children: [
{
path: 'A1',
component: A1Component,
canActivate: [EntryRoutingACLGuard],
},
{
path: 'A2',
component: A2Component,
canActivate: [EntryRoutingACLGuard],
},
{
path: 'A3',
component: A3Component,
canActivate: [EntryRoutingACLGuard],
},
],
},
];
entry-checking-with-role-permission.service.ts
This file is used for setting entries and sub-entries at side-menu hidden or show with result of NbAccessChecker
’s isGranted()
. I also implement a function: moduleIsLoadable()
for checking whether a module is loadable or not. If it returns true means Angular can load this module for the current login account. Otherwise, Angular cannot load the module for this login account.
Excerpt content of entry-checking-with-role-permission.service.ts
@Injectable()
export class EntryCheckingWithRolePermissionService {
constructor(private _acesChkr: NbAccessChecker) {}
/**
* Set visibility of a entry of side-menu.
*/
setVisibilityOfEntries(entries: MenuItem[]): MenuItem[] {
entries.forEach((element) => {
if (
element.data &&
element.data['permission'] &&
element.data['resource']
) {
this._acesChkr
.isGranted(element.data['permission'], element.data['resource'])
.subscribe((granted) => {
element.hidden = !granted;
});
} else {
// Set an entry to hidden by default if it does not provide information of permission and resource with data object.
element.hidden = true;
}
// set for sub-entry
if (
!element.hidden &&
item.children != null &&
item.children !== undefined
) {
element.children.forEach((subElement) => {
if (
subElement.data &&
subElement.data['permission'] &&
subElement.data['resource']
) {
this._acesChkr
.isGranted(
subElement.data['permission'],
subElement.data['resource']
)
.subscribe((granted) => {
subElement.hidden = !granted;
});
} else {
/**
* Align a sub-entry's hidden same as entry's hidden by default
* if data object of a sub-entry does not provide information of
* permission and resource.
*/
subElement.hidden = element.hidden;
}
});
}
});
return entries;
}
/**
* Answer a module is Loadable for an account.
*
*/
moduleIsLoadable(
route: Route
): boolean | Observable<boolean> | Promise<boolean> {
if (route.data && route.data['permission'] && route.data['resource']) {
/**
* Notice: RxJS 6 can use toPromise(), RxJS 7 still can use toPromise() but it becomes depricated.
* Start from RxJS 8, you cannot use toPromise() anymore, and it's recommended to use
* lastValueFrom() or firstValueFrom()
*/
return this._acesChkr
.isGranted(route.data['permission'], route.data['resource'])
.toPromise()
.then((resp) => {
if (resp === false) {
/**
* If you want to show message about cannot load this module,
* you can put your code about displaying message here.
*/
}
return resp;
});
}
/**
* If a route without data object, information of permission and resource, set it to be unloadable for keeping it a safe state.
*/
return false;
}
}
entry-routing-acl.guard.ts
This file is implemented for canLoad
and canActivate
directives. canLoad
directive, which is from Angular, is used for checking a lazy-loaded module can be loaded for a login account. It returns true and then load the module if a login account has adequate roles with permissions and resources. Otherwise, it returns false. It works if a tab of a browser is only used for single login account. canLoad
directive won’t work if multiple accounts, which have different activated modules, use same tab of a browser successively without refreshing page. So it’s time to introduce canActivate
directive for route-guard enhancement.
canActivate
directive will guard route paths of children under a module. canActivate
directive does following instructions in steps:
canActivate
function return false.isGranted()
of NbAccessChecker
with permission and resource information from data object at intent url. Transform result of isGranted()
from Observable
to Promise
and use then()
to handle what you want such as displaying a message, finally return this result of the then()
.isGranted()
of NbAccessChecker
. It’s also needed to transform result of isGranted() from
Observableto
Promiseand then use
then()to handle what you want such as displaying a message. Finally, please return result of the
then()`.
Excerpt content of entry-routing-acl.guard.ts
@Injectable()
export EntryRoutingACLGuard implements canLoad, canActivate {
private _menuEntries = [];
constructor(
private _itemChekr: EntryCheckingWithRolePermissionService,
private _acesChekr: NbAccessChecker,
) {
this._menuEntries = MENU_ITEMS;
this._menuEntries = this._itemChekr.setVisibilityOfEntries(this._menuEntries);
}
/**
* confirm a module is loadable for the login account
*/
canLoad(
route: Route, segments: UrlSegment[],
): Observable<boolean> | Promise<boolean> | boolean {
return this._itemChekr.moduleIsLoadable(route);
}
/**
* return part of resolved url at routing
*/
getResolvedUrl(route: ActivatedRouteSnapshot): string {
return route.pathFromRoot
.map(v => v.url.map(segment => segment.toString()).join('/'))
.join('/');
}
/**
* confirm a intent url can be routed for the login account
*/
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> | Promise<boolean> | boolean {
// get url that after the hashtag at browser's url field
let tmpPartOfUrl_1 = this.getResolvedUrl(next);
// get last part of url from the intent url
const childUrlStr = next.children.map(v => {
return v.url.map(part => part.toString()).join('/');
});
if (childUrlStr.length > 0) {
/**
* append childUrlStr to tail of tmpPartOfUrl_1 if length of childUrlStr is greater than zero
*/
tmpPartOfUrl_1 += `/${ childUrlStr }`;
}
/**
* replace continuous slashes to single slash.
*/
const partOfUrl = tmpPartOfUrl_1.replace(/\/\//gi, '/');
/**
* Find out matched link of children of items under _menuEntries
*/
let matchUrlElement: MenuItem = {title: ''};
for (const { title, link, children } of this._menuEntries) {
if (link === partOfUrl) {
/**
* Jump out the loop if it finds out an element at main item
*/
matchUrlElement = { title, link };
break;
}
if (children !== undefined) {
/**
* Further find out content of link if there exists sub-items.
* Jump out the loop if it finds out an element at sub-items.
*/
matchUrlElement = children.find(element => {
return element.link = partOfUrl;
});
if (matchUrlElement !== undefined) {
break;
}
}
}
if (matchUrlElement === undefined) {
// if there is no link same as the intent url, return false.
return false;
}
if (
(matchUrlElement.link !== partOfUrl) ||
(
(matchUrlElement.link === partOfUrl) &&
(matchUrlElement.hidden === true)
)
) {
/**
* Return false if a found link is not matched or
* the found link is matched but it's hidden for this login account
*/
return false;
}
/**
* Check whether the data object of intent url is existing or not.
* If it exists, and then use NbAccessChecker's isGranted() to
* check the route can be reached by Angular for this login account.
* Notice: If it doesn't exist, and then to check data object of the intent url's parent link.
*/
if (next.data !== undefined && Object.entries(next.data).length > 0) {
/**
* Examine a link from <module-name>-routings.module.ts can be routed for
* the login account's permission and resource
*/
return this._acesChekr.isGranted(next.data['permission'], next.data['resource']).toPromise().then(resp=> {
if (resp === false) {
/**
* Put your code here if you have a requirement about
* when Angular cannot route to the intent url for the login account.
*/
}
return resp;
});
}
/**
* Check whether next.parent['data'] is existing or not.
* If it exists, and then to use NbAccessChecker's isGranted() to check the route can be routed by Angular for the login account.
* Note: Use next variable(Angular's ActivatedRouteSnapshot-type) with
* NbAccessChecker's isGranted()
*/
if (
next.parent['data'] !== undefined &&
Object.entries()next.parent['data'].length > 0
) {
/**
* Examine a link from <module-name>-routings.module.ts can be routed
* for the login account's permission and resource
*/
return this._acesChekr.isGranted(next.parent['data']['permission'], next.parent['data']['resource']).toPromise().then(resp => {
if (resp === false) {
/**
* Put your code here if you have a requirement about
* when Angular cannot route to the intent url for the login account.
*/
}
return resp;
});
}
}
}
If you have done above things and reach here, congratulations! Your web system now have ability for setting visibility and activation of modules for all accounts.
Ref. 1: https://github.com/akveo/ngx-admin/issues/1779
Ref. 2: Inspired implementation from dynamic entries of sidemenu https://github.com/akveo/nebular/issues/274#issuecomment-399325274
Ref. 3: Nebular ACL configuration and usage https://akveo.github.io/nebular/docs/security/acl-configuration--usage#acl