Add deny directive

This commit is contained in:
Dan Percic 2022-12-19 11:36:52 +02:00
parent 05d6488bc8
commit bba66ea0d6
10 changed files with 1054 additions and 164 deletions

View File

@ -1,8 +1,8 @@
import { ChangeDetectionStrategy, Component, Type } from '@angular/core';
import { IqserPermissionsModule } from '.';
import { IqserPermissionsModule } from '../../index';
import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { IqserPermissionsService } from './services/permissions.service';
import { IqserRolesService } from './services/roles.service';
import { IqserPermissionsService } from '../../services/permissions.service';
import { IqserRolesService } from '../../services/roles.service';
import { BehaviorSubject } from 'rxjs';
class BaseTestComponent {
@ -41,7 +41,7 @@ function getFixtureContent(): HTMLElement {
describe('Permission directive', () => {
@Component({
template: ` <ng-template [allow]="'ADMIN'" (permissionsAuthorized)="isAuthorized()" (permissionsUnauthorized)="isUnauthorized()">
template: ` <ng-template [allow]="'ADMIN'" (authorized)="isAuthorized()" (unauthorized)="isUnauthorized()">
<div>123</div>
</ng-template>`,
})

View File

@ -0,0 +1,47 @@
import { Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Observable } from 'rxjs';
import { List } from '../../../utils';
import { assertTemplate, IqserPermissionsDirective } from '../permissions.directive';
@Directive({
selector: '[allow]',
})
export class IqserAllowDirective extends 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';
constructor(templateRef: TemplateRef<unknown>) {
super(templateRef);
}
@Input()
set allow(value: string | List) {
this.setPermissions(value);
}
@Input()
set allowThen(template: TemplateRef<unknown>) {
assertTemplate('allowThen', template);
this.setThenTemplateRef(template);
}
@Input()
set allowElse(template: TemplateRef<unknown>) {
assertTemplate('allowElse', template);
this.setElseTemplateRef(template);
}
@Input()
set allowIf(value: boolean | Promise<boolean> | Observable<boolean>) {
this.setIf(value);
}
}

View File

@ -0,0 +1,788 @@
import { ChangeDetectionStrategy, Component, Type } from '@angular/core';
import { IqserPermissionsModule } from '../../index';
import { ComponentFixture, ComponentFixtureAutoDetect, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { IqserPermissionsService } from '../../services/permissions.service';
import { IqserRolesService } from '../../services/roles.service';
import { BehaviorSubject } from 'rxjs';
class BaseTestComponent {
isAuthorized() {}
isUnauthorized() {}
}
const ADMIN = 'ADMIN' as const;
const GUEST = 'GUEST' as const;
let fixture: ComponentFixture<BaseTestComponent>;
let permissionsService: IqserPermissionsService;
let rolesService: IqserRolesService;
let isAuthorizedSpy: jest.SpyInstance;
let isUnauthorizedSpy: jest.SpyInstance;
function configureTestBed(component: Type<BaseTestComponent>) {
TestBed.configureTestingModule({
declarations: [component],
imports: [IqserPermissionsModule.forRoot()],
providers: [{ provide: ComponentFixtureAutoDetect, useValue: true }],
});
fixture = TestBed.createComponent(component);
isAuthorizedSpy = jest.spyOn(fixture.componentInstance, 'isAuthorized');
isUnauthorizedSpy = jest.spyOn(fixture.componentInstance, 'isUnauthorized');
permissionsService = TestBed.inject(IqserPermissionsService);
rolesService = TestBed.inject(IqserRolesService);
}
function getFixtureContent(): HTMLElement {
return fixture.debugElement.nativeElement.querySelector('div');
}
describe('Permission directive', () => {
@Component({
template: ` <ng-template [deny]="'ADMIN'" (authorized)="isAuthorized()" (unauthorized)="isUnauthorized()">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should show the component', fakeAsync(() => {
permissionsService.load([GUEST]);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
tick();
}));
it('should not show the component', fakeAsync(() => {
permissionsService.load([ADMIN, GUEST]);
tick();
expect(getFixtureContent()).toEqual(null);
expect(isAuthorizedSpy).toHaveBeenCalledTimes(0);
}));
it('should show component when permission removed', fakeAsync(() => {
permissionsService.load([ADMIN, GUEST]);
tick();
expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1);
expect(getFixtureContent()).toEqual(null);
permissionsService.remove(ADMIN);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
expect(isAuthorizedSpy).toHaveBeenCalledTimes(1);
}));
it('should hide component when permission added', fakeAsync(() => {
permissionsService.load([GUEST]);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
expect(isUnauthorizedSpy).toHaveBeenCalledTimes(1);
}));
});
describe('Permission directive with roles', () => {
@Component({
template: ` <ng-template [deny]="'ADMIN'">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {}
const awesomePermissions = 'AWESOME';
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when key of role is the same', fakeAsync(() => {
rolesService.add({ ADMIN: [awesomePermissions] });
permissionsService.add(awesomePermissions);
tick();
expect(getFixtureContent()).toBe(null);
}));
it('should show the component when user deletes all roles', fakeAsync(() => {
permissionsService.add(awesomePermissions);
rolesService.add({ ADMIN: [awesomePermissions] });
tick();
expect(getFixtureContent()).toEqual(null);
rolesService.clear();
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should show the component when user deletes one role', fakeAsync(() => {
permissionsService.add(awesomePermissions);
rolesService.add({ ADMIN: [awesomePermissions] });
tick();
expect(getFixtureContent()).toEqual(null);
rolesService.remove(ADMIN);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
});
describe('Permission directive with roles array', () => {
@Component({
template: ` <ng-template [deny]="['ADMIN', 'GUEST']">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {}
const awesomePermission = 'AWESOME';
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when key of role is the same', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add({ ADMIN: [awesomePermission] });
rolesService.add({ GUEST: [awesomePermission] });
tick();
expect(getFixtureContent()).toBe(null);
}));
it('should show the component when user deletes all roles', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add({ ADMIN: [awesomePermission] });
rolesService.add({ GUEST: [awesomePermission] });
tick();
expect(getFixtureContent()).toEqual(null);
rolesService.clear();
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should hide the component when user deletes one role', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add({ ADMIN: [awesomePermission] });
rolesService.add({ GUEST: [awesomePermission] });
tick();
expect(getFixtureContent()).toEqual(null);
rolesService.remove(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive testing different selectors *deny', () => {
@Component({
template: ` <div *deny="['ADMIN']">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('Should hide the component when key of role is the same', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add('AWESOME');
rolesService.add({ ADMIN: ['AWESOME'] });
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should show the component when key of role is the same', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ GG: ['Awsesome'] });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
});
describe('Permission directive angular testing different async functions in roles', () => {
@Component({
template: ` <div *deny="'ADMIN'">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when promise returns truthy value', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => true });
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should show the component when promise returns falsy value', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => false });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
});
describe('Permission directive angular testing different async functions in roles via array', () => {
@Component({
template: ` <div *deny="['ADMIN', 'GUEST']">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when returns truthy value', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => true });
rolesService.add({ GUEST: () => true });
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should hide the component when one returns falsy value', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => false });
rolesService.add({ GUEST: () => true });
tick();
expect(getFixtureContent()).toBe(null);
}));
it('should show the component when all return falsy value', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => false });
rolesService.add({ GUEST: () => false });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
it('should hide the component when 1 passes second fails', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
rolesService.add({ ADMIN: () => false });
rolesService.add({ GUEST: ['AWESOME'] });
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive with different async functions in permissions via array', () => {
@Component({
template: ` <div *deny="['ADMIN', 'GUEST']">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when returns truthy value', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => true });
permissionsService.add({ GUEST: () => true });
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should show the component when returns falsy value', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => false });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
it('should hide the component when returns truthy value', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({
ADMIN: () => true,
GUEST: () => true,
});
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should not show the component when functions with name and store fulfils', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({
ADMIN: (name, store) => {
expect(name).toBeTruthy();
expect(store[name!].name).toBeTruthy();
return name === ADMIN;
},
});
permissionsService.add({ GUEST: () => false });
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive testing different async functions in permissions via string', () => {
@Component({
template: ` <div *deny="'ADMIN'">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should show the component when promise returns truthy value', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => true });
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should show the component when promise returns false value', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => false });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
it('should show the component when only one of the promises fulfills', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => false });
permissionsService.add({ GUEST: () => true });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
it('should show the component when all promises fails', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({ ADMIN: () => false });
permissionsService.add({ GUEST: () => false });
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
}));
it('should not show the component when functions with name and store fulfils', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('<div>123</div>');
permissionsService.add({
ADMIN: (name, store) => {
expect(name).toBeTruthy();
expect(store[name!].name).toBeTruthy();
return name === ADMIN;
},
});
permissionsService.add({ GUEST: () => false });
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permissions directive with else block', () => {
@Component({
template: `
<div *deny="['FAILED_BLOCK']; else elseBlock">main</div>
<ng-template #elseBlock>
<div>elseBlockContent</div>
</ng-template>
<ng-template #thenBlock>
<div>thenBlockContent</div>
</ng-template>
`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should show else block when permissions fail', fakeAsync(() => {
let content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`main`);
permissionsService.add(ADMIN);
tick();
content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`elseBlockContent`);
}));
it('should show else block when permissions change', fakeAsync(() => {
rolesService.add({ FAILED_BLOCK: () => true });
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('elseBlockContent');
rolesService.remove('FAILED_BLOCK');
tick();
const content2 = getFixtureContent();
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`main`);
}));
});
describe('Permissions directive with then block', () => {
@Component({
template: `
<div *deny="['THEN_BLOCK']; else elseBlock; then: thenBlock">main</div>
<ng-template #elseBlock>
<div>elseBlockContent</div>
</ng-template>
<ng-template #thenBlock>
<div>thenBlockContent</div>
</ng-template>
`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should pass and show then block', fakeAsync(() => {
rolesService.add({ THEN_BLOCK: () => false });
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`thenBlockContent`);
}));
});
describe('Permissions directive with empty argument', () => {
@Component({
template: `
<ng-template [deny]="">
<div>123</div>
</ng-template>
`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should succeed and show the component', () => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`123`);
});
});
describe('Permissions directive with an empty array', () => {
@Component({
template: `
<ng-template [deny]="[]">
<div>123</div>
</ng-template>
`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should succeed and show the component', () => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`123`);
});
});
describe('Permission directive with true if condition', () => {
@Component({
template: ` <ng-template [deny]="'ADMIN'" [denyIf]="true">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should show the component when permission is absent', () => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
});
it('should hide the component when permission is present', fakeAsync(() => {
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive with true if condition', () => {
@Component({
template: ` <ng-template [deny]="'ADMIN'" [denyIf]="false">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when permission is present', fakeAsync(() => {
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should hide the component when permission is absent', fakeAsync(() => {
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive with true promise if condition', () => {
@Component({
template: ` <ng-template [deny]="'ADMIN'" [denyIf]="condition">
<div>123</div>
</ng-template>`,
})
class TestComponent extends BaseTestComponent {
condition = Promise.resolve(true);
}
beforeEach(() => configureTestBed(TestComponent));
it('should show the component when permission is absent', () => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
});
it('should hide the component when permission is present', fakeAsync(() => {
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permission directive with false promise if condition', () => {
@Component({
template: ` <div *deny="'ADMIN'; if: condition">
<div>123</div>
</div>`,
})
class TestComponent extends BaseTestComponent {
condition = Promise.resolve(false);
}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when permission is present', fakeAsync(() => {
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should hide the component when permission is absent', fakeAsync(() => {
expect(getFixtureContent()).toEqual(null);
}));
});
describe('Permissions directive with then block and if condition', () => {
@Component({
template: `
<div *deny="['THEN_BLOCK']; if: condition; else: elseBlock; then: thenBlock">main</div>
<ng-template #elseBlock>
<div>elseBlockContent</div>
</ng-template>
<ng-template #thenBlock>
<div>thenBlockContent</div>
</ng-template>
`,
})
class TestComponent extends BaseTestComponent {
condition = new BehaviorSubject(true);
}
beforeEach(() => configureTestBed(TestComponent));
it('should show then block when permission missing', fakeAsync(() => {
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`thenBlockContent`);
}));
it('should show else block when permission added', fakeAsync(() => {
permissionsService.add('THEN_BLOCK');
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`elseBlockContent`);
}));
it('should show else block when permission added but condition is false', fakeAsync(() => {
permissionsService.add('THEN_BLOCK');
(fixture.componentInstance as TestComponent).condition.next(false);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`elseBlockContent`);
}));
it('should show else block when permission is missing and condition is false', fakeAsync(() => {
(fixture.componentInstance as TestComponent).condition.next(false);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`elseBlockContent`);
}));
});
describe('Permission directive with false promise, if condition and change detection on push', () => {
@Component({
template: ` <div *deny="'ADMIN'; if: condition">
<div>123</div>
</div>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
class TestComponent extends BaseTestComponent {
condition = new BehaviorSubject(false);
}
beforeEach(() => configureTestBed(TestComponent));
it('should hide the component when permission is present', fakeAsync(() => {
permissionsService.add(ADMIN);
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should hide the component when permission is present and condition is true', fakeAsync(() => {
permissionsService.add(ADMIN);
(fixture.componentInstance as TestComponent).condition.next(true);
tick();
expect(getFixtureContent()).toEqual(null);
}));
it('should hide the component when permission is absent', fakeAsync(() => {
expect(getFixtureContent()).toEqual(null);
}));
it('should show the component when permission is missing and condition is true', fakeAsync(() => {
(fixture.componentInstance as TestComponent).condition.next(true);
tick();
const content = getFixtureContent();
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual(`<div>123</div>`);
}));
});

View File

@ -0,0 +1,55 @@
import { Directive, Input, OnDestroy, OnInit, TemplateRef } from '@angular/core';
import { Observable } from 'rxjs';
import { List } from '../../../utils';
import { assertTemplate, IqserPermissionsDirective } from '../permissions.directive';
@Directive({
selector: '[deny]',
})
export class IqserDenyDirective extends 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_deny: 'binding';
constructor(templateRef: TemplateRef<unknown>) {
super(templateRef);
}
@Input()
set deny(value: string | List) {
this.setPermissions(value);
}
@Input()
set denyThen(template: TemplateRef<unknown>) {
assertTemplate('denyThen', template);
this.setThenTemplateRef(template);
}
@Input()
set denyElse(template: TemplateRef<unknown>) {
assertTemplate('denyElse', template);
this.setElseTemplateRef(template);
}
@Input()
set denyIf(value: boolean | Promise<boolean> | Observable<boolean>) {
this.setIf(value);
}
protected override _validate() {
if (!this._permissions) {
return true;
}
return !(this._permissionsService.hasAtLeastOne(this._permissions) || this._rolesService.hasAtLeastOne(this._permissions));
}
}

View File

@ -0,0 +1,133 @@
import {
ChangeDetectorRef,
Directive,
EmbeddedViewRef,
EventEmitter,
inject,
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()
export abstract class IqserPermissionsDirective implements OnDestroy, OnInit {
@Output() readonly authorized = new EventEmitter();
@Output() readonly unauthorized = new EventEmitter();
protected readonly _permissionsService = inject(IqserPermissionsService);
protected readonly _rolesService = inject(IqserRolesService);
protected readonly _viewContainer = inject(ViewContainerRef);
protected readonly _changeDetector = inject(ChangeDetectorRef);
protected _permissions?: string | List;
protected _thenTemplateRef: TemplateRef<unknown>;
protected _elseTemplateRef?: TemplateRef<unknown>;
protected _thenViewRef: EmbeddedViewRef<unknown> | boolean = false;
protected _elseViewRef: EmbeddedViewRef<unknown> | boolean = false;
protected readonly _updateView = new Subject<void>();
protected readonly _subscription = new Subscription();
protected readonly _if = new BehaviorSubject<Promise<boolean> | Observable<boolean>>(of(true));
protected constructor(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();
}
/**
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();
}
protected setPermissions(value: string | List) {
this._permissions = value;
this._updateView.next();
}
protected setThenTemplateRef(template: TemplateRef<unknown>) {
this._thenTemplateRef = template;
this._thenViewRef = false;
this._updateView.next();
}
protected setElseTemplateRef(template: TemplateRef<unknown>) {
this._elseTemplateRef = template;
this._elseViewRef = false;
this._updateView.next();
}
protected setIf(value: boolean | Promise<boolean> | Observable<boolean>) {
if (isBoolean(value)) {
this._if.next(of(value));
} else {
this._if.next(value);
}
}
protected _validateRolesAndPermissions() {
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(map(() => this._validate()));
}
protected _validate() {
if (!this._permissions) {
return true;
}
return this._permissionsService.has(this._permissions) || this._rolesService.has(this._permissions);
}
protected _showElseBlock() {
if (this._elseViewRef) {
return;
}
this.unauthorized.emit();
this._thenViewRef = false;
this._elseViewRef = this._showTemplate(this._elseTemplateRef);
}
protected _showThenBlock() {
if (this._thenViewRef) {
return;
}
this.authorized.emit();
this._elseViewRef = false;
this._thenViewRef = this._showTemplate(this._thenTemplateRef);
}
protected _showTemplate(template?: TemplateRef<unknown>) {
this._viewContainer.clear();
return template ? this._viewContainer.createEmbeddedView(template) : true;
}
}
export 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)}'.`);
}
}

View File

@ -2,6 +2,6 @@ export * from './types';
export * from './services/permissions-guard.service';
export * from './services/permissions.service';
export * from './services/roles.service';
export * from './permissions.directive';
export * from './directives/permissions.directive';
export * from './permissions.module';
export * from './utils';

View File

@ -1,156 +0,0 @@
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)}'.`);
}
}

View File

@ -1,12 +1,13 @@
import { NgModule, Optional } from '@angular/core';
import { IqserPermissionsDirective } from './permissions.directive';
import { IqserAllowDirective } from './directives/allow/allow.directive';
import { IqserPermissionsService } from './services/permissions.service';
import { IqserPermissionsGuard } from './services/permissions-guard.service';
import { IqserRolesService } from './services/roles.service';
import { IqserDenyDirective } from './directives/deny/deny.directive';
@NgModule({
declarations: [IqserPermissionsDirective],
exports: [IqserPermissionsDirective],
declarations: [IqserAllowDirective, IqserDenyDirective],
exports: [IqserAllowDirective, IqserDenyDirective],
})
export class IqserPermissionsModule {
constructor(@Optional() permissionsService: IqserPermissionsService) {

View File

@ -34,6 +34,17 @@ export class IqserPermissionsService {
return results.every(result => result);
}
hasAtLeastOne(permissions: List | string): boolean {
const isEmpty = !permissions || permissions.length === 0;
if (isEmpty) {
return true;
}
const all = this.#permissions$.value;
const results = toArray(permissions).map(permission => all[permission]?.(permission, all) ?? false);
return results.some(result => result);
}
load(permissions: IqserPermissions | List) {
return this.#reduce(permissions);
}

View File

@ -51,6 +51,17 @@ export class IqserRolesService {
return this.#checkPermissionsIfNeeded(validations).every(result => result);
}
hasAtLeastOne(roles: string | List): boolean {
const isEmpty = !roles || roles.length === 0;
if (isEmpty) {
return true;
}
const validations = toArray(roles).map(role => this.#runValidation(role));
return this.#checkPermissionsIfNeeded(validations).some(result => result);
}
#checkPermissionsIfNeeded(results: List<string | boolean | List>) {
return results.map(result => (isBoolean(result) ? result : this._permissionsService.has(result)));
}