141 lines
4.6 KiB
TypeScript
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)}'.`);
|
|
}
|
|
}
|