Last active
July 27, 2023 09:41
-
-
Save dmorosinotto/9160d5fa9be72dc3edbc362a04d6b7f2 to your computer and use it in GitHub Desktop.
*hasRole="ROLE" Angular structural directive
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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