157 lines
5.1 KiB
TypeScript
157 lines
5.1 KiB
TypeScript
import {
|
|
ChangeDetectorRef,
|
|
Directive,
|
|
EmbeddedViewRef,
|
|
EventEmitter,
|
|
Input,
|
|
OnDestroy,
|
|
OnInit,
|
|
Output,
|
|
TemplateRef,
|
|
ViewContainerRef,
|
|
ɵstringify as stringify,
|
|
} from '@angular/core';
|
|
|
|
import { BehaviorSubject, combineLatest, merge, Observable, of, Subject, Subscription, switchMap } from 'rxjs';
|
|
import { map, tap } from 'rxjs/operators';
|
|
|
|
import { IqserRolesService } from './services/roles.service';
|
|
import { IqserPermissionsService } from './services/permissions.service';
|
|
import { List } from '../utils';
|
|
import { isBoolean } from './utils';
|
|
|
|
@Directive({
|
|
selector: '[allow]',
|
|
})
|
|
export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|
/**
|
|
* Assert the correct type of the expression bound to the `allow` input within the template.
|
|
*
|
|
* The presence of this static field is a signal to the Ivy template type check compiler that
|
|
* when the `IqserPermissionsDirective` structural directive renders its template, the type of the expression bound
|
|
* to `allow` should be narrowed in some way.
|
|
* For `allow`, the binding expression itself is used to
|
|
* narrow its type, which allows the strictNullChecks feature of TypeScript to work with `IqserPermissionsDirective`.
|
|
*/
|
|
static ngTemplateGuard_allow: 'binding';
|
|
|
|
@Output() readonly permissionsAuthorized = new EventEmitter();
|
|
@Output() readonly permissionsUnauthorized = new EventEmitter();
|
|
|
|
#permissions?: string | List;
|
|
#thenTemplateRef: TemplateRef<unknown>;
|
|
#elseTemplateRef?: TemplateRef<unknown>;
|
|
#thenViewRef: EmbeddedViewRef<unknown> | boolean = false;
|
|
#elseViewRef: EmbeddedViewRef<unknown> | boolean = false;
|
|
|
|
readonly #updateView = new Subject<void>();
|
|
readonly #subscription = new Subscription();
|
|
readonly #if = new BehaviorSubject<Promise<boolean> | Observable<boolean>>(of(true));
|
|
|
|
constructor(
|
|
private readonly _permissionsService: IqserPermissionsService,
|
|
private readonly _rolesService: IqserRolesService,
|
|
private readonly _viewContainer: ViewContainerRef,
|
|
private readonly _changeDetector: ChangeDetectorRef,
|
|
templateRef: TemplateRef<unknown>,
|
|
) {
|
|
this.#thenTemplateRef = templateRef;
|
|
|
|
const ifCondition$ = this.#if.pipe(switchMap(condition => condition));
|
|
|
|
this.#subscription = combineLatest([ifCondition$, this.#updateView])
|
|
.pipe(
|
|
switchMap(([ifCondition]) => this.#validateRolesAndPermissions().pipe(map(hasPermission => ifCondition && hasPermission))),
|
|
tap(isAllowed => (isAllowed ? this.#showThenBlock() : this.#showElseBlock())),
|
|
tap(() => this._changeDetector.markForCheck()),
|
|
)
|
|
.subscribe();
|
|
}
|
|
|
|
@Input()
|
|
set allow(value: string | List) {
|
|
this.#permissions = value;
|
|
this.#updateView.next();
|
|
}
|
|
|
|
@Input()
|
|
set allowThen(template: TemplateRef<unknown>) {
|
|
assertTemplate('allowThen', template);
|
|
this.#thenTemplateRef = template;
|
|
this.#thenViewRef = false;
|
|
this.#updateView.next();
|
|
}
|
|
|
|
@Input()
|
|
set allowElse(template: TemplateRef<unknown>) {
|
|
assertTemplate('allowElse', template);
|
|
this.#elseTemplateRef = template;
|
|
this.#elseViewRef = false;
|
|
this.#updateView.next();
|
|
}
|
|
|
|
@Input()
|
|
set allowIf(value: boolean | Promise<boolean> | Observable<boolean>) {
|
|
if (isBoolean(value)) {
|
|
this.#if.next(of(value));
|
|
} else {
|
|
this.#if.next(value);
|
|
}
|
|
}
|
|
|
|
/**
|
|
This assures that when the directive has an empty input (such as [allow]="") the view is updated
|
|
*/
|
|
ngOnInit() {
|
|
this.#updateView.next();
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
this.#subscription.unsubscribe();
|
|
}
|
|
|
|
#validateRolesAndPermissions() {
|
|
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(map(() => this.#validate()));
|
|
}
|
|
|
|
#validate() {
|
|
if (!this.#permissions) {
|
|
return true;
|
|
}
|
|
|
|
return this._permissionsService.has(this.#permissions) || this._rolesService.has(this.#permissions);
|
|
}
|
|
|
|
#showElseBlock() {
|
|
if (this.#elseViewRef) {
|
|
return;
|
|
}
|
|
|
|
this.permissionsUnauthorized.emit();
|
|
this.#thenViewRef = false;
|
|
this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef);
|
|
}
|
|
|
|
#showThenBlock() {
|
|
if (this.#thenViewRef) {
|
|
return;
|
|
}
|
|
|
|
this.permissionsAuthorized.emit();
|
|
this.#elseViewRef = false;
|
|
this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef);
|
|
}
|
|
|
|
#showTemplate(template?: TemplateRef<unknown>) {
|
|
this._viewContainer.clear();
|
|
return template ? this._viewContainer.createEmbeddedView(template) : true;
|
|
}
|
|
}
|
|
|
|
function assertTemplate(property: string, templateRef: TemplateRef<unknown>): void {
|
|
const isTemplateRefOrNull = !!(!templateRef || templateRef.createEmbeddedView);
|
|
if (!isTemplateRefOrNull) {
|
|
throw new Error(`${property} must be a TemplateRef, but received '${stringify(templateRef)}'.`);
|
|
}
|
|
}
|