Browse Source

refactor(server): Merge Auth & Roles guard, expand RequestContext scope

Michael Bromley 7 years ago
parent
commit
078b5e8bd2

+ 2 - 5
server/src/api/api.module.ts

@@ -11,7 +11,6 @@ import { AuthGuard } from './common/auth-guard';
 import { GraphqlConfigService } from './common/graphql-config.service';
 import { GraphqlConfigService } from './common/graphql-config.service';
 import { IdInterceptor } from './common/id-interceptor';
 import { IdInterceptor } from './common/id-interceptor';
 import { RequestContextService } from './common/request-context.service';
 import { RequestContextService } from './common/request-context.service';
-import { RolesGuard } from './common/roles-guard';
 import { AdministratorResolver } from './resolvers/administrator.resolver';
 import { AdministratorResolver } from './resolvers/administrator.resolver';
 import { AssetResolver } from './resolvers/asset.resolver';
 import { AssetResolver } from './resolvers/asset.resolver';
 import { AuthResolver } from './resolvers/auth.resolver';
 import { AuthResolver } from './resolvers/auth.resolver';
@@ -19,6 +18,7 @@ import { ChannelResolver } from './resolvers/channel.resolver';
 import { ConfigResolver } from './resolvers/config.resolver';
 import { ConfigResolver } from './resolvers/config.resolver';
 import { CustomerResolver } from './resolvers/customer.resolver';
 import { CustomerResolver } from './resolvers/customer.resolver';
 import { FacetResolver } from './resolvers/facet.resolver';
 import { FacetResolver } from './resolvers/facet.resolver';
+import { OrderResolver } from './resolvers/order.resolver';
 import { ProductOptionResolver } from './resolvers/product-option.resolver';
 import { ProductOptionResolver } from './resolvers/product-option.resolver';
 import { ProductResolver } from './resolvers/product.resolver';
 import { ProductResolver } from './resolvers/product.resolver';
 import { RoleResolver } from './resolvers/role.resolver';
 import { RoleResolver } from './resolvers/role.resolver';
@@ -31,6 +31,7 @@ const exportedProviders = [
     ConfigResolver,
     ConfigResolver,
     FacetResolver,
     FacetResolver,
     CustomerResolver,
     CustomerResolver,
+    OrderResolver,
     ProductOptionResolver,
     ProductOptionResolver,
     ProductResolver,
     ProductResolver,
     RoleResolver,
     RoleResolver,
@@ -56,10 +57,6 @@ const exportedProviders = [
             provide: APP_GUARD,
             provide: APP_GUARD,
             useClass: AuthGuard,
             useClass: AuthGuard,
         },
         },
