adjust permissions services

This commit is contained in:
Dan Percic 2022-10-11 11:35:07 +03:00
parent b607cff5f9
commit c6edb0cd96
14 changed files with 2036 additions and 489 deletions

View File

@ -2,5 +2,9 @@ import { ValidationFn } from './permissions-router-data.model';
export interface IqserPermission {
name: string;
validationFunction?: ValidationFn;
validationFn?: ValidationFn<IqserPermission>;
}
export interface IqserPermissionsObject {
[name: string]: IqserPermission;
}

View File

@ -1,8 +1,7 @@
import { ActivatedRouteSnapshot, NavigationExtras, Route, RouterStateSnapshot } from '@angular/router';
export interface IqserPermissionsRouterData {
only?: string | string[] | OnlyFn;
except?: string | string[] | ExceptFn;
allow: string | string[] | AllowFn;
redirectTo?: RedirectTo | RedirectToFn;
}
@ -11,22 +10,30 @@ export interface IqserRedirectToNavigationParameters {
navigationExtras?: NavigationExtras | NavigationExtrasFn;
}
export type OnlyFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | string[];
export type ExceptFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | string[];
export type AllowFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | string[];
export type RedirectTo =
| string
| { [name: string]: IqserRedirectToNavigationParameters | string | RedirectToFn }
| IqserRedirectToNavigationParameters;
| string[]
| IqserRedirectToNavigationParameters
| { [name: string]: IqserRedirectToNavigationParameters | string | RedirectToFn };
export type RedirectToFn = (
rejectedPermissionName?: string,
route?: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
) => RedirectTo;
export type RedirectToFn = (failedPermission: string, route?: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => RedirectTo;
export type NavigationCommandsFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => any[];
export type NavigationExtrasFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => NavigationExtras;
export type ValidationFn = (name?: string, store?: any) => Promise<void | string | boolean> | boolean | string[];
export type ValidationFn<T> = (name: string, store: Record<string, T>) => Promise<void | string | boolean> | boolean | string[];
export type IqserActivatedRouteSnapshot = ActivatedRouteSnapshot & {
data: {
permissions: IqserPermissionsRouterData;
};
};
export type IqserRoute = Route & {
data: {
permissions: IqserPermissionsRouterData;
};
};
export const DEFAULT_REDIRECT_KEY = 'default';

View File

@ -2,5 +2,5 @@ import { ValidationFn } from './permissions-router-data.model';
export interface IqserRole {
name: string;
validationFunction: ValidationFn | string[];
validationFn: ValidationFn<IqserRole> | string[];
}

View File

@ -0,0 +1,863 @@
import { Component, Type } from '@angular/core';
import { IqserPermissionsModule } from '.';
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
import { IqserPermissionsService } from './services/permissions.service';
import { IqserRolesService } from './services/roles.service';
const ADMIN = 'ADMIN' as const;
const GUEST = 'GUEST' as const;
function detectChanges(fixture: ComponentFixture<unknown>) {
tick();
fixture.detectChanges();
}
let fixture: ComponentFixture<unknown>;
let permissionsService: IqserPermissionsService;
let rolesService: IqserRolesService;
function configureTestBed(component: Type<unknown>) {
TestBed.configureTestingModule({ declarations: [component], imports: [IqserPermissionsModule.forRoot()] });
fixture = TestBed.createComponent(component);
detectChanges(fixture);
permissionsService = fixture.debugElement.injector.get(IqserPermissionsService);
rolesService = fixture.debugElement.injector.get(IqserRolesService);
}
describe('Permission directive angular', () => {
@Component({
template: ` <ng-template [iqserPermissions]="'ADMIN'">
<div>123</div>
</ng-template>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component', fakeAsync(() => {
permissionsService.load([ADMIN, GUEST]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('Should not show the component', fakeAsync(() => {
permissionsService.load([GUEST]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
}));
it('Should show component when permission added', fakeAsync(() => {
permissionsService.load([GUEST]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add(ADMIN);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('123');
}));
it('Should hide component when permission removed', fakeAsync(() => {
permissionsService.load([ADMIN, GUEST]);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('123');
permissionsService.remove(ADMIN);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
}));
});
describe('Permission directive angular roles', () => {
@Component({
template: ` <ng-template [iqserPermissions]="'ADMIN'">
<div>123</div>
</ng-template>`,
})
class TestComponent {}
const awesomePermissions = 'AWESOME';
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when key of role is the same', fakeAsync(() => {
rolesService.add('ADMIN', [awesomePermissions]);
permissionsService.add(awesomePermissions);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should show the component when permissions array is the same ', fakeAsync(() => {
rolesService.add('ADMIN', [awesomePermissions]);
permissionsService.add(awesomePermissions);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should hide the component when user deletes all roles', fakeAsync(() => {
permissionsService.add(awesomePermissions);
rolesService.add('ADMIN', [awesomePermissions]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
rolesService.clear();
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('should hide the component when user deletes one role', fakeAsync(() => {
permissionsService.add(awesomePermissions);
rolesService.add('ADMIN', [awesomePermissions]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
rolesService.remove('ADMIN');
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
});
describe('Permission directive angular roles array', () => {
@Component({
template: ` <ng-template [iqserPermissions]="['ADMIN', 'GUEST']">
<div>123</div>
</ng-template>`,
})
class TestComponent {}
const awesomePermission = 'AWESOME';
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when key of role is the same', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add('ADMIN', [awesomePermission]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should show the component when there is permission ', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add('ADMIN', ['AWESOME']);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
}));
it('should hide the component when user deletes all roles', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add('ADMIN', [awesomePermission]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
rolesService.clear();
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('should hide the component when user deletes one roles', fakeAsync(() => {
permissionsService.add(awesomePermission);
rolesService.add('ADMIN', [awesomePermission]);
detectChanges(fixture);
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toBeTruthy();
expect(content.innerHTML).toEqual('123');
rolesService.remove('ADMIN');
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
});
describe('Permission directive angular testing different selectors *iqserPermissions', () => {
@Component({
template: ` <div *iqserPermissions="['ADMIN']">
<div>123</div>
</div>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when key of role is the same', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('AWESOME');
rolesService.add('ADMIN', ['AWESOME']);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should hide the component when key of role is the same', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('GG', ['Awsesome']);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
});
describe('Permission directive angular testing different async functions in roles', () => {
@Component({
template: ` <div *iqserPermissions="'ADMIN'">
<div>123</div>
</div>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => false);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise rejects', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
});
describe('Permission directive angular testing different async functions in roles via array', () => {
@Component({
template: ` <div *iqserPermissions="['ADMIN', 'GUEST']">
<div>123</div>
</div>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise returns false value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.resolve(false));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should not show the component when promise rejects', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of the promises fulfills ', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => Promise.reject());
rolesService.add('GUEST', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => {
return Promise.reject();
});
rolesService.add('GUEST', () => {
return Promise.resolve();
});
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when all promises fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => {
return Promise.reject();
});
rolesService.add('GUEST', () => {
return Promise.reject();
});
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of promises returns true', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('GUEST', () => {
return true;
});
rolesService.add('ADMIN', () => {
return Promise.reject();
});
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when 1 passes second fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => {
return Promise.reject();
});
permissionsService.add('AWESOME');
rolesService.add('GUEST', ['AWESOME']);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one rejects but another one fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
rolesService.add('ADMIN', () => {
return Promise.reject();
});
permissionsService.add('AWESOME');
rolesService.add('GUEST', ['AWESOME']);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
});
describe('Permission directive angular testing different async functions in permissions via array', () => {
@Component({
template: ` <div *iqserPermissions="['ADMIN', 'GUEST']">
<div>123</div>
</div>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise returns false value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => false);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise rejects', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of the promises fulfills ', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve());
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve());
permissionsService.add('GUEST', () => Promise.resolve());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when all promises fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of promises returns true', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('GUEST', () => true);
permissionsService.add('ADMIN', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when all promises fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
permissionsService.add('GUEST', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one rejects but another one fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
permissionsService.add('GUEST', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one rejects but another one fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => true);
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when functions with name and store fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', (name, store) => {
expect(name).toBeTruthy();
expect(store[name!].name).toBeTruthy();
return name === 'ADMIN';
});
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
});
describe('Permission directive angular testing different async functions in permissions via string', () => {
@Component({
template: ` <div *iqserPermissions="'ADMIN'">
<div>123</div>
</div>`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise returns false value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => false);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when promise returns truthy value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when promise rejects', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of the promises fulfills ', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve());
permissionsService.add('GUEST', () => Promise.resolve(true));
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one of the promises fulfills with 0 value', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve());
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when all promises fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.reject());
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toEqual(null);
}));
it('Should show the component when one of promises returns true', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('GUEST', () => Promise.reject());
permissionsService.add('ADMIN', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should not show the component when all promises fails', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => Promise.resolve(true));
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when one rejects but another one fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', () => true);
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
it('Should show the component when functions with name and store fulfils', fakeAsync(() => {
const content = fixture.debugElement.nativeElement.querySelector('div');
expect(content).toEqual(null);
permissionsService.add('ADMIN', (name, store) => {
expect(name).toBeTruthy();
expect(store[name!].name).toBeTruthy();
return name === 'ADMIN';
});
permissionsService.add('GUEST', () => Promise.reject());
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual('<div>123</div>');
}));
});
describe('Permissions directive testing else block', () => {
@Component({
template: `
<div *iqserPermissions="['FAILED_BLOCK']; else elseBlock">main</div>
<ng-template #elseBlock>
<div>elseBlock</div>
</ng-template>
<ng-template #thenBlock>
<div>thenBlock</div>
</ng-template>
`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should fail and show else block', () => {
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`elseBlock`);
});
it('Should add element remove element and show then block', fakeAsync(() => {
rolesService.add('FAILED_BLOCK', () => true);
detectChanges(fixture);
const content3 = fixture.debugElement.nativeElement.querySelector('div');
expect(content3).toBeTruthy();
expect(content3.innerHTML).toEqual('main');
rolesService.remove('FAILED_BLOCK');
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`elseBlock`);
}));
});
describe('Permissions directive testing then block', () => {
@Component({
template: `
<div *iqserPermissions="['THEN_BLOCK']; else elseBlock; then: thenBlock">main</div>
<ng-template #elseBlock>
<div>elseBlock</div>
</ng-template>
<ng-template #thenBlock>
<div>thenBlock</div>
</ng-template>
`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Should fail and show then block', fakeAsync(() => {
rolesService.add('THEN_BLOCK', () => true);
detectChanges(fixture);
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`thenBlock`);
}));
});
describe('Permissions directive when no permission specified should return true', () => {
@Component({
template: `
<ng-template [iqserPermissions]="">
<div>123</div>
</ng-template>
`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Except and only should success and show then block', () => {
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`123`);
});
});
describe('Permissions directive when no permission specified as array should return true', () => {
@Component({
template: `
<ng-template [iqserPermissions]="[]">
<div>123</div>
</ng-template>
`,
})
class TestComponent {}
beforeEach(fakeAsync(() => configureTestBed(TestComponent)));
it('Except and only should success and show then block', () => {
const content2 = fixture.debugElement.nativeElement.querySelector('div');
expect(content2).toBeTruthy();
expect(content2.innerHTML).toEqual(`123`);
});
});

View File

@ -1,205 +1,147 @@
import {
ChangeDetectorRef,
Directive,
EmbeddedViewRef,
EventEmitter,
Input,
OnChanges,
OnDestroy,
OnInit,
Output,
SimpleChanges,
TemplateRef,
ViewContainerRef,
ɵstringify as stringify,
} from '@angular/core';
import { merge, Subscription } from 'rxjs';
import { skip, take } from 'rxjs/operators';
import { merge, Subject, Subscription, switchMap } from 'rxjs';
import { tap } from 'rxjs/operators';
import { isBoolean, notEmptyValue } from './utils';
import { notEmpty } from './utils';
import { IqserRolesService } from './services/roles.service';
import { IqserPermissionsService } from './services/permissions.service';
type NgTemplate = TemplateRef<unknown>;
@Directive({
selector: '[ngxPermissionsOnly],[ngxPermissionsExcept]',
selector: '[iqserPermissions]',
})
export class NgxPermissionsDirective implements OnInit, OnDestroy, OnChanges {
@Input() ngxPermissionsOnly!: string | string[];
@Input() ngxPermissionsOnlyThen!: TemplateRef<any>;
@Input() ngxPermissionsOnlyElse!: TemplateRef<any>;
export class IqserPermissionsDirective implements OnDestroy, OnInit {
/**
* Assert the correct type of the expression bound to the `iqserPermissions` 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 `iqserPermissions` should be narrowed in some way.
* For `iqserPermissions`, the binding expression itself is used to
* narrow its type, which allows the strictNullChecks feature of TypeScript to work with `IqserPermissionsDirective`.
*/
static ngTemplateGuard_iqserPermissions: 'binding';
@Input() ngxPermissionsExcept!: string | string[];
@Input() ngxPermissionsExceptElse!: TemplateRef<any>;
@Input() ngxPermissionsExceptThen!: TemplateRef<any>;
@Output() readonly permissionsAuthorized = new EventEmitter();
@Output() readonly permissionsUnauthorized = new EventEmitter();
@Input() ngxPermissionsThen!: TemplateRef<any>;
@Input() ngxPermissionsElse!: TemplateRef<any>;
#permissions?: string | string[];
#thenTemplateRef: TemplateRef<unknown>;
#elseTemplateRef?: TemplateRef<unknown>;
#thenViewRef?: EmbeddedViewRef<unknown>;
#elseViewRef?: EmbeddedViewRef<unknown>;
@Output() permissionsAuthorized = new EventEmitter();
@Output() permissionsUnauthorized = new EventEmitter();
private initPermissionSubscription?: Subscription;
// skip first run cause merge will fire twice
private firstMergeUnusedRun = 1;
private currentAuthorizedState?: boolean;
readonly #updateView = new Subject<void>();
readonly #subscription = new Subscription();
constructor(
private permissionsService: IqserPermissionsService,
private rolesService: IqserRolesService,
private viewContainer: ViewContainerRef,
private changeDetector: ChangeDetectorRef,
private templateRef: TemplateRef<any>,
) {}
ngOnInit(): void {
this.viewContainer.clear();
this.initPermissionSubscription = this.validateExceptOnlyPermissions();
private readonly _permissionsService: IqserPermissionsService,
private readonly _rolesService: IqserRolesService,
private readonly _viewContainer: ViewContainerRef,
templateRef: TemplateRef<unknown>,
) {
this.#thenTemplateRef = templateRef;
this.#subscription = this.#updateView.pipe(switchMap(() => this.#waitForRolesAndPermissions())).subscribe();
}
ngOnChanges(changes: SimpleChanges): void {
const onlyChanges = changes['ngxPermissionsOnly'];
const exceptChanges = changes['ngxPermissionsExcept'];
if (onlyChanges || exceptChanges) {
// Due to bug when you pass empty array
if (onlyChanges && onlyChanges.firstChange) {
return;
}
if (exceptChanges && exceptChanges.firstChange) {
return;
}
@Input()
set iqserPermissions(value: string | string[]) {
this.#permissions = value;
this.#updateView.next();
}
merge(this.permissionsService.permissions$, this.rolesService.roles$)
.pipe(skip(this.firstMergeUnusedRun), take(1))
.subscribe(() => {
if (notEmptyValue(this.ngxPermissionsExcept)) {
this.validateExceptAndOnlyPermissions();
return;
}
@Input()
set iqserPermissionsThen(template: NgTemplate) {
assertTemplate('iqserPermissionsThen', template);
this.#thenTemplateRef = template;
this.#thenViewRef = undefined;
this.#updateView.next();
}
if (notEmptyValue(this.ngxPermissionsOnly)) {
this.validateOnlyPermissions();
return;
}
@Input()
set iqserPermissionsElse(template: NgTemplate) {
assertTemplate('iqserPermissionsElse', template);
this.#elseTemplateRef = template;
this.#elseViewRef = undefined;
this.#updateView.next();
}
this.handleAuthorisedPermission(this.getAuthorisedTemplates());
});
}
/**
This assures that when the directive has an empty input (such as [iqserPermissions]="") the view is updated
*/
ngOnInit() {
this.#updateView.next();
}
ngOnDestroy(): void {
if (this.initPermissionSubscription) {
this.initPermissionSubscription.unsubscribe();
}
this.#subscription.unsubscribe();
}
private validateExceptOnlyPermissions(): Subscription {
return merge(this.permissionsService.permissions$, this.rolesService.roles$)
.pipe(skip(this.firstMergeUnusedRun))
.subscribe(() => {
if (notEmptyValue(this.ngxPermissionsExcept)) {
this.validateExceptAndOnlyPermissions();
return;
}
if (notEmptyValue(this.ngxPermissionsOnly)) {
this.validateOnlyPermissions();
return;
}
this.handleAuthorisedPermission(this.getAuthorisedTemplates());
});
#waitForRolesAndPermissions() {
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(
tap(() => (notEmpty(this.#permissions) ? this.#validate() : this.#showThenBlock())),
);
}
private validateExceptAndOnlyPermissions(): void {
Promise.all([
this.permissionsService.hasPermission(this.ngxPermissionsExcept),
this.rolesService.hasOnlyRoles(this.ngxPermissionsExcept),
])
.then(([hasPermission, hasRole]) => {
if (hasPermission || hasRole) {
this.handleUnauthorisedPermission(this.ngxPermissionsExceptElse || this.ngxPermissionsElse);
return;
}
if (!!this.ngxPermissionsOnly) {
throw false;
}
this.handleAuthorisedPermission(this.ngxPermissionsExceptThen || this.ngxPermissionsThen || this.templateRef);
})
.catch(() => {
if (!!this.ngxPermissionsOnly) {
this.validateOnlyPermissions();
} else {
this.handleAuthorisedPermission(this.ngxPermissionsExceptThen || this.ngxPermissionsThen || this.templateRef);
}
});
}
private validateOnlyPermissions(): void {
Promise.all([
this.permissionsService.hasPermission(this.ngxPermissionsOnly),
this.rolesService.hasOnlyRoles(this.ngxPermissionsOnly),
])
#validate(): void {
Promise.all([this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)])
.then(([hasPermissions, hasRoles]) => {
if (hasPermissions || hasRoles) {
this.handleAuthorisedPermission(this.ngxPermissionsOnlyThen || this.ngxPermissionsThen || this.templateRef);
} else {
this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse || this.ngxPermissionsElse);
return this.#showThenBlock();
}
return this.#showElseBlock();
})
.catch(() => {
this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse || this.ngxPermissionsElse);
});
.catch(() => this.#showElseBlock());
}
private handleUnauthorisedPermission(template: TemplateRef<any>): void {
if (isBoolean(this.currentAuthorizedState) && !this.currentAuthorizedState) {
#showElseBlock() {
if (this.#elseViewRef) {
return;
}
this.currentAuthorizedState = false;
this.permissionsUnauthorized.emit();
if (!this.elseBlockDefined()) {
return;
} else {
this.showTemplateBlockInView(template);
}
this.#thenViewRef = undefined;
this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef);
}
private handleAuthorisedPermission(template: TemplateRef<any>): void {
if (isBoolean(this.currentAuthorizedState) && this.currentAuthorizedState) {
#showThenBlock() {
if (this.#thenViewRef) {
return;
}
this.currentAuthorizedState = true;
this.permissionsAuthorized.emit();
if (!this.thenBlockDefined()) {
return;
} else {
this.showTemplateBlockInView(template);
}
this.#elseViewRef = undefined;
this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef);
}
private showTemplateBlockInView(template: TemplateRef<any>): void {
this.viewContainer.clear();
#showTemplate(template?: NgTemplate) {
this._viewContainer.clear();
if (!template) {
return;
}
this.viewContainer.createEmbeddedView(template);
this.changeDetector.markForCheck();
}
private getAuthorisedTemplates(): TemplateRef<any> {
return this.ngxPermissionsOnlyThen || this.ngxPermissionsExceptThen || this.ngxPermissionsThen || this.templateRef;
}
private elseBlockDefined(): boolean {
return !!this.ngxPermissionsExceptElse || !!this.ngxPermissionsElse;
}
private thenBlockDefined() {
return !!this.ngxPermissionsExceptThen || !!this.ngxPermissionsThen;
return this._viewContainer.createEmbeddedView(template);
}
}
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,11 +1,12 @@
import { NgModule, Optional } from '@angular/core';
import { NgxPermissionsDirective } from './permissions.directive';
import { IqserPermissionsDirective } from './permissions.directive';
import { IqserPermissionsService } from './services/permissions.service';
import { IqserPermissionsGuard } from './services/permissions-guard.service';
import { IqserRolesService } from './services/roles.service';
@NgModule({
declarations: [NgxPermissionsDirective],
declarations: [IqserPermissionsDirective],
exports: [IqserPermissionsDirective],
})
export class IqserPermissionsModule {
constructor(@Optional() permissionsService: IqserPermissionsService) {

View File

@ -0,0 +1,462 @@
import { TestBed } from '@angular/core/testing';
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
import { IqserActivatedRouteSnapshot, IqserPermissionsModule } from '..';
import { IqserPermissionsService } from './permissions.service';
import { IqserRolesService } from './roles.service';
import { IqserPermissionsGuard } from './permissions-guard.service';
const ADMIN = 'ADMIN' as const;
const defaultRouterState = {} as RouterStateSnapshot;
const defaultRouter: Partial<Router> = {
navigate: () => Promise.resolve(true),
};
let router: Router;
let routerNavigationSpy: jest.SpyInstance;
let testRoute: Partial<IqserActivatedRouteSnapshot>;
let permissionGuard: IqserPermissionsGuard;
let permissionsService: IqserPermissionsService;
function configureTestBed() {
router = { ...defaultRouter } as Router;
routerNavigationSpy = jest.spyOn(router, 'navigate');
TestBed.configureTestingModule({
imports: [IqserPermissionsModule.forRoot()],
providers: [
{
provide: Router,
useValue: router,
},
],
});
permissionGuard = TestBed.inject(IqserPermissionsGuard);
permissionsService = TestBed.inject(IqserPermissionsService);
}
describe('Permissions guard', () => {
beforeEach(async () => {
configureTestBed();
permissionsService.add([ADMIN]);
});
it('should create an instance', () => {
expect(permissionGuard).toBeTruthy();
});
it('should return true when only fulfils', async () => {
testRoute = {
data: {
permissions: {
allow: ADMIN,
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(true);
});
it('should return true when only is empty array', async () => {
testRoute = {
data: {
permissions: {
allow: [],
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(true);
});
it('should return true when no permissions specified', async () => {
testRoute = {};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(true);
});
it('should return false when only doesnt match', async () => {
testRoute = {
data: {
permissions: {
allow: 'DOESNT MATCH',
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(false);
});
it('should return false when no permission match', async () => {
testRoute = {
data: {
permissions: {
allow: ['DOESNT MATCH', 'DOESNT MATCH 2'],
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(false);
});
it('should return false when only doesnt match and navigate to 404', async () => {
testRoute = {
data: {
permissions: {
allow: 'DOESNT MATCH',
redirectTo: './404',
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['./404']);
});
it('should return false when only doesnt match and navigate to array 404', async () => {
testRoute = {
data: {
permissions: {
allow: 'DOESNT MATCH',
redirectTo: ['./404'],
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['./404']);
});
it('should return false when only doesnt match and navigate to redirectTo function', async () => {
testRoute = {
data: {
permissions: {
allow: 'DOESNT MATCH',
redirectTo: () => ['./403'],
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['./403']);
});
});
describe('Permissions guard dynamically', () => {
beforeEach(async () => {
configureTestBed();
permissionsService.add(ADMIN);
});
it('should return true when only function matches', async () => {
testRoute = {
params: {
id: 44,
},
data: {
permissions: {
allow: route => {
if ((route as ActivatedRouteSnapshot).params['id'] === 44) {
return [ADMIN];
}
return 'notManager';
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(true);
});
it('should return false when only function is called and should redirect', async () => {
testRoute = {
params: {
id: 100,
},
data: {
permissions: {
allow: route => {
if ((route as ActivatedRouteSnapshot).params['id'] === 44) {
return [ADMIN];
}
return 'notManager';
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(routerNavigationSpy).not.toHaveBeenCalled();
expect(result).toEqual(false);
});
});
describe('Permissions guard dynamic redirectTo', () => {
beforeEach(() => {
configureTestBed();
permissionsService.add(ADMIN);
});
it('should redirect to parameters from navigationCommands and navigationExtras', async () => {
testRoute = {
data: {
permissions: {
allow: 'TIED',
redirectTo: {
navigationCommands: ['123'],
navigationExtras: {
skipLocationChange: true,
},
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['123'], { skipLocationChange: true });
});
it('should redirect to function parameters from navigationCommands and navigationExtras', async () => {
testRoute = {
data: {
permissions: {
allow: 'TIED',
redirectTo: {
navigationCommands: () => ['123'],
navigationExtras: () => ({
skipLocationChange: true,
}),
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['123'], { skipLocationChange: true });
});
});
describe('Permissions guard with multiple redirectTo rules', () => {
beforeEach(() => {
configureTestBed();
permissionsService.add('canReadAgenda');
});
it('should fail on canEditAgenda and redirect to dashboard', async () => {
testRoute = {
data: {
permissions: {
allow: ['canReadAgenda', 'canEditAgenda', 'canRun'],
redirectTo: {
canReadAgenda: 'agendaList',
canEditAgenda: 'dashboard',
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['dashboard']);
});
it('should redirect to dashboard when canEditAgenda fails', async () => {
testRoute = {
data: {
permissions: {
allow: ['canReadAgenda', 'canEditAgenda', 'canRun'],
redirectTo: {
canReadAgenda: 'agendaList',
canEditAgenda: () => 'dashboard',
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['dashboard']);
});
it('should redirect to 123 when canEditAgenda fails with navigation parameters', async () => {
testRoute = {
data: {
permissions: {
allow: ['canEditAgenda', 'canRun'],
redirectTo: {
canReadAgenda: 'agendaList',
canEditAgenda: {
navigationCommands: ['123'],
navigationExtras: {
skipLocationChange: true,
},
},
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['123'], { skipLocationChange: true });
});
it('should redirect to 123 when canEditAgenda returns parameters from function', async () => {
testRoute = {
data: {
permissions: {
allow: ['canReadAgenda', 'canEditAgenda', 'canRun'],
redirectTo: {
canReadAgenda: 'agendaList',
canEditAgenda: () => {
return {
navigationCommands: ['123'],
navigationExtras: {
skipLocationChange: true,
},
};
},
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['123'], { skipLocationChange: true });
});
it('should redirect to default when a permission fails with no redirection rule', async () => {
permissionsService.add(['canEditAgenda']);
testRoute = {
data: {
permissions: {
allow: ['canEditAgenda', 'Can run'],
redirectTo: {
canEditAgenda: 'dashboard',
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['login']);
expect(routerNavigationSpy).toHaveBeenCalledTimes(1);
});
it('should activate path when nothing fails', async () => {
permissionsService.add('canEditAgenda');
testRoute = {
data: {
permissions: {
allow: ['canEditAgenda'],
redirectTo: {
canReadAgenda: 'agendaList',
canEditAgenda: 'dashboard',
canRun: 'run',
default: 'login',
},
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(true);
expect(routerNavigationSpy).not.toHaveBeenCalled();
});
});
describe('Permissions guard redirectTo as function', () => {
beforeEach(() => {
configureTestBed();
permissionsService.add('canReadAgenda');
});
it('should dynamically redirect', async () => {
testRoute = {
data: {
permissions: {
allow: ['canRun'],
redirectTo: failedPermission => failedPermission,
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(false);
expect(routerNavigationSpy).toHaveBeenCalledWith(['canRun']);
expect(routerNavigationSpy).toHaveBeenCalledTimes(1);
});
it('should allow to pass when at least one of parameters allow passing', async () => {
testRoute = {
data: {
permissions: {
allow: ['canReadAgenda', 'CAN_SWIM'],
redirectTo: () => 'login',
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(true);
expect(routerNavigationSpy).not.toHaveBeenCalled();
});
});
describe('Role guard with redirectTo as function', () => {
let roleService: IqserRolesService;
beforeEach(() => {
configureTestBed();
roleService = TestBed.inject(IqserRolesService);
permissionsService.add('canReadAgenda');
permissionsService.add('AWESOME');
roleService.add('ADMIN', ['AWESOME', 'canReadAgenda']);
});
it('should dynamically pass if one satisfies', async () => {
roleService.add('RUN', ['BLABLA', 'BLABLA2']);
testRoute = {
data: {
permissions: {
allow: ['RUN', 'AWESOME'],
redirectTo: failedPermission => failedPermission,
},
},
};
const result = await permissionGuard.canActivate(testRoute as ActivatedRouteSnapshot, defaultRouterState);
expect(result).toEqual(true);
expect(routerNavigationSpy).not.toHaveBeenCalled();
});
});

View File

@ -10,27 +10,23 @@ import {
RouterStateSnapshot,
} from '@angular/router';
import { firstValueFrom, forkJoin, from, Observable, of } from 'rxjs';
import { firstValueFrom, forkJoin, from, of } from 'rxjs';
import { first, mergeMap, tap } from 'rxjs/operators';
import {
DEFAULT_REDIRECT_KEY,
ExceptFn,
IqserPermissionsRouterData,
IqserRedirectToNavigationParameters,
NavigationCommandsFn,
NavigationExtrasFn,
OnlyFn,
RedirectTo,
RedirectToFn,
} from '../models/permissions-router-data.model';
import { IqserPermissionsService } from './permissions.service';
import { IqserRolesService } from './roles.service';
import { isFunction, isPlainObject, transformStringToArray } from '../utils';
import { isFunction, isRedirectWithParameters, isString, transformPermission } from '../utils';
export interface IqserPermissionsData {
only?: string | string[];
except?: string | string[];
allow: string | string[];
redirectTo?: RedirectTo | RedirectToFn;
}
@ -42,167 +38,69 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
private readonly _router: Router,
) {}
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
return this.#hasPermissions(route, state);
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.#checkPermissions(route, state);
}
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
return this.#hasPermissions(childRoute, state);
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.#checkPermissions(childRoute, state);
}
canLoad(route: Route): boolean | Observable<boolean> | Promise<boolean> {
return this.#hasPermissions(route);
canLoad(route: Route) {
return this.#checkPermissions(route);
}
passingOnlyPermissionsValidation(
permissions: IqserPermissionsData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): Promise<boolean> {
if (
isFunction<RedirectToFn>(permissions.redirectTo) ||
(isPlainObject(permissions.redirectTo) && !this.#isRedirectionWithParameters(permissions.redirectTo))
) {
return this.#onlyRedirectCheck(permissions, route, state);
}
return this.#checkOnlyPermissions(permissions, route, state);
}
#hasPermissions(route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
const routeDataPermissions = !!route && route.data ? (route.data['permissions'] as IqserPermissionsRouterData) : {};
const permissions = this.#transformPermission(routeDataPermissions, route, state);
if (this.#isParameterAvailable(permissions.except)) {
return this.#passingExceptPermissionsValidation(permissions, route, state);
#validate(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
if (isFunction<RedirectToFn>(permissions.redirectTo) || !isRedirectWithParameters(permissions.redirectTo)) {
return this.#checkRedirect(permissions, route, state);
}
if (this.#isParameterAvailable(permissions.only)) {
return this.passingOnlyPermissionsValidation(permissions, route, state);
return this.#validatePermissions(permissions, route, state);
}
#checkPermissions(route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
const routePermissions = route?.data ? (route.data['permissions'] as IqserPermissionsRouterData) : undefined;
if (!routePermissions) {
return Promise.resolve(true);
}
return true;
}
const permissions = transformPermission(routePermissions, route, state);
#transformPermission(
permissions: IqserPermissionsRouterData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): IqserPermissionsData {
const only = isFunction<OnlyFn>(permissions.only) ? permissions.only(route, state) : transformStringToArray(permissions.only);
const except = isFunction<ExceptFn>(permissions.except)
? permissions.except(route, state)
: transformStringToArray(permissions.except);
const redirectTo = permissions.redirectTo;
return {
only,
except,
redirectTo,
};
}
#isParameterAvailable(permission?: string | string[]) {
return !!permission && permission.length > 0;
}
#passingExceptPermissionsValidation(
permissions: IqserPermissionsData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): Promise<boolean> {
if (
!!permissions.redirectTo &&
(isFunction<RedirectToFn>(permissions.redirectTo) ||
(isPlainObject(permissions.redirectTo) && !this.#isRedirectionWithParameters(permissions.redirectTo)))
) {
let failedPermission = '';
const res = from(permissions.except ?? []).pipe(
mergeMap(permissionsExcept => {
return forkJoin([
this._permissionsService.hasPermission(permissionsExcept),
this._rolesService.hasOnlyRoles(permissionsExcept),
]).pipe(
tap(hasPermissions => {
const dontHavePermissions = hasPermissions.every(hasPermission => hasPermission === false);
if (!dontHavePermissions) {
failedPermission = permissionsExcept;
}
}),
);
}),
first(hasPermissions => hasPermissions.some(hasPermission => hasPermission === true), false),
mergeMap(isAllFalse => {
if (!!failedPermission) {
this.#handleRedirectOfFailedPermission(permissions, failedPermission, route, state);
return of(false);
}
if (!isAllFalse && permissions.only) {
return this.#onlyRedirectCheck(permissions, route, state);
}
return of(!isAllFalse);
}),
);
return firstValueFrom(res);
if (permissions?.allow?.length > 0) {
return this.#validate(permissions, route, state);
}
return Promise.all([
this._permissionsService.hasPermission(permissions.except),
this._rolesService.hasOnlyRoles(permissions.except),
]).then(([hasPermission, hasRoles]) => {
if (hasPermission || hasRoles) {
if (permissions.redirectTo) {
this.#redirectToAnotherRoute(permissions.redirectTo, route, state);
}
return false;
}
if (permissions.only) {
return this.#checkOnlyPermissions(permissions, route, state);
}
return true;
});
return Promise.resolve(true);
}
#redirectToAnotherRoute(
permissionRedirectTo: RedirectTo | RedirectToFn,
route: ActivatedRouteSnapshot | Route,
failedPermissionName: string,
state?: RouterStateSnapshot,
failedPermissionName?: string,
): void {
) {
const redirectTo = isFunction<RedirectToFn>(permissionRedirectTo)
? permissionRedirectTo(failedPermissionName, route, state)
: permissionRedirectTo;
if (this.#isRedirectionWithParameters(redirectTo)) {
if (isRedirectWithParameters(redirectTo)) {
redirectTo.navigationCommands = this.#transformNavigationCommands(redirectTo.navigationCommands, route, state);
redirectTo.navigationExtras = this.#transformNavigationExtras(redirectTo.navigationExtras ?? {}, route, state);
this._router.navigate(redirectTo.navigationCommands, redirectTo.navigationExtras);
return;
return this._router.navigate(redirectTo.navigationCommands, redirectTo.navigationExtras);
}
if (Array.isArray(redirectTo)) {
this._router.navigate(redirectTo);
} else {
this._router.navigate([redirectTo]);
return this._router.navigate(redirectTo);
}
}
#isRedirectionWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {
return isPlainObject(object) && (!!object.navigationCommands || !!object.navigationExtras);
return this._router.navigate([redirectTo]);
}
#transformNavigationCommands(
navigationCommands: any[] | NavigationCommandsFn,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): any[] {
) {
return isFunction<NavigationCommandsFn>(navigationCommands) ? navigationCommands(route, state) : navigationCommands;
}
@ -214,24 +112,21 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
return isFunction<NavigationExtrasFn>(navigationExtras) ? navigationExtras(route, state) : navigationExtras;
}
#onlyRedirectCheck(
permissions: IqserPermissionsData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): Promise<boolean> {
let failedPermission = '';
#checkRedirect(permissions: IqserPermissionsData, route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
if (!permissions.allow || permissions.allow.length === 0) {
return Promise.resolve(true);
}
const res = from(permissions.only ?? []).pipe(
mergeMap(permissionsOnly => {
return forkJoin([
this._permissionsService.hasPermission(permissionsOnly),
this._rolesService.hasOnlyRoles(permissionsOnly),
]).pipe(
let failedPermission = '';
const res = from(permissions.allow).pipe(
mergeMap(permission => {
return forkJoin([this._permissionsService.has(permission), this._rolesService.has(permission)]).pipe(
tap(hasPermissions => {
const failed = hasPermissions.every(hasPermission => hasPermission === false);
if (failed) {
failedPermission = permissionsOnly;
failedPermission = permission;
console.log(`Permission ${permission} is not allowed`);
}
}),
);
@ -247,16 +142,18 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
if (isFunction<RedirectToFn>(permissions.redirectTo)) {
if (pass) {
return of(true);
} else {
this.#handleRedirectOfFailedPermission(permissions, failedPermission, route, state);
return of(false);
}
} else {
if (!!failedPermission) {
this.#handleRedirectOfFailedPermission(permissions, failedPermission, route, state);
}
return of(!pass);
const redirectHandle = this.#handleRedirectOfFailedPermission(permissions, failedPermission, route, state);
return redirectHandle.then(() => false);
}
if (!!failedPermission) {
const redirectHandle = this.#handleRedirectOfFailedPermission(permissions, failedPermission, route, state);
return redirectHandle.then(() => !pass);
}
return of(!pass);
}),
);
@ -269,29 +166,44 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
) {
if (this.#isFailedPermissionPropertyOfRedirectTo(permissions, failedPermission)) {
if (!isFunction<RedirectToFn>(permissions.redirectTo)) {
// @ts-ignore
this.#redirectToAnotherRoute(permissions.redirectTo[failedPermission], route, state, failedPermission);
}
} else {
if (isFunction<RedirectToFn>(permissions.redirectTo)) {
this.#redirectToAnotherRoute(permissions.redirectTo, route, state, failedPermission);
} else {
if (permissions.redirectTo) {
// @ts-ignore
this.#redirectToAnotherRoute(permissions.redirectTo[DEFAULT_REDIRECT_KEY], route, state, failedPermission);
}
}
const failedPermissionRedirectTo = this.#getFailedPermissionRedirectTo(permissions.redirectTo, failedPermission);
if (failedPermissionRedirectTo) {
return this.#redirectToAnotherRoute(failedPermissionRedirectTo, route, failedPermission, state);
}
if (isFunction<RedirectToFn>(permissions.redirectTo) || isString(permissions.redirectTo) || Array.isArray(permissions.redirectTo)) {
return this.#redirectToAnotherRoute(permissions.redirectTo, route, failedPermission, state);
}
if (permissions.redirectTo && !isRedirectWithParameters(permissions.redirectTo)) {
const defaultRoute = permissions.redirectTo[DEFAULT_REDIRECT_KEY];
if (!defaultRoute) {
return Promise.resolve(false);
}
return this.#redirectToAnotherRoute(defaultRoute, route, failedPermission, state);
}
return Promise.resolve(false);
}
#isFailedPermissionPropertyOfRedirectTo(permissions: IqserPermissionsData, failedPermission: string): boolean {
// @ts-ignore
return !!permissions.redirectTo && permissions.redirectTo[failedPermission];
#getFailedPermissionRedirectTo(redirectTo: RedirectTo | RedirectToFn | undefined, failedPermission: string) {
if (
!!redirectTo &&
!isFunction<RedirectToFn>(redirectTo) &&
!isString(redirectTo) &&
!isRedirectWithParameters(redirectTo) &&
!Array.isArray(redirectTo)
) {
return redirectTo[failedPermission];
}
return undefined;
}
#checkOnlyPermissions(
#validatePermissions(
purePermissions: IqserPermissionsData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
@ -300,19 +212,19 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
...purePermissions,
};
return Promise.all([
this._permissionsService.hasPermission(permissions.only),
this._rolesService.hasOnlyRoles(permissions.only),
]).then(([hasPermission, hasRole]) => {
if (hasPermission || hasRole) {
return true;
}
return Promise.all([this._permissionsService.has(permissions.allow), this._rolesService.has(permissions.allow)]).then(
([hasPermission, hasRole]) => {
if (hasPermission || hasRole) {
return true;
}
if (permissions.redirectTo) {
this.#redirectToAnotherRoute(permissions.redirectTo, route, state);
}
if (permissions.redirectTo) {
const redirect = this.#redirectToAnotherRoute(permissions.redirectTo, route, permissions.allow[0], state);
return redirect.then(() => false);
}
return false;
});
return false;
},
);
}
}

View File

@ -0,0 +1,157 @@
import { TestBed } from '@angular/core/testing';
import { IqserPermissionsModule } from '../permissions.module';
import { IqserPermissionsService } from './permissions.service';
const ADMIN = 'ADMIN' as const;
const GUEST = 'GUEST' as const;
describe('Permissions Service', () => {
let permissionsService: IqserPermissionsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [IqserPermissionsModule.forRoot()],
});
permissionsService = TestBed.inject(IqserPermissionsService);
});
it('should create an instance', () => {
expect(permissionsService).toBeTruthy();
});
it('should add permission to permissions object', () => {
expect(permissionsService.get(ADMIN)).toBeFalsy();
permissionsService.add(ADMIN);
expect(permissionsService.get(ADMIN)).toBeTruthy();
});
it('should remove permission from permissions object', () => {
expect(permissionsService.get(ADMIN)).toBeFalsy();
permissionsService.add(ADMIN);
expect(permissionsService.get(ADMIN)).toBeTruthy();
permissionsService.remove(ADMIN);
expect(permissionsService.get(ADMIN)).toBeFalsy();
});
it('should remove all permissions from permissions object', () => {
expect(Object.keys(permissionsService.get()).length).toEqual(0);
permissionsService.add(ADMIN);
permissionsService.add(GUEST);
expect(Object.keys(permissionsService.get()).length).toEqual(2);
permissionsService.clear();
expect(Object.keys(permissionsService.get()).length).toEqual(0);
});
it('should add multiple permissions', () => {
expect(Object.keys(permissionsService.get()).length).toEqual(0);
permissionsService.add([ADMIN, GUEST]);
expect(Object.keys(permissionsService.get()).length).toEqual(2);
expect(permissionsService.get()).toEqual({
ADMIN: { name: 'ADMIN' },
GUEST: { name: 'GUEST' },
});
});
it('should return true when permission name is present in permissions object', async () => {
expect(Object.keys(permissionsService.get()).length).toEqual(0);
permissionsService.add([ADMIN, GUEST]);
expect(Object.keys(permissionsService.get()).length).toEqual(2);
let result = await permissionsService.has('ADMIN');
expect(result).toEqual(true);
result = await permissionsService.has('SHOULDNOTHAVEROLE');
expect(result).toEqual(false);
result = await permissionsService.has(['ADMIN']);
expect(result).toEqual(true);
result = await permissionsService.has(['ADMIN', 'IRIISISTABLE']);
expect(result).toEqual(true);
});
it('should return true when permission function return true', async () => {
expect(Object.keys(permissionsService.get()).length).toEqual(0);
permissionsService.add(ADMIN, () => true);
expect(Object.keys(permissionsService.get()).length).toEqual(1);
let result = await permissionsService.has('ADMIN');
expect(result).toEqual(true);
permissionsService.add(GUEST, () => false);
expect(Object.keys(permissionsService.get()).length).toEqual(2);
result = await permissionsService.has('GUEST');
expect(result).toEqual(false);
permissionsService.add('TEST1', () => Promise.resolve(true));
expect(Object.keys(permissionsService.get()).length).toEqual(3);
result = await permissionsService.has('TEST1');
expect(result).toEqual(true);
permissionsService.add('TEST2', () => Promise.resolve(false));
expect(Object.keys(permissionsService.get()).length).toEqual(4);
result = await permissionsService.has('TEST2');
expect(result).toEqual(false);
});
// TODO: permissions array with function should not be allowed
it('should return true when permissions array function return true', async () => {
expect(Object.keys(permissionsService.get()).length).toEqual(0);
permissionsService.add([ADMIN], () => true);
expect(Object.keys(permissionsService.get()).length).toEqual(1);
let result = await permissionsService.has('ADMIN');
expect(result).toEqual(true);
permissionsService.add([GUEST], () => false);
expect(Object.keys(permissionsService.get()).length).toEqual(2);
result = await permissionsService.has('GUEST');
expect(result).toEqual(false);
permissionsService.add(['TEST1'], () => Promise.resolve(true));
expect(Object.keys(permissionsService.get()).length).toEqual(3);
result = await permissionsService.has('TEST1');
expect(result).toEqual(true);
permissionsService.add(['TEST9'], () => Promise.resolve(false));
expect(Object.keys(permissionsService.get()).length).toEqual(4);
result = await permissionsService.has(['TEST9']);
expect(result).toEqual(false);
});
it('should call validationFn with permission name and store', async () => {
permissionsService.add('TEST11', (name, store) => {
expect(name).toEqual('TEST11');
expect(store['TEST11']).toBeTruthy();
return Promise.resolve(true);
});
expect(Object.keys(permissionsService.get()).length).toEqual(1);
const result = await permissionsService.has(['TEST11']);
expect(result).toEqual(true);
});
it('should return true when called with empty parameters', async () => {
const result = await permissionsService.has('');
expect(result).toEqual(true);
});
it('should return true when called with empty array', async () => {
const result = await permissionsService.has([]);
expect(result).toEqual(true);
});
});

View File

@ -1,15 +1,11 @@
import { Injectable } from '@angular/core';
import { BehaviorSubject, from, Observable, of } from 'rxjs';
import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs';
import { catchError, first, map, mergeAll, switchMap } from 'rxjs/operators';
import { isBoolean, isFunction, transformStringToArray } from '../utils';
import { isFunction, toArray } from '../utils';
import { ValidationFn } from '../models/permissions-router-data.model';
import { IqserPermission } from '../models/permission.model';
export interface IqserPermissionsObject {
[name: string]: IqserPermission;
}
import { IqserPermission, IqserPermissionsObject } from '../models/permission.model';
@Injectable()
export class IqserPermissionsService {
@ -23,103 +19,85 @@ export class IqserPermissionsService {
/**
* Remove all permissions from permissions source
*/
public flushPermissions(): void {
clear(): void {
this.#permissions$.next({});
}
public hasPermission(permission?: string | string[]): Promise<boolean> {
has(permission?: string | string[]): Promise<boolean> {
if (!permission || (Array.isArray(permission) && permission.length === 0)) {
return Promise.resolve(true);
}
permission = transformStringToArray(permission);
return this.hasArrayPermission(permission);
permission = toArray(permission);
return this.#hasArray(permission);
}
public loadPermissions(permissions: string[], validationFunction?: ValidationFn): void {
const newPermissions = permissions.reduce((source, name) => this.reducePermission(source, name, validationFunction), {});
load(permissions: string[], validationFn?: ValidationFn<IqserPermission>): void {
const newPermissions = permissions.reduce((source, name) => this.#reduce(source, name, validationFn), {});
this.#permissions$.next(newPermissions);
}
public addPermission(permission: string | string[], validationFunction?: ValidationFn): void {
if (Array.isArray(permission)) {
const permissions = permission.reduce(
(source, name) => this.reducePermission(source, name, validationFunction),
this.#permissions$.value,
);
add(permission: string | string[], validationFn?: ValidationFn<IqserPermission>) {
const permissions = toArray(permission).reduce(
(source, name) => this.#reduce(source, name, validationFn),
this.#permissions$.value,
);
this.#permissions$.next(permissions);
} else {
const permissions = this.reducePermission(this.#permissions$.value, permission, validationFunction);
this.#permissions$.next(permissions);
}
return this.#permissions$.next(permissions);
}
public removePermission(permissionName: string): void {
const permissions = {
...this.#permissions$.value,
};
delete permissions[permissionName];
remove(name: string): void {
const permissions = { ...this.#permissions$.value };
delete permissions[name];
this.#permissions$.next(permissions);
}
public getPermission(name: string): IqserPermission | undefined {
return this.#permissions$.value[name];
get(): IqserPermissionsObject;
get(name: string): IqserPermission | undefined;
get(name?: string): IqserPermission | IqserPermissionsObject | undefined {
return name ? this.#permissions$.value[name] : this.#permissions$.value;
}
public getPermissions(): IqserPermissionsObject {
return this.#permissions$.value;
}
private reducePermission(source: IqserPermissionsObject, name: string, validationFunction?: ValidationFn): IqserPermissionsObject {
if (!!validationFunction && isFunction(validationFunction)) {
return {
...source,
[name]: { name, validationFunction },
};
#reduce(source: IqserPermissionsObject, name: string, validationFn?: ValidationFn<IqserPermission>): IqserPermissionsObject {
if (!!validationFn && isFunction(validationFn)) {
return { ...source, [name]: { name, validationFn } };
}
return {
...source,
[name]: { name },
};
return { ...source, [name]: { name } };
}
private hasArrayPermission(permissions: string[]): Promise<boolean> {
#hasArray(permissions: string[]): Promise<boolean> {
const promises = permissions.map(key => {
if (this.hasPermissionValidationFunction(key)) {
const validationFunction = this.#permissions$.value[key].validationFunction;
if (this.#hasValidationFn(key)) {
const validationFunction = this.#permissions$.value[key].validationFn;
if (!validationFunction) {
return of(false);
}
const immutableValue = { ...this.#permissions$.value };
return of(null).pipe(
map(() => validationFunction(key, immutableValue)),
switchMap(promise => (isBoolean(promise) ? of(promise) : promise)),
switchMap(async () => validationFunction(key, immutableValue)),
catchError(() => of(false)),
);
}
// check for name of the permission if there is no validation function
return of(!!this.#permissions$.value[key]);
});
return from(promises)
.pipe(
mergeAll(),
first(data => data !== false, false),
map(data => data !== false),
)
.toPromise()
.then((data: any) => data);
const res = from(promises).pipe(
mergeAll(),
first(data => data !== false, false),
map(data => data !== false),
);
return firstValueFrom(res);
}
private hasPermissionValidationFunction(key: string): boolean {
#hasValidationFn(key: string): boolean {
return (
!!this.#permissions$.value[key] &&
!!this.#permissions$.value[key].validationFunction &&
isFunction(this.#permissions$.value[key].validationFunction)
!!this.#permissions$.value[key].validationFn &&
isFunction(this.#permissions$.value[key].validationFn)
);
}
}

View File

@ -0,0 +1,209 @@
import { TestBed } from '@angular/core/testing';
import { ValidationFn } from '../models/permissions-router-data.model';
import { IqserRolesService } from './roles.service';
import { IqserPermissionsService } from './permissions.service';
import { IqserPermissionsModule } from '../permissions.module';
import { IqserRole } from '../models/role.model';
const ADMIN = 'ADMIN' as const;
const GUEST = 'GUEST' as const;
describe('Roles Service', () => {
let rolesService: IqserRolesService;
let permissionsService: IqserPermissionsService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [IqserPermissionsModule.forRoot()],
});
rolesService = TestBed.inject(IqserRolesService);
permissionsService = TestBed.inject(IqserPermissionsService);
});
it('should create an instance', () => {
expect(rolesService).toBeTruthy();
});
it('should add role to role object', () => {
expect(rolesService.get(ADMIN)).toBeFalsy();
rolesService.add(ADMIN, ['edit', 'remove']);
expect(rolesService.get(ADMIN)).toBeTruthy();
expect(rolesService.get()).toEqual({ ADMIN: { name: ADMIN, validationFn: ['edit', 'remove'] } });
});
it('should remove role from role object', () => {
expect(rolesService.get(ADMIN)).toBeFalsy();
rolesService.add(ADMIN, ['edit', 'remove']);
expect(rolesService.get(ADMIN)).toBeTruthy();
rolesService.remove(ADMIN);
expect(rolesService.get(ADMIN)).toBeFalsy();
});
it('should remove all roles from object', () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add(ADMIN, ['edit', 'remove']);
rolesService.add(GUEST, ['edit', 'remove']);
expect(Object.keys(rolesService.get()).length).toEqual(2);
rolesService.clear();
expect(Object.keys(rolesService.get()).length).toEqual(0);
});
it('should add multiple roles', () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add({
ADMIN: ['Nice'],
GUEST: ['Awesome'],
});
expect(Object.keys(rolesService.get()).length).toEqual(2);
expect(rolesService.get()).toEqual({
ADMIN: { name: ADMIN, validationFn: ['Nice'] },
GUEST: { name: GUEST, validationFn: ['Awesome'] },
});
});
it('return true when role name is present in Roles object', async () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add({
ADMIN: ['Nice'],
GUEST: ['Awesome'],
});
expect(Object.keys(rolesService.get()).length).toEqual(2);
let result = await rolesService.has(ADMIN);
expect(result).toEqual(true);
result = await rolesService.has('SHOULDNOTHAVEROLE');
expect(result).toEqual(false);
result = await rolesService.has([ADMIN, 'IRIISISTABLE']);
expect(result).toEqual(true);
});
it('return true when role permission name is present in Roles object', async () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add({
ADMIN: ['Nice'],
GUEST: ['Awesome'],
});
expect(Object.keys(rolesService.get()).length).toEqual(2);
let result = await rolesService.has(ADMIN);
expect(result).toEqual(true);
result = await rolesService.has([ADMIN, 'IRRISISTABLE']);
expect(result).toEqual(true);
result = await rolesService.has('SHOULDNOTHAVEROLE');
expect(result).toEqual(false);
result = await rolesService.has(['SHOULDNOTHAVEROLE']);
expect(result).toEqual(false);
});
it('should return role when requested with has role', () => {
rolesService.add('role', () => true);
const role = rolesService.get('role');
if (!role) {
return expect(role).toBeTruthy();
}
expect(role.name).toBe('role');
const validationFn = role.validationFn as ValidationFn<IqserRole>;
expect(validationFn(role.name, rolesService.get())).toEqual(true);
});
it('should return true when checking with empty permission(not specified)', async () => {
const result = await rolesService.has('');
expect(result).toEqual(true);
});
it('should return false when permission array is empty', async () => {
const result = await rolesService.has('Empty');
expect(result).toEqual(false);
});
it('should return false when role is not specified in the list', async () => {
rolesService.add('test', ['One']);
const result = await rolesService.has('nice');
expect(result).toBe(false);
});
it('should return true when passing empty array', async () => {
rolesService.add('test', ['One']);
const result = await rolesService.has([]);
expect(result).toBe(true);
});
it('should add permissions to roles automatically', async () => {
rolesService.add('test', ['one', 'two']);
const result = await rolesService.has('test');
expect(result).toBe(true);
});
it('should remove roles and permissions add the same time', async () => {
rolesService.add('test', ['one', 'two']);
let result = await rolesService.has('test');
expect(result).toBe(true);
result = await permissionsService.has('one');
expect(result).toBe(true);
rolesService.clear();
permissionsService.clear();
result = await rolesService.has('test');
expect(result).toBe(false);
result = await permissionsService.has('one');
expect(result).toBe(false);
});
it('should remove all permissions and roles', () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add(ADMIN, ['edit', 'remove']);
rolesService.add(GUEST, ['edit1', 'remove2']);
expect(Object.keys(rolesService.get()).length).toEqual(2);
expect(Object.keys(permissionsService.get()).length).toEqual(4);
rolesService.clear();
permissionsService.clear();
expect(Object.keys(rolesService.get()).length).toEqual(0);
expect(Object.keys(permissionsService.get()).length).toEqual(0);
});
it('should add multiple roles with permissions', () => {
expect(Object.keys(rolesService.get()).length).toEqual(0);
rolesService.add({
ADMIN: ['Nice'],
GUEST: ['Awesome', 'Another awesome'],
});
expect(Object.keys(rolesService.get()).length).toEqual(2);
expect(rolesService.get()).toEqual({
ADMIN: { name: ADMIN, validationFn: ['Nice'] },
GUEST: { name: GUEST, validationFn: ['Awesome', 'Another awesome'] },
});
expect(Object.keys(rolesService.get()).length).toEqual(2);
expect(Object.keys(permissionsService.get()).length).toEqual(3);
});
});

View File

@ -6,7 +6,7 @@ import { catchError, every, first, map, mergeAll, mergeMap, switchMap } from 'rx
import { IqserPermissionsService } from './permissions.service';
import { ValidationFn } from '../models/permissions-router-data.model';
import { IqserRole } from '../models/role.model';
import { isBoolean, isFunction, isPromise, transformStringToArray } from '../utils';
import { isFunction, isString, toArray } from '../utils';
export interface IqserRolesObject {
[name: string]: IqserRole;
@ -21,86 +21,76 @@ export class IqserRolesService {
this.roles$ = this.#roles$.asObservable();
}
addRole(name: string, validationFunction: ValidationFn | string[]) {
const roles = {
...this.#roles$.value,
[name]: { name, validationFunction },
};
this.#roles$.next(roles);
add(role: string, validationFn: ValidationFn<IqserRole> | string[]): void;
add(rolesObj: Record<string, ValidationFn<IqserRole> | string[]>): void;
add(role: string | Record<string, ValidationFn<IqserRole> | string[]>, validationFn?: ValidationFn<IqserRole> | string[]) {
if (isString(role) && validationFn) {
return this.#add(role, validationFn);
}
if (typeof role === 'object') {
return Object.keys(role).forEach(key => this.#add(key, role[key]));
}
throw new Error('Invalid add role arguments');
}
addRoleWithPermissions(name: string, permissions: string[]) {
this._permissionsService.addPermission(permissions);
this.addRole(name, permissions);
}
addRoles(rolesObj: { [name: string]: ValidationFn | string[] }) {
Object.keys(rolesObj).forEach((key, index) => {
this.addRole(key, rolesObj[key]);
});
}
addRolesWithPermissions(rolesObj: { [name: string]: string[] }) {
Object.keys(rolesObj).forEach((key, index) => {
this.addRoleWithPermissions(key, rolesObj[key]);
});
}
flushRoles() {
clear() {
this.#roles$.next({});
}
flushRolesAndPermissions() {
this.flushRoles();
this._permissionsService.flushPermissions();
}
removeRole(roleName: string) {
const roles = {
...this.#roles$.value,
};
delete roles[roleName];
remove(role: string) {
const roles = { ...this.#roles$.value };
delete roles[role];
this.#roles$.next(roles);
}
getRoles(): IqserRolesObject {
return this.#roles$.value;
get(): IqserRolesObject;
get(role: string): IqserRole | undefined;
get(role?: string): IqserRolesObject | IqserRole | undefined {
return role ? this.#roles$.value[role] : this.#roles$.value;
}
getRole(name: string): IqserRole | undefined {
return this.#roles$.value[name];
}
hasOnlyRoles(names?: string | string[]): Promise<boolean> {
has(names?: string | string[]): Promise<boolean> {
const isNamesEmpty = !names || (Array.isArray(names) && names.length === 0);
if (isNamesEmpty) {
return Promise.resolve(true);
}
names = transformStringToArray(names);
names = toArray(names);
return Promise.all([this.#hasRoleKey(names), this.#hasRolePermission(this.#roles$.value, names)]).then(
([hasRoles, hasPermissions]) => {
return !!hasRoles || !!hasPermissions;
},
);
return Promise.all([this.#hasRoleKey(names), this.#hasPermission(names)]).then(([hasRoles, hasPermissions]) => {
return !!hasRoles || !!hasPermissions;
});
}
#add(role: string, validationFn: ValidationFn<IqserRole> | string[]) {
const roles: IqserRolesObject = {
...this.#roles$.value,
[role]: { name: role, validationFn },
};
if (Array.isArray(validationFn)) {
this._permissionsService.add(validationFn);
}
return this.#roles$.next(roles);
}
#hasRoleKey(roleName: string[]): Promise<boolean> {
const promises = roleName.map(key => {
const hasValidationFunction =
!!this.#roles$.value[key] &&
!!this.#roles$.value[key].validationFunction &&
isFunction(this.#roles$.value[key].validationFunction);
const role = this.#roles$.value[key];
const hasValidationFn = !!role && !!role.validationFn;
if (hasValidationFunction && !isPromise(this.#roles$.value[key].validationFunction)) {
const validationFunction = this.#roles$.value[key].validationFunction as ValidationFn;
if (hasValidationFn && isFunction<ValidationFn<IqserRole>>(role.validationFn)) {
const validationFn = role.validationFn;
const immutableValue = { ...this.#roles$.value };
return of(null).pipe(
map(() => validationFunction(key, immutableValue)),
switchMap(promise => (isBoolean(promise) ? of(promise) : promise)),
switchMap(async () => validationFn(key, immutableValue)),
catchError(() => of(false)),
);
}
@ -117,12 +107,14 @@ export class IqserRolesService {
return firstValueFrom(res);
}
#hasRolePermission(roles: IqserRolesObject, roleNames: string[]): Promise<boolean | undefined> {
#hasPermission(roleNames: string[]): Promise<boolean | undefined> {
const res = from(roleNames).pipe(
mergeMap(key => {
if (roles[key] && Array.isArray(roles[key].validationFunction)) {
return from(<string[]>roles[key].validationFunction).pipe(
mergeMap(permission => this._permissionsService.hasPermission(permission)),
const role = this.#roles$.value[key];
if (role && !isFunction<ValidationFn<IqserRole>>(role.validationFn)) {
return from(role.validationFn).pipe(
mergeMap(permission => this._permissionsService.has(permission)),
every(hasPermission => hasPermission === true),
);
}

View File

@ -1,38 +1,49 @@
export function isFunction<T>(value: any): value is T {
import { AllowFn, IqserPermissionsRouterData, IqserRedirectToNavigationParameters } from './models/permissions-router-data.model';
import { ActivatedRouteSnapshot, Route, RouterStateSnapshot } from '@angular/router';
import { IqserPermissionsData } from './services/permissions-guard.service';
export function isFunction<T>(value: unknown): value is T {
return typeof value === 'function';
}
export function isPlainObject(value: any): boolean {
export function isPlainObject(value: unknown): boolean {
if (Object.prototype.toString.call(value) !== '[object Object]') {
return false;
} else {
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype === Object.prototype;
}
export function isString(value: any): value is string {
export function isString(value: unknown): value is string {
return !!value && typeof value === 'string';
}
export function isBoolean(value: any): value is boolean {
return typeof value === 'boolean';
export function notEmpty(value?: string | string[]): boolean {
return Array.isArray(value) ? value.length > 0 : !!value;
}
export function isPromise(promise: any) {
return Object.prototype.toString.call(promise) === '[object Promise]';
}
export function notEmptyValue(value: string | string[]): boolean {
if (Array.isArray(value)) {
return value.length > 0;
}
return !!value;
}
export function transformStringToArray(value?: string | string[]): string[] {
export function toArray(value?: string | string[]): string[] {
if (isString(value)) {
return [value];
}
return value ?? [];
}
export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {
return isPlainObject(object) && (!!object.navigationCommands || !!object.navigationExtras);
}
export function transformPermission(
permissions: IqserPermissionsRouterData,
route: ActivatedRouteSnapshot | Route,
state?: RouterStateSnapshot,
): IqserPermissionsData {
const only = isFunction<AllowFn>(permissions.allow) ? permissions.allow(route, state) : toArray(permissions.allow);
const redirectTo = permissions.redirectTo;
return {
allow: only,
redirectTo,
};
}

9
tsconfig.spec.json Normal file
View File

@ -0,0 +1,9 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc/spec",
"types": ["jest", "node"],
"esModuleInterop": true
},
"include": ["./src/lib/**/*.spec.ts", "./src/lib/**/*.d.ts"]
}