Skip to content

Commit

Permalink
feat(core): Allow Roles to be created in other channels
Browse files Browse the repository at this point in the history
Relates to #12
  • Loading branch information
michaelbromley committed Nov 5, 2019
1 parent 6fc421a commit df5f006
Show file tree
Hide file tree
Showing 11 changed files with 267 additions and 76 deletions.
1 change: 1 addition & 0 deletions packages/common/src/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export type CreatePromotionInput = {
};

export type CreateRoleInput = {
channelId?: Maybe<Scalars['ID']>,
code: Scalars['String'],
description: Scalars['String'],
permissions: Array<Permission>,
Expand Down
113 changes: 112 additions & 1 deletion packages/core/e2e/channel.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,21 @@ import path from 'path';
import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
import { initialData } from './fixtures/e2e-initial-data';
import {
CreateAdministrator,
CreateChannel,
CreateRole,
CurrencyCode,
LanguageCode,
Me,
Permission,
} from './graphql/generated-e2e-admin-types';
import { ME } from './graphql/shared-definitions';
import { CREATE_ADMINISTRATOR, CREATE_ROLE, ME } from './graphql/shared-definitions';
import { assertThrowsWithMessage } from './utils/assert-throws-with-message';

describe('Channels', () => {
const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
const SECOND_CHANNEL_TOKEN = 'second_channel_token';
let secondChannelAdminRole: CreateRole.CreateRole;

beforeAll(async () => {
await server.init({
Expand Down Expand Up @@ -67,10 +71,117 @@ describe('Channels', () => {
it('superadmin has all permissions on new channel', async () => {
const { me } = await adminClient.query<Me.Query>(ME);

expect(me!.channels.length).toBe(2);

const secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN);
const nonOwnerPermissions = Object.values(Permission).filter(p => p !== Permission.Owner);
expect(secondChannelData!.permissions).toEqual(nonOwnerPermissions);
});

it('createRole on second Channel', async () => {
const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(
CREATE_ROLE,
{
input: {
description: 'second channel admin',
code: 'second-channel-admin',
channelId: 'T_2',
permissions: [
Permission.ReadCatalog,
Permission.ReadSettings,
Permission.ReadAdministrator,
Permission.CreateAdministrator,
Permission.UpdateAdministrator,
],
},
},
);

expect(createRole.channels).toEqual([
{
id: 'T_2',
code: 'second-channel',
token: SECOND_CHANNEL_TOKEN,
},
]);

secondChannelAdminRole = createRole;
});

it('createAdministrator with second-channel-admin role', async () => {
const { createAdministrator } = await adminClient.query<
CreateAdministrator.Mutation,
CreateAdministrator.Variables
>(CREATE_ADMINISTRATOR, {
input: {
firstName: 'Admin',
lastName: 'Two',
emailAddress: 'admin2@test.com',
password: 'test',
roleIds: [secondChannelAdminRole.id],
},
});

expect(createAdministrator.user.roles.map(r => r.description)).toEqual(['second channel admin']);
});

it(
'cannot create role on channel for which admin does not have CreateAdministrator permission',
assertThrowsWithMessage(async () => {
await adminClient.asUserWithCredentials('admin2@test.com', 'test');
await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(CREATE_ROLE, {
input: {
description: 'read default channel catalog',
code: 'read default channel catalog',
channelId: 'T_1',
permissions: [Permission.ReadCatalog],
},
});
}, 'You are not currently authorized to perform this action'),
);

it('can create role on channel for which admin has CreateAdministrator permission', async () => {
const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(
CREATE_ROLE,
{
input: {
description: 'read second channel catalog',
code: 'read-second-channel-catalog',
channelId: 'T_2',
permissions: [Permission.ReadCatalog],
},
},
);

expect(createRole.channels).toEqual([
{
id: 'T_2',
code: 'second-channel',
token: SECOND_CHANNEL_TOKEN,
},
]);
});

it('createRole with no channelId implicitly uses active channel', async () => {
const { createRole } = await adminClient.query<CreateRole.Mutation, CreateRole.Variables>(
CREATE_ROLE,
{
input: {
description: 'update second channel catalog',
code: 'update-second-channel-catalog',
permissions: [Permission.UpdateCatalog],
},
},
);

expect(createRole.channels).toEqual([
{
id: 'T_2',
code: 'second-channel',
token: SECOND_CHANNEL_TOKEN,
},
]);
});
});

const CREATE_CHANNEL = gql`
Expand Down
30 changes: 13 additions & 17 deletions packages/core/e2e/graphql/generated-e2e-admin-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ export type CreatePromotionInput = {
};

export type CreateRoleInput = {
channelId?: Maybe<Scalars['ID']>;
code: Scalars['String'];
description: Scalars['String'];
permissions: Array<Permission>;
Expand Down Expand Up @@ -2007,11 +2008,6 @@ export type MutationDeleteProductVariantArgs = {
id: Scalars['ID'];
};

export type MutationAssignProductToChannelArgs = {
productId: Scalars['ID'];
channelId: Scalars['ID'];
};

export type MutationCreatePromotionArgs = {
input: CreatePromotionInput;
};
Expand Down Expand Up @@ -3406,12 +3402,6 @@ 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 CreateChannelMutationVariables = {
input: CreateChannelInput;
};
Expand Down Expand Up @@ -4381,6 +4371,12 @@ export type CreatePromotionMutation = { __typename?: 'Mutation' } & {
createPromotion: { __typename?: 'Promotion' } & PromotionFragment;
};

export type MeQueryVariables = {};

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

export type UpdateOptionGroupMutationVariables = {
input: UpdateProductOptionGroupInput;
};
Expand Down Expand Up @@ -5066,12 +5062,6 @@ 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 CreateChannel {
export type Variables = CreateChannelMutationVariables;
export type Mutation = CreateChannelMutation;
Expand Down Expand Up @@ -5714,6 +5704,12 @@ export namespace CreatePromotion {
export type CreatePromotion = PromotionFragment;
}

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

export namespace UpdateOptionGroup {
export type Variables = UpdateOptionGroupMutationVariables;
export type Mutation = UpdateOptionGroupMutation;
Expand Down
6 changes: 4 additions & 2 deletions packages/core/src/api/resolvers/admin/role.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';

import { Role } from '../../../entity/role/role.entity';
import { RoleService } from '../../../service/services/role.service';
import { RequestContext } from '../../common/request-context';
import { Allow } from '../../decorators/allow.decorator';
import { Ctx } from '../../decorators/request-context.decorator';

@Resolver('Roles')
export class RoleResolver {
Expand All @@ -30,9 +32,9 @@ export class RoleResolver {

@Mutation()
@Allow(Permission.CreateAdministrator)
createRole(@Args() args: MutationCreateRoleArgs): Promise<Role> {
createRole(@Ctx() ctx: RequestContext, @Args() args: MutationCreateRoleArgs): Promise<Role> {
const { input } = args;
return this.roleService.create(input);
return this.roleService.create(ctx, input);
}

@Mutation()
Expand Down
25 changes: 2 additions & 23 deletions packages/core/src/api/resolvers/base/base-auth.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Request, Response } from 'express';
import { ForbiddenError, InternalServerError } from '../../../common/error/errors';
import { ConfigService } from '../../../config/config.service';
import { User } from '../../../entity/user/user.entity';
import { getUserChannelsPermissions } from '../../../service/helpers/utils/get-user-channels-permissions';
import { AuthService } from '../../../service/services/auth.service';
import { UserService } from '../../../service/services/user.service';
import { extractAuthToken } from '../../common/extract-auth-token';
Expand Down Expand Up @@ -108,29 +109,7 @@ export class BaseAuthResolver {
return {
id: user.id as string,
identifier: user.identifier,
channels: this.getCurrentUserChannels(user),
channels: getUserChannelsPermissions(user) as CurrentUserChannel[],
};
}

private getCurrentUserChannels(user: User): CurrentUserChannel[] {
const channelsMap: { [code: string]: CurrentUserChannel } = {};

for (const role of user.roles) {
for (const channel of role.channels) {
if (!channelsMap[channel.code]) {
channelsMap[channel.code] = {
token: channel.token,
code: channel.code,
permissions: [],
};
}
channelsMap[channel.code].permissions = unique([
...channelsMap[channel.code].permissions,
...role.permissions,
]);
}
}

return Object.values(channelsMap);
}
}
18 changes: 12 additions & 6 deletions packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
import {
MutationLoginArgs,
LoginResult,
Permission,
MutationLoginArgs,
MutationRefreshCustomerVerificationArgs,
MutationRegisterCustomerAccountArgs,
MutationRequestPasswordResetArgs,
Expand All @@ -11,10 +10,15 @@ import {
MutationUpdateCustomerEmailAddressArgs,
MutationUpdateCustomerPasswordArgs,
MutationVerifyCustomerAccountArgs,
Permission,
} from '@vendure/common/lib/generated-shop-types';
import { Request, Response } from 'express';

import { ForbiddenError, PasswordResetTokenError, VerificationTokenError } from '../../../common/error/errors';
import {
ForbiddenError,
PasswordResetTokenError,
VerificationTokenError,
} from '../../../common/error/errors';
import { ConfigService } from '../../../config/config.service';
import { AuthService } from '../../../service/services/auth.service';
import { CustomerService } from '../../../service/services/customer.service';
Expand Down Expand Up @@ -48,9 +52,11 @@ export class ShopAuthResolver 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);
}

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/api/schema/admin-api/role.api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Mutation {
input RoleListOptions

input CreateRoleInput {
channelId: ID
code: String!
description: String!
permissions: [Permission!]!
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Permission } from '@vendure/common/lib/generated-types';
import { ID } from '@vendure/common/lib/shared-types';
import { unique } from '@vendure/common/lib/unique';

import { User } from '../../../entity/user/user.entity';

export interface UserChannelPermissions {
id: ID;
token: string;
code: string;
permissions: Permission[];
}

/**
* Returns an array of Channels and permissions on those Channels for the given User.
*/
export function getUserChannelsPermissions(user: User): UserChannelPermissions[] {
const channelsMap: { [code: string]: UserChannelPermissions } = {};

for (const role of user.roles) {
for (const channel of role.channels) {
if (!channelsMap[channel.code]) {
channelsMap[channel.code] = {
id: channel.id,
token: channel.token,
code: channel.code,
permissions: [],
};
}
channelsMap[channel.code].permissions = unique([
...channelsMap[channel.code].permissions,
...role.permissions,
]);
}
}

return Object.values(channelsMap);
}

0 comments on commit df5f006

Please sign in to comment.