wip permissions

This commit is contained in:
Dan Percic 2022-09-30 11:08:51 +03:00
parent 5ea75f1f05
commit b607cff5f9
11 changed files with 900 additions and 0 deletions

View File

@ -21,3 +21,4 @@ export * from './lib/caching';
export * from './lib/users';
export * from './lib/translations';
export * from './lib/standalone';
export * from './lib/permissions';

View 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';

View File

@ -0,0 +1,6 @@
import { ValidationFn } from './permissions-router-data.model';
export interface IqserPermission {
name: string;
validationFunction?: ValidationFn;
}

View 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';

View File

@ -0,0 +1,6 @@
import { ValidationFn } from './permissions-router-data.model';
export interface IqserRole {
name: string;
validationFunction: ValidationFn | string[];
}

View 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;
}
}

View 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],
};
}
}

View 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;
});
}
}

View 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)
);
}
}

View 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);
}
}

View 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 ?? [];
}