Browse Source

feat(admin-ui): Set up infrastructure for permission-based UI display

Relates to #94
Michael Bromley 6 years ago
parent
commit
6bd5181576

+ 12 - 5
packages/admin-ui/src/app/common/generated-types.ts

@@ -2136,7 +2136,8 @@ export type MutationRemoveMembersFromZoneArgs = {
 
 export type MutationSetAsLoggedInArgs = {
   username: Scalars['String'],
-  loginTime: Scalars['String']
+  loginTime: Scalars['String'],
+  permissions: Array<Scalars['String']>
 };
 
 
@@ -2382,6 +2383,10 @@ export enum Permission {
   ReadOrder = 'ReadOrder',
   UpdateOrder = 'UpdateOrder',
   DeleteOrder = 'DeleteOrder',
+  CreatePromotion = 'CreatePromotion',
+  ReadPromotion = 'ReadPromotion',
+  UpdatePromotion = 'UpdatePromotion',
+  DeletePromotion = 'DeletePromotion',
   CreateSettings = 'CreateSettings',
   ReadSettings = 'ReadSettings',
   UpdateSettings = 'UpdateSettings',
@@ -3448,6 +3453,7 @@ export type UserStatus = {
   username: Scalars['String'],
   isLoggedIn: Scalars['Boolean'],
   loginTime: Scalars['String'],
+  permissions: Array<Scalars['String']>,
 };
 
 export type Zone = Node & {
@@ -3559,16 +3565,17 @@ export type RequestCompletedMutation = ({ __typename?: 'Mutation' } & Pick<Mutat
 
 export type SetAsLoggedInMutationVariables = {
   username: Scalars['String'],
-  loginTime: Scalars['String']
+  loginTime: Scalars['String'],
+  permissions: Array<Scalars['String']>
 };
 
 
-export type SetAsLoggedInMutation = ({ __typename?: 'Mutation' } & { setAsLoggedIn: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime'>) });
+export type SetAsLoggedInMutation = ({ __typename?: 'Mutation' } & { setAsLoggedIn: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime' | 'permissions'>) });
 
 export type SetAsLoggedOutMutationVariables = {};
 
 
-export type SetAsLoggedOutMutation = ({ __typename?: 'Mutation' } & { setAsLoggedOut: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime'>) });
+export type SetAsLoggedOutMutation = ({ __typename?: 'Mutation' } & { setAsLoggedOut: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime' | 'permissions'>) });
 
 export type SetUiLanguageMutationVariables = {
   languageCode: LanguageCode
@@ -3585,7 +3592,7 @@ export type GetNetworkStatusQuery = ({ __typename?: 'Query' } & { networkStatus:
 export type GetUserStatusQueryVariables = {};
 
 
-export type GetUserStatusQuery = ({ __typename?: 'Query' } & { userStatus: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime'>) });
+export type GetUserStatusQuery = ({ __typename?: 'Query' } & { userStatus: ({ __typename?: 'UserStatus' } & Pick<UserStatus, 'username' | 'isLoggedIn' | 'loginTime' | 'permissions'>) });
 
 export type GetUiStateQueryVariables = {};
 

+ 12 - 8
packages/admin-ui/src/app/core/providers/auth/auth.service.ts

@@ -27,10 +27,11 @@ export class AuthService {
         return this.dataService.auth.attemptLogin(username, password, rememberMe).pipe(
             switchMap(response => {
                 this.setChannelToken(response.login.user.channels);
-                return this.serverConfigService.getServerConfig();
+                return this.serverConfigService.getServerConfig().then(() => response.login.user);
             }),
-            switchMap(() => {
-                return this.dataService.client.loginSuccess(username);
+            switchMap(user => {
+                const { permissions } = this.getActiveChannel(user.channels);
+                return this.dataService.client.loginSuccess(username, permissions);
             }),
         );
     }
@@ -80,18 +81,21 @@ export class AuthService {
                     return of(false) as any;
                 }
                 this.setChannelToken(result.me.channels);
-                return this.dataService.client.loginSuccess(result.me.identifier);
+                const { permissions } = this.getActiveChannel(result.me.channels);
+                return this.dataService.client.loginSuccess(result.me.identifier, permissions);
             }),
             mapTo(true),
             catchError(err => of(false)),
         );
     }
 
+    private getActiveChannel(userChannels: CurrentUserFragment['channels']) {
+        const defaultChannel = userChannels.find(c => c.code === DEFAULT_CHANNEL_CODE);
+        return defaultChannel || userChannels[0];
+    }
+
     private setChannelToken(userChannels: CurrentUserFragment['channels']) {
         const defaultChannel = userChannels.find(c => c.code === DEFAULT_CHANNEL_CODE);
-        this.localStorageService.set(
-            'activeChannelToken',
-            defaultChannel ? defaultChannel.token : userChannels[0].token,
-        );
+        this.localStorageService.set('activeChannelToken', this.getActiveChannel(userChannels).token);
     }
 }

+ 1 - 0
packages/admin-ui/src/app/data/client-state/client-defaults.ts

@@ -10,6 +10,7 @@ export const clientDefaults = {
         username: '',
         isLoggedIn: false,
         loginTime: '',
+        permissions: [],
         __typename: 'UserStatus',
     } as GetUserStatus.UserStatus,
     uiState: {

+ 3 - 1
packages/admin-ui/src/app/data/client-state/client-resolvers.ts

@@ -32,13 +32,14 @@ export const clientResolvers: ResolverDefinition = {
             return updateRequestsInFlight(cache, -1);
         },
         setAsLoggedIn: (_, args: SetAsLoggedIn.Variables, { cache }): GetUserStatus.UserStatus => {
-            const { username, loginTime } = args;
+            const { username, loginTime, permissions } = args;
             const data: GetUserStatus.Query = {
                 userStatus: {
                     __typename: 'UserStatus',
                     username,
                     loginTime,
                     isLoggedIn: true,
+                    permissions,
                 },
             };
             cache.writeData({ data });
@@ -51,6 +52,7 @@ export const clientResolvers: ResolverDefinition = {
                     username: '',
                     loginTime: '',
                     isLoggedIn: false,
+                    permissions: [],
                 },
             };
             cache.writeData({ data });

+ 2 - 1
packages/admin-ui/src/app/data/client-state/client-types.graphql

@@ -7,7 +7,7 @@ type Query {
 type Mutation {
     requestStarted: Int!
     requestCompleted: Int!
-    setAsLoggedIn(username: String!, loginTime: String!): UserStatus!
+    setAsLoggedIn(username: String!, loginTime: String!, permissions: [String!]!): UserStatus!
     setAsLoggedOut: UserStatus!
     setUiLanguage(languageCode: LanguageCode): LanguageCode
 }
@@ -20,6 +20,7 @@ type UserStatus {
     username: String!
     isLoggedIn: Boolean!
     loginTime: String!
+    permissions: [String!]!
 }
 
 type UiState {

+ 5 - 2
packages/admin-ui/src/app/data/definitions/client-definitions.ts

@@ -13,11 +13,12 @@ export const REQUEST_COMPLETED = gql`
 `;
 
 export const SET_AS_LOGGED_IN = gql`
-    mutation SetAsLoggedIn($username: String!, $loginTime: String!) {
-        setAsLoggedIn(username: $username, loginTime: $loginTime) @client {
+    mutation SetAsLoggedIn($username: String!, $loginTime: String!, $permissions: [String!]!) {
+        setAsLoggedIn(username: $username, loginTime: $loginTime, permissions: $permissions) @client {
             username
             isLoggedIn
             loginTime
+            permissions
         }
     }
 `;
@@ -28,6 +29,7 @@ export const SET_AS_LOGGED_OUT = gql`
             username
             isLoggedIn
             loginTime
+            permissions
         }
     }
 `;
@@ -52,6 +54,7 @@ export const GET_USER_STATUS = gql`
             username
             isLoggedIn
             loginTime
+            permissions
         }
     }
 `;

+ 2 - 1
packages/admin-ui/src/app/data/providers/client-data.service.ts

@@ -40,12 +40,13 @@ export class ClientDataService {
         return this.baseDataService.query<GetNetworkStatus.Query>(GET_NEWTORK_STATUS, {}, 'cache-first');
     }
 
-    loginSuccess(username: string) {
+    loginSuccess(username: string, permissions: string[]) {
         return this.baseDataService.mutate<SetAsLoggedIn.Mutation, SetAsLoggedIn.Variables>(
             SET_AS_LOGGED_IN,
             {
                 username,
                 loginTime: Date.now().toString(),
+                permissions,
             },
         );
     }

+ 4 - 0
packages/admin-ui/src/app/settings/components/permission-grid/permission-grid.component.ts

@@ -29,6 +29,10 @@ export class PermissionGridComponent {
             label: _('settings.order'),
             permissions: ['CreateOrder', 'ReadOrder', 'UpdateOrder', 'DeleteOrder'],
         },
+        {
+            label: _('settings.promotion'),
+            permissions: ['CreatePromotion', 'ReadPromotion', 'UpdatePromotion', 'DeletePromotion'],
+        },
         {
             label: _('settings.administrator'),
             permissions: [

+ 80 - 0
packages/admin-ui/src/app/shared/directives/if-permissions.directive.ts

@@ -0,0 +1,80 @@
+import { Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef } from '@angular/core';
+import { DataService } from '@vendure/admin-ui/src/app/data/providers/data.service';
+
+/**
+ * Conditionally shows/hides templates based on the current active user having the specified permission.
+ * Based on the ngIf source. Also support "else" templates:
+ *
+ * @example
+ * ```html
+ * <button *vdrIfPermissions="'DeleteCatalog'; else unauthorized">Delete Product</button>
+ * <ng-template #unauthorized>Not allowed!</ng-template>
+ * ```
+ */
+@Directive({
+    selector: '[vdrIfPermissions]',
+})
+export class IfPermissionsDirective {
+    private readonly _thenTemplateRef: TemplateRef<any> | null = null;
+    private _elseTemplateRef: TemplateRef<any> | null = null;
+    private _thenViewRef: EmbeddedViewRef<any> | null = null;
+    private _elseViewRef: EmbeddedViewRef<any> | null = null;
+    private permissionToCheck = '__initial_value__';
+
+    constructor(
+        private _viewContainer: ViewContainerRef,
+        templateRef: TemplateRef<any>,
+        private dataService: DataService,
+    ) {
+        this._thenTemplateRef = templateRef;
+    }
+
+    /**
+     * The permission to check to determine whether to show the template.
+     */
+    @Input()
+    set vdrIfPermissions(permission: string) {
+        this.permissionToCheck = permission;
+        this._updateView(permission);
+    }
+
+    /**
+     * A template to show if the current user does not have the speicified permission.
+     */
+    @Input()
+    set vdrIfPermissionsElse(templateRef: TemplateRef<any> | null) {
+        assertTemplate('vdrIfPermissionsElse', templateRef);
+        this._elseTemplateRef = templateRef;
+        this._elseViewRef = null; // clear previous view if any.
+        this._updateView(this.permissionToCheck);
+    }
+
+    private _updateView(permission: string) {
+        this.dataService.client.userStatus().single$.subscribe(({ userStatus }) => {
+            if (userStatus.permissions.includes(permission)) {
+                if (!this._thenViewRef) {
+                    this._viewContainer.clear();
+                    this._elseViewRef = null;
+                    if (this._thenTemplateRef) {
+                        this._thenViewRef = this._viewContainer.createEmbeddedView(this._thenTemplateRef);
+                    }
+                }
+            } else {
+                if (!this._elseViewRef) {
+                    this._viewContainer.clear();
+                    this._thenViewRef = null;
+                    if (this._elseTemplateRef) {
+                        this._elseViewRef = this._viewContainer.createEmbeddedView(this._elseTemplateRef);
+                    }
+                }
+            }
+        });
+    }
+}
+
+function assertTemplate(property: string, templateRef: TemplateRef<any> | null): void {
+    const isTemplateRefOrNull = !!(!templateRef || templateRef.createEmbeddedView);
+    if (!isTemplateRefOrNull) {
+        throw new Error(`${property} must be a TemplateRef, but received '${templateRef}'.`);
+    }
+}

+ 2 - 0
packages/admin-ui/src/app/shared/shared.module.ts

@@ -13,6 +13,7 @@ import {
     ActionBarLeftComponent,
     ActionBarRightComponent,
 } from './components/action-bar/action-bar.component';
+import { IfPermissionsDirective } from './directives/if-permissions.directive';
 import { ModalService } from './providers/modal/modal.service';
 import { CanDeactivateDetailGuard } from './providers/routing/can-deactivate-detail-guard';
 import { AffixedInputComponent } from './shared-declarations';
@@ -110,6 +111,7 @@ const DECLARATIONS = [
     LabeledDataComponent,
     StringToColorPipe,
     ObjectTreeComponent,
+    IfPermissionsDirective,
 ];
 
 @NgModule({