Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Last active July 27, 2023 09:41
Show Gist options
  • Save dmorosinotto/9160d5fa9be72dc3edbc362a04d6b7f2 to your computer and use it in GitHub Desktop.
Save dmorosinotto/9160d5fa9be72dc3edbc362a04d6b7f2 to your computer and use it in GitHub Desktop.
*hasRole="ROLE" Angular structural directive
import { Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef } from "@angular/core";
import { AuthService } from "./auth.service";
import { Subscription, Subject } from "rxjs";
@Directive({
selector: "[hasRole]",
standalone: true
})
export class HasRoleDirective {
constructor(
private auth: AuthService,
private _templateRef: TemplateRef<any>,
private _viewContainer: ViewContainerRef
) {}
//USE *hasRole="'AAA | BBB & CCC | !DDD'" flat condition with boolean logic like min-terms in Karnaugh map
// OR *hasRole="['EEE','!FFF']" with array it checks if ANY of roles conditions match - same as string 'EEE | !FFF'
// AND *hasRole="{GGG: false, 'H|I':false}" with object it checks if ALL the props(=role condition) match - like '(!GGG) & (H|I)'
private _role?: string | string[];
@Input() public set hasRole(role: string | string[]) {
if (this._role !== role) {
this._role = role;
this._updateView();
}
}
//USE *hasRole="'XXX' and data!=null"
// Support AND additional condition that MUST BE VERIFIED
private _condAnd?: boolean = true;
@Input() public set hasRoleAnd(cond: boolean) {
if (this._condAnd !== cond) {
this._condAnd = cond;
this._updateView();
}
}
//USE *hasRole="'YYY' or force==true"
//Support OR additional condition that alows FORCING SHOW (ignore roles if cond is true)
private _condOr?: boolean = false;
@Input() public set hasRoleOr(cond: boolean) {
if (this._condOr !== cond) {
this._condOr = cond;
this._updateView();
}
}
private _sub?: Subscription;
private _viewRef?: EmbeddedViewRef<any>;
private _updateView() {
this._cleanUp(this._sub);
// let visible = this.auth.hasRole(this._role);
this._sub = this.auth.hasRole$(this._role, this._destory$).subscribe(visible => {
if (this._condOr || (this._condAnd && visible)) {
if (!this._viewRef) {
this._viewRef = this._viewContainer.createEmbeddedView(this._templateRef);
}
} else {
this._cleanUp();
}
});
}
private _cleanUp(sub?: Subscription) {
if (sub) sub.unsubscribe();
if (this._viewRef) {
this._viewContainer.clear();
this._viewRef = undefined;
}
}
private _destroy$ = new Subject<void>
ngOnDestroy() {
if (this._destroy$) {
this._destroy$.next();
this._destroy$.complete();
}
this._cleanUp(this._sub);
}
}
//FAKE AUTH SERVICE
import { Injectable } from "@angular/core";
import { HttpRequest } from "@angular/common/http";
import { BehaviorSubject, Observable, identity } from "rxjs";
import { map, takeUntil } from "rxjs/operators";
@Injectable({
providedIn: "root"
})
export class AuthService {
private _authToken = new BehaviorSubject("");
public get isLoggedIn(): boolean {
return !!this._authToken.getValue();
}
public isLogged$ = this._authToken.pipe(map(Boolean));
public hasRole(role: string | string[] | Record<string,boolean>): boolean {
const userRoles = this._getUserRoles(this._authToken.getValue());
return this._userHas_ANYOrArray_ALLAndRecord(role, userRoles);
}
public hasRole$(role: string | string[] | Record<string,boolean>, destory$?: Observable<any>): Observable<boolean> {
return this._authToken.pipe(
map(token => {
const userRoles = this._getUserRoles(token);
return this._userHas_ANYOrArray_ALLAndRecord(role, userRoles);
}),
isObservable(destory$) ? takeUntil(destory$) : identity
);
}
private _getUserRoles(token: string): string[] {
//FAKE LOGIC
if (!token) return [];
else if (token.length%2) return ['USER']
else return ['USER','ADMIN']
}
private _normalizeRole = (role: string) => (role || '').toUpperCase().trim(); //NORMALIZE ROLE STRING
private _userHas_ANYOrArray_ALLAndRecord(roles: string | string[] | Record<string, boolean>, userRoles: string[]): boolean {
if (!roles) return !!userRoles?.length; //RETURN true IF USER IS LOGGED (userRoles HAS AT LEAST 1 ROLE ELEMENT)
const userROLES = (userRoles || []).map(this._normalizeRole);
if (typeof roles === 'string') {
//CHECK string -> USER HAS SPECIFIC ROLE - SUPPORT BOOL EXPRESSION: 'AAA|BBB&CCC|!DDD'
return this._checkRole(roles, userROLES);
} else if (Array.isArray(roles)) {
//CHECK ARRAY<string> -> USER HAS AT LEAST 1 OF THE ROLES - EVERY ARRAY ITEM CAN BE A BOOL EXPRESSION
return roles.some((role) => this._checkRole(role, userROLES));
} else {
//CHECK RECORD<string, boolean> -> USER HAS ALL THE ROLES SPECIFIED AS PROPS NAME IN THE OBJECT
//YOU CAN "NEGATE" CONDITION IF THE VALUE OF THE PROPO IS false es: { 'USER': true, 'ADMIN': false}
return Object.keys(roles).every((role) => this._checkRole(role, userROLES) == Boolean(roles[role]));
}
}
private _checkRole(role: string, userRoles: string[]): boolean {
//CAN TEST SOMETHING LIKE role='AAA | BBB & CCC | !DDD'
//FLAT CONDITION OF MIN-TERMS FROM KARNAUGH MAP ^_^
role = this._normalizeRole(role);
if (role.indexOf('|') > 0) {
//CHECK OR -> USER HAS AT LEAST ONE OF THE ROLES
const roles = role.split('|').filter(s=>s.trim());
return roles.some((role) => this._checkRole(role, userRoles));
} else if (role.indexOf('&') > 0) {
//CHECK AND -> USER HAS ALL THE ROLES
const roles= role.split('&').filter(s=>s.trim())
return roles.every((role) => this._checkRole(role, userRoles));
} else if (role.startsWith('!')) {
//CHECK NOT -> USER MUST NOT HAVE THE SPECIFIC ROLE
return !this._checkRole(role.substring(1), userRoles);
} else return userRoles.includes(role); //NORMALLY JUST CHECK IF USER HAS ROLE
}
public Login(username?: string, password?: string) {
//FAKE LOGIC
const FAKE_TOKEN = btoa(`${username}:${password}`);
this._authToken.next(`Bearer ${FAKE_TOKEN}`);
}
public Logout() {
this._authToken.next("");
}
public addAuthorizationHeader(req: HttpRequest<any>): HttpRequest<any> {
// Get the auth token from the service.
const token = this._authToken.getValue();
if (!token) return req;
// Clone the request and replace the original headers with
// cloned headers, updated with the Authorization (token).
return req.clone({
headers: req.headers.set("Authorization", "Bearer " + token)
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment