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

141 lines
4.6 KiB
TypeScript

import {
ChangeDetectorRef,
Directive,
EmbeddedViewRef,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output,
TemplateRef,
ViewContainerRef,
ɵstringify as stringify,
} from '@angular/core';
import { merge, Subject, Subscription, switchMap } from 'rxjs';
import { tap } from 'rxjs/operators';
import { IqserRolesService } from './services/roles.service';
import { IqserPermissionsService } from './services/permissions.service';
import { List } 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();
constructor(
private readonly _permissionsService: IqserPermissionsService,
private readonly _rolesService: IqserRolesService,
private readonly _viewContainer: ViewContainerRef,
private readonly _changeDetector: ChangeDetectorRef,
templateRef: TemplateRef<unknown>,
) {
this.#thenTemplateRef = templateRef;
this.#subscription = this.#updateView.pipe(switchMap(() => this.#waitForRolesAndPermissions())).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();
}
/**
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();
}
#waitForRolesAndPermissions() {
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(tap(() => this.#validate()));
}
#validate(): void {
if (!this.#permissions) {
return this.#showThenBlock();
}
const promises = [this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)];
const result = Promise.all(promises).then(([hasPermission, hasRole]) => hasPermission || hasRole);
result.then(isAllowed => (isAllowed ? this.#showThenBlock() : this.#showElseBlock())).catch(() => this.#showElseBlock());
}
#showElseBlock() {
if (this.#elseViewRef) {
return;
}
this.permissionsUnauthorized.emit();
this.#thenViewRef = false;
this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef);
this._changeDetector.markForCheck();
}
#showThenBlock() {
if (this.#thenViewRef) {
return;
}
this.permissionsAuthorized.emit();
this.#elseViewRef = false;
this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef);
this._changeDetector.markForCheck();
}
#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)}'.`);
}
}