Browse Source

feat(core): Access to orderByCode configurable by strategy

Wanztwurst 4 years ago
parent
commit
2554822182

+ 40 - 0
packages/core/e2e/shop-order.e2e-spec.ts

@@ -1312,6 +1312,46 @@ describe('Shop orders', () => {
                     }, `You are not currently authorized to perform this action`),
                 );
             });
+
+            describe('3 hours after the Order has been placed', () => {
+                let dateNowMock: any;
+                beforeAll(() => {
+                    // mock Date.now: add 3 hours
+                    const nowIn3H = Date.now() + 3 * 3600 * 1000;
+                    dateNowMock = jest.spyOn(global.Date, 'now').mockImplementation(() => nowIn3H);
+                });
+
+                it('still works when authenticated as owner', async () => {
+                    authenticatedUserEmailAddress = customers[0].emailAddress;
+                    await shopClient.asUserWithCredentials(authenticatedUserEmailAddress, password);
+                    const result = await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
+                        GET_ORDER_BY_CODE,
+                        {
+                            code: activeOrder.code,
+                        },
+                    );
+
+                    expect(result.orderByCode!.id).toBe(activeOrder.id);
+                });
+
+                it(
+                    'access denied when anonymous',
+                    assertThrowsWithMessage(async () => {
+                        await shopClient.asAnonymousUser();
+                        await shopClient.query<GetOrderByCode.Query, GetOrderByCode.Variables>(
+                            GET_ORDER_BY_CODE,
+                            {
+                                code: activeOrder.code,
+                            },
+                        );
+                    }, `You are not currently authorized to perform this action`),
+                );
+
+                afterAll(() => {
+                    // restore Date.now
+                    dateNowMock.mockRestore();
+                });
+            });
         });
     });
 

+ 7 - 19
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -36,6 +36,7 @@ import {
 } from '../../../common/error/generated-graphql-shop-errors';
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
+import { ConfigService } from '../../../config';
 import { Country } from '../../../entity';
 import { Order } from '../../../entity/order/order.entity';
 import { ActiveOrderService, CountryService } from '../../../service';
@@ -56,6 +57,7 @@ export class ShopOrderResolver {
         private sessionService: SessionService,
         private countryService: CountryService,
         private activeOrderService: ActiveOrderService,
+        private configService: ConfigService,
     ) {}
 
     @Query()
