common-ui/src/lib/permissions/permissions.directive.ts
2022-10-29 14:41:03 +03:00

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)}'.`);
}
}