diff --git a/packages/admin-ui/src/lib/catalog/src/components/product-options-editor/product-options-editor.component.ts b/packages/admin-ui/src/lib/catalog/src/components/product-options-editor/product-options-editor.component.ts index dffd9b040f..51832bc0de 100644 --- a/packages/admin-ui/src/lib/catalog/src/components/product-options-editor/product-options-editor.component.ts +++ b/packages/admin-ui/src/lib/catalog/src/components/product-options-editor/product-options-editor.component.ts @@ -12,6 +12,7 @@ import { LanguageCode, NotificationService, Permission, + PermissionsService, ProductOptionFragment, ProductOptionGroupFragment, ServerConfigService, @@ -48,12 +49,13 @@ export class ProductOptionsEditorComponent extends BaseDetailComponent): CustomFieldConfig[] { - return this.serverConfigService.getCustomFieldsFor(key); + return this.serverConfigService.getCustomFieldsFor(key).filter(f => { + if (f.requiresPermission?.length) { + return this.permissionsService.userHasPermissions(f.requiresPermission); + } + return true; + }); } protected setQueryParam(key: string, value: any) { @@ -184,7 +191,13 @@ export abstract class TypedBaseDetailComponent< protected entity: ResultOf[Field]; protected constructor() { - super(inject(ActivatedRoute), inject(Router), inject(ServerConfigService), inject(DataService)); + super( + inject(ActivatedRoute), + inject(Router), + inject(ServerConfigService), + inject(DataService), + inject(PermissionsService), + ); } override init() { diff --git a/packages/admin-ui/src/lib/core/src/common/base-list.component.ts b/packages/admin-ui/src/lib/core/src/common/base-list.component.ts index 864e6b8728..3934090bf8 100644 --- a/packages/admin-ui/src/lib/core/src/common/base-list.component.ts +++ b/packages/admin-ui/src/lib/core/src/common/base-list.component.ts @@ -10,6 +10,7 @@ import { QueryResult } from '../data/query-result'; import { ServerConfigService } from '../data/server-config'; import { DataTableFilterCollection } from '../providers/data-table/data-table-filter-collection'; import { DataTableSortCollection } from '../providers/data-table/data-table-sort-collection'; +import { PermissionsService } from '../providers/permissions/permissions.service'; import { CustomFieldConfig, CustomFields, LanguageCode } from './generated-types'; import { SelectionManager } from './utilities/selection-manager'; @@ -178,7 +179,6 @@ export class BaseListComponent> = []; private collections: Array> = []; constructor() { @@ -263,6 +264,11 @@ export class TypedBaseListComponent< } getCustomFieldConfig(key: Exclude | string): CustomFieldConfig[] { - return this.serverConfigService.getCustomFieldsFor(key); + return this.serverConfigService.getCustomFieldsFor(key).filter(f => { + if (f.requiresPermission?.length) { + return this.permissionsService.userHasPermissions(f.requiresPermission); + } + return true; + }); } } diff --git a/packages/admin-ui/src/lib/core/src/common/generated-types.ts b/packages/admin-ui/src/lib/core/src/common/generated-types.ts index 89eb71a0b1..308d353466 100644 --- a/packages/admin-ui/src/lib/core/src/common/generated-types.ts +++ b/packages/admin-ui/src/lib/core/src/common/generated-types.ts @@ -321,6 +321,7 @@ export type BooleanCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; @@ -1317,6 +1318,7 @@ export type CustomField = { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; @@ -1515,6 +1517,7 @@ export type DateTimeCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; step?: Maybe; type: Scalars['String']['output']; ui?: Maybe; @@ -1820,6 +1823,7 @@ export type FloatCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; step?: Maybe; type: Scalars['String']['output']; ui?: Maybe; @@ -2025,6 +2029,7 @@ export type IntCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; step?: Maybe; type: Scalars['String']['output']; ui?: Maybe; @@ -2489,6 +2494,7 @@ export type LocaleStringCustomFieldConfig = CustomField & { nullable?: Maybe; pattern?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; @@ -2502,6 +2508,7 @@ export type LocaleTextCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; @@ -5489,6 +5496,7 @@ export type RelationCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; scalarFields: Array; type: Scalars['String']['output']; ui?: Maybe; @@ -6000,6 +6008,7 @@ export type StringCustomFieldConfig = CustomField & { options?: Maybe>; pattern?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; @@ -6241,6 +6250,7 @@ export type TextCustomFieldConfig = CustomField & { name: Scalars['String']['output']; nullable?: Maybe; readonly?: Maybe; + requiresPermission?: Maybe>; type: Scalars['String']['output']; ui?: Maybe; }; diff --git a/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts b/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts index 2ef9547351..eddddb4124 100644 --- a/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts +++ b/packages/admin-ui/src/lib/core/src/data/definitions/settings-definitions.ts @@ -560,6 +560,7 @@ export const CUSTOM_FIELD_CONFIG_FRAGMENT = gql` } readonly nullable + requiresPermission ui } `; diff --git a/packages/admin-ui/src/lib/core/src/data/server-config.ts b/packages/admin-ui/src/lib/core/src/data/server-config.ts index 3d9dfa4036..5e7aa8d88d 100644 --- a/packages/admin-ui/src/lib/core/src/data/server-config.ts +++ b/packages/admin-ui/src/lib/core/src/data/server-config.ts @@ -8,7 +8,6 @@ import { GetServerConfigQuery, OrderProcessState, PermissionDefinition, - ServerConfig, } from '../common/generated-types'; import { GET_GLOBAL_SETTINGS, GET_SERVER_CONFIG } from './definitions/settings-definitions'; diff --git a/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts b/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts index bd555a9985..d090f2ea5c 100644 --- a/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/alerts/alerts.service.ts @@ -1,18 +1,9 @@ import { Injectable } from '@angular/core'; import { notNullOrUndefined } from '@vendure/common/lib/shared-utils'; -import { - BehaviorSubject, - combineLatest, - interval, - isObservable, - Observable, - of, - Subject, - switchMap, -} from 'rxjs'; -import { filter, first, map, mapTo, startWith, take } from 'rxjs/operators'; +import { BehaviorSubject, combineLatest, interval, isObservable, Observable, Subject, switchMap } from 'rxjs'; +import { map, startWith, take } from 'rxjs/operators'; import { Permission } from '../../common/generated-types'; -import { DataService } from '../../data/providers/data.service'; +import { PermissionsService } from '../permissions/permissions.service'; export interface AlertConfig { id: string; @@ -86,9 +77,9 @@ export class AlertsService { private alertsMap = new Map>(); private configUpdated = new Subject(); - constructor(private dataService: DataService) { + constructor(private permissionsService: PermissionsService) { const alerts$ = this.configUpdated.pipe( - mapTo([...this.alertsMap.values()]), + map(() => [...this.alertsMap.values()]), startWith([...this.alertsMap.values()]), ); @@ -103,26 +94,17 @@ export class AlertsService { } configureAlert(config: AlertConfig) { - this.hasSufficientPermissions(config.requiredPermissions) - .pipe(first()) - .subscribe(hasSufficientPermissions => { - if (hasSufficientPermissions) { - this.alertsMap.set(config.id, new Alert(config)); - this.configUpdated.next(); - } - }); + if (this.hasSufficientPermissions(config.requiredPermissions)) { + this.alertsMap.set(config.id, new Alert(config)); + this.configUpdated.next(); + } } hasSufficientPermissions(permissions?: Permission[]) { if (!permissions || permissions.length === 0) { - return of(true); + return true; } - return this.dataService.client.userStatus().stream$.pipe( - filter(({ userStatus }) => userStatus.isLoggedIn), - map(({ userStatus }) => - permissions.some(permission => userStatus.permissions.includes(permission)), - ), - ); + return this.permissionsService.userHasPermissions(permissions); } refresh(id?: string) { diff --git a/packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts b/packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts index 69fed9c64c..10c616f361 100644 --- a/packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/auth/auth.service.ts @@ -7,6 +7,7 @@ import { AttemptLoginMutation, CurrentUserFragment } from '../../common/generate import { DataService } from '../../data/providers/data.service'; import { ServerConfigService } from '../../data/server-config'; import { LocalStorageService } from '../local-storage/local-storage.service'; +import { PermissionsService } from '../permissions/permissions.service'; /** * This service handles logic relating to authentication of the current user. @@ -19,6 +20,7 @@ export class AuthService { private localStorageService: LocalStorageService, private dataService: DataService, private serverConfigService: ServerConfigService, + private permissionsService: PermissionsService, ) {} /** @@ -39,7 +41,8 @@ export class AuthService { }), switchMap(login => { if (login.__typename === 'CurrentUser') { - const { id } = this.getActiveChannel(login.channels); + const activeChannel = this.getActiveChannel(login.channels); + this.permissionsService.setCurrentUserPermissions(activeChannel.permissions); return this.dataService.administrator.getActiveAdministrator().single$.pipe( switchMap(({ activeAdministrator }) => { if (activeAdministrator) { @@ -47,7 +50,7 @@ export class AuthService { .loginSuccess( activeAdministrator.id, `${activeAdministrator.firstName} ${activeAdministrator.lastName}`, - id, + activeChannel.id, login.channels, ) .pipe(map(() => login)); @@ -107,8 +110,8 @@ export class AuthService { return of(false) as any; } this.setChannelToken(me.channels); - - const { id } = this.getActiveChannel(me.channels); + const activeChannel = this.getActiveChannel(me.channels); + this.permissionsService.setCurrentUserPermissions(activeChannel.permissions); return this.dataService.administrator.getActiveAdministrator().single$.pipe( switchMap(({ activeAdministrator }) => { if (activeAdministrator) { @@ -116,7 +119,7 @@ export class AuthService { .loginSuccess( activeAdministrator.id, `${activeAdministrator.firstName} ${activeAdministrator.lastName}`, - id, + activeChannel.id, me.channels, ) .pipe(map(() => true)); diff --git a/packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts b/packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts index c340ceb2a0..07e0a4c021 100644 --- a/packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts +++ b/packages/admin-ui/src/lib/core/src/providers/channel/channel.service.ts @@ -6,6 +6,7 @@ import { map, shareReplay, tap } from 'rxjs/operators'; import { UserStatusFragment } from '../../common/generated-types'; import { DataService } from '../../data/providers/data.service'; import { LocalStorageService } from '../local-storage/local-storage.service'; +import { PermissionsService } from '../permissions/permissions.service'; @Injectable({ providedIn: 'root', @@ -13,7 +14,11 @@ import { LocalStorageService } from '../local-storage/local-storage.service'; export class ChannelService { defaultChannelIsActive$: Observable; - constructor(private dataService: DataService, private localStorageService: LocalStorageService) { + constructor( + private dataService: DataService, + private localStorageService: LocalStorageService, + private permissionsService: PermissionsService, + ) { this.defaultChannelIsActive$ = this.dataService.client .userStatus() .mapStream(({ userStatus }) => { @@ -30,6 +35,7 @@ export class ChannelService { const activeChannel = userStatus.channels.find(c => c.id === channelId); if (activeChannel) { this.localStorageService.set('activeChannelToken', activeChannel.token); + this.permissionsService.setCurrentUserPermissions(activeChannel.permissions); } }), ); diff --git a/packages/admin-ui/src/lib/core/src/providers/permissions/permissions.service.ts b/packages/admin-ui/src/lib/core/src/providers/permissions/permissions.service.ts new file mode 100644 index 0000000000..1b199eaf15 --- /dev/null +++ b/packages/admin-ui/src/lib/core/src/providers/permissions/permissions.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { Permission } from '../../common/generated-types'; + +/** + * @description + * This service is used internally to power components & logic that are dependent on knowing the + * current user's permissions in the currently-active channel. + */ +@Injectable({ + providedIn: 'root', +}) +export class PermissionsService { + private currentUserPermissions: string[] = []; + private _currentUserPermissions$ = new BehaviorSubject([]); + currentUserPermissions$ = this._currentUserPermissions$.asObservable(); + + /** + * @description + * This is called whenever: + * - the user logs in + * - the active channel changes + * + * Since active user validation occurs as part of the main auth guard, we can be assured + * that if the user is logged in, then this method will be called with the user's permissions + * before any other components are rendered lower down in the component tree. + */ + setCurrentUserPermissions(permissions: string[]) { + this.currentUserPermissions = permissions; + this._currentUserPermissions$.next(permissions); + } + + userHasPermissions(requiredPermissions: Array): boolean { + for (const perm of requiredPermissions) { + if (this.currentUserPermissions.includes(perm)) { + return true; + } + } + return false; + } +} diff --git a/packages/admin-ui/src/lib/core/src/public_api.ts b/packages/admin-ui/src/lib/core/src/public_api.ts index 9149dc2c67..c93f716a77 100644 --- a/packages/admin-ui/src/lib/core/src/public_api.ts +++ b/packages/admin-ui/src/lib/core/src/public_api.ts @@ -122,6 +122,7 @@ export * from './providers/nav-builder/nav-builder.service'; export * from './providers/notification/notification.service'; export * from './providers/overlay-host/overlay-host.service'; export * from './providers/page/page.service'; +export * from './providers/permissions/permissions.service'; export * from './shared/components/action-bar/action-bar.component'; export * from './shared/components/action-bar-items/action-bar-items.component'; export * from './shared/components/address-form/address-form.component'; diff --git a/packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.ts b/packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.ts index d6066239a6..00fb91683b 100644 --- a/packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.ts +++ b/packages/admin-ui/src/lib/core/src/shared/components/tabbed-custom-fields/tabbed-custom-fields.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; -import { AbstractControl, FormGroup } from '@angular/forms'; +import { AbstractControl } from '@angular/forms'; import { DefaultFormComponentId } from '@vendure/common/lib/shared-types'; import { CustomFieldConfig } from '../../../common/generated-types'; diff --git a/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.spec.ts b/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.spec.ts index e1d976ad1e..2f0ef7a29c 100644 --- a/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.spec.ts +++ b/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.spec.ts @@ -1,8 +1,6 @@ import { Component, Input } from '@angular/core'; -import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; -import { of } from 'rxjs'; - -import { DataService } from '../../data/providers/data.service'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PermissionsService } from '../../providers/permissions/permissions.service'; import { IfPermissionsDirective } from './if-permissions.directive'; @@ -12,9 +10,10 @@ describe('vdrIfPermissions directive', () => { beforeEach(() => { fixture = TestBed.configureTestingModule({ declarations: [TestComponent, IfPermissionsDirective], - providers: [{ provide: DataService, useClass: MockDataService }], }).createComponent(TestComponent); fixture.detectChanges(); // initial binding + + TestBed.inject(PermissionsService).setCurrentUserPermissions(['ValidPermission']); }); it('has permission (single)', () => { @@ -79,20 +78,3 @@ describe('vdrIfPermissions directive', () => { export class TestComponent { @Input() permissionToTest: string | string[] | null = ''; } - -class MockDataService { - client = { - userStatus() { - return { - mapStream: (mapFn: any) => - of( - mapFn({ - userStatus: { - permissions: ['ValidPermission'], - }, - }), - ), - }; - }, - }; -} diff --git a/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.ts b/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.ts index 14769ddfbc..30de72e58a 100644 --- a/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.ts +++ b/packages/admin-ui/src/lib/core/src/shared/directives/if-permissions.directive.ts @@ -1,16 +1,9 @@ -import { - ChangeDetectorRef, - Directive, - EmbeddedViewRef, - Input, - TemplateRef, - ViewContainerRef, -} from '@angular/core'; +import { ChangeDetectorRef, Directive, Input, TemplateRef, ViewContainerRef } from '@angular/core'; import { of } from 'rxjs'; -import { tap } from 'rxjs/operators'; +import { map, tap } from 'rxjs/operators'; import { Permission } from '../../common/generated-types'; -import { DataService } from '../../data/providers/data.service'; +import { PermissionsService } from '../../providers/permissions/permissions.service'; import { IfDirectiveBase } from './if-directive-base'; @@ -39,8 +32,8 @@ export class IfPermissionsDirective extends IfDirectiveBase, - private dataService: DataService, private changeDetectorRef: ChangeDetectorRef, + private permissionsService: PermissionsService, ) { super(_viewContainer, templateRef, permissions => { if (permissions == null) { @@ -48,17 +41,10 @@ export class IfPermissionsDirective extends IfDirectiveBase { - for (const permission of permissions) { - if (userStatus.permissions.includes(permission)) { - return true; - } - } - return false; - }) - .pipe(tap(() => this.changeDetectorRef.markForCheck())); + return this.permissionsService.currentUserPermissions$.pipe( + map(() => this.permissionsService.userHasPermissions(permissions)), + tap(() => this.changeDetectorRef.markForCheck()), + ); }); } diff --git a/packages/admin-ui/src/lib/core/src/shared/pipes/has-permission.pipe.ts b/packages/admin-ui/src/lib/core/src/shared/pipes/has-permission.pipe.ts index c19e2ea457..2518f4069c 100644 --- a/packages/admin-ui/src/lib/core/src/shared/pipes/has-permission.pipe.ts +++ b/packages/admin-ui/src/lib/core/src/shared/pipes/has-permission.pipe.ts @@ -1,7 +1,6 @@ import { ChangeDetectorRef, OnDestroy, Pipe, PipeTransform } from '@angular/core'; -import { Observable, Subscription } from 'rxjs'; - -import { DataService } from '../../data/providers/data.service'; +import { Subscription } from 'rxjs'; +import { PermissionsService } from '../../providers/permissions/permissions.service'; /** * @description @@ -20,15 +19,13 @@ import { DataService } from '../../data/providers/data.service'; }) export class HasPermissionPipe implements PipeTransform, OnDestroy { private hasPermission = false; - private currentPermissions$: Observable; private lastPermissions: string | null = null; private subscription: Subscription; - constructor(private dataService: DataService, private changeDetectorRef: ChangeDetectorRef) { - this.currentPermissions$ = this.dataService.client - .userStatus() - .mapStream(data => data.userStatus.permissions); - } + constructor( + private permissionsService: PermissionsService, + private changeDetectorRef: ChangeDetectorRef, + ) {} transform(input: string | string[]): any { const requiredPermissions = Array.isArray(input) ? input : [input]; @@ -37,8 +34,8 @@ export class HasPermissionPipe implements PipeTransform, OnDestroy { this.lastPermissions = requiredPermissionsString; this.hasPermission = false; this.dispose(); - this.subscription = this.currentPermissions$.subscribe(permissions => { - this.hasPermission = this.checkPermissions(permissions, requiredPermissions); + this.subscription = this.permissionsService.currentUserPermissions$.subscribe(() => { + this.hasPermission = this.permissionsService.userHasPermissions(requiredPermissions); this.changeDetectorRef.markForCheck(); }); } @@ -50,15 +47,6 @@ export class HasPermissionPipe implements PipeTransform, OnDestroy { this.dispose(); } - private checkPermissions(userPermissions: string[], requiredPermissions: string[]): boolean { - for (const perm of requiredPermissions) { - if (userPermissions.includes(perm)) { - return true; - } - } - return false; - } - private dispose() { if (this.subscription) { this.subscription.unsubscribe();