@@ -102,25 +104,11 @@ export class ShopOrderResolver {
         if (ctx.authorizedAsOwnerOnly) {
             const order = await this.orderService.findOneByCode(ctx, args.code);
 
-            if (order) {
-                // For guest Customers, allow access to the Order for the following
-                // time period
-                const anonymousAccessLimit = ms('2h');
-                const orderPlaced = order.orderPlacedAt ? +order.orderPlacedAt : 0;
-                const activeUserMatches = !!(
-                    order &&
-                    order.customer &&
-                    order.customer.user &&
-                    order.customer.user.id === ctx.activeUserId
-                );
-                const now = +new Date();
-                const isWithinAnonymousAccessLimit = now - orderPlaced < anonymousAccessLimit;
-                if (
-                    (ctx.activeUserId && activeUserMatches) ||
-                    (!ctx.activeUserId && isWithinAnonymousAccessLimit)
-                ) {
-                    return this.orderService.findOne(ctx, order.id);
-                }
+            if (
+                order &&
+                (await this.configService.orderOptions.orderByCodeAccessStrategy.canAccessOrder(ctx, order))
+            ) {
+                return order;
             }
             // We throw even if the order does not exist, since giving a different response
             // opens the door to an enumeration attack to find valid order codes.

+ 2 - 0
packages/core/src/config/config.module.ts

@@ -77,6 +77,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             orderItemPriceCalculationStrategy,
             process,
             orderCodeStrategy,
+            orderByCodeAccessStrategy,
             stockAllocationStrategy,
         } = this.configService.orderOptions;
         const { customFulfillmentProcess } = this.configService.shippingOptions;
@@ -94,6 +95,7 @@ export class ConfigModule implements OnApplicationBootstrap, OnApplicationShutdo
             mergeStrategy,
             checkoutMergeStrategy,
             orderCodeStrategy,
+            orderByCodeAccessStrategy,
             entityIdStrategy,
             productVariantPriceCalculationStrategy,
             orderItemPriceCalculationStrategy,

+ 2 - 0
packages/core/src/config/default-config.ts

@@ -22,6 +22,7 @@ import { DefaultOrderItemPriceCalculationStrategy } from './order/default-order-
 import { DefaultOrderPlacedStrategy } from './order/default-order-placed-strategy';
 import { DefaultStockAllocationStrategy } from './order/default-stock-allocation-strategy';
 import { MergeOrdersStrategy } from './order/merge-orders-strategy';
+import { DefaultOrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { DefaultOrderCodeStrategy } from './order/order-code-strategy';
 import { UseGuestStrategy } from './order/use-guest-strategy';
 import { defaultPromotionActions, defaultPromotionConditions } from './promotion';
@@ -120,6 +121,7 @@ export const defaultConfig: RuntimeVendureConfig = {
         process: [],
         stockAllocationStrategy: new DefaultStockAllocationStrategy(),
         orderCodeStrategy: new DefaultOrderCodeStrategy(),
+        orderByCodeAccessStrategy: new DefaultOrderByCodeAccessStrategy('2h'),
         changedPriceHandlingStrategy: new DefaultChangedPriceHandlingStrategy(),
         orderPlacedStrategy: new DefaultOrderPlacedStrategy(),
     },

+ 71 - 0
packages/core/src/config/order/order-by-code-access-strategy.ts

@@ -0,0 +1,71 @@
+import ms from 'ms';
+
+import { RequestContext } from '../../api/common/request-context';
+import { InjectableStrategy } from '../../common/types/injectable-strategy';
+import { Order } from '../../entity/order/order.entity';
+
+/**
+ * @description
+ * The OrderByCodeAccessStrategy determines how access to a placed Order via the
+ * orderByCode query is granted.
+ * With a custom strategy anonymous access could be made permanent or tied to specific
+ * conditions like IP range or an Order status.
+ *
+ * @example
+ * This example grants access to the requested Order to anyone – unless it's Monday.
+ * ```TypeScript
+ * export class NotMondayOrderByCodeAccessStrategy implements OrderByCodeAccessStrategy {
+ *     canAccessOrder(ctx: RequestContext, order: Order): boolean {
+ *         const MONDAY = 1;
+ *         const today = (new Date()).getDay();
+ *
+ *         return today !== MONDAY;
+ *     }
+ * }
+ * ```
+ *
+ * @docsCategory orders
+ * @docsPage OrderByCodeAccessStrategy
+ */
+export interface OrderByCodeAccessStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * Gives or denies permission to access the requested Order
+     */
+    canAccessOrder(ctx: RequestContext, order: Order): boolean | Promise<boolean>;
+}
+
+/**
+ * @description
+ * The default OrderByCodeAccessStrategy used by Vendure. It permitts permanent access to
+ * the Customer owning the Order and anyone within a given time period after placing the Order
+ * (defaults to 2h).
+ *
+ * @param anonymousAccessDuration value for [ms](https://github.com/vercel/ms), e.g. `2h` for 2 hours or `5d` for 5 days
+ *
+ * @docsCategory orders
+ * @docsPage OrderByCodeAccessStrategy
+ */
+export class DefaultOrderByCodeAccessStrategy implements OrderByCodeAccessStrategy {
+    private anonymousAccessDuration;
+
+    constructor(anonymousAccessDuration: string) {
+        this.anonymousAccessDuration = anonymousAccessDuration;
+    }
+
+    canAccessOrder(ctx: RequestContext, order: Order): boolean {
+        // Order owned by active user
+        const activeUserMatches = order?.customer?.user?.id === ctx.activeUserId;
+
+        // For guest Customers, allow access to the Order for the following
+        // time period
+        const anonymousAccessPermitted = () => {
+            const anonymousAccessLimit = ms(this.anonymousAccessDuration);
+            const orderPlaced = order.orderPlacedAt ? +order.orderPlacedAt : 0;
+            const now = Date.now();
+            return now - orderPlaced < anonymousAccessLimit;
+        };
+
+        return (ctx.activeUserId && activeUserMatches) || (!ctx.activeUserId && anonymousAccessPermitted());
+    }
+}

+ 11 - 0
packages/core/src/config/vendure-config.ts

@@ -23,6 +23,7 @@ import { JobQueueStrategy } from './job-queue/job-queue-strategy';
 import { VendureLogger } from './logger/vendure-logger';
 import { ChangedPriceHandlingStrategy } from './order/changed-price-handling-strategy';
 import { CustomOrderProcess } from './order/custom-order-process';
+import { OrderByCodeAccessStrategy } from './order/order-by-code-access-strategy';
 import { OrderCodeStrategy } from './order/order-code-strategy';
 import { OrderItemPriceCalculationStrategy } from './order/order-item-price-calculation-strategy';
 import { OrderMergeStrategy } from './order/order-merge-strategy';
@@ -471,6 +472,16 @@ export interface OrderOptions {
      * @default DefaultOrderCodeStrategy
      */
     orderCodeStrategy?: OrderCodeStrategy;
+    /**
+     * @description
+     * Defines the strategy used to check if and how an Order may be retrieved via the orderByCode query.
+     *
+     * The default strategy permitts permanent access to the Customer owning the Order and anyone
+     * within 2 hours after placing the Order.
+     *
+     * @default DefaultOrderByCodeAccessStrategy
+     */
+    orderByCodeAccessStrategy?: OrderByCodeAccessStrategy;
     /**
      * @description
      * Defines how we handle the situation where an OrderItem exists in an Order, and