Browse Source

feat(core): Allow Roles to be created in other channels

Relates to #12
Michael Bromley 6 years ago
parent
commit
df5f006c06

+ 1 - 0
packages/common/src/generated-types.ts

@@ -533,6 +533,7 @@ export type CreatePromotionInput = {
 };
 };
 
 
 export type CreateRoleInput = {
 export type CreateRoleInput = {
+  channelId?: Maybe<Scalars['ID']>,
   code: Scalars['String'],
   code: Scalars['String'],
   description: Scalars['String'],
   description: Scalars['String'],
   permissions: Array<Permission>,
   permissions: Array<Permission>,

+ 112 - 1
packages/core/e2e/channel.e2e-spec.ts

@@ -6,17 +6,21 @@ import path from 'path';
 import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
 import { dataDir, TEST_SETUP_TIMEOUT_MS, testConfig } from './config/test-config';
 import { initialData } from './fixtures/e2e-initial-data';
 import { initialData } from './fixtures/e2e-initial-data';
 import {
 import {
+    CreateAdministrator,
     CreateChannel,
     CreateChannel,
+    CreateRole,
     CurrencyCode,
     CurrencyCode,
     LanguageCode,
     LanguageCode,
     Me,
     Me,
     Permission,
     Permission,
 } from './graphql/generated-e2e-admin-types';
 } 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', () => {
 describe('Channels', () => {
     const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
     const { server, adminClient, shopClient } = createTestEnvironment(testConfig);
     const SECOND_CHANNEL_TOKEN = 'second_channel_token';
     const SECOND_CHANNEL_TOKEN = 'second_channel_token';
+    let secondChannelAdminRole: CreateRole.CreateRole;
 
 
     beforeAll(async () => {
     beforeAll(async () => {
         await server.init({
         await server.init({
@@ -67,10 +71,117 @@ describe('Channels', () => {
     it('superadmin has all permissions on new channel', async () => {
     it('superadmin has all permissions on new channel', async () => {
         const { me } = await adminClient.query<Me.Query>(ME);
         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 secondChannelData = me!.channels.find(c => c.token === SECOND_CHANNEL_TOKEN);
         const nonOwnerPermissions = Object.values(Permission).filter(p => p !== Permission.Owner);
         const nonOwnerPermissions = Object.values(Permission).filter(p => p !== Permission.Owner);
         expect(secondChannelData!.permissions).toEqual(nonOwnerPermissions);
         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`
 const CREATE_CHANNEL = gql`

+ 13 - 17
packages/core/e2e/graphql/generated-e2e-admin-types.ts

@@ -533,6 +533,7 @@ export type CreatePromotionInput = {
 };
 };
 
 
 export type CreateRoleInput = {
 export type CreateRoleInput = {
+    channelId?: Maybe<Scalars['ID']>;
     code: Scalars['String'];
     code: Scalars['String'];
     description: Scalars['String'];
     description: Scalars['String'];
     permissions: Array<Permission>;
     permissions: Array<Permission>;
@@ -2007,11 +2008,6 @@ export type MutationDeleteProductVariantArgs = {
     id: Scalars['ID'];
     id: Scalars['ID'];
 };
 };
 
 
-export type MutationAssignProductToChannelArgs = {
-    productId: Scalars['ID'];
-    channelId: Scalars['ID'];
-};
-
 export type MutationCreatePromotionArgs = {
 export type MutationCreatePromotionArgs = {
     input: CreatePromotionInput;
     input: CreatePromotionInput;
 };
 };
@@ -3406,12 +3402,6 @@ export type GetCustomerCountQuery = { __typename?: 'Query' } & {
     customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'>;
     customers: { __typename?: 'CustomerList' } & Pick<CustomerList, 'totalItems'>;
 };
 };
 
 
-export type MeQueryVariables = {};
-
-export type MeQuery = { __typename?: 'Query' } & {
-    me: Maybe<{ __typename?: 'CurrentUser' } & CurrentUserFragment>;
-};
-
 export type CreateChannelMutationVariables = {
 export type CreateChannelMutationVariables = {
     input: CreateChannelInput;
     input: CreateChannelInput;
 };
 };
@@ -4381,6 +4371,12 @@ export type CreatePromotionMutation = { __typename?: 'Mutation' } & {
     createPromotion: { __typename?: 'Promotion' } & PromotionFragment;
     createPromotion: { __typename?: 'Promotion' } & PromotionFragment;
 };
 };
 
 
+export type MeQueryVariables = {};
+
+export type MeQuery = { __typename?: 'Query' } & {
+    me: Maybe<{ __typename?: 'CurrentUser' } & CurrentUserFragment>;
+};
+
 export type UpdateOptionGroupMutationVariables = {
 export type UpdateOptionGroupMutationVariables = {
     input: UpdateProductOptionGroupInput;
     input: UpdateProductOptionGroupInput;
 };
 };
@@ -5066,12 +5062,6 @@ export namespace GetCustomerCount {
     export type Customers = GetCustomerCountQuery['customers'];
     export type Customers = GetCustomerCountQuery['customers'];
 }
 }
 
 
-export namespace Me {
-    export type Variables = MeQueryVariables;
-    export type Query = MeQuery;
-    export type Me = CurrentUserFragment;
-}
-
 export namespace CreateChannel {
 export namespace CreateChannel {
     export type Variables = CreateChannelMutationVariables;
     export type Variables = CreateChannelMutationVariables;
     export type Mutation = CreateChannelMutation;
     export type Mutation = CreateChannelMutation;
@@ -5714,6 +5704,12 @@ export namespace CreatePromotion {
     export type CreatePromotion = PromotionFragment;
     export type CreatePromotion = PromotionFragment;
 }
 }
 
 
+export namespace Me {
+    export type Variables = MeQueryVariables;
+    export type Query = MeQuery;
+    export type Me = CurrentUserFragment;
+}
+
 export namespace UpdateOptionGroup {
 export namespace UpdateOptionGroup {
     export type Variables = UpdateOptionGroupMutationVariables;
     export type Variables = UpdateOptionGroupMutationVariables;
     export type Mutation = UpdateOptionGroupMutation;
     export type Mutation = UpdateOptionGroupMutation;

+ 4 - 2
packages/core/src/api/resolvers/admin/role.resolver.ts

@@ -10,7 +10,9 @@ import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 
 import { Role } from '../../../entity/role/role.entity';
 import { Role } from '../../../entity/role/role.entity';
 import { RoleService } from '../../../service/services/role.service';
 import { RoleService } from '../../../service/services/role.service';
+import { RequestContext } from '../../common/request-context';
 import { Allow } from '../../decorators/allow.decorator';
 import { Allow } from '../../decorators/allow.decorator';
+import { Ctx } from '../../decorators/request-context.decorator';
 
 
 @Resolver('Roles')
 @Resolver('Roles')
 export class RoleResolver {
 export class RoleResolver {
@@ -30,9 +32,9 @@ export class RoleResolver {
 
 
     @Mutation()
     @Mutation()
     @Allow(Permission.CreateAdministrator)
     @Allow(Permission.CreateAdministrator)
-    createRole(@Args() args: MutationCreateRoleArgs): Promise<Role> {
+    createRole(@Ctx() ctx: RequestContext, @Args() args: MutationCreateRoleArgs): Promise<Role> {
         const { input } = args;
         const { input } = args;
-        return this.roleService.create(input);
+        return this.roleService.create(ctx, input);
     }
     }
 
 
     @Mutation()
     @Mutation()

+ 2 - 23
packages/core/src/api/resolvers/base/base-auth.resolver.ts

@@ -10,6 +10,7 @@ import { Request, Response } from 'express';
 import { ForbiddenError, InternalServerError } from '../../../common/error/errors';
 import { ForbiddenError, InternalServerError } from '../../../common/error/errors';
 import { ConfigService } from '../../../config/config.service';
 import { ConfigService } from '../../../config/config.service';
 import { User } from '../../../entity/user/user.entity';
 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 { AuthService } from '../../../service/services/auth.service';
 import { UserService } from '../../../service/services/user.service';
 import { UserService } from '../../../service/services/user.service';
 import { extractAuthToken } from '../../common/extract-auth-token';
 import { extractAuthToken } from '../../common/extract-auth-token';
@@ -108,29 +109,7 @@ export class BaseAuthResolver {
         return {
         return {
             id: user.id as string,
             id: user.id as string,
             identifier: user.identifier,
             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);
-    }
 }
 }

+ 12 - 6
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -1,8 +1,7 @@
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
-    MutationLoginArgs,
     LoginResult,
     LoginResult,
-    Permission,
+    MutationLoginArgs,
     MutationRefreshCustomerVerificationArgs,
     MutationRefreshCustomerVerificationArgs,
     MutationRegisterCustomerAccountArgs,
     MutationRegisterCustomerAccountArgs,
     MutationRequestPasswordResetArgs,
     MutationRequestPasswordResetArgs,
@@ -11,10 +10,15 @@ import {
     MutationUpdateCustomerEmailAddressArgs,
     MutationUpdateCustomerEmailAddressArgs,
     MutationUpdateCustomerPasswordArgs,
     MutationUpdateCustomerPasswordArgs,
     MutationVerifyCustomerAccountArgs,
     MutationVerifyCustomerAccountArgs,
+    Permission,
 } from '@vendure/common/lib/generated-shop-types';
 } from '@vendure/common/lib/generated-shop-types';
 import { Request, Response } from 'express';
 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 { ConfigService } from '../../../config/config.service';
 import { AuthService } from '../../../service/services/auth.service';
 import { AuthService } from '../../../service/services/auth.service';
 import { CustomerService } from '../../../service/services/customer.service';
 import { CustomerService } from '../../../service/services/customer.service';
@@ -48,9 +52,11 @@ export class ShopAuthResolver extends BaseAuthResolver {
 
 
     @Mutation()
     @Mutation()
     @Allow(Permission.Public)
     @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);
         return super.logout(ctx, req, res);
     }
     }
 
 

+ 1 - 0
packages/core/src/api/schema/admin-api/role.api.graphql

@@ -14,6 +14,7 @@ type Mutation {
 input RoleListOptions
 input RoleListOptions
 
 
 input CreateRoleInput {
 input CreateRoleInput {
+    channelId: ID
     code: String!
     code: String!
     description: String!
     description: String!
     permissions: [Permission!]!
     permissions: [Permission!]!

+ 38 - 0
packages/core/src/service/helpers/utils/get-user-channels-permissions.ts

@@ -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);
+}

+ 70 - 20
packages/core/src/service/services/role.service.ts

@@ -11,11 +11,21 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import { unique } from '@vendure/common/lib/unique';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
-import { EntityNotFoundError, InternalServerError, UserInputError } from '../../common/error/errors';
+import { RequestContext } from '../../api/common/request-context';
+import {
+    EntityNotFoundError,
+    ForbiddenError,
+    InternalServerError,
+    UserInputError,
+} from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { ListQueryOptions } from '../../common/types/common-types';
-import { assertFound } from '../../common/utils';
+import { assertFound, idsAreEqual } from '../../common/utils';
+import { Channel } from '../../entity/channel/channel.entity';
 import { Role } from '../../entity/role/role.entity';
 import { Role } from '../../entity/role/role.entity';
+import { User } from '../../entity/user/user.entity';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+import { getUserChannelsPermissions } from '../helpers/utils/get-user-channels-permissions';
 import { patchEntity } from '../helpers/utils/patch-entity';
 import { patchEntity } from '../helpers/utils/patch-entity';
 
 
 import { ChannelService } from './channel.service';
 import { ChannelService } from './channel.service';
@@ -74,15 +84,39 @@ export class RoleService {
         return Object.values(Permission);
         return Object.values(Permission);
     }
     }
 
 
-    async create(input: CreateRoleInput): Promise<Role> {
-        this.checkPermissionsAreValid(input.permissions);
-        const role = new Role({
-            code: input.code,
-            description: input.description,
-            permissions: unique([Permission.Authenticated, ...input.permissions]),
+    /**
+     * Returns true if the User has the specified permission on that Channel
+     */
+    async userHasPermissionOnChannel(userId: ID, channelId: ID, permission: Permission): Promise<boolean> {
+        const user = await getEntityOrThrow(this.connection, User, userId, {
+            relations: ['roles', 'roles.channels'],
         });
         });
-        role.channels = [this.channelService.getDefaultChannel()];
-        return this.connection.manager.save(role);
+        const userChannels = getUserChannelsPermissions(user);
+        const channel = userChannels.find(c => idsAreEqual(c.id, channelId));
+        if (!channel) {
+            return false;
+        }
+        return channel.permissions.includes(permission);
+    }
+
+    async create(ctx: RequestContext, input: CreateRoleInput): Promise<Role> {
+        this.checkPermissionsAreValid(input.permissions);
+        const targetChannel = input.channelId
+            ? await getEntityOrThrow(this.connection, Channel, input.channelId)
+            : ctx.channel;
+
+        if (!ctx.activeUserId) {
+            throw new ForbiddenError();
+        }
+        const hasPermission = await this.userHasPermissionOnChannel(
+            ctx.activeUserId,
+            targetChannel.id,
+            Permission.CreateAdministrator,
+        );
+        if (!hasPermission) {
+            throw new ForbiddenError();
+        }
+        return this.createRoleForChannel(input, targetChannel);
     }
     }
 
 
     async update(input: UpdateRoleInput): Promise<Role> {
     async update(input: UpdateRoleInput): Promise<Role> {
@@ -142,11 +176,14 @@ export class RoleService {
                 await this.connection.getRepository(Role).save(superAdminRole);
                 await this.connection.getRepository(Role).save(superAdminRole);
             }
             }
         } catch (err) {
         } catch (err) {
-            await this.create({
-                code: SUPER_ADMIN_ROLE_CODE,
-                description: SUPER_ADMIN_ROLE_DESCRIPTION,
-                permissions: allPermissions,
-            });
+            await this.createRoleForChannel(
+                {
+                    code: SUPER_ADMIN_ROLE_CODE,
+                    description: SUPER_ADMIN_ROLE_DESCRIPTION,
+                    permissions: allPermissions,
+                },
+                this.channelService.getDefaultChannel(),
+            );
         }
         }
     }
     }
 
 
@@ -154,11 +191,24 @@ export class RoleService {
         try {
         try {
             await this.getCustomerRole();
             await this.getCustomerRole();
         } catch (err) {
         } catch (err) {
-            await this.create({
-                code: CUSTOMER_ROLE_CODE,
-                description: CUSTOMER_ROLE_DESCRIPTION,
-                permissions: [Permission.Authenticated],
-            });
+            await this.createRoleForChannel(
+                {
+                    code: CUSTOMER_ROLE_CODE,
+                    description: CUSTOMER_ROLE_DESCRIPTION,
+                    permissions: [Permission.Authenticated],
+                },
+                this.channelService.getDefaultChannel(),
+            );
         }
         }
     }
     }
+
+    private createRoleForChannel(input: CreateRoleInput, channel: Channel) {
+        const role = new Role({
+            code: input.code,
+            description: input.description,
+            permissions: unique([Permission.Authenticated, ...input.permissions]),
+        });
+        role.channels = [channel];
+        return this.connection.manager.save(role);
+    }
 }
 }

+ 13 - 6
packages/testing/src/simple-graphql-client.ts

@@ -34,7 +34,7 @@ export type QueryParams = { [key: string]: string | number };
  */
  */
 export class SimpleGraphQLClient {
 export class SimpleGraphQLClient {
     private authToken: string;
     private authToken: string;
-    private channelToken: string;
+    private channelToken: string | null = null;
     private headers: { [key: string]: any } = {};
     private headers: { [key: string]: any } = {};
 
 
     constructor(private vendureConfig: Required<VendureConfig>, private apiUrl: string = '') {}
     constructor(private vendureConfig: Required<VendureConfig>, private apiUrl: string = '') {}
@@ -48,6 +48,15 @@ export class SimpleGraphQLClient {
         this.headers.Authorization = `Bearer ${this.authToken}`;
         this.headers.Authorization = `Bearer ${this.authToken}`;
     }
     }
 
 
+    /**
+     * @description
+     * Sets the authToken to be used in each GraphQL request.
+     */
+    setChannelToken(token: string | null) {
+        this.channelToken = token;
+        this.headers[this.vendureConfig.channelTokenKey] = this.channelToken;
+    }
+
     /**
     /**
      * @description
      * @description
      * Returns the authToken currently being used.
      * Returns the authToken currently being used.
@@ -56,11 +65,6 @@ export class SimpleGraphQLClient {
         return this.authToken;
         return this.authToken;
     }
     }
 
 
-    /** @internal */
-    setChannelToken(token: string) {
-        this.headers[this.vendureConfig.channelTokenKey] = token;
-    }
-
     /**
     /**
      * @description
      * @description
      * Performs both query and mutation operations.
      * Performs both query and mutation operations.
@@ -109,6 +113,9 @@ export class SimpleGraphQLClient {
             );
             );
         }
         }
         const result = await this.query(LOGIN, { username, password });
         const result = await this.query(LOGIN, { username, password });
+        if (result.login.user.channels.length === 1) {
+            this.setChannelToken(result.login.user.channels[0].token);
+        }
         return result.login;
         return result.login;
     }
     }
 
 

File diff suppressed because it is too large
+ 0 - 0
schema-admin.json


Some files were not shown because too many files changed in this diff