diff --git a/packages/core/src/api/common/get-api-type.ts b/packages/core/src/api/common/get-api-type.ts new file mode 100644 index 0000000000..2eb1657be9 --- /dev/null +++ b/packages/core/src/api/common/get-api-type.ts @@ -0,0 +1,21 @@ +import { GraphQLResolveInfo } from 'graphql'; + +/** + * @description + * Which of the GraphQL APIs the current request came via. + * + * @docsCategory request + */ +export type ApiType = 'admin' | 'shop'; + +/** + * Inspects the GraphQL "info" resolver argument to determine which API + * the request came through. + */ +export function getApiType(info: GraphQLResolveInfo): ApiType { + const query = info.schema.getQueryType(); + if (query) { + return !!query.getFields().administrators ? 'admin' : 'shop'; + } + return 'shop'; +} diff --git a/packages/core/src/api/common/request-context.service.ts b/packages/core/src/api/common/request-context.service.ts index 4c06e95a96..aec1006e2e 100644 --- a/packages/core/src/api/common/request-context.service.ts +++ b/packages/core/src/api/common/request-context.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { LanguageCode, Permission } from '@vendure/common/lib/generated-types'; import { Request } from 'express'; +import { GraphQLResolveInfo } from 'graphql'; import { idsAreEqual } from '../../common/utils'; import { ConfigService } from '../../config/config.service'; @@ -9,6 +10,7 @@ import { AuthenticatedSession } from '../../entity/session/authenticated-session import { Session } from '../../entity/session/session.entity'; import { User } from '../../entity/user/user.entity'; import { ChannelService } from '../../service/services/channel.service'; +import { getApiType } from './get-api-type'; import { RequestContext } from './request-context'; @@ -26,11 +28,13 @@ export class RequestContextService { */ async fromRequest( req: Request, + info: GraphQLResolveInfo, requiredPermissions?: Permission[], session?: Session, ): Promise { const channelToken = this.getChannelToken(req); const channel = this.channelService.getChannelFromToken(channelToken); + const apiType = getApiType(info); const hasOwnerPermission = !!requiredPermissions && requiredPermissions.includes(Permission.Owner); const languageCode = this.getLanguageCode(req); @@ -39,6 +43,7 @@ export class RequestContextService { const authorizedAsOwnerOnly = !isAuthorized && hasOwnerPermission; const translationFn = (req as any).t; return new RequestContext({ + apiType, channel, languageCode, session, diff --git a/packages/core/src/api/common/request-context.ts b/packages/core/src/api/common/request-context.ts index 71ee6a70fa..f4920aba75 100644 --- a/packages/core/src/api/common/request-context.ts +++ b/packages/core/src/api/common/request-context.ts @@ -8,13 +8,14 @@ import { AuthenticatedSession } from '../../entity/session/authenticated-session import { Session } from '../../entity/session/session.entity'; import { User } from '../../entity/user/user.entity'; +import { ApiType } from './get-api-type'; + /** * @description * The RequestContext holds information relevant to the current request, which may be * required at various points of the stack. * - * @docsCategory - * @docsWeight 1 + * @docsCategory request */ export class RequestContext { private readonly _languageCode: LanguageCode; @@ -23,11 +24,13 @@ export class RequestContext { private readonly _isAuthorized: boolean; private readonly _authorizedAsOwnerOnly: boolean; private readonly _translationFn: i18next.TranslationFunction; + private readonly _apiType: ApiType; /** * @internal */ constructor(options: { + apiType: ApiType; channel: Channel; session?: Session; languageCode?: LanguageCode; @@ -35,7 +38,8 @@ export class RequestContext { authorizedAsOwnerOnly: boolean; translationFn?: i18next.TranslationFunction; }) { - const { channel, session, languageCode, translationFn } = options; + const { apiType, channel, session, languageCode, translationFn } = options; + this._apiType = apiType; this._channel = channel; this._session = session; this._languageCode = @@ -45,6 +49,10 @@ export class RequestContext { this._translationFn = translationFn || (((key: string) => key) as any); } + get apiType(): ApiType { + return this._apiType; + } + get channel(): Channel { return this._channel; } @@ -77,6 +85,7 @@ export class RequestContext { } /** + * @description * True if the current session is authorized to access the current resolver method. */ get isAuthorized(): boolean { @@ -84,6 +93,7 @@ export class RequestContext { } /** + * @description * True if the current anonymous session is only authorized to operate on entities that * are owned by the current session. */ diff --git a/packages/core/src/api/decorators/api.decorator.ts b/packages/core/src/api/decorators/api.decorator.ts index 670101f906..ef983be99c 100644 --- a/packages/core/src/api/decorators/api.decorator.ts +++ b/packages/core/src/api/decorators/api.decorator.ts @@ -1,7 +1,7 @@ import { createParamDecorator } from '@nestjs/common'; import { GraphQLResolveInfo } from 'graphql'; -export type ApiType = 'admin' | 'shop'; +import { getApiType } from '../common/get-api-type'; /** * Resolver param decorator which returns which Api the request came though. @@ -9,9 +9,5 @@ export type ApiType = 'admin' | 'shop'; * depending whether it is being called from the shop API or the admin API. */ export const Api = createParamDecorator((data, [root, args, ctx, info]) => { - const query = (info as GraphQLResolveInfo).schema.getQueryType(); - if (query) { - return !!query.getFields().administrators ? 'admin' : 'shop'; - } - return 'shop'; + return getApiType(info); }); diff --git a/packages/core/src/api/middleware/auth-guard.ts b/packages/core/src/api/middleware/auth-guard.ts index aee4cef894..5f59d8a2a5 100644 --- a/packages/core/src/api/middleware/auth-guard.ts +++ b/packages/core/src/api/middleware/auth-guard.ts @@ -3,6 +3,7 @@ import { Reflector } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import { Permission } from '@vendure/common/lib/generated-types'; import { Request, Response } from 'express'; +import { GraphQLResolveInfo } from 'graphql'; import { ForbiddenError } from '../../common/error/errors'; import { ConfigService } from '../../config/config.service'; @@ -29,7 +30,9 @@ export class AuthGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { - const ctx = GqlExecutionContext.create(context).getContext(); + const graphQlContext = GqlExecutionContext.create(context); + const ctx = graphQlContext.getContext(); + const info = graphQlContext.getInfo(); const req: Request = ctx.req; const res: Response = ctx.res; const authDisabled = this.configService.authOptions.disableAuth; @@ -37,7 +40,7 @@ export class AuthGuard implements CanActivate { const isPublic = !!permissions && permissions.includes(Permission.Public); const hasOwnerPermission = !!permissions && permissions.includes(Permission.Owner); const session = await this.getSession(req, res, hasOwnerPermission); - const requestContext = await this.requestContextService.fromRequest(req, permissions, session); + const requestContext = await this.requestContextService.fromRequest(req, info, permissions, session); (req as any)[REQUEST_CONTEXT_KEY] = requestContext; if (authDisabled || !permissions || isPublic) { diff --git a/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts b/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts index 37b25d7a4c..44529ce74c 100644 --- a/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/collection-entity.resolver.ts @@ -7,8 +7,9 @@ import { Translated } from '../../../common/types/locale-types'; import { Collection, Product, ProductVariant } from '../../../entity'; import { CollectionService } from '../../../service/services/collection.service'; import { ProductVariantService } from '../../../service/services/product-variant.service'; +import { ApiType } from '../../common/get-api-type'; import { RequestContext } from '../../common/request-context'; -import { Api, ApiType } from '../../decorators/api.decorator'; +import { Api } from '../../decorators/api.decorator'; import { Ctx } from '../../decorators/request-context.decorator'; @Resolver('Collection') diff --git a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts index 8aeeaa85c6..dafd594525 100644 --- a/packages/core/src/api/resolvers/entity/product-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/product-entity.resolver.ts @@ -6,8 +6,9 @@ import { ProductVariant } from '../../../entity/product-variant/product-variant. import { Product } from '../../../entity/product/product.entity'; import { CollectionService } from '../../../service/services/collection.service'; import { ProductVariantService } from '../../../service/services/product-variant.service'; +import { ApiType } from '../../common/get-api-type'; import { RequestContext } from '../../common/request-context'; -import { Api, ApiType } from '../../decorators/api.decorator'; +import { Api } from '../../decorators/api.decorator'; import { Ctx } from '../../decorators/request-context.decorator'; @Resolver('Product') diff --git a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts index e2ea67b53b..13445a4e69 100644 --- a/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts +++ b/packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts @@ -4,8 +4,9 @@ import { Translated } from '../../../common/types/locale-types'; import { FacetValue, ProductOption } from '../../../entity'; import { ProductVariant } from '../../../entity/product-variant/product-variant.entity'; import { ProductVariantService } from '../../../service/services/product-variant.service'; +import { ApiType } from '../../common/get-api-type'; import { RequestContext } from '../../common/request-context'; -import { Api, ApiType } from '../../decorators/api.decorator'; +import { Api } from '../../decorators/api.decorator'; import { Ctx } from '../../decorators/request-context.decorator'; @Resolver('ProductVariant') diff --git a/packages/core/src/data-import/providers/importer/importer.ts b/packages/core/src/data-import/providers/importer/importer.ts index 865d7aa514..d34b7257a6 100644 --- a/packages/core/src/data-import/providers/importer/importer.ts +++ b/packages/core/src/data-import/providers/importer/importer.ts @@ -128,6 +128,7 @@ export class Importer { } else { const channel = await this.channelService.getDefaultChannel(); return new RequestContext({ + apiType: 'admin', isAuthorized: true, authorizedAsOwnerOnly: false, channel, diff --git a/packages/core/src/data-import/providers/populator/populator.ts b/packages/core/src/data-import/providers/populator/populator.ts index 9ba4189346..55da3a9e70 100644 --- a/packages/core/src/data-import/providers/populator/populator.ts +++ b/packages/core/src/data-import/providers/populator/populator.ts @@ -112,6 +112,7 @@ export class Populator { private async createRequestContext(data: InitialData) { const channel = await this.channelService.getDefaultChannel(); const ctx = new RequestContext({ + apiType: 'admin', isAuthorized: true, authorizedAsOwnerOnly: false, channel, diff --git a/packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts b/packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts index 879dea4956..32c5a24a2d 100644 --- a/packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts +++ b/packages/core/src/service/helpers/tax-calculator/tax-calculator-test-fixtures.ts @@ -76,6 +76,7 @@ export function createRequestContext(pricesIncludeTax: boolean): RequestContext pricesIncludeTax, }); const ctx = new RequestContext({ + apiType: 'admin', channel, authorizedAsOwnerOnly: false, languageCode: LanguageCode.en,