update services
This commit is contained in:
parent
9275701f93
commit
7d4e0a851a
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
import 'jest-extended';
|
||||||
@ -1,6 +1,4 @@
|
|||||||
export * from './models/permission.model';
|
export * from './types';
|
||||||
export * from './models/permissions-router-data.model';
|
|
||||||
export * from './models/role.model';
|
|
||||||
export * from './services/permissions-guard.service';
|
export * from './services/permissions-guard.service';
|
||||||
export * from './services/permissions.service';
|
export * from './services/permissions.service';
|
||||||
export * from './services/roles.service';
|
export * from './services/roles.service';
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import { ValidationFn } from './permissions-router-data.model';
|
|
||||||
|
|
||||||
export interface IqserPermission {
|
|
||||||
name: string;
|
|
||||||
validationFn?: ValidationFn<IqserPermission>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IqserPermissionsObject {
|
|
||||||
[name: string]: IqserPermission;
|
|
||||||
}
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import { ValidationFn } from './permissions-router-data.model';
|
|
||||||
|
|
||||||
export interface IqserRole {
|
|
||||||
name: string;
|
|
||||||
validationFn: ValidationFn<IqserRole> | string[];
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -14,9 +14,9 @@ import {
|
|||||||
import { merge, Subject, Subscription, switchMap } from 'rxjs';
|
import { merge, Subject, Subscription, switchMap } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import { tap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { notEmpty } from './utils';
|
|
||||||
import { IqserRolesService } from './services/roles.service';
|
import { IqserRolesService } from './services/roles.service';
|
||||||
import { IqserPermissionsService } from './services/permissions.service';
|
import { IqserPermissionsService } from './services/permissions.service';
|
||||||
|
import { List } from '../utils';
|
||||||
|
|
||||||
type NgTemplate = TemplateRef<unknown>;
|
type NgTemplate = TemplateRef<unknown>;
|
||||||
|
|
||||||
@ -38,11 +38,11 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
@Output() readonly permissionsAuthorized = new EventEmitter();
|
@Output() readonly permissionsAuthorized = new EventEmitter();
|
||||||
@Output() readonly permissionsUnauthorized = new EventEmitter();
|
@Output() readonly permissionsUnauthorized = new EventEmitter();
|
||||||
|
|
||||||
#permissions?: string | string[];
|
#permissions?: string | List;
|
||||||
#thenTemplateRef: TemplateRef<unknown>;
|
#thenTemplateRef: TemplateRef<unknown>;
|
||||||
#elseTemplateRef?: TemplateRef<unknown>;
|
#elseTemplateRef?: TemplateRef<unknown>;
|
||||||
#thenViewRef?: EmbeddedViewRef<unknown>;
|
#thenViewRef: EmbeddedViewRef<unknown> | boolean = false;
|
||||||
#elseViewRef?: EmbeddedViewRef<unknown>;
|
#elseViewRef: EmbeddedViewRef<unknown> | boolean = false;
|
||||||
|
|
||||||
readonly #updateView = new Subject<void>();
|
readonly #updateView = new Subject<void>();
|
||||||
readonly #subscription = new Subscription();
|
readonly #subscription = new Subscription();
|
||||||
@ -58,7 +58,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Input()
|
@Input()
|
||||||
set allow(value: string | string[]) {
|
set allow(value: string | List) {
|
||||||
this.#permissions = value;
|
this.#permissions = value;
|
||||||
this.#updateView.next();
|
this.#updateView.next();
|
||||||
}
|
}
|
||||||
@ -67,7 +67,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
set allowThen(template: NgTemplate) {
|
set allowThen(template: NgTemplate) {
|
||||||
assertTemplate('allowThen', template);
|
assertTemplate('allowThen', template);
|
||||||
this.#thenTemplateRef = template;
|
this.#thenTemplateRef = template;
|
||||||
this.#thenViewRef = undefined;
|
this.#thenViewRef = false;
|
||||||
this.#updateView.next();
|
this.#updateView.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
set allowElse(template: NgTemplate) {
|
set allowElse(template: NgTemplate) {
|
||||||
assertTemplate('allowElse', template);
|
assertTemplate('allowElse', template);
|
||||||
this.#elseTemplateRef = template;
|
this.#elseTemplateRef = template;
|
||||||
this.#elseViewRef = undefined;
|
this.#elseViewRef = false;
|
||||||
this.#updateView.next();
|
this.#updateView.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,21 +91,17 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#waitForRolesAndPermissions() {
|
#waitForRolesAndPermissions() {
|
||||||
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(
|
return merge(this._permissionsService.permissions$, this._rolesService.roles$).pipe(tap(() => this.#validate()));
|
||||||
tap(() => (notEmpty(this.#permissions) ? this.#validate() : this.#showThenBlock())),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#validate(): void {
|
#validate(): void {
|
||||||
Promise.all([this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)])
|
if (!this.#permissions) {
|
||||||
.then(([hasPermissions, hasRoles]) => {
|
return this.#showThenBlock();
|
||||||
if (hasPermissions || hasRoles) {
|
}
|
||||||
return this.#showThenBlock();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.#showElseBlock();
|
const promises = [this._permissionsService.has(this.#permissions), this._rolesService.has(this.#permissions)];
|
||||||
})
|
const result = Promise.all(promises).then(([hasPermission, hasRole]) => hasPermission || hasRole);
|
||||||
.catch(() => this.#showElseBlock());
|
result.then(isAllowed => (isAllowed ? this.#showThenBlock() : this.#showElseBlock())).catch(() => this.#showElseBlock());
|
||||||
}
|
}
|
||||||
|
|
||||||
#showElseBlock() {
|
#showElseBlock() {
|
||||||
@ -114,8 +110,8 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.permissionsUnauthorized.emit();
|
this.permissionsUnauthorized.emit();
|
||||||
this.#thenViewRef = undefined;
|
this.#thenViewRef = false;
|
||||||
this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef);
|
this.#elseViewRef = this.#showTemplate(this.#elseTemplateRef) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#showThenBlock() {
|
#showThenBlock() {
|
||||||
@ -124,18 +120,13 @@ export class IqserPermissionsDirective implements OnDestroy, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.permissionsAuthorized.emit();
|
this.permissionsAuthorized.emit();
|
||||||
this.#elseViewRef = undefined;
|
this.#elseViewRef = false;
|
||||||
this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef);
|
this.#thenViewRef = this.#showTemplate(this.#thenTemplateRef) ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
#showTemplate(template?: NgTemplate) {
|
#showTemplate(template?: NgTemplate) {
|
||||||
this._viewContainer.clear();
|
this._viewContainer.clear();
|
||||||
|
return template ? this._viewContainer.createEmbeddedView(template) : undefined;
|
||||||
if (!template) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return this._viewContainer.createEmbeddedView(template);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -82,7 +82,7 @@ describe('Permissions guard', () => {
|
|||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when only doesnt match', async () => {
|
it('should return false when allow doesnt match', async () => {
|
||||||
testRoute = {
|
testRoute = {
|
||||||
data: {
|
data: {
|
||||||
permissions: {
|
permissions: {
|
||||||
@ -440,11 +440,11 @@ describe('Role guard with redirectTo as function', () => {
|
|||||||
roleService = TestBed.inject(IqserRolesService);
|
roleService = TestBed.inject(IqserRolesService);
|
||||||
permissionsService.add('canReadAgenda');
|
permissionsService.add('canReadAgenda');
|
||||||
permissionsService.add('AWESOME');
|
permissionsService.add('AWESOME');
|
||||||
roleService.add('ADMIN', ['AWESOME', 'canReadAgenda']);
|
roleService.add({ ADMIN: ['AWESOME', 'canReadAgenda'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should dynamically pass if one satisfies', async () => {
|
it('should dynamically pass if one satisfies', async () => {
|
||||||
roleService.add('RUN', ['BLABLA', 'BLABLA2']);
|
roleService.add({ RUN: ['BLABLA', 'BLABLA2'] });
|
||||||
|
|
||||||
testRoute = {
|
testRoute = {
|
||||||
data: {
|
data: {
|
||||||
|
|||||||
@ -15,18 +15,20 @@ import { first, mergeMap, tap } from 'rxjs/operators';
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_REDIRECT_KEY,
|
DEFAULT_REDIRECT_KEY,
|
||||||
IqserPermissionsRouterData,
|
IqserActivatedRouteSnapshot,
|
||||||
|
IqserRoute,
|
||||||
NavigationCommandsFn,
|
NavigationCommandsFn,
|
||||||
NavigationExtrasFn,
|
NavigationExtrasFn,
|
||||||
RedirectTo,
|
RedirectTo,
|
||||||
RedirectToFn,
|
RedirectToFn,
|
||||||
} from '../models/permissions-router-data.model';
|
} from '../types';
|
||||||
import { IqserPermissionsService } from './permissions.service';
|
import { IqserPermissionsService } from './permissions.service';
|
||||||
import { IqserRolesService } from './roles.service';
|
import { IqserRolesService } from './roles.service';
|
||||||
import { isFunction, isRedirectWithParameters, isString, transformPermission } from '../utils';
|
import { isArray, isFunction, isRedirectWithParameters, isString, transformPermission } from '../utils';
|
||||||
|
import { List } from '../../utils';
|
||||||
|
|
||||||
export interface IqserPermissionsData {
|
export interface IqserPermissionsData {
|
||||||
allow: string | string[];
|
allow: string | List;
|
||||||
redirectTo?: RedirectTo | RedirectToFn;
|
redirectTo?: RedirectTo | RedirectToFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -38,15 +40,15 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
|
|||||||
private readonly _router: Router,
|
private readonly _router: Router,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
canActivate(route: IqserActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
return this.#checkPermissions(route, state);
|
return this.#checkPermissions(route, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
canActivateChild(childRoute: IqserActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||||
return this.#checkPermissions(childRoute, state);
|
return this.#checkPermissions(childRoute, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
canLoad(route: Route) {
|
canLoad(route: IqserRoute) {
|
||||||
return this.#checkPermissions(route);
|
return this.#checkPermissions(route);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,15 +60,15 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
|
|||||||
return this.#validatePermissions(permissions, route, state);
|
return this.#validatePermissions(permissions, route, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
#checkPermissions(route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) {
|
#checkPermissions(route: IqserActivatedRouteSnapshot | IqserRoute, state?: RouterStateSnapshot) {
|
||||||
const routePermissions = route?.data ? (route.data['permissions'] as IqserPermissionsRouterData) : undefined;
|
const routePermissions = route.data?.permissions;
|
||||||
if (!routePermissions) {
|
if (!routePermissions) {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
const permissions = transformPermission(routePermissions, route, state);
|
const permissions = transformPermission(routePermissions, route, state);
|
||||||
|
|
||||||
if (permissions?.allow?.length > 0) {
|
if (permissions.allow?.length > 0) {
|
||||||
return this.#validate(permissions, route, state);
|
return this.#validate(permissions, route, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -126,7 +128,6 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
|
|||||||
|
|
||||||
if (failed) {
|
if (failed) {
|
||||||
failedPermission = permission;
|
failedPermission = permission;
|
||||||
console.log(`Permission ${permission} is not allowed`);
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -172,7 +173,7 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
|
|||||||
return this.#redirectToAnotherRoute(failedPermissionRedirectTo, route, failedPermission, state);
|
return this.#redirectToAnotherRoute(failedPermissionRedirectTo, route, failedPermission, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isFunction<RedirectToFn>(permissions.redirectTo) || isString(permissions.redirectTo) || Array.isArray(permissions.redirectTo)) {
|
if (isFunction<RedirectToFn>(permissions.redirectTo) || isString(permissions.redirectTo) || isArray(permissions.redirectTo)) {
|
||||||
return this.#redirectToAnotherRoute(permissions.redirectTo, route, failedPermission, state);
|
return this.#redirectToAnotherRoute(permissions.redirectTo, route, failedPermission, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +196,7 @@ export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateC
|
|||||||
!isFunction<RedirectToFn>(redirectTo) &&
|
!isFunction<RedirectToFn>(redirectTo) &&
|
||||||
!isString(redirectTo) &&
|
!isString(redirectTo) &&
|
||||||
!isRedirectWithParameters(redirectTo) &&
|
!isRedirectWithParameters(redirectTo) &&
|
||||||
!Array.isArray(redirectTo)
|
!isArray(redirectTo)
|
||||||
) {
|
) {
|
||||||
return redirectTo[failedPermission];
|
return redirectTo[failedPermission];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,6 +8,10 @@ const GUEST = 'GUEST' as const;
|
|||||||
describe('Permissions Service', () => {
|
describe('Permissions Service', () => {
|
||||||
let permissionsService: IqserPermissionsService;
|
let permissionsService: IqserPermissionsService;
|
||||||
|
|
||||||
|
function getPermissionsLength() {
|
||||||
|
return Object.keys(permissionsService.get()).length;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [IqserPermissionsModule.forRoot()],
|
imports: [IqserPermissionsModule.forRoot()],
|
||||||
@ -35,32 +39,29 @@ describe('Permissions Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all permissions from permissions object', () => {
|
it('should remove all permissions from permissions object', () => {
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
|
|
||||||
permissionsService.add(ADMIN);
|
permissionsService.add(ADMIN);
|
||||||
permissionsService.add(GUEST);
|
permissionsService.add(GUEST);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(2);
|
expect(getPermissionsLength()).toEqual(2);
|
||||||
|
|
||||||
permissionsService.clear();
|
permissionsService.clear();
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add multiple permissions', () => {
|
it('should add multiple permissions', () => {
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
|
|
||||||
permissionsService.add([ADMIN, GUEST]);
|
permissionsService.add([ADMIN, GUEST]);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(2);
|
expect(getPermissionsLength()).toEqual(2);
|
||||||
expect(permissionsService.get()).toEqual({
|
expect(Object.keys(permissionsService.get())).toEqual([ADMIN, GUEST]);
|
||||||
ADMIN: { name: 'ADMIN' },
|
|
||||||
GUEST: { name: 'GUEST' },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when permission name is present in permissions object', async () => {
|
it('should return true when permission name is present in permissions object', async () => {
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
|
|
||||||
permissionsService.add([ADMIN, GUEST]);
|
permissionsService.add([ADMIN, GUEST]);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(2);
|
expect(getPermissionsLength()).toEqual(2);
|
||||||
|
|
||||||
let result = await permissionsService.has('ADMIN');
|
let result = await permissionsService.has('ADMIN');
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
@ -72,74 +73,75 @@ describe('Permissions Service', () => {
|
|||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
result = await permissionsService.has(['ADMIN', 'IRIISISTABLE']);
|
result = await permissionsService.has(['ADMIN', 'IRIISISTABLE']);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when permission function return true', async () => {
|
it('should return true when permission function return true', async () => {
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
|
|
||||||
permissionsService.add(ADMIN, () => true);
|
permissionsService.add({ ADMIN: () => true });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(1);
|
expect(getPermissionsLength()).toEqual(1);
|
||||||
|
|
||||||
let result = await permissionsService.has('ADMIN');
|
let result = await permissionsService.has('ADMIN');
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
permissionsService.add(GUEST, () => false);
|
permissionsService.add({ GUEST: () => false });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(2);
|
expect(getPermissionsLength()).toEqual(2);
|
||||||
|
|
||||||
result = await permissionsService.has('GUEST');
|
result = await permissionsService.has('GUEST');
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
|
|
||||||
permissionsService.add('TEST1', () => Promise.resolve(true));
|
permissionsService.add({ TEST1: () => Promise.resolve(true) });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(3);
|
expect(getPermissionsLength()).toEqual(3);
|
||||||
|
|
||||||
result = await permissionsService.has('TEST1');
|
result = await permissionsService.has('TEST1');
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
permissionsService.add('TEST2', () => Promise.resolve(false));
|
permissionsService.add({ TEST2: () => Promise.resolve(false) });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(4);
|
expect(getPermissionsLength()).toEqual(4);
|
||||||
|
|
||||||
result = await permissionsService.has('TEST2');
|
result = await permissionsService.has('TEST2');
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: permissions array with function should not be allowed
|
|
||||||
it('should return true when permissions array function return true', async () => {
|
it('should return true when permissions array function return true', async () => {
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
|
|
||||||
permissionsService.add([ADMIN], () => true);
|
permissionsService.add({ ADMIN: () => true });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(1);
|
expect(getPermissionsLength()).toEqual(1);
|
||||||
|
|
||||||
let result = await permissionsService.has('ADMIN');
|
let result = await permissionsService.has('ADMIN');
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
permissionsService.add([GUEST], () => false);
|
permissionsService.add({ GUEST: () => false });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(2);
|
expect(getPermissionsLength()).toEqual(2);
|
||||||
|
|
||||||
result = await permissionsService.has('GUEST');
|
result = await permissionsService.has('GUEST');
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
|
|
||||||
permissionsService.add(['TEST1'], () => Promise.resolve(true));
|
permissionsService.add({ TEST1: () => Promise.resolve(true) });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(3);
|
expect(getPermissionsLength()).toEqual(3);
|
||||||
|
|
||||||
result = await permissionsService.has('TEST1');
|
result = await permissionsService.has('TEST1');
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
permissionsService.add(['TEST9'], () => Promise.resolve(false));
|
permissionsService.add({ TEST9: () => Promise.resolve(false) });
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(4);
|
expect(getPermissionsLength()).toEqual(4);
|
||||||
|
|
||||||
result = await permissionsService.has(['TEST9']);
|
result = await permissionsService.has(['TEST9']);
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call validationFn with permission name and store', async () => {
|
it('should call validationFn with permission name and store', async () => {
|
||||||
permissionsService.add('TEST11', (name, store) => {
|
permissionsService.add({
|
||||||
expect(name).toEqual('TEST11');
|
TEST11: (name, store) => {
|
||||||
expect(store['TEST11']).toBeTruthy();
|
expect(name).toEqual('TEST11');
|
||||||
return Promise.resolve(true);
|
expect(store['TEST11']).toBeTruthy();
|
||||||
|
return Promise.resolve(true);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(1);
|
expect(getPermissionsLength()).toEqual(1);
|
||||||
|
|
||||||
const result = await permissionsService.has(['TEST11']);
|
const result = await permissionsService.has(['TEST11']);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|||||||
@ -1,103 +1,79 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { catchError, first, map, mergeAll, switchMap } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { isFunction, toArray } from '../utils';
|
import { isArray, isString, toArray } from '../utils';
|
||||||
import { ValidationFn } from '../models/permissions-router-data.model';
|
import { IqserPermissions, PermissionValidationFn } from '../types';
|
||||||
import { IqserPermission, IqserPermissionsObject } from '../models/permission.model';
|
import { List } from '../../utils';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IqserPermissionsService {
|
export class IqserPermissionsService {
|
||||||
readonly permissions$: Observable<IqserPermissionsObject>;
|
readonly permissions$: Observable<IqserPermissions>;
|
||||||
readonly #permissions$ = new BehaviorSubject<IqserPermissionsObject>({});
|
readonly #permissions$ = new BehaviorSubject<IqserPermissions>({});
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.permissions$ = this.#permissions$.asObservable();
|
this.permissions$ = this.#permissions$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove all permissions from permissions source
|
|
||||||
*/
|
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.#permissions$.next({});
|
this.#permissions$.next({});
|
||||||
}
|
}
|
||||||
|
|
||||||
has(permission?: string | string[]): Promise<boolean> {
|
has(permission: string): Promise<boolean>;
|
||||||
if (!permission || (Array.isArray(permission) && permission.length === 0)) {
|
has(permissions: List): Promise<boolean>;
|
||||||
return Promise.resolve(true);
|
has(permissions: string | List): Promise<boolean>;
|
||||||
}
|
has(value: string | List): Promise<boolean> {
|
||||||
|
const isEmpty = !value || value.length === 0;
|
||||||
permission = toArray(permission);
|
return isEmpty ? Promise.resolve(true) : this.#has(toArray(value));
|
||||||
return this.#hasArray(permission);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load(permissions: string[], validationFn?: ValidationFn<IqserPermission>): void {
|
load(permissions: IqserPermissions | List) {
|
||||||
const newPermissions = permissions.reduce((source, name) => this.#reduce(source, name, validationFn), {});
|
return this.#reduce(permissions);
|
||||||
this.#permissions$.next(newPermissions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add(permission: string | string[], validationFn?: ValidationFn<IqserPermission>) {
|
add(permissions: string | List | IqserPermissions) {
|
||||||
const permissions = toArray(permission).reduce(
|
const _permissions = isString(permissions) ? { [permissions]: () => true } : permissions;
|
||||||
(source, name) => this.#reduce(source, name, validationFn),
|
return this.#reduce(_permissions, this.#permissions$.value);
|
||||||
this.#permissions$.value,
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.#permissions$.next(permissions);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(name: string): void {
|
remove(permission: string) {
|
||||||
const permissions = { ...this.#permissions$.value };
|
const permissions = { ...this.#permissions$.value };
|
||||||
delete permissions[name];
|
delete permissions[permission];
|
||||||
this.#permissions$.next(permissions);
|
this.#permissions$.next(permissions);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): IqserPermissionsObject;
|
get(): IqserPermissions;
|
||||||
get(name: string): IqserPermission | undefined;
|
get(permission: string): PermissionValidationFn | undefined;
|
||||||
get(name?: string): IqserPermission | IqserPermissionsObject | undefined {
|
get(permission?: string): IqserPermissions | PermissionValidationFn | undefined {
|
||||||
return name ? this.#permissions$.value[name] : this.#permissions$.value;
|
return permission ? this.#permissions$.value[permission] : this.#permissions$.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
#reduce(source: IqserPermissionsObject, name: string, validationFn?: ValidationFn<IqserPermission>): IqserPermissionsObject {
|
#reduce(permissions: IqserPermissions | List, initialValue = {} as IqserPermissions) {
|
||||||
if (!!validationFn && isFunction(validationFn)) {
|
if (isArray(permissions)) {
|
||||||
return { ...source, [name]: { name, validationFn } };
|
return this.#permissions$.next(this.#reduceList(permissions, initialValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
return { ...source, [name]: { name } };
|
return this.#permissions$.next(this.#reduceObject(permissions, initialValue));
|
||||||
}
|
}
|
||||||
|
|
||||||
#hasArray(permissions: string[]): Promise<boolean> {
|
#reduceObject(permissions: IqserPermissions, initialValue: IqserPermissions = {}) {
|
||||||
const promises = permissions.map(key => {
|
return Object.entries(permissions).reduce((acc, [permission, validationFn]) => {
|
||||||
if (this.#hasValidationFn(key)) {
|
return { ...acc, [permission]: validationFn };
|
||||||
const validationFunction = this.#permissions$.value[key].validationFn;
|
}, initialValue);
|
||||||
if (!validationFunction) {
|
}
|
||||||
return of(false);
|
|
||||||
}
|
|
||||||
const immutableValue = { ...this.#permissions$.value };
|
|
||||||
|
|
||||||
return of(null).pipe(
|
#reduceList(permissions: List, initialValue: IqserPermissions = {}): IqserPermissions {
|
||||||
switchMap(async () => validationFunction(key, immutableValue)),
|
return permissions.reduce((acc, permission) => {
|
||||||
catchError(() => of(false)),
|
return { ...acc, [permission]: () => true };
|
||||||
);
|
}, initialValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
return of(!!this.#permissions$.value[key]);
|
#has(permissions: List): Promise<boolean> {
|
||||||
|
const promises = permissions.map(permission => {
|
||||||
|
const validationFn = this.#permissions$.value[permission];
|
||||||
|
return validationFn?.(permission, this.#permissions$.value) ?? false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = from(promises).pipe(
|
return Promise.all(promises).then(results => results.every(result => result));
|
||||||
mergeAll(),
|
|
||||||
first(data => data !== false, false),
|
|
||||||
map(data => data !== false),
|
|
||||||
);
|
|
||||||
|
|
||||||
return firstValueFrom(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
#hasValidationFn(key: string): boolean {
|
|
||||||
return (
|
|
||||||
!!this.#permissions$.value[key] &&
|
|
||||||
!!this.#permissions$.value[key].validationFn &&
|
|
||||||
isFunction(this.#permissions$.value[key].validationFn)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,17 +1,23 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { ValidationFn } from '../models/permissions-router-data.model';
|
import { RoleValidationFn } from '../types';
|
||||||
import { IqserRolesService } from './roles.service';
|
import { IqserRolesService } from './roles.service';
|
||||||
import { IqserPermissionsService } from './permissions.service';
|
import { IqserPermissionsService } from './permissions.service';
|
||||||
import { IqserPermissionsModule } from '../permissions.module';
|
import { IqserPermissionsModule } from '../permissions.module';
|
||||||
import { IqserRole } from '../models/role.model';
|
|
||||||
|
|
||||||
const ADMIN = 'ADMIN' as const;
|
const ADMIN = 'ADMIN' as const;
|
||||||
const GUEST = 'GUEST' as const;
|
|
||||||
|
|
||||||
describe('Roles Service', () => {
|
describe('Roles Service', () => {
|
||||||
let rolesService: IqserRolesService;
|
let rolesService: IqserRolesService;
|
||||||
let permissionsService: IqserPermissionsService;
|
let permissionsService: IqserPermissionsService;
|
||||||
|
|
||||||
|
function getRolesLength() {
|
||||||
|
return Object.keys(rolesService.get()).length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPermissionsLength() {
|
||||||
|
return Object.keys(permissionsService.get()).length;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [IqserPermissionsModule.forRoot()],
|
imports: [IqserPermissionsModule.forRoot()],
|
||||||
@ -28,16 +34,16 @@ describe('Roles Service', () => {
|
|||||||
it('should add role to role object', () => {
|
it('should add role to role object', () => {
|
||||||
expect(rolesService.get(ADMIN)).toBeFalsy();
|
expect(rolesService.get(ADMIN)).toBeFalsy();
|
||||||
|
|
||||||
rolesService.add(ADMIN, ['edit', 'remove']);
|
rolesService.add({ ADMIN: ['edit', 'remove'] });
|
||||||
|
|
||||||
expect(rolesService.get(ADMIN)).toBeTruthy();
|
expect(rolesService.get(ADMIN)).toBeTruthy();
|
||||||
expect(rolesService.get()).toEqual({ ADMIN: { name: ADMIN, validationFn: ['edit', 'remove'] } });
|
expect(rolesService.get()).toEqual({ ADMIN: ['edit', 'remove'] });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove role from role object', () => {
|
it('should remove role from role object', () => {
|
||||||
expect(rolesService.get(ADMIN)).toBeFalsy();
|
expect(rolesService.get(ADMIN)).toBeFalsy();
|
||||||
|
|
||||||
rolesService.add(ADMIN, ['edit', 'remove']);
|
rolesService.add({ ADMIN: ['edit', 'remove'] });
|
||||||
expect(rolesService.get(ADMIN)).toBeTruthy();
|
expect(rolesService.get(ADMIN)).toBeTruthy();
|
||||||
|
|
||||||
rolesService.remove(ADMIN);
|
rolesService.remove(ADMIN);
|
||||||
@ -45,39 +51,41 @@ describe('Roles Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all roles from object', () => {
|
it('should remove all roles from object', () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
|
|
||||||
rolesService.add(ADMIN, ['edit', 'remove']);
|
rolesService.add({
|
||||||
rolesService.add(GUEST, ['edit', 'remove']);
|
ADMIN: ['edit', 'remove'],
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
GUEST: ['edit', 'remove'],
|
||||||
|
});
|
||||||
|
expect(getRolesLength()).toEqual(2);
|
||||||
|
|
||||||
rolesService.clear();
|
rolesService.clear();
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add multiple roles', () => {
|
it('should add multiple roles', () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
rolesService.add({
|
rolesService.add({
|
||||||
ADMIN: ['Nice'],
|
ADMIN: ['Nice'],
|
||||||
GUEST: ['Awesome'],
|
GUEST: ['Awesome'],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
expect(rolesService.get()).toEqual({
|
expect(rolesService.get()).toEqual({
|
||||||
ADMIN: { name: ADMIN, validationFn: ['Nice'] },
|
ADMIN: ['Nice'],
|
||||||
GUEST: { name: GUEST, validationFn: ['Awesome'] },
|
GUEST: ['Awesome'],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('return true when role name is present in Roles object', async () => {
|
it('return true when role name is present in roles object', async () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
|
|
||||||
rolesService.add({
|
rolesService.add({
|
||||||
ADMIN: ['Nice'],
|
ADMIN: ['Nice'],
|
||||||
GUEST: ['Awesome'],
|
GUEST: ['Awesome'],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
|
|
||||||
let result = await rolesService.has(ADMIN);
|
let result = await rolesService.has(ADMIN);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
@ -86,23 +94,24 @@ describe('Roles Service', () => {
|
|||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
|
|
||||||
result = await rolesService.has([ADMIN, 'IRIISISTABLE']);
|
result = await rolesService.has([ADMIN, 'IRIISISTABLE']);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('return true when role permission name is present in Roles object', async () => {
|
it('return true when role permission name is present in Roles object', async () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
|
|
||||||
rolesService.add({
|
rolesService.add({
|
||||||
ADMIN: ['Nice'],
|
ADMIN: ['Nice'],
|
||||||
GUEST: ['Awesome'],
|
GUEST: ['Awesome'],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
|
|
||||||
let result = await rolesService.has(ADMIN);
|
let result = await rolesService.has(ADMIN);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(true);
|
||||||
|
|
||||||
result = await rolesService.has([ADMIN, 'IRRISISTABLE']);
|
result = await rolesService.has([ADMIN, 'IRRISISTABLE']);
|
||||||
expect(result).toEqual(true);
|
expect(result).toEqual(false);
|
||||||
|
|
||||||
result = await rolesService.has('SHOULDNOTHAVEROLE');
|
result = await rolesService.has('SHOULDNOTHAVEROLE');
|
||||||
expect(result).toEqual(false);
|
expect(result).toEqual(false);
|
||||||
@ -112,17 +121,11 @@ describe('Roles Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return role when requested with has role', () => {
|
it('should return role when requested with has role', () => {
|
||||||
rolesService.add('role', () => true);
|
rolesService.add({ role: () => true });
|
||||||
const role = rolesService.get('role');
|
const validationFn = rolesService.get('role') as RoleValidationFn;
|
||||||
|
|
||||||
if (!role) {
|
expect(validationFn).toBeTruthy();
|
||||||
return expect(role).toBeTruthy();
|
expect(validationFn('role', rolesService.get())).toEqual(true);
|
||||||
}
|
|
||||||
|
|
||||||
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 () => {
|
it('should return true when checking with empty permission(not specified)', async () => {
|
||||||
@ -136,31 +139,32 @@ describe('Roles Service', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return false when role is not specified in the list', async () => {
|
it('should return false when role is not specified in the list', async () => {
|
||||||
rolesService.add('test', ['One']);
|
rolesService.add({ test: ['One'] });
|
||||||
|
|
||||||
const result = await rolesService.has('nice');
|
const result = await rolesService.has('nice');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return true when passing empty array', async () => {
|
it('should return true when passing empty array', async () => {
|
||||||
rolesService.add('test', ['One']);
|
rolesService.add({ test: ['One'] });
|
||||||
|
|
||||||
const result = await rolesService.has([]);
|
const result = await rolesService.has([]);
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add permissions to roles automatically', async () => {
|
it('should add permissions to roles automatically', async () => {
|
||||||
rolesService.add('test', ['one', 'two']);
|
rolesService.add({ test: ['one', 'two'] });
|
||||||
|
|
||||||
const result = await rolesService.has('test');
|
const result = await rolesService.has('test');
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove roles and permissions add the same time', async () => {
|
it('should remove roles and permissions add the same time', async () => {
|
||||||
rolesService.add('test', ['one', 'two']);
|
rolesService.add({ test: ['one', 'two'] });
|
||||||
|
|
||||||
let result = await rolesService.has('test');
|
let result = await rolesService.has('test');
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
|
|
||||||
result = await permissionsService.has('one');
|
result = await permissionsService.has('one');
|
||||||
expect(result).toBe(true);
|
expect(result).toBe(true);
|
||||||
|
|
||||||
@ -169,41 +173,42 @@ describe('Roles Service', () => {
|
|||||||
|
|
||||||
result = await rolesService.has('test');
|
result = await rolesService.has('test');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
|
|
||||||
result = await permissionsService.has('one');
|
result = await permissionsService.has('one');
|
||||||
expect(result).toBe(false);
|
expect(result).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove all permissions and roles', () => {
|
it('should remove all permissions and roles', () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
|
|
||||||
rolesService.add(ADMIN, ['edit', 'remove']);
|
rolesService.add({ ADMIN: ['edit', 'remove'] });
|
||||||
rolesService.add(GUEST, ['edit1', 'remove2']);
|
rolesService.add({ GUEST: ['edit1', 'remove2'] });
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(4);
|
expect(getPermissionsLength()).toEqual(4);
|
||||||
|
|
||||||
rolesService.clear();
|
rolesService.clear();
|
||||||
permissionsService.clear();
|
permissionsService.clear();
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(0);
|
expect(getPermissionsLength()).toEqual(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add multiple roles with permissions', () => {
|
it('should add multiple roles with permissions', () => {
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(0);
|
expect(getRolesLength()).toEqual(0);
|
||||||
|
|
||||||
rolesService.add({
|
rolesService.add({
|
||||||
ADMIN: ['Nice'],
|
ADMIN: ['Nice'],
|
||||||
GUEST: ['Awesome', 'Another awesome'],
|
GUEST: ['Awesome', 'Another awesome'],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
expect(rolesService.get()).toEqual({
|
expect(rolesService.get()).toEqual({
|
||||||
ADMIN: { name: ADMIN, validationFn: ['Nice'] },
|
ADMIN: ['Nice'],
|
||||||
GUEST: { name: GUEST, validationFn: ['Awesome', 'Another awesome'] },
|
GUEST: ['Awesome', 'Another awesome'],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(Object.keys(rolesService.get()).length).toEqual(2);
|
expect(getRolesLength()).toEqual(2);
|
||||||
expect(Object.keys(permissionsService.get()).length).toEqual(3);
|
expect(getPermissionsLength()).toEqual(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,38 +1,27 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs';
|
import { BehaviorSubject, Observable } from 'rxjs';
|
||||||
import { catchError, every, first, map, mergeAll, mergeMap, switchMap } from 'rxjs/operators';
|
import { IqserRoles, RoleValidationFn } from '../types';
|
||||||
|
import { isArray, isBoolean, isString, toArray } from '../utils';
|
||||||
|
import { List } from '../../utils';
|
||||||
import { IqserPermissionsService } from './permissions.service';
|
import { IqserPermissionsService } from './permissions.service';
|
||||||
import { ValidationFn } from '../models/permissions-router-data.model';
|
|
||||||
import { IqserRole } from '../models/role.model';
|
|
||||||
import { isFunction, isString, toArray } from '../utils';
|
|
||||||
|
|
||||||
export interface IqserRolesObject {
|
|
||||||
[name: string]: IqserRole;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class IqserRolesService {
|
export class IqserRolesService {
|
||||||
readonly roles$: Observable<IqserRolesObject>;
|
readonly roles$: Observable<IqserRoles>;
|
||||||
readonly #roles$ = new BehaviorSubject<IqserRolesObject>({});
|
readonly #roles$ = new BehaviorSubject<IqserRoles>({});
|
||||||
|
|
||||||
constructor(private readonly _permissionsService: IqserPermissionsService) {
|
constructor(private readonly _permissionsService: IqserPermissionsService) {
|
||||||
this.roles$ = this.#roles$.asObservable();
|
this.roles$ = this.#roles$.asObservable();
|
||||||
}
|
}
|
||||||
|
|
||||||
add(role: string, validationFn: ValidationFn<IqserRole> | string[]): void;
|
add(roles: string | List | IqserRoles) {
|
||||||
add(rolesObj: Record<string, ValidationFn<IqserRole> | string[]>): void;
|
const _roles = isString(roles) ? { [roles]: () => true } : roles;
|
||||||
add(role: string | Record<string, ValidationFn<IqserRole> | string[]>, validationFn?: ValidationFn<IqserRole> | string[]) {
|
this.#reduce(_roles, this.#roles$.value);
|
||||||
if (isString(role) && validationFn) {
|
}
|
||||||
return this.#add(role, validationFn);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof role === 'object') {
|
load(roles: List | IqserRoles) {
|
||||||
return Object.keys(role).forEach(key => this.#add(key, role[key]));
|
this.#reduce(roles);
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid add role arguments');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
@ -45,85 +34,55 @@ export class IqserRolesService {
|
|||||||
this.#roles$.next(roles);
|
this.#roles$.next(roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(): IqserRolesObject;
|
get(): IqserRoles;
|
||||||
|
get(role: string): RoleValidationFn | List | undefined;
|
||||||
get(role: string): IqserRole | undefined;
|
get(role?: string): IqserRoles | RoleValidationFn | List | undefined {
|
||||||
|
|
||||||
get(role?: string): IqserRolesObject | IqserRole | undefined {
|
|
||||||
return role ? this.#roles$.value[role] : this.#roles$.value;
|
return role ? this.#roles$.value[role] : this.#roles$.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
has(names?: string | string[]): Promise<boolean> {
|
has(roles: string | List): Promise<boolean> {
|
||||||
const isNamesEmpty = !names || (Array.isArray(names) && names.length === 0);
|
const isEmpty = !roles || roles.length === 0;
|
||||||
|
|
||||||
if (isNamesEmpty) {
|
if (isEmpty) {
|
||||||
return Promise.resolve(true);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
names = toArray(names);
|
const validations = toArray(roles).map(role => this.#runValidation(role));
|
||||||
|
return Promise.all(validations)
|
||||||
return Promise.all([this.#hasRoleKey(names), this.#hasPermission(names)]).then(([hasRoles, hasPermissions]) => {
|
.then(results => this.#checkPermissionsIfNeeded(results))
|
||||||
return !!hasRoles || !!hasPermissions;
|
.then(results => results.every(result => result === true));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#add(role: string, validationFn: ValidationFn<IqserRole> | string[]) {
|
#checkPermissionsIfNeeded(results: List<string | boolean | List>) {
|
||||||
const roles: IqserRolesObject = {
|
return results.map(result => (isBoolean(result) ? result : this._permissionsService.has(result)));
|
||||||
...this.#roles$.value,
|
}
|
||||||
[role]: { name: role, validationFn },
|
|
||||||
};
|
|
||||||
|
|
||||||
if (Array.isArray(validationFn)) {
|
#runValidation(role: string) {
|
||||||
this._permissionsService.add(validationFn);
|
const validationFn = this.#roles$.value[role];
|
||||||
|
|
||||||
|
if (isArray(validationFn)) {
|
||||||
|
return this._permissionsService.has(validationFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.#roles$.next(roles);
|
return validationFn?.(role, this.#roles$.value) ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#hasRoleKey(roleName: string[]): Promise<boolean> {
|
#reduce(roles: List | IqserRoles, initialValue: IqserRoles = {}) {
|
||||||
const promises = roleName.map(key => {
|
const result = isArray(roles) ? this.#reduceList(roles, initialValue) : this.#reduceObject(roles, initialValue);
|
||||||
const role = this.#roles$.value[key];
|
return this.#roles$.next(result);
|
||||||
const hasValidationFn = !!role && !!role.validationFn;
|
}
|
||||||
|
|
||||||
if (hasValidationFn && isFunction<ValidationFn<IqserRole>>(role.validationFn)) {
|
#reduceObject(roles: IqserRoles, initialValue: IqserRoles = {}) {
|
||||||
const validationFn = role.validationFn;
|
return Object.entries(roles).reduce((acc, [role, validationFn]) => {
|
||||||
const immutableValue = { ...this.#roles$.value };
|
if (isArray(validationFn)) {
|
||||||
|
this._permissionsService.add(validationFn);
|
||||||
return of(null).pipe(
|
|
||||||
switchMap(async () => validationFn(key, immutableValue)),
|
|
||||||
catchError(() => of(false)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return of(false);
|
return { ...acc, [role]: validationFn };
|
||||||
});
|
}, initialValue);
|
||||||
|
|
||||||
const res = from(promises).pipe(
|
|
||||||
mergeAll(),
|
|
||||||
first(data => data !== false, false),
|
|
||||||
map(data => data !== false),
|
|
||||||
);
|
|
||||||
|
|
||||||
return firstValueFrom(res);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#hasPermission(roleNames: string[]): Promise<boolean | undefined> {
|
#reduceList(roles: List, initialValue: IqserRoles = {}) {
|
||||||
const res = from(roleNames).pipe(
|
return roles.reduce((acc, role) => ({ ...acc, [role]: () => true }), initialValue);
|
||||||
mergeMap(key => {
|
|
||||||
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),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return of(false);
|
|
||||||
}),
|
|
||||||
first(hasPermission => hasPermission === true, false),
|
|
||||||
);
|
|
||||||
|
|
||||||
return firstValueFrom(res);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,11 @@
|
|||||||
import { ActivatedRouteSnapshot, NavigationExtras, Route, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, NavigationExtras, Route, RouterStateSnapshot } from '@angular/router';
|
||||||
|
import { List } from '../utils';
|
||||||
|
|
||||||
|
export type IqserPermissions = Record<string, PermissionValidationFn>;
|
||||||
|
export type IqserRoles = Record<string, RoleValidationFn | List>;
|
||||||
|
|
||||||
export interface IqserPermissionsRouterData {
|
export interface IqserPermissionsRouterData {
|
||||||
allow: string | string[] | AllowFn;
|
allow: string | List | AllowFn;
|
||||||
redirectTo?: RedirectTo | RedirectToFn;
|
redirectTo?: RedirectTo | RedirectToFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -10,29 +14,29 @@ export interface IqserRedirectToNavigationParameters {
|
|||||||
navigationExtras?: NavigationExtras | NavigationExtrasFn;
|
navigationExtras?: NavigationExtras | NavigationExtrasFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllowFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | string[];
|
export type AllowFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => string | List;
|
||||||
|
|
||||||
export type RedirectTo =
|
export type RedirectTo =
|
||||||
| string
|
| string
|
||||||
| string[]
|
| List
|
||||||
| IqserRedirectToNavigationParameters
|
| IqserRedirectToNavigationParameters
|
||||||
| { [name: string]: IqserRedirectToNavigationParameters | string | RedirectToFn };
|
| Record<string, IqserRedirectToNavigationParameters | string | RedirectToFn>;
|
||||||
|
|
||||||
export type RedirectToFn = (failedPermission: 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 NavigationCommandsFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => any[];
|
||||||
export type NavigationExtrasFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => NavigationExtras;
|
export type NavigationExtrasFn = (route: ActivatedRouteSnapshot | Route, state?: RouterStateSnapshot) => NavigationExtras;
|
||||||
export type ValidationFn<T> = (name: string, store: Record<string, T>) => Promise<void | string | boolean> | boolean | string[];
|
export type RoleValidationFn = (name: string, store: IqserRoles) => Promise<string | List | boolean> | boolean | List;
|
||||||
|
export type PermissionValidationFn = (name: string, store: IqserPermissions) => Promise<boolean> | boolean;
|
||||||
|
|
||||||
export type IqserActivatedRouteSnapshot = ActivatedRouteSnapshot & {
|
export type IqserActivatedRouteSnapshot = ActivatedRouteSnapshot & {
|
||||||
data: {
|
data: {
|
||||||
permissions: IqserPermissionsRouterData;
|
permissions?: IqserPermissionsRouterData;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IqserRoute = Route & {
|
export type IqserRoute = Route & {
|
||||||
data: {
|
data: {
|
||||||
permissions: IqserPermissionsRouterData;
|
permissions?: IqserPermissionsRouterData;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { AllowFn, IqserPermissionsRouterData, IqserRedirectToNavigationParameters } from './models/permissions-router-data.model';
|
import { AllowFn, IqserPermissionsRouterData, IqserRedirectToNavigationParameters } from './types';
|
||||||
import { ActivatedRouteSnapshot, Route, RouterStateSnapshot } from '@angular/router';
|
import { ActivatedRouteSnapshot, Route, RouterStateSnapshot } from '@angular/router';
|
||||||
import { IqserPermissionsData } from './services/permissions-guard.service';
|
import { IqserPermissionsData } from './services/permissions-guard.service';
|
||||||
|
import { List } from '../utils';
|
||||||
|
|
||||||
export function isFunction<T>(value: unknown): value is T {
|
export function isFunction<T>(value: unknown): value is T {
|
||||||
return typeof value === 'function';
|
return typeof value === 'function';
|
||||||
@ -16,18 +17,20 @@ export function isPlainObject(value: unknown): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function isString(value: unknown): value is string {
|
export function isString(value: unknown): value is string {
|
||||||
return !!value && typeof value === 'string';
|
return typeof value === 'string';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function notEmpty(value?: string | string[]): boolean {
|
export function isBoolean(value: unknown): value is boolean {
|
||||||
return Array.isArray(value) ? value.length > 0 : !!value;
|
return typeof value === 'boolean';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toArray(value?: string | string[]): string[] {
|
export function isArray<T>(value: unknown): value is List<T>;
|
||||||
if (isString(value)) {
|
export function isArray<T>(value: unknown): value is T[] {
|
||||||
return [value];
|
return Array.isArray(value);
|
||||||
}
|
}
|
||||||
return value ?? [];
|
|
||||||
|
export function toArray(value?: string | List): List {
|
||||||
|
return isString(value) ? [value] : value ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {
|
export function isRedirectWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {
|
||||||
@ -39,11 +42,11 @@ export function transformPermission(
|
|||||||
route: ActivatedRouteSnapshot | Route,
|
route: ActivatedRouteSnapshot | Route,
|
||||||
state?: RouterStateSnapshot,
|
state?: RouterStateSnapshot,
|
||||||
): IqserPermissionsData {
|
): IqserPermissionsData {
|
||||||
const only = isFunction<AllowFn>(permissions.allow) ? permissions.allow(route, state) : toArray(permissions.allow);
|
const allow = isFunction<AllowFn>(permissions.allow) ? permissions.allow(route, state) : toArray(permissions.allow);
|
||||||
const redirectTo = permissions.redirectTo;
|
const redirectTo = permissions.redirectTo;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
allow: only,
|
allow,
|
||||||
redirectTo,
|
redirectTo,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user