Skip to content

Commit

Permalink
feat(core): Expose active user permissions in Admin API
Browse files Browse the repository at this point in the history
Relates to #94
  • Loading branch information
michaelbromley committed Sep 20, 2019
1 parent 8a73778 commit b7cd6e5
Show file tree
Hide file tree
Showing 15 changed files with 178 additions and 34 deletions.
12 changes: 10 additions & 2 deletions packages/admin-ui/src/app/common/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -879,7 +879,14 @@ export type CurrentUser = {
__typename?: 'CurrentUser',
id: Scalars['ID'],
identifier: Scalars['String'],
channelTokens: Array<Scalars['String']>,
channels: Array<CurrentUserChannel>,
};

export type CurrentUserChannel = {
__typename?: 'CurrentUserChannel',
token: Scalars['String'],
code: Scalars['String'],
permissions: Array<Permission>,
};

export type Customer = Node & {
Expand Down Expand Up @@ -3519,7 +3526,7 @@ export type AssignRoleToAdministratorMutationVariables = {

export type AssignRoleToAdministratorMutation = ({ __typename?: 'Mutation' } & { assignRoleToAdministrator: ({ __typename?: 'Administrator' } & AdministratorFragment) });

export type CurrentUserFragment = ({ __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier' | 'channelTokens'>);
export type CurrentUserFragment = ({ __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier'> & { channels: Array<({ __typename?: 'CurrentUserChannel' } & Pick<CurrentUserChannel, 'code' | 'token' | 'permissions'>)> });

export type AttemptLoginMutationVariables = {
username: Scalars['String'],
Expand Down Expand Up @@ -4403,6 +4410,7 @@ export namespace AssignRoleToAdministrator {

export namespace CurrentUser {
export type Fragment = CurrentUserFragment;
export type Channels = (NonNullable<CurrentUserFragment['channels'][0]>);
}

export namespace AttemptLogin {
Expand Down
15 changes: 10 additions & 5 deletions packages/admin-ui/src/app/core/providers/auth/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { catchError, mapTo, mergeMap, switchMap } from 'rxjs/operators';
import { DEFAULT_CHANNEL_CODE } from 'shared/shared-constants';

import { SetAsLoggedIn } from '../../../common/generated-types';
import { CurrentUserChannel, CurrentUserFragment, SetAsLoggedIn } from '../../../common/generated-types';
import { DataService } from '../../../data/providers/data.service';
import { ServerConfigService } from '../../../data/server-config';
import { LocalStorageService } from '../local-storage/local-storage.service';
Expand All @@ -25,7 +26,7 @@ export class AuthService {
logIn(username: string, password: string, rememberMe: boolean): Observable<SetAsLoggedIn.Mutation> {
return this.dataService.auth.attemptLogin(username, password, rememberMe).pipe(
switchMap(response => {
this.setChannelToken(response.login.user.channelTokens[0]);
this.setChannelToken(response.login.user.channels);
return this.serverConfigService.getServerConfig();
}),
switchMap(() => {
Expand Down Expand Up @@ -78,15 +79,19 @@ export class AuthService {
if (!result.me) {
return of(false) as any;
}
this.setChannelToken(result.me.channelTokens[0]);
this.setChannelToken(result.me.channels);
return this.dataService.client.loginSuccess(result.me.identifier);
}),
mapTo(true),
catchError(err => of(false)),
);
}

private setChannelToken(channelToken: string) {
this.localStorageService.set('activeChannelToken', channelToken);
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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ export const CURRENT_USER_FRAGMENT = gql`
fragment CurrentUser on CurrentUser {
id
identifier
channelTokens
channels {
code
token
permissions
}
}
`;

Expand Down
9 changes: 8 additions & 1 deletion packages/common/src/generated-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,14 @@ export type CurrentUser = {
__typename?: 'CurrentUser';
id: Scalars['ID'];
identifier: Scalars['String'];
channelTokens: Array<Scalars['String']>;
channels: Array<CurrentUserChannel>;
};

export type CurrentUserChannel = {
__typename?: 'CurrentUserChannel';
token: Scalars['String'];
code: Scalars['String'];
permissions: Array<Permission>;
};

export type Customer = Node & {
Expand Down
9 changes: 8 additions & 1 deletion packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,14 @@ export type CurrentUser = {
__typename?: 'CurrentUser',
id: Scalars['ID'],
identifier: Scalars['String'],
channelTokens: Array<Scalars['String']>,
channels: Array<CurrentUserChannel>,
};

export type CurrentUserChannel = {
__typename?: 'CurrentUserChannel',
token: Scalars['String'],
code: Scalars['String'],
permissions: Array<Permission>,
};

export type Customer = Node & {
Expand Down
46 changes: 45 additions & 1 deletion packages/core/e2e/auth.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
/* tslint:disable:no-non-null-assertion */
import { SUPER_ADMIN_USER_IDENTIFIER, SUPER_ADMIN_USER_PASSWORD } from '@vendure/common/lib/shared-constants';
import { DocumentNode } from 'graphql';
import gql from 'graphql-tag';
import path from 'path';

import { TEST_SETUP_TIMEOUT_MS } from './config/test-config';
import { CURRENT_USER_FRAGMENT } from './graphql/fragments';
import {
CreateAdministrator,
CreateRole,
Me,
MutationCreateProductArgs,
MutationLoginArgs,
MutationUpdateProductArgs,
Permission,
} from './graphql/generated-e2e-admin-types';
import { ATTEMPT_LOGIN, CREATE_ADMINISTRATOR, CREATE_PRODUCT, CREATE_ROLE, GET_PRODUCT_LIST, UPDATE_PRODUCT } from './graphql/shared-definitions';
import {
ATTEMPT_LOGIN,
CREATE_ADMINISTRATOR,
CREATE_PRODUCT,
CREATE_ROLE,
GET_PRODUCT_LIST,
UPDATE_PRODUCT,
} from './graphql/shared-definitions';
import { TestAdminClient } from './test-client';
import { TestServer } from './test-server';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';

describe('Authorization & permissions', () => {
const client = new TestAdminClient();
Expand All @@ -38,6 +49,13 @@ describe('Authorization & permissions', () => {
await client.asAnonymousUser();
});

it(
'me is not permitted',
assertThrowsWithMessage(async () => {
await client.query<Me.Query>(ME);
}, 'You are not currently authorized to perform this action'),
);

it('can attempt login', async () => {
await assertRequestAllowed<MutationLoginArgs>(ATTEMPT_LOGIN, {
username: SUPER_ADMIN_USER_IDENTIFIER,
Expand All @@ -56,6 +74,12 @@ describe('Authorization & permissions', () => {
await client.asUserWithCredentials(identifier, password);
});

it('me returns correct permissions', async () => {
const { me } = await client.query<Me.Query>(ME);

expect(me!.channels[0].permissions).toEqual(['ReadCatalog']);
});

it('can read', async () => {
await assertRequestAllowed(GET_PRODUCT_LIST);
});
Expand Down Expand Up @@ -90,6 +114,17 @@ describe('Authorization & permissions', () => {
await client.asUserWithCredentials(identifier, password);
});

it('me returns correct permissions', async () => {
const { me } = await client.query<Me.Query>(ME);

expect(me!.channels[0].permissions).toEqual([
'CreateCustomer',
'ReadCustomer',
'UpdateCustomer',
'DeleteCustomer',
]);
});

it('can create', async () => {
await assertRequestAllowed(
gql`
Expand Down Expand Up @@ -181,3 +216,12 @@ describe('Authorization & permissions', () => {
};
}
});

export const ME = gql`
query Me {
me {
...CurrentUser
}
}
${CURRENT_USER_FRAGMENT}
`;
6 changes: 5 additions & 1 deletion packages/core/e2e/graphql/fragments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,11 @@ export const CURRENT_USER_FRAGMENT = gql`
fragment CurrentUser on CurrentUser {
id
identifier
channelTokens
channels {
code
token
permissions
}
}
`;
export const VARIANT_WITH_STOCK_FRAGMENT = gql`
Expand Down
31 changes: 26 additions & 5 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -878,7 +878,14 @@ export type CurrentUser = {
__typename?: 'CurrentUser';
id: Scalars['ID'];
identifier: Scalars['String'];
channelTokens: Array<Scalars['String']>;
channels: Array<CurrentUserChannel>;
};

export type CurrentUserChannel = {
__typename?: 'CurrentUserChannel';
token: Scalars['String'];
code: Scalars['String'];
permissions: Array<Permission>;
};

export type Customer = Node & {
Expand Down Expand Up @@ -3352,6 +3359,12 @@ export type GetCustomerCountQuery = { __typename?: 'Query' } & {
customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'>;
};

export type MeQueryVariables = {};

export type MeQuery = { __typename?: 'Query' } & {
me: Maybe<{ __typename?: 'CurrentUser' } & CurrentUserFragment>;
};

export type GetCollectionsWithAssetsQueryVariables = {};

export type GetCollectionsWithAssetsQuery = { __typename?: 'Query' } & {
Expand Down Expand Up @@ -4041,10 +4054,11 @@ export type TaxRateFragment = { __typename?: 'TaxRate' } & Pick<
customerGroup: Maybe<{ __typename?: 'CustomerGroup' } & Pick<CustomerGroup, 'id' | 'name'>>;
};

export type CurrentUserFragment = { __typename?: 'CurrentUser' } & Pick<
CurrentUser,
'id' | 'identifier' | 'channelTokens'
>;
export type CurrentUserFragment = { __typename?: 'CurrentUser' } & Pick<CurrentUser, 'id' | 'identifier'> & {
channels: Array<
{ __typename?: 'CurrentUserChannel' } & Pick<CurrentUserChannel, 'code' | 'token' | 'permissions'>
>;
};

export type VariantWithStockFragment = { __typename?: 'ProductVariant' } & Pick<
ProductVariant,
Expand Down Expand Up @@ -4955,6 +4969,12 @@ export namespace GetCustomerCount {
export type Customers = GetCustomerCountQuery['customers'];
}

export namespace Me {
export type Variables = MeQueryVariables;
export type Query = MeQuery;
export type Me = CurrentUserFragment;
}

export namespace GetCollectionsWithAssets {
export type Variables = GetCollectionsWithAssetsQueryVariables;
export type Query = GetCollectionsWithAssetsQuery;
Expand Down Expand Up @@ -5405,6 +5425,7 @@ export namespace TaxRate {

export namespace CurrentUser {
export type Fragment = CurrentUserFragment;
export type Channels = NonNullable<CurrentUserFragment['channels'][0]>;
}

export namespace VariantWithStock {
Expand Down
9 changes: 8 additions & 1 deletion packages/core/e2e/graphql/generated-e2e-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -623,7 +623,14 @@ export type CurrentUser = {
__typename?: 'CurrentUser';
id: Scalars['ID'];
identifier: Scalars['String'];
channelTokens: Array<Scalars['String']>;
channels: Array<CurrentUserChannel>;
};

export type CurrentUserChannel = {
__typename?: 'CurrentUserChannel';
token: Scalars['String'];
code: Scalars['String'];
permissions: Array<Permission>;
};

export type Customer = Node & {
Expand Down
4 changes: 3 additions & 1 deletion packages/core/mock-data/simple-graphql-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ const LOGIN = gql`
user {
id
identifier
channelTokens
channels {
token
}
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions packages/core/src/api/resolvers/admin/auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import { MutationLoginArgs, LoginResult, Permission } from '@vendure/common/lib/generated-types';
import { LoginResult, MutationLoginArgs, Permission } from '@vendure/common/lib/generated-types';
import { Request, Response } from 'express';

import { ConfigService } from '../../../config/config.service';
Expand Down Expand Up @@ -31,14 +31,16 @@ export class AuthResolver extends BaseAuthResolver {

@Mutation()
@Allow(Permission.Public)
logout(@Ctx() ctx: RequestContext,
@Context('req') req: Request,
@Context('res') res: Response): Promise<boolean> {
logout(
@Ctx() ctx: RequestContext,
@Context('req') req: Request,
@Context('res') res: Response,
): Promise<boolean> {
return super.logout(ctx, req, res);
}

@Query()
@Allow(Permission.Authenticated)
@Allow(Permission.Authenticated, Permission.Owner)
me(@Ctx() ctx: RequestContext) {
return super.me(ctx);
}
Expand Down
Loading

0 comments on commit b7cd6e5

Please sign in to comment.