wip permissions
This commit is contained in:
parent
5ea75f1f05
commit
b607cff5f9
@ -21,3 +21,4 @@ export * from './lib/caching';
|
||||
export * from './lib/users';
|
||||
export * from './lib/translations';
|
||||
export * from './lib/standalone';
|
||||
export * from './lib/permissions';
|
||||
|
||||
9
src/lib/permissions/index.ts
Normal file
9
src/lib/permissions/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
export * from './models/permission.model';
|
||||
export * from './models/permissions-router-data.model';
|
||||
export * from './models/role.model';
|
||||
export * from './services/permissions-guard.service';
|
||||
export * from './services/permissions.service';
|
||||
export * from './services/roles.service';
|
||||
export * from './permissions.directive';
|
||||
export * from './permissions.module';
|
||||
export * from './utils';
|
||||
6
src/lib/permissions/models/permission.model.ts
Normal file
6
src/lib/permissions/models/permission.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ValidationFn } from './permissions-router-data.model';
|
||||
|
||||
export interface IqserPermission {
|
||||
name: string;
|
||||
validationFunction?: ValidationFn;
|
||||
}
|
||||
32
src/lib/permissions/models/permissions-router-data.model.ts
Normal file
32
src/lib/permissions/models/permissions-router-data.model.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { ActivatedRouteSnapshot, NavigationExtras, Route, RouterStateSnapshot } from '@angular/router';
|
||||
|
||||
export interface IqserPermissionsRouterData {
|
||||
only?: string | string[] | OnlyFn;
|
||||
except?: string | string[] | ExceptFn;
|
||||
redirectTo?: RedirectTo | RedirectToFn;
|
||||
}
|
||||
|
||||
export interface IqserRedirectToNavigationParameters {
|
||||
navigationCommands: any[] | NavigationCommandsFn;
|
||||
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 RedirectTo =
|
||||
| string
|
||||
| { [name: string]: IqserRedirectToNavigationParameters | string | RedirectToFn }
|
||||
| IqserRedirectToNavigationParameters;
|
||||
|
||||
export type RedirectToFn = (
|
||||
rejectedPermissionName?: 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 const DEFAULT_REDIRECT_KEY = 'default';
|
||||
6
src/lib/permissions/models/role.model.ts
Normal file
6
src/lib/permissions/models/role.model.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { ValidationFn } from './permissions-router-data.model';
|
||||
|
||||
export interface IqserRole {
|
||||
name: string;
|
||||
validationFunction: ValidationFn | string[];
|
||||
}
|
||||
205
src/lib/permissions/permissions.directive.ts
Normal file
205
src/lib/permissions/permissions.directive.ts
Normal file
@ -0,0 +1,205 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Directive,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges,
|
||||
TemplateRef,
|
||||
ViewContainerRef,
|
||||
} from '@angular/core';
|
||||
|
||||
import { merge, Subscription } from 'rxjs';
|
||||
import { skip, take } from 'rxjs/operators';
|
||||
|
||||
import { isBoolean, notEmptyValue } from './utils';
|
||||
import { IqserRolesService } from './services/roles.service';
|
||||
import { IqserPermissionsService } from './services/permissions.service';
|
||||
|
||||
@Directive({
|
||||
selector: '[ngxPermissionsOnly],[ngxPermissionsExcept]',
|
||||
})
|
||||
export class NgxPermissionsDirective implements OnInit, OnDestroy, OnChanges {
|
||||
@Input() ngxPermissionsOnly!: string | string[];
|
||||
@Input() ngxPermissionsOnlyThen!: TemplateRef<any>;
|
||||
@Input() ngxPermissionsOnlyElse!: TemplateRef<any>;
|
||||
|
||||
@Input() ngxPermissionsExcept!: string | string[];
|
||||
@Input() ngxPermissionsExceptElse!: TemplateRef<any>;
|
||||
@Input() ngxPermissionsExceptThen!: TemplateRef<any>;
|
||||
|
||||
@Input() ngxPermissionsThen!: TemplateRef<any>;
|
||||
@Input() ngxPermissionsElse!: TemplateRef<any>;
|
||||
|
||||
@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;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
merge(this.permissionsService.permissions$, this.rolesService.roles$)
|
||||
.pipe(skip(this.firstMergeUnusedRun), take(1))
|
||||
.subscribe(() => {
|
||||
if (notEmptyValue(this.ngxPermissionsExcept)) {
|
||||
this.validateExceptAndOnlyPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (notEmptyValue(this.ngxPermissionsOnly)) {
|
||||
this.validateOnlyPermissions();
|
||||
return;
|
||||
}
|
||||
|
||||
this.handleAuthorisedPermission(this.getAuthorisedTemplates());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.initPermissionSubscription) {
|
||||
this.initPermissionSubscription.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());
|
||||
});
|
||||
}
|
||||
|
||||
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),
|
||||
])
|
||||
.then(([hasPermissions, hasRoles]) => {
|
||||
if (hasPermissions || hasRoles) {
|
||||
this.handleAuthorisedPermission(this.ngxPermissionsOnlyThen || this.ngxPermissionsThen || this.templateRef);
|
||||
} else {
|
||||
this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse || this.ngxPermissionsElse);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
this.handleUnauthorisedPermission(this.ngxPermissionsOnlyElse || this.ngxPermissionsElse);
|
||||
});
|
||||
}
|
||||
|
||||
private handleUnauthorisedPermission(template: TemplateRef<any>): void {
|
||||
if (isBoolean(this.currentAuthorizedState) && !this.currentAuthorizedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentAuthorizedState = false;
|
||||
this.permissionsUnauthorized.emit();
|
||||
|
||||
if (!this.elseBlockDefined()) {
|
||||
return;
|
||||
} else {
|
||||
this.showTemplateBlockInView(template);
|
||||
}
|
||||
}
|
||||
|
||||
private handleAuthorisedPermission(template: TemplateRef<any>): void {
|
||||
if (isBoolean(this.currentAuthorizedState) && this.currentAuthorizedState) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentAuthorizedState = true;
|
||||
this.permissionsAuthorized.emit();
|
||||
|
||||
if (!this.thenBlockDefined()) {
|
||||
return;
|
||||
} else {
|
||||
this.showTemplateBlockInView(template);
|
||||
}
|
||||
}
|
||||
|
||||
private showTemplateBlockInView(template: TemplateRef<any>): void {
|
||||
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;
|
||||
}
|
||||
}
|
||||
23
src/lib/permissions/permissions.module.ts
Normal file
23
src/lib/permissions/permissions.module.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { NgModule, Optional } from '@angular/core';
|
||||
import { NgxPermissionsDirective } 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],
|
||||
})
|
||||
export class IqserPermissionsModule {
|
||||
constructor(@Optional() permissionsService: IqserPermissionsService) {
|
||||
if (!permissionsService) {
|
||||
throw new Error('Call forRoot() in AppModule to use IqserPermissionsModule');
|
||||
}
|
||||
}
|
||||
|
||||
static forRoot() {
|
||||
return {
|
||||
ngModule: IqserPermissionsModule,
|
||||
providers: [IqserPermissionsService, IqserPermissionsGuard, IqserRolesService],
|
||||
};
|
||||
}
|
||||
}
|
||||
318
src/lib/permissions/services/permissions-guard.service.ts
Normal file
318
src/lib/permissions/services/permissions-guard.service.ts
Normal file
@ -0,0 +1,318 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import {
|
||||
ActivatedRouteSnapshot,
|
||||
CanActivate,
|
||||
CanActivateChild,
|
||||
CanLoad,
|
||||
NavigationExtras,
|
||||
Route,
|
||||
Router,
|
||||
RouterStateSnapshot,
|
||||
} from '@angular/router';
|
||||
|
||||
import { firstValueFrom, forkJoin, from, Observable, 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';
|
||||
|
||||
export interface IqserPermissionsData {
|
||||
only?: string | string[];
|
||||
except?: string | string[];
|
||||
redirectTo?: RedirectTo | RedirectToFn;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IqserPermissionsGuard implements CanActivate, CanLoad, CanActivateChild {
|
||||
constructor(
|
||||
private readonly _permissionsService: IqserPermissionsService,
|
||||
private readonly _rolesService: IqserRolesService,
|
||||
private readonly _router: Router,
|
||||
) {}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Promise<boolean> | boolean {
|
||||
return this.#hasPermissions(route, state);
|
||||
}
|
||||
|
||||
canActivateChild(childRoute: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> | Promise<boolean> | boolean {
|
||||
return this.#hasPermissions(childRoute, state);
|
||||
}
|
||||
|
||||
canLoad(route: Route): boolean | Observable<boolean> | Promise<boolean> {
|
||||
return this.#hasPermissions(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);
|
||||
}
|
||||
|
||||
if (this.#isParameterAvailable(permissions.only)) {
|
||||
return this.passingOnlyPermissionsValidation(permissions, route, state);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
#redirectToAnotherRoute(
|
||||
permissionRedirectTo: RedirectTo | RedirectToFn,
|
||||
route: ActivatedRouteSnapshot | Route,
|
||||
state?: RouterStateSnapshot,
|
||||
failedPermissionName?: string,
|
||||
): void {
|
||||
const redirectTo = isFunction<RedirectToFn>(permissionRedirectTo)
|
||||
? permissionRedirectTo(failedPermissionName, route, state)
|
||||
: permissionRedirectTo;
|
||||
|
||||
if (this.#isRedirectionWithParameters(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;
|
||||
}
|
||||
|
||||
if (Array.isArray(redirectTo)) {
|
||||
this._router.navigate(redirectTo);
|
||||
} else {
|
||||
this._router.navigate([redirectTo]);
|
||||
}
|
||||
}
|
||||
|
||||
#isRedirectionWithParameters(object: any | IqserRedirectToNavigationParameters): object is IqserRedirectToNavigationParameters {
|
||||
return isPlainObject(object) && (!!object.navigationCommands || !!object.navigationExtras);
|
||||
}
|
||||
|
||||
#transformNavigationCommands(
|
||||
navigationCommands: any[] | NavigationCommandsFn,
|
||||
route: ActivatedRouteSnapshot | Route,
|
||||
state?: RouterStateSnapshot,
|
||||
): any[] {
|
||||
return isFunction<NavigationCommandsFn>(navigationCommands) ? navigationCommands(route, state) : navigationCommands;
|
||||
}
|
||||
|
||||
#transformNavigationExtras(
|
||||
navigationExtras: NavigationExtras | NavigationExtrasFn,
|
||||
route: ActivatedRouteSnapshot | Route,
|
||||
state?: RouterStateSnapshot,
|
||||
): NavigationExtras {
|
||||
return isFunction<NavigationExtrasFn>(navigationExtras) ? navigationExtras(route, state) : navigationExtras;
|
||||
}
|
||||
|
||||
#onlyRedirectCheck(
|
||||
permissions: IqserPermissionsData,
|
||||
route: ActivatedRouteSnapshot | Route,
|
||||
state?: RouterStateSnapshot,
|
||||
): Promise<boolean> {
|
||||
let failedPermission = '';
|
||||
|
||||
const res = from(permissions.only ?? []).pipe(
|
||||
mergeMap(permissionsOnly => {
|
||||
return forkJoin([
|
||||
this._permissionsService.hasPermission(permissionsOnly),
|
||||
this._rolesService.hasOnlyRoles(permissionsOnly),
|
||||
]).pipe(
|
||||
tap(hasPermissions => {
|
||||
const failed = hasPermissions.every(hasPermission => hasPermission === false);
|
||||
|
||||
if (failed) {
|
||||
failedPermission = permissionsOnly;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}),
|
||||
first(hasPermissions => {
|
||||
if (isFunction<RedirectToFn>(permissions.redirectTo)) {
|
||||
return hasPermissions.some(hasPermission => hasPermission === true);
|
||||
}
|
||||
|
||||
return hasPermissions.every(hasPermission => hasPermission === false);
|
||||
}, false),
|
||||
mergeMap(pass => {
|
||||
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);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
return firstValueFrom(res);
|
||||
}
|
||||
|
||||
#handleRedirectOfFailedPermission(
|
||||
permissions: IqserPermissionsData,
|
||||
failedPermission: string,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#isFailedPermissionPropertyOfRedirectTo(permissions: IqserPermissionsData, failedPermission: string): boolean {
|
||||
// @ts-ignore
|
||||
return !!permissions.redirectTo && permissions.redirectTo[failedPermission];
|
||||
}
|
||||
|
||||
#checkOnlyPermissions(
|
||||
purePermissions: IqserPermissionsData,
|
||||
route: ActivatedRouteSnapshot | Route,
|
||||
state?: RouterStateSnapshot,
|
||||
): Promise<boolean> {
|
||||
const permissions: IqserPermissionsData = {
|
||||
...purePermissions,
|
||||
};
|
||||
|
||||
return Promise.all([
|
||||
this._permissionsService.hasPermission(permissions.only),
|
||||
this._rolesService.hasOnlyRoles(permissions.only),
|
||||
]).then(([hasPermission, hasRole]) => {
|
||||
if (hasPermission || hasRole) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (permissions.redirectTo) {
|
||||
this.#redirectToAnotherRoute(permissions.redirectTo, route, state);
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
}
|
||||
}
|
||||
125
src/lib/permissions/services/permissions.service.ts
Normal file
125
src/lib/permissions/services/permissions.service.ts
Normal file
@ -0,0 +1,125 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, from, Observable, of } from 'rxjs';
|
||||
import { catchError, first, map, mergeAll, switchMap } from 'rxjs/operators';
|
||||
|
||||
import { isBoolean, isFunction, transformStringToArray } from '../utils';
|
||||
import { ValidationFn } from '../models/permissions-router-data.model';
|
||||
import { IqserPermission } from '../models/permission.model';
|
||||
|
||||
export interface IqserPermissionsObject {
|
||||
[name: string]: IqserPermission;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IqserPermissionsService {
|
||||
readonly permissions$: Observable<IqserPermissionsObject>;
|
||||
readonly #permissions$ = new BehaviorSubject<IqserPermissionsObject>({});
|
||||
|
||||
constructor() {
|
||||
this.permissions$ = this.#permissions$.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all permissions from permissions source
|
||||
*/
|
||||
public flushPermissions(): void {
|
||||
this.#permissions$.next({});
|
||||
}
|
||||
|
||||
public hasPermission(permission?: string | string[]): Promise<boolean> {
|
||||
if (!permission || (Array.isArray(permission) && permission.length === 0)) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
permission = transformStringToArray(permission);
|
||||
return this.hasArrayPermission(permission);
|
||||
}
|
||||
|
||||
public loadPermissions(permissions: string[], validationFunction?: ValidationFn): void {
|
||||
const newPermissions = permissions.reduce((source, name) => this.reducePermission(source, name, validationFunction), {});
|
||||
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,
|
||||
);
|
||||
|
||||
this.#permissions$.next(permissions);
|
||||
} else {
|
||||
const permissions = this.reducePermission(this.#permissions$.value, permission, validationFunction);
|
||||
|
||||
this.#permissions$.next(permissions);
|
||||
}
|
||||
}
|
||||
|
||||
public removePermission(permissionName: string): void {
|
||||
const permissions = {
|
||||
...this.#permissions$.value,
|
||||
};
|
||||
delete permissions[permissionName];
|
||||
this.#permissions$.next(permissions);
|
||||
}
|
||||
|
||||
public getPermission(name: string): IqserPermission | undefined {
|
||||
return this.#permissions$.value[name];
|
||||
}
|
||||
|
||||
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 },
|
||||
};
|
||||
}
|
||||
return {
|
||||
...source,
|
||||
[name]: { name },
|
||||
};
|
||||
}
|
||||
|
||||
private hasArrayPermission(permissions: string[]): Promise<boolean> {
|
||||
const promises = permissions.map(key => {
|
||||
if (this.hasPermissionValidationFunction(key)) {
|
||||
const validationFunction = this.#permissions$.value[key].validationFunction;
|
||||
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)),
|
||||
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);
|
||||
}
|
||||
|
||||
private hasPermissionValidationFunction(key: string): boolean {
|
||||
return (
|
||||
!!this.#permissions$.value[key] &&
|
||||
!!this.#permissions$.value[key].validationFunction &&
|
||||
isFunction(this.#permissions$.value[key].validationFunction)
|
||||
);
|
||||
}
|
||||
}
|
||||
137
src/lib/permissions/services/roles.service.ts
Normal file
137
src/lib/permissions/services/roles.service.ts
Normal file
@ -0,0 +1,137 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, firstValueFrom, from, Observable, of } from 'rxjs';
|
||||
import { catchError, every, first, map, mergeAll, mergeMap, switchMap } from 'rxjs/operators';
|
||||
|
||||
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';
|
||||
|
||||
export interface IqserRolesObject {
|
||||
[name: string]: IqserRole;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class IqserRolesService {
|
||||
readonly roles$: Observable<IqserRolesObject>;
|
||||
readonly #roles$ = new BehaviorSubject<IqserRolesObject>({});
|
||||
|
||||
constructor(private readonly _permissionsService: IqserPermissionsService) {
|
||||
this.roles$ = this.#roles$.asObservable();
|
||||
}
|
||||
|
||||
addRole(name: string, validationFunction: ValidationFn | string[]) {
|
||||
const roles = {
|
||||
...this.#roles$.value,
|
||||
[name]: { name, validationFunction },
|
||||
};
|
||||
this.#roles$.next(roles);
|
||||
}
|
||||
|
||||
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() {
|
||||
this.#roles$.next({});
|
||||
}
|
||||
|
||||
flushRolesAndPermissions() {
|
||||
this.flushRoles();
|
||||
this._permissionsService.flushPermissions();
|
||||
}
|
||||
|
||||
removeRole(roleName: string) {
|
||||
const roles = {
|
||||
...this.#roles$.value,
|
||||
};
|
||||
delete roles[roleName];
|
||||
this.#roles$.next(roles);
|
||||
}
|
||||
|
||||
getRoles(): IqserRolesObject {
|
||||
return this.#roles$.value;
|
||||
}
|
||||
|
||||
getRole(name: string): IqserRole | undefined {
|
||||
return this.#roles$.value[name];
|
||||
}
|
||||
|
||||
hasOnlyRoles(names?: string | string[]): Promise<boolean> {
|
||||
const isNamesEmpty = !names || (Array.isArray(names) && names.length === 0);
|
||||
|
||||
if (isNamesEmpty) {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
names = transformStringToArray(names);
|
||||
|
||||
return Promise.all([this.#hasRoleKey(names), this.#hasRolePermission(this.#roles$.value, names)]).then(
|
||||
([hasRoles, hasPermissions]) => {
|
||||
return !!hasRoles || !!hasPermissions;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#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);
|
||||
|
||||
if (hasValidationFunction && !isPromise(this.#roles$.value[key].validationFunction)) {
|
||||
const validationFunction = this.#roles$.value[key].validationFunction as ValidationFn;
|
||||
const immutableValue = { ...this.#roles$.value };
|
||||
|
||||
return of(null).pipe(
|
||||
map(() => validationFunction(key, immutableValue)),
|
||||
switchMap(promise => (isBoolean(promise) ? of(promise) : promise)),
|
||||
catchError(() => of(false)),
|
||||
);
|
||||
}
|
||||
|
||||
return of(false);
|
||||
});
|
||||
|
||||
const res = from(promises).pipe(
|
||||
mergeAll(),
|
||||
first(data => data !== false, false),
|
||||
map(data => data !== false),
|
||||
);
|
||||
|
||||
return firstValueFrom(res);
|
||||
}
|
||||
|
||||
#hasRolePermission(roles: IqserRolesObject, 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)),
|
||||
every(hasPermission => hasPermission === true),
|
||||
);
|
||||
}
|
||||
|
||||
return of(false);
|
||||
}),
|
||||
first(hasPermission => hasPermission === true, false),
|
||||
);
|
||||
|
||||
return firstValueFrom(res);
|
||||
}
|
||||
}
|
||||
38
src/lib/permissions/utils.ts
Normal file
38
src/lib/permissions/utils.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export function isFunction<T>(value: any): value is T {
|
||||
return typeof value === 'function';
|
||||
}
|
||||
|
||||
export function isPlainObject(value: any): boolean {
|
||||
if (Object.prototype.toString.call(value) !== '[object Object]') {
|
||||
return false;
|
||||
} else {
|
||||
const prototype = Object.getPrototypeOf(value);
|
||||
return prototype === null || prototype === Object.prototype;
|
||||
}
|
||||
}
|
||||
|
||||
export function isString(value: any): value is string {
|
||||
return !!value && typeof value === 'string';
|
||||
}
|
||||
|
||||
export function isBoolean(value: any): value is boolean {
|
||||
return typeof value === 'boolean';
|
||||
}
|
||||
|
||||
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[] {
|
||||
if (isString(value)) {
|
||||
return [value];
|
||||
}
|
||||
return value ?? [];
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user