I came across a situation in angular application where user should be restricted to edit/modify fields in the page based on user role. I like to share the idea I implemented. I hope it will be really helpful for some of you.

I am gonna take this below screen as an example, Let’s say we have 3 tab contents are loaded via routes in User Settings page.

  • Personal Detail Tab
  • Contact Detail Tab
  • Payment Detail Tab

When logged in user role is ADMIN or OWNER, they should be able to edit fields.

Tab details are loaded via routes

When logged in user role is SUPPORT, we should block UI so that we can prevent the user to edit/modify fields. But they can view the values in the field.

Simple and scalable solution I used to solve this problem is composed of four-piece of code

  • A Rule ( Simple JSON object )
  • A Service to run through page access rules based on logged user role
  • A directive to listen for route change, pass page name to rule engine service and toggle host element class
  • A CSS class to block UI and prevent click action

Step 1: Define a rule

ADMIN, OWNER, SUPPORT are user roles. I used the user role as KEY in my rule JSON file. In the next level down I used the route config path as KEY. Last level down I used canEdit as property to differentiate that user can edit the page or not.

 // personal, contact & payment are route config path
private RULES_BY_ROLE = {
        // ADMIN is user role
        ADMIN: {
            // personal is route config path
            personal: {
                canEdit: true
            },
            contact: {
                canEdit: true
            },
            payment: {
                canEdit: true
            }
        },
        SUPPORT: {
            personal: {
                canEdit: false
            },
            contact: {
                canEdit: false
            },
            payment: {
                canEdit: false
            }
        },
        OWNER: {
            personal: {
                canEdit: true
            },
            contact: {
                canEdit: true
            },
            payment: {
                canEdit: true
            }
        }
    };
Application Route config

Step 2: Create a Service to check rules based on user role

import { Injectable } from '@angular/core';
import { AuthService } from './auth.service';

@Injectable()
export class RuleEngineService {

    private RULES_BY_ROLE = {
        ADMIN: {
            personal: {
                canEdit: true
            },
            contact: {
                canEdit: true
            },
            payment: {
                canEdit: true
            }
        },
        SUPPORT: {
            personal: {
                canEdit: false
            },
            contact: {
                canEdit: false
            },
            payment: {
                canEdit: false
            }
        },
        OWNER: {
            personal: {
                canEdit: true
            },
            contact: {
                canEdit: true
            },
            payment: {
                canEdit: true
            }
        }
    };

    constructor(private authService: AuthService) { }

    doCheckPageAcess(pageName: string): boolean {
        if (!pageName) {
            return false;
        }
        const role = this.authService.getUser().role;
        const matchedRule = this.RULES_BY_ROLE[role];
        if (!matchedRule) {
            return;
        }
        const pageRule = matchedRule[pageName];
        return pageRule.canEdit;
    }

}
  • First I will lookup RULES_BY_ROLE json object based on user role
  • Second I will lookup matchedRule based on pageName
  • Third I will return boolean value of the property value “canEdit”

Step 3: Create a Directive to listen route change and toggle protected class

import { Directive, OnInit, Input, HostBinding } from '@angular/core';
import { Router, NavigationEnd, ActivatedRoute } from '@angular/router';
import { filter } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { RuleEngineService } from '../rule-engine.service';

@Directive({ selector: '[appCanAccess]' })
export class CanAccessDirective implements OnInit {

    private navStart: Observable<NavigationEnd>;

    @HostBinding('class.protected') canBlock = false;

    constructor(private router: Router, private rulesEngine: RuleEngineService) {
        this.navStart = router.events.pipe(
            filter(evt => evt instanceof NavigationEnd)
        ) as Observable<NavigationEnd>;
    }

    ngOnInit() {
        this.doAccessCheck();

        this.navStart.subscribe(() => {
            this.doAccessCheck();
        });
    }

    private doAccessCheck() {
        const urlTree = this.router.routerState.snapshot.url;
        // Extract the page name from url
        const pageName = urlTree.split('/').pop();
        // If page has access, dont add 'protected' class to host element
        this.canBlock = !this.rulesEngine.doCheckPageAcess(pageName);
    }
}

In the directive, I am listening for router NavigationEnd event and calling doCheckPageAccess() method in RulesEngineService to know if the page should be blocked or not.

When a page doesn’t have access for the logged-in user, the service will return false. I will add class “protected” to the host element otherwise, I will remove class “protected”

Step 4: A CSS Class to block the UI

ui.protected.segment{
    user-select: none;
    pointer-events: none;
    cursor: not-allowed;
}
.ui.protected.segment::before{
    content: " ";
    display: block;
    background-color: #00000040;
    height: 100%;
    position: absolute;
    width: 100%;
    z-index: 1;
    margin: -1em;
}

In CSS Class, couple of things I am doing. First I am creating new pseudo element which is same height & width of the parent and it will mask the actual DOM.

Second I used pointer-events property to prevent any click event on the DOM.

Note :

  • I mocked AuthService to mimic the behavior testing.
  • Just restricting UI is half of the work is done. Please don’t forget to protect API & Data layer ( Back end ).

You can download the full source in my github repo


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.