-        {
-            provide: APP_GUARD,
-            useClass: RolesGuard,
-        },
         {
         {
             provide: APP_INTERCEPTOR,
             provide: APP_INTERCEPTOR,
             useClass: AssetInterceptor,
             useClass: AssetInterceptor,

+ 59 - 16
server/src/api/common/auth-guard.ts

@@ -1,12 +1,33 @@
-import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
+import { CanActivate, ExecutionContext, Injectable, ReflectMetadata } from '@nestjs/common';
 import { Reflector } from '@nestjs/core';
 import { Reflector } from '@nestjs/core';
 import { GqlExecutionContext } from '@nestjs/graphql';
 import { GqlExecutionContext } from '@nestjs/graphql';
+import { Request } from 'express';
+import { Permission } from 'shared/generated-types';
 
 
+import { idsAreEqual } from '../../common/utils';
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
-import { AuthService } from '../../service/providers/auth.service';
+import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
+import { Session } from '../../entity/session/session.entity';
 
 
-import { extractAuthToken } from './extract-auth-token';
-import { PERMISSIONS_METADATA_KEY } from './roles-guard';
+import { RequestContext } from './request-context';
+import { REQUEST_CONTEXT_KEY, RequestContextService } from './request-context.service';
+
+export const PERMISSIONS_METADATA_KEY = '__permissions__';
+
+/**
+ * Attatches metadata to the resolver defining which permissions are required to execute the
+ * operation.
+ *
+ * @example
+ * ```
+ *  @Allow(Permission.SuperAdmin)
+ *  @Query()
+ *  getAdministrators() {
+ *      // ...
+ *  }
+ * ```
+ */
+export const Allow = (...permissions: Permission[]) => ReflectMetadata(PERMISSIONS_METADATA_KEY, permissions);
 
 
 /**
 /**
  * A guard which checks for the existence of a valid session token in the request and if found,
  * A guard which checks for the existence of a valid session token in the request and if found,
@@ -19,28 +40,50 @@ export class AuthGuard implements CanActivate {
     constructor(
     constructor(
         private reflector: Reflector,
         private reflector: Reflector,
         private configService: ConfigService,
         private configService: ConfigService,
-        private authService: AuthService,
+        private requestContextService: RequestContextService,
     ) {}
     ) {}
 
 
     async canActivate(context: ExecutionContext): Promise<boolean> {
     async canActivate(context: ExecutionContext): Promise<boolean> {
-        const ctx = GqlExecutionContext.create(context).getContext();
+        const req: Request = GqlExecutionContext.create(context).getContext().req;
         const authDisabled = this.configService.authOptions.disableAuth;
         const authDisabled = this.configService.authOptions.disableAuth;
-        const permissions = this.reflector.get<string[]>(PERMISSIONS_METADATA_KEY, context.getHandler());
+        const permissions = this.reflector.get<string[]>(
+            PERMISSIONS_METADATA_KEY,
+            context.getHandler(),
+        ) as Permission[];
+        const requestContext = await this.requestContextService.fromRequest(req);
+        req[REQUEST_CONTEXT_KEY] = requestContext;
 
 
         if (authDisabled || !permissions) {
         if (authDisabled || !permissions) {
             return true;
             return true;
+        } else {
+            return this.userHasRequiredPermissionsOnChannel(permissions, requestContext);
         }
         }
+    }
 
 
-        const token = extractAuthToken(ctx.req, this.configService.authOptions.tokenMethod);
-        if (!token) {
-            return false;
-        }
-        const activeSession = await this.authService.validateSession(token);
-        if (activeSession) {
-            ctx.req.user = activeSession.user;
-            return true;
-        } else {
+    private isAuthenticatedSession(session?: Session): session is AuthenticatedSession {
+        return !!session && !!(session as AuthenticatedSession).user;
+    }
+
+    private userHasRequiredPermissionsOnChannel(
+        permissions: Permission[],
+        requestContext: RequestContext,
+    ): boolean {
+        const user = requestContext.user;
+        if (!user) {
             return false;
             return false;
         }
         }
+        const permissionsOnChannel = user.roles
+            .filter(role => role.channels.find(c => idsAreEqual(c.id, requestContext.channel.id)))
+            .reduce((output, role) => [...output, ...role.permissions], [] as Permission[]);
+        return arraysIntersect(permissions, permissionsOnChannel);
     }
     }
 }
 }
+
+/**
+ * Returns true if any element of arr1 appears in arr2.
+ */
+function arraysIntersect<T>(arr1: T[], arr2: T[]): boolean {
+    return arr1.reduce((intersects, role) => {
+        return intersects || arr2.includes(role);
+    }, false);
+}

+ 15 - 0
server/src/api/common/request-context.decorator.ts

@@ -0,0 +1,15 @@
+import { Context } from '@nestjs/graphql';
+
+import { REQUEST_CONTEXT_KEY } from './request-context.service';
+
+/**
+ * Resolver param decorator which extracts the RequestContext from the incoming
+ * request object.
+ */
+export function Ctx() {
+    return Context('req', {
+        transform(req) {
+            return req[REQUEST_CONTEXT_KEY];
+        },
+    });
+}

+ 0 - 19
server/src/api/common/request-context.pipe.ts

@@ -1,19 +0,0 @@
-import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
-
-import { I18nError } from '../../i18n/i18n-error';
-import { ChannelService } from '../../service/providers/channel.service';
-
-import { RequestContext } from './request-context';
-import { RequestContextService } from './request-context.service';
-
-/**
- * Creates a new RequestContext based on the token passed in the query string of the request.
- */
-@Injectable()
-export class RequestContextPipe implements PipeTransform<any, RequestContext> {
-    constructor(private requestContextService: RequestContextService) {}
-
-    transform(value: any, metadata: ArgumentMetadata) {
-        return this.requestContextService.fromRequest(value.req);
-    }
-}

+ 52 - 11
server/src/api/common/request-context.service.ts

@@ -1,37 +1,78 @@
 import { Injectable } from '@nestjs/common';
 import { Injectable } from '@nestjs/common';
 import { Request } from 'express';
 import { Request } from 'express';
+import { LanguageCode } from 'shared/generated-types';
 
 
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
+import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
+import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { I18nError } from '../../i18n/i18n-error';
+import { AuthService } from '../../service/providers/auth.service';
 import { ChannelService } from '../../service/providers/channel.service';
 import { ChannelService } from '../../service/providers/channel.service';
 
 
+import { extractAuthToken } from './extract-auth-token';
 import { RequestContext } from './request-context';
 import { RequestContext } from './request-context';
 
 
+export const REQUEST_CONTEXT_KEY = 'vendureRequestContext';
+
 /**
 /**
  * Creates new RequestContext instances.
  * Creates new RequestContext instances.
  */
  */
 @Injectable()
 @Injectable()
 export class RequestContextService {
 export class RequestContextService {
-    constructor(private channelService: ChannelService, private configService: ConfigService) {}
+    constructor(
+        private channelService: ChannelService,
+        private authService: AuthService,
+        private configService: ConfigService,
+    ) {}
 
 
     /**
     /**
      * Creates a new RequestContext based on an Express request object.
      * Creates a new RequestContext based on an Express request object.
      */
      */
-    fromRequest(req: Request & { user?: User }): RequestContext {
+    async fromRequest(req: Request): Promise<RequestContext> {
+        const channelToken = this.getChannelToken(req);
+        const channel = channelToken && this.channelService.getChannelFromToken(channelToken);
+        if (channel) {
+            const session = await this.getSession(req);
+            let user: User | undefined;
+            if (this.isAuthenticatedSession(session)) {
+                user = session.user;
+            }
+            const languageCode = this.getLanguageCode(req);
+            return new RequestContext({
+                channel,
+                languageCode,
+                user,
+                session,
+            });
+        }
+        throw new I18nError(`error.unexpected-request-context`);
+    }
+
+    private getChannelToken(req: Request): string | undefined {
         const tokenKey = this.configService.channelTokenKey;
         const tokenKey = this.configService.channelTokenKey;
-        let channelTokenKey = '';
+        let channelToken: string | undefined;
+
         if (req && req.query && req.query[tokenKey]) {
         if (req && req.query && req.query[tokenKey]) {
-            channelTokenKey = req.query[tokenKey];
+            channelToken = req.query[tokenKey];
         } else if (req && req.headers && req.headers[tokenKey]) {
         } else if (req && req.headers && req.headers[tokenKey]) {
-            channelTokenKey = req.headers[tokenKey] as string;
-        } else if (req && req.user) {
-            channelTokenKey = req.user.roles[0].channels[0].token;
+            channelToken = req.headers[tokenKey] as string;
         }
         }
-        if (channelTokenKey) {
-            const channel = this.channelService.getChannelFromToken(channelTokenKey);
-            return new RequestContext(channel);
+        return channelToken;
+    }
+
+    private async getSession(req: Request): Promise<Session | undefined> {
+        const authToken = extractAuthToken(req, this.configService.authOptions.tokenMethod);
+        if (authToken) {
+            return await this.authService.validateSession(authToken);
         }
         }
-        throw new I18nError(`error.unexpected-request-context`);
+    }
+
+    private getLanguageCode(req: Request): LanguageCode | undefined {
+        return req.body && req.body.variables && req.body.variables.languageCode;
+    }
+
+    private isAuthenticatedSession(session?: Session): session is AuthenticatedSession {
+        return !!session && !!(session as AuthenticatedSession).user;
     }
     }
 }
 }

+ 20 - 17
server/src/api/common/request-context.ts

@@ -3,38 +3,41 @@ import { ID } from 'shared/shared-types';
 
 
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
+import { Session } from '../../entity/session/session.entity';
+import { User } from '../../entity/user/user.entity';
 
 
 /**
 /**
  * The RequestContext is intended to hold information relevant to the current request, which may be
  * The RequestContext is intended to hold information relevant to the current request, which may be
- * required at various points of the stack. Primarily, the current token and active Channel id is
- * exposed, as well as the active language.
+ * required at various points of the stack.
  */
  */
 export class RequestContext {
 export class RequestContext {
     get channel(): Channel {
     get channel(): Channel {
-        return this._channel || ({} as any);
+        return this._channel;
     }
     }
 
 
     get channelId(): ID | undefined {
     get channelId(): ID | undefined {
-        return this._channel && this._channel.id;
+        return this._channel.id;
     }
     }
 
 
     get languageCode(): LanguageCode {
     get languageCode(): LanguageCode {
-        if (this._languageCode) {
-            return this._languageCode;
-        } else if (this._channel) {
-            return this._channel.defaultLanguageCode;
-        } else {
-            return DEFAULT_LANGUAGE_CODE;
-        }
+        return this._languageCode;
     }
     }
 
 
-    private _languageCode: LanguageCode;
+    get user(): User | undefined {
+        return this._user;
+    }
 
 
-    constructor(private _channel?: Channel) {}
+    private readonly _languageCode: LanguageCode;
+    private readonly _channel: Channel;
+    private readonly _session?: Session;
+    private readonly _user?: User;
 
 
-    setLanguageCode(value: LanguageCode | null | undefined) {
-        if (value) {
-            this._languageCode = value;
-        }
+    constructor(options: { channel: Channel; user?: User; session?: Session; languageCode?: LanguageCode }) {
+        const { channel, session, languageCode, user } = options;
+        this._channel = channel;
+        this._session = session;
+        this._user = user;
+        this._languageCode =
+            languageCode || (channel && channel.defaultLanguageCode) || DEFAULT_LANGUAGE_CODE;
     }
     }
 }
 }

+ 0 - 61
server/src/api/common/roles-guard.ts

@@ -1,61 +0,0 @@
-import { ExecutionContext, Injectable, ReflectMetadata } from '@nestjs/common';
-import { Reflector } from '@nestjs/core';
-import { Permission } from 'shared/generated-types';
-
-import { idsAreEqual } from '../../common/utils';
-import { ConfigService } from '../../config/config.service';
-import { User } from '../../entity/user/user.entity';
-
-import { RequestContextService } from './request-context.service';
-
-export const PERMISSIONS_METADATA_KEY = '__permissions__';
-
-/**
- * Attatches metadata to the resolver defining which permissions are required to execute the
- * operation.
- *
- * @example
- * ```
- *  @Allow(Permission.SuperAdmin)
- *  @Query()
- *  getAdministrators() {
- *      // ...
- *  }
- * ```
- */
-export const Allow = (...permissions: Permission[]) => ReflectMetadata(PERMISSIONS_METADATA_KEY, permissions);
-
-@Injectable()
-export class RolesGuard {
-    constructor(
-        private readonly reflector: Reflector,
-        private requestContextService: RequestContextService,
-        private configService: ConfigService,
-    ) {}
-
-    canActivate(context: ExecutionContext): boolean {
-        const permissions = this.reflector.get<string[]>(PERMISSIONS_METADATA_KEY, context.getHandler());
-        if (this.configService.authOptions.disableAuth || !permissions) {
-            return true;
-        }
-        const req = context.getArgByIndex(2).req;
-        const ctx = this.requestContextService.fromRequest(req);
-        const user: User = req.user;
-        if (!user) {
-            return false;
-        }
-        const permissionsOnChannel = user.roles
-            .filter(role => role.channels.find(c => idsAreEqual(c.id, ctx.channel.id)))
-            .reduce((output, role) => [...output, ...role.permissions], [] as Permission[]);
-        return arraysIntersect(permissions, permissionsOnChannel);
-    }
-}
-
-/**
- * Returns true if any element of arr1 appears in arr2.
- */
-function arraysIntersect<T>(arr1: T[], arr2: T[]): boolean {
-    return arr1.reduce((intersects, role) => {
-        return intersects || arr2.includes(role);
-    }, false);
-}

+ 1 - 1
server/src/api/resolvers/administrator.resolver.ts

@@ -11,8 +11,8 @@ import { PaginatedList } from 'shared/shared-types';
 
 
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { Administrator } from '../../entity/administrator/administrator.entity';
 import { AdministratorService } from '../../service/providers/administrator.service';
 import { AdministratorService } from '../../service/providers/administrator.service';
+import { Allow } from '../common/auth-guard';
 import { Decode } from '../common/id-interceptor';
 import { Decode } from '../common/id-interceptor';
-import { Allow } from '../common/roles-guard';
 
 
 @Resolver('Administrator')
 @Resolver('Administrator')
 export class AdministratorResolver {
 export class AdministratorResolver {

+ 1 - 2
server/src/api/resolvers/asset.resolver.ts

@@ -4,8 +4,7 @@ import { PaginatedList } from 'shared/shared-types';
 
 
 import { Asset } from '../../entity/asset/asset.entity';
 import { Asset } from '../../entity/asset/asset.entity';
 import { AssetService } from '../../service/providers/asset.service';
 import { AssetService } from '../../service/providers/asset.service';
-import { RequestContextPipe } from '../common/request-context.pipe';
-import { Allow } from '../common/roles-guard';
+import { Allow } from '../common/auth-guard';
 
 
 @Resolver('Assets')
 @Resolver('Assets')
 export class AssetResolver {
 export class AssetResolver {

+ 1 - 1
server/src/api/resolvers/auth.resolver.ts

@@ -7,8 +7,8 @@ import { ConfigService } from '../../config/config.service';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
 import { AuthService } from '../../service/providers/auth.service';
 import { AuthService } from '../../service/providers/auth.service';
 import { ChannelService } from '../../service/providers/channel.service';
 import { ChannelService } from '../../service/providers/channel.service';
+import { Allow } from '../common/auth-guard';
 import { extractAuthToken } from '../common/extract-auth-token';
 import { extractAuthToken } from '../common/extract-auth-token';
-import { Allow } from '../common/roles-guard';
 
 
 @Resolver('Auth')
 @Resolver('Auth')
 export class AuthResolver {
 export class AuthResolver {

+ 1 - 1
server/src/api/resolvers/channel.resolver.ts

@@ -3,7 +3,7 @@ import { Permission } from 'shared/generated-types';
 
 
 import { Channel } from '../../entity/channel/channel.entity';
 import { Channel } from '../../entity/channel/channel.entity';
 import { ChannelService } from '../../service/providers/channel.service';
 import { ChannelService } from '../../service/providers/channel.service';
-import { Allow } from '../common/roles-guard';
+import { Allow } from '../common/auth-guard';
 
 
 @Resolver('Channel')
 @Resolver('Channel')
 export class ChannelResolver {
 export class ChannelResolver {

+ 1 - 1
server/src/api/resolvers/customer.resolver.ts

@@ -5,8 +5,8 @@ import { PaginatedList } from 'shared/shared-types';
 import { Address } from '../../entity/address/address.entity';
 import { Address } from '../../entity/address/address.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { Customer } from '../../entity/customer/customer.entity';
 import { CustomerService } from '../../service/providers/customer.service';
 import { CustomerService } from '../../service/providers/customer.service';
+import { Allow } from '../common/auth-guard';
 import { Decode } from '../common/id-interceptor';
 import { Decode } from '../common/id-interceptor';
-import { Allow } from '../common/roles-guard';
 
 
 @Resolver('Customer')
 @Resolver('Customer')
 export class CustomerResolver {
 export class CustomerResolver {

+ 1 - 1
server/src/api/resolvers/facet.resolver.ts

@@ -15,7 +15,7 @@ import { Facet } from '../../entity/facet/facet.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/providers/facet-value.service';
 import { FacetValueService } from '../../service/providers/facet-value.service';
 import { FacetService } from '../../service/providers/facet.service';
 import { FacetService } from '../../service/providers/facet.service';
-import { Allow } from '../common/roles-guard';
+import { Allow } from '../common/auth-guard';
 
 
 @Resolver('Facet')
 @Resolver('Facet')
 export class FacetResolver {
 export class FacetResolver {

+ 1 - 1
server/src/api/resolvers/product-option.resolver.ts

@@ -6,7 +6,7 @@ import { ProductOptionGroup } from '../../entity/product-option-group/product-op
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductOptionGroupService } from '../../service/providers/product-option-group.service';
 import { ProductOptionGroupService } from '../../service/providers/product-option-group.service';
 import { ProductOptionService } from '../../service/providers/product-option.service';
 import { ProductOptionService } from '../../service/providers/product-option.service';
-import { Allow } from '../common/roles-guard';
+import { Allow } from '../common/auth-guard';
 
 
 @Resolver('ProductOptionGroup')
 @Resolver('ProductOptionGroup')
 export class ProductOptionResolver {
 export class ProductOptionResolver {

+ 12 - 14
server/src/api/resolvers/product.resolver.ts

@@ -1,4 +1,4 @@
-import { Args, Context, Mutation, Query, Resolver } from '@nestjs/graphql';
+import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
 import {
 import {
     AddOptionGroupToProductVariables,
     AddOptionGroupToProductVariables,
     ApplyFacetValuesToProductVariantsVariables,
     ApplyFacetValuesToProductVariantsVariables,
@@ -22,10 +22,10 @@ import { I18nError } from '../../i18n/i18n-error';
 import { FacetValueService } from '../../service/providers/facet-value.service';
 import { FacetValueService } from '../../service/providers/facet-value.service';
 import { ProductVariantService } from '../../service/providers/product-variant.service';
 import { ProductVariantService } from '../../service/providers/product-variant.service';
 import { ProductService } from '../../service/providers/product.service';
 import { ProductService } from '../../service/providers/product.service';
+import { Allow } from '../common/auth-guard';
 import { Decode } from '../common/id-interceptor';
 import { Decode } from '../common/id-interceptor';
 import { RequestContext } from '../common/request-context';
 import { RequestContext } from '../common/request-context';
-import { RequestContextPipe } from '../common/request-context.pipe';
-import { Allow } from '../common/roles-guard';
+import { Ctx } from '../common/request-context.decorator';
 
 
 @Resolver('Product')
 @Resolver('Product')
 export class ProductResolver {
 export class ProductResolver {
@@ -38,20 +38,18 @@ export class ProductResolver {
     @Query()
     @Query()
     @Allow(Permission.ReadCatalog)
     @Allow(Permission.ReadCatalog)
     async products(
     async products(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: GetProductListVariables,
         @Args() args: GetProductListVariables,
     ): Promise<PaginatedList<Translated<Product>>> {
     ): Promise<PaginatedList<Translated<Product>>> {
-        ctx.setLanguageCode(args.languageCode);
         return this.productService.findAll(ctx, args.options || undefined);
         return this.productService.findAll(ctx, args.options || undefined);
     }
     }
 
 
     @Query()
     @Query()
     @Allow(Permission.ReadCatalog)
     @Allow(Permission.ReadCatalog)
     async product(
     async product(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: GetProductWithVariantsVariables,
         @Args() args: GetProductWithVariantsVariables,
     ): Promise<Translated<Product> | undefined> {
     ): Promise<Translated<Product> | undefined> {
-        ctx.setLanguageCode(args.languageCode);
         return this.productService.findOne(ctx, args.id);
         return this.productService.findOne(ctx, args.id);
     }
     }
 
 
@@ -59,7 +57,7 @@ export class ProductResolver {
     @Allow(Permission.CreateCatalog)
     @Allow(Permission.CreateCatalog)
     @Decode('assetIds', 'featuredAssetId')
     @Decode('assetIds', 'featuredAssetId')
     async createProduct(
     async createProduct(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: CreateProductVariables,
         @Args() args: CreateProductVariables,
     ): Promise<Translated<Product>> {
     ): Promise<Translated<Product>> {
         const { input } = args;
         const { input } = args;
@@ -70,7 +68,7 @@ export class ProductResolver {
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
     @Decode('assetIds', 'featuredAssetId')
     @Decode('assetIds', 'featuredAssetId')
     async updateProduct(
     async updateProduct(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: UpdateProductVariables,
         @Args() args: UpdateProductVariables,
     ): Promise<Translated<Product>> {
     ): Promise<Translated<Product>> {
         const { input } = args;
         const { input } = args;
@@ -81,7 +79,7 @@ export class ProductResolver {
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
     @Decode('productId', 'optionGroupId')
     @Decode('productId', 'optionGroupId')
     async addOptionGroupToProduct(
     async addOptionGroupToProduct(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: AddOptionGroupToProductVariables,
         @Args() args: AddOptionGroupToProductVariables,
     ): Promise<Translated<Product>> {
     ): Promise<Translated<Product>> {
         const { productId, optionGroupId } = args;
         const { productId, optionGroupId } = args;
@@ -92,7 +90,7 @@ export class ProductResolver {
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
     @Decode('productId', 'optionGroupId')
     @Decode('productId', 'optionGroupId')
     async removeOptionGroupFromProduct(
     async removeOptionGroupFromProduct(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: RemoveOptionGroupFromProductVariables,
         @Args() args: RemoveOptionGroupFromProductVariables,
     ): Promise<Translated<Product>> {
     ): Promise<Translated<Product>> {
         const { productId, optionGroupId } = args;
         const { productId, optionGroupId } = args;
@@ -103,7 +101,7 @@ export class ProductResolver {
     @Allow(Permission.CreateCatalog)
     @Allow(Permission.CreateCatalog)
     @Decode('productId')
     @Decode('productId')
     async generateVariantsForProduct(
     async generateVariantsForProduct(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: GenerateProductVariantsVariables,
         @Args() args: GenerateProductVariantsVariables,
     ): Promise<Translated<Product>> {
     ): Promise<Translated<Product>> {
         const { productId, defaultPrice, defaultSku } = args;
         const { productId, defaultPrice, defaultSku } = args;
@@ -114,7 +112,7 @@ export class ProductResolver {
     @Mutation()
     @Mutation()
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
     async updateProductVariants(
     async updateProductVariants(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: UpdateProductVariantsVariables,
         @Args() args: UpdateProductVariantsVariables,
     ): Promise<Array<Translated<ProductVariant>>> {
     ): Promise<Array<Translated<ProductVariant>>> {
         const { input } = args;
         const { input } = args;
@@ -125,7 +123,7 @@ export class ProductResolver {
     @Allow(Permission.UpdateCatalog)
     @Allow(Permission.UpdateCatalog)
     @Decode('facetValueIds', 'productVariantIds')
     @Decode('facetValueIds', 'productVariantIds')
     async applyFacetValuesToProductVariants(
     async applyFacetValuesToProductVariants(
-        @Context(RequestContextPipe) ctx: RequestContext,
+        @Ctx() ctx: RequestContext,
         @Args() args: ApplyFacetValuesToProductVariantsVariables,
         @Args() args: ApplyFacetValuesToProductVariantsVariables,
     ): Promise<Array<Translated<ProductVariant>>> {
     ): Promise<Array<Translated<ProductVariant>>> {
         const { facetValueIds, productVariantIds } = args;
         const { facetValueIds, productVariantIds } = args;

+ 1 - 1
server/src/api/resolvers/role.resolver.ts

@@ -10,7 +10,7 @@ import { PaginatedList } from 'shared/shared-types';
 
 
 import { Role } from '../../entity/role/role.entity';
 import { Role } from '../../entity/role/role.entity';
 import { RoleService } from '../../service/providers/role.service';
 import { RoleService } from '../../service/providers/role.service';
-import { Allow } from '../common/roles-guard';
+import { Allow } from '../common/auth-guard';
 
 
 @Resolver('Roles')
 @Resolver('Roles')
 export class RoleResolver {
 export class RoleResolver {

+ 4 - 0
server/src/entity/entities.ts

@@ -23,6 +23,8 @@ import { ProductVariant } from './product-variant/product-variant.entity';
 import { ProductTranslation } from './product/product-translation.entity';
 import { ProductTranslation } from './product/product-translation.entity';
 import { Product } from './product/product.entity';
 import { Product } from './product/product.entity';
 import { Role } from './role/role.entity';
 import { Role } from './role/role.entity';
+import { AnonymousSession } from './session/anonymous-session.entity';
+import { AuthenticatedSession } from './session/authenticated-session.entity';
 import { Session } from './session/session.entity';
 import { Session } from './session/session.entity';
 import { User } from './user/user.entity';
 import { User } from './user/user.entity';
 
 
@@ -34,7 +36,9 @@ export const coreEntitiesMap = {
     Adjustment,
     Adjustment,
     AdjustmentSource,
     AdjustmentSource,
     Administrator,
     Administrator,
+    AnonymousSession,
     Asset,
     Asset,
+    AuthenticatedSession,
     Channel,
     Channel,
     Customer,
     Customer,
     Facet,
     Facet,

+ 16 - 0
server/src/entity/session/anonymous-session.entity.ts

@@ -0,0 +1,16 @@
+import { DeepPartial } from 'shared/shared-types';
+import { ChildEntity, ManyToOne } from 'typeorm';
+
+import { Order } from '../order/order.entity';
+
+import { Session } from './session.entity';
+
+@ChildEntity()
+export class AnonymousSession extends Session {
+    constructor(input: DeepPartial<AnonymousSession>) {
+        super(input);
+    }
+
+    @ManyToOne(type => Order)
+    order: Order;
+}

+ 16 - 0
server/src/entity/session/authenticated-session.entity.ts

@@ -0,0 +1,16 @@
+import { DeepPartial } from 'shared/shared-types';
+import { ChildEntity, Column, Index, ManyToOne } from 'typeorm';
+
+import { User } from '../user/user.entity';
+
+import { Session } from './session.entity';
+
+@ChildEntity()
+export class AuthenticatedSession extends Session {
+    constructor(input: DeepPartial<AuthenticatedSession>) {
+        super(input);
+    }
+
+    @ManyToOne(type => User)
+    user: User;
+}

+ 4 - 13
server/src/entity/session/session.entity.ts

@@ -1,26 +1,17 @@
 import { DeepPartial } from 'shared/shared-types';
 import { DeepPartial } from 'shared/shared-types';
-import { Column, Entity, Index, ManyToOne } from 'typeorm';
+import { Column, Entity, Index, ManyToOne, TableInheritance } from 'typeorm';
 
 
 import { VendureEntity } from '../base/base.entity';
 import { VendureEntity } from '../base/base.entity';
-import { Order } from '../order/order.entity';
+import { Customer } from '../customer/customer.entity';
 import { User } from '../user/user.entity';
 import { User } from '../user/user.entity';
 
 
 @Entity()
 @Entity()
-export class Session extends VendureEntity {
-    constructor(input: DeepPartial<Session>) {
-        super(input);
-    }
-
+@TableInheritance({ column: { type: 'varchar', name: 'type' } })
+export abstract class Session extends VendureEntity {
     @Index({ unique: true })
     @Index({ unique: true })
     @Column()
     @Column()
     token: string;
     token: string;
 
 
-    @ManyToOne(type => User)
-    user?: User;
-
-    @ManyToOne(type => Order)
-    activeOrder?: Order;
-
     @Column() expires: Date;
     @Column() expires: Date;
 
 
     @Column() invalidated: boolean;
     @Column() invalidated: boolean;

+ 6 - 5
server/src/service/providers/auth.service.ts

@@ -6,6 +6,7 @@ import { ID } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 import { Connection } from 'typeorm';
 
 
 import { ConfigService } from '../../config/config.service';
 import { ConfigService } from '../../config/config.service';
+import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { Session } from '../../entity/session/session.entity';
 import { User } from '../../entity/user/user.entity';
 import { User } from '../../entity/user/user.entity';
 
 
@@ -26,14 +27,14 @@ export class AuthService {
     /**
     /**
      * Authenticates a user's credentials and if okay, creates a new session.
      * Authenticates a user's credentials and if okay, creates a new session.
      */
      */
-    async authenticate(identifier: string, password: string): Promise<Session> {
+    async authenticate(identifier: string, password: string): Promise<AuthenticatedSession> {
         const user = await this.getUserFromIdentifier(identifier);
         const user = await this.getUserFromIdentifier(identifier);
         const passwordMatches = await this.passwordService.check(password, user.passwordHash);
         const passwordMatches = await this.passwordService.check(password, user.passwordHash);
         if (!passwordMatches) {
         if (!passwordMatches) {
             throw new UnauthorizedException();
             throw new UnauthorizedException();
         }
         }
         const token = await this.generateSessionToken();
         const token = await this.generateSessionToken();
-        const session = new Session({
+        const session = new AuthenticatedSession({
             token,
             token,
             user,
             user,
             expires: this.getExpiryDate(),
             expires: this.getExpiryDate(),
@@ -41,7 +42,7 @@ export class AuthService {
         });
         });
         await this.invalidateUserSessions(user);
         await this.invalidateUserSessions(user);
         // save the new session
         // save the new session
-        const newSession = this.connection.getRepository(Session).save(session);
+        const newSession = await this.connection.getRepository(Session).save(session);
         return newSession;
         return newSession;
     }
     }
 
 
@@ -63,14 +64,14 @@ export class AuthService {
      * Invalidates all existing sessions for the given user.
      * Invalidates all existing sessions for the given user.
      */
      */
     async invalidateUserSessions(user: User): Promise<void> {
     async invalidateUserSessions(user: User): Promise<void> {
-        await this.connection.getRepository(Session).update({ user }, { invalidated: true });
+        await this.connection.getRepository(AuthenticatedSession).update({ user }, { invalidated: true });
     }
     }
 
 
     /**
     /**
      * Invalidates all sessions for the user associated with the given session token.
      * Invalidates all sessions for the user associated with the given session token.
      */
      */
     async invalidateSessionByToken(token: string): Promise<void> {
     async invalidateSessionByToken(token: string): Promise<void> {
-        const session = await this.connection.getRepository(Session).findOne({
+        const session = await this.connection.getRepository(AuthenticatedSession).findOne({
             where: { token },
             where: { token },
             relations: ['user'],
             relations: ['user'],
         });
         });