ソースを参照

feat(core): Add NotVerifiedError to AuthenticationResult

Closes #500
Michael Bromley 5 年 前
コミット
ee39263e99

+ 2 - 1
packages/admin-ui/src/lib/core/src/common/generated-types.ts

@@ -6205,7 +6205,7 @@ export type UpdatePaymentMethodMutation = { updatePaymentMethod: (
 
 export type GlobalSettingsFragment = (
   { __typename?: 'GlobalSettings' }
-  & Pick<GlobalSettings, 'availableLanguages' | 'trackInventory'>
+  & Pick<GlobalSettings, 'id' | 'availableLanguages' | 'trackInventory'>
 );
 
 export type GetGlobalSettingsQueryVariables = Exact<{ [key: string]: never; }>;
@@ -6383,6 +6383,7 @@ export type GetServerConfigQueryVariables = Exact<{ [key: string]: never; }>;
 
 export type GetServerConfigQuery = { globalSettings: (
     { __typename?: 'GlobalSettings' }
+    & Pick<GlobalSettings, 'id'>
     & { serverConfig: (
       { __typename?: 'ServerConfig' }
       & Pick<ServerConfig, 'permittedAssetTypes'>

+ 14 - 24
packages/common/src/generated-shop-types.ts

@@ -399,6 +399,7 @@ export enum ErrorCode {
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
     PASSWORD_RESET_TOKEN_EXPIRED_ERROR = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR',
+    NOT_VERIFIED_ERROR = 'NOT_VERIFIED_ERROR',
 }
 
 export type ErrorResult = {
@@ -456,8 +457,6 @@ export type SearchInput = {
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;
     sort?: Maybe<SearchResultSortParameter>;
-    priceRange?: Maybe<PriceRangeInput>;
-    priceRangeWithTax?: Maybe<PriceRangeInput>;
 };
 
 export type SearchResultSortParameter = {
@@ -1528,6 +1527,13 @@ export type PasswordResetTokenExpiredError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned if attempting to authenticate before the email address has been verfified */
+export type NotVerifiedError = ErrorResult & {
+    __typename?: 'NotVerifiedError';
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError;
 
 export type RemoveOrderItemsResult = Order | OrderModificationError;
@@ -1585,9 +1591,13 @@ export type ResetPasswordResult =
     | PasswordResetTokenExpiredError
     | NativeAuthStrategyError;
 
-export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+export type NativeAuthenticationResult =
+    | CurrentUser
+    | InvalidCredentialsError
+    | NotVerifiedError
+    | NativeAuthStrategyError;
 
-export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError | NotVerifiedError;
 
 export type Address = Node & {
     __typename?: 'Address';
@@ -2142,7 +2152,6 @@ export type SearchResponse = {
     items: Array<SearchResult>;
     totalItems: Scalars['Int'];
     facetValues: Array<FacetValueResult>;
-    prices: SearchResponsePriceData;
 };
 
 /**
@@ -2457,25 +2466,6 @@ export type Zone = Node & {
     members: Array<Country>;
 };
 
-export type SearchResponsePriceData = {
-    __typename?: 'SearchResponsePriceData';
-    range: PriceRange;
-    rangeWithTax: PriceRange;
-    buckets: Array<PriceRangeBucket>;
-    bucketsWithTax: Array<PriceRangeBucket>;
-};
-
-export type PriceRangeBucket = {
-    __typename?: 'PriceRangeBucket';
-    to: Scalars['Int'];
-    count: Scalars['Int'];
-};
-
-export type PriceRangeInput = {
-    min: Scalars['Int'];
-    max: Scalars['Int'];
-};
-
 export type CollectionListOptions = {
     skip?: Maybe<Scalars['Int']>;
     take?: Maybe<Scalars['Int']>;

+ 13 - 22
packages/core/e2e/graphql/generated-e2e-shop-types.ts

@@ -391,6 +391,7 @@ export enum ErrorCode {
     IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR = 'IDENTIFIER_CHANGE_TOKEN_EXPIRED_ERROR',
     PASSWORD_RESET_TOKEN_INVALID_ERROR = 'PASSWORD_RESET_TOKEN_INVALID_ERROR',
     PASSWORD_RESET_TOKEN_EXPIRED_ERROR = 'PASSWORD_RESET_TOKEN_EXPIRED_ERROR',
+    NOT_VERIFIED_ERROR = 'NOT_VERIFIED_ERROR',
 }
 
 export type ErrorResult = {
@@ -448,8 +449,6 @@ export type SearchInput = {
     take?: Maybe<Scalars['Int']>;
     skip?: Maybe<Scalars['Int']>;
     sort?: Maybe<SearchResultSortParameter>;
-    priceRange?: Maybe<PriceRangeInput>;
-    priceRangeWithTax?: Maybe<PriceRangeInput>;
 };
 
 export type SearchResultSortParameter = {
@@ -1489,6 +1488,12 @@ export type PasswordResetTokenExpiredError = ErrorResult & {
     message: Scalars['String'];
 };
 
+/** Returned if attempting to authenticate before the email address has been verfified */
+export type NotVerifiedError = ErrorResult & {
+    errorCode: ErrorCode;
+    message: Scalars['String'];
+};
+
 export type UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError;
 
 export type RemoveOrderItemsResult = Order | OrderModificationError;
@@ -1546,9 +1551,13 @@ export type ResetPasswordResult =
     | PasswordResetTokenExpiredError
     | NativeAuthStrategyError;
 
-export type NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError;
+export type NativeAuthenticationResult =
+    | CurrentUser
+    | InvalidCredentialsError
+    | NotVerifiedError
+    | NativeAuthStrategyError;
 
-export type AuthenticationResult = CurrentUser | InvalidCredentialsError;
+export type AuthenticationResult = CurrentUser | InvalidCredentialsError | NotVerifiedError;
 
 export type Address = Node & {
     id: Scalars['ID'];
@@ -2056,7 +2065,6 @@ export type SearchResponse = {
     items: Array<SearchResult>;
     totalItems: Scalars['Int'];
     facetValues: Array<FacetValueResult>;
-    prices: SearchResponsePriceData;
 };
 
 /**
@@ -2344,23 +2352,6 @@ export type Zone = Node & {
     members: Array<Country>;
 };
 
-export type SearchResponsePriceData = {
-    range: PriceRange;
-    rangeWithTax: PriceRange;
-    buckets: Array<PriceRangeBucket>;
-    bucketsWithTax: Array<PriceRangeBucket>;
-};
-
-export type PriceRangeBucket = {
-    to: Scalars['Int'];
-    count: Scalars['Int'];
-};
-
-export type PriceRangeInput = {
-    min: Scalars['Int'];
-    max: Scalars['Int'];
-};
-
 export type CollectionListOptions = {
     skip?: Maybe<Scalars['Int']>;
     take?: Maybe<Scalars['Int']>;

+ 7 - 1
packages/core/e2e/shop-auth.e2e-spec.ts

@@ -18,7 +18,7 @@ import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
-import { TEST_SETUP_TIMEOUT_MS, testConfig } from '../../../e2e-common/test-config';
+import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
 import {
     CreateAdministrator,
@@ -402,6 +402,12 @@ describe('Shop auth & accounts', () => {
             ).toEqual(pick(input, ['firstName', 'lastName', 'emailAddress', 'phoneNumber']));
         });
 
+        it('login fails before verification', async () => {
+            const result = await shopClient.asUserWithCredentials(emailAddress, password);
+            expect(result.errorCode).toBe(ErrorCode.NOT_VERIFIED_ERROR);
+            expect(result.message).toBe('Please verify this email address before logging in');
+        });
+
         it('verification fails with password', async () => {
             const { verifyCustomerAccount } = await shopClient.query<Verify.Mutation, Verify.Variables>(
                 VERIFY_EMAIL,

+ 8 - 0
packages/core/src/api/resolvers/shop/shop-auth.resolver.ts

@@ -157,6 +157,14 @@ export class ShopAuthResolver extends BaseAuthResolver {
             customer.user!,
             NATIVE_AUTH_STRATEGY_NAME,
         );
+        if (isGraphQlErrorResult(session)) {
+            // This code path should never be reached - in this block
+            // the type of `session` is `NotVerifiedError`, however we
+            // just successfully verified the user above. So throw it
+            // so that we have some record of the error if it somehow
+            // occurs.
+            throw session;
+        }
         setSessionToken({
             req,
             res,

+ 11 - 2
packages/core/src/api/schema/shop-api/shop.api.graphql

@@ -318,6 +318,15 @@ type PasswordResetTokenExpiredError implements ErrorResult {
     message: String!
 }
 
+"""
+Returned if `authOptions.requireVerification` is set to `true` (which is the default)
+and an unverified user attempts to authenticate.
+"""
+type NotVerifiedError implements ErrorResult {
+    errorCode: ErrorCode!
+    message: String!
+}
+
 union UpdateOrderItemsResult = Order | OrderModificationError | OrderLimitError | NegativeQuantityError
 union RemoveOrderItemsResult = Order | OrderModificationError
 union SetOrderShippingMethodResult = Order | OrderModificationError
@@ -352,5 +361,5 @@ union UpdateCustomerEmailAddressResult =
     | NativeAuthStrategyError
 union RequestPasswordResetResult = Success | NativeAuthStrategyError
 union ResetPasswordResult = CurrentUser | PasswordResetTokenInvalidError | PasswordResetTokenExpiredError | NativeAuthStrategyError
-union NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NativeAuthStrategyError
-union AuthenticationResult = CurrentUser | InvalidCredentialsError
+union NativeAuthenticationResult = CurrentUser | InvalidCredentialsError | NotVerifiedError | NativeAuthStrategyError
+union AuthenticationResult = CurrentUser | InvalidCredentialsError | NotVerifiedError

+ 0 - 14
packages/core/src/common/error/errors.ts

@@ -97,17 +97,3 @@ export class EntityNotFoundError extends I18nError {
         super('error.entity-with-id-not-found', { entityName, id }, 'ENTITY_NOT_FOUND', LogLevel.Warn);
     }
 }
-
-/**
- * @description
- * This error should be thrown when the `requireVerification` in {@link AuthOptions} is set to
- * `true` and an unverified user attempts to authenticate.
- *
- * @docsCategory errors
- * @docsPage Error Types
- */
-export class NotVerifiedError extends I18nError {
-    constructor() {
-        super('error.email-address-not-verified', {}, 'NOT_VERIFIED', LogLevel.Warn);
-    }
-}

+ 11 - 1
packages/core/src/common/error/generated-graphql-shop-errors.ts

@@ -248,8 +248,18 @@ export class PasswordResetTokenExpiredError extends ErrorResult {
   }
 }
 
+export class NotVerifiedError extends ErrorResult {
+  readonly __typename = 'NotVerifiedError';
+  readonly errorCode = 'NOT_VERIFIED_ERROR' as any;
+  readonly message = 'NOT_VERIFIED_ERROR';
+  constructor(
+  ) {
+    super();
+  }
+}
+
 
-const errorTypeNames = new Set(['NativeAuthStrategyError', 'InvalidCredentialsError', 'OrderStateTransitionError', 'EmailAddressConflictError', 'OrderModificationError', 'OrderLimitError', 'NegativeQuantityError', 'OrderPaymentStateError', 'PaymentFailedError', 'PaymentDeclinedError', 'CouponCodeInvalidError', 'CouponCodeExpiredError', 'CouponCodeLimitError', 'AlreadyLoggedInError', 'MissingPasswordError', 'PasswordAlreadySetError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError']);
+const errorTypeNames = new Set(['NativeAuthStrategyError', 'InvalidCredentialsError', 'OrderStateTransitionError', 'EmailAddressConflictError', 'OrderModificationError', 'OrderLimitError', 'NegativeQuantityError', 'OrderPaymentStateError', 'PaymentFailedError', 'PaymentDeclinedError', 'CouponCodeInvalidError', 'CouponCodeExpiredError', 'CouponCodeLimitError', 'AlreadyLoggedInError', 'MissingPasswordError', 'PasswordAlreadySetError', 'VerificationTokenInvalidError', 'VerificationTokenExpiredError', 'IdentifierChangeTokenInvalidError', 'IdentifierChangeTokenExpiredError', 'PasswordResetTokenInvalidError', 'PasswordResetTokenExpiredError', 'NotVerifiedError']);
 function isGraphQLError(input: any): input is import('@vendure/common/lib/generated-types').ErrorResult {
   return input instanceof ErrorResult || errorTypeNames.has(input.__typename);
 }

+ 1 - 1
packages/core/src/i18n/messages/en.json

@@ -13,7 +13,6 @@
     "country-code-not-valid": "The countryCode \"{ countryCode }\" was not recognized",
     "customer-does-not-belong-to-customer-group": "Customer does not belong to this CustomerGroup",
     "default-channel-not-found": "Default channel not found",
-    "email-address-not-verified": "Please verify this email address before logging in",
     "entity-has-no-translation-in-language": "Translatable entity '{ entityName }' has not been translated into the requested language ({ languageCode })",
     "entity-with-id-not-found": "No { entityName } with the id '{ id }' could be found",
     "field-invalid-datetime-range-max": "The custom field '{ name }' value [{ value }] is greater than the maximum [{ max }]",
@@ -59,6 +58,7 @@
     "MISSING_PASSWORD_ERROR": "A password must be provided.",
     "NEGATIVE_QUANTITY_ERROR": "The quantity for an OrderItem cannot be negative",
     "NOTHING_TO_REFUND_ERROR": "Nothing to refund",
+    "NOT_VERIFIED_ERROR": "Please verify this email address before logging in",
     "ORDER_LIMIT_ERROR": "Cannot add items. An order may consist of a maximum of { maxItems } items",
     "ORDER_MODIFICATION_ERROR": "Order contents may only be modified when in the \"AddingItems\" state",
     "ORDER_PAYMENT_STATE_ERROR": "A Payment may only be added when Order is in \"ArrangingPayment\" state",

+ 9 - 6
packages/core/src/service/services/auth.service.ts

@@ -3,14 +3,17 @@ import { ID } from '@vendure/common/lib/shared-types';
 
 import { ApiType } from '../../api/common/get-api-type';
 import { RequestContext } from '../../api/common/request-context';
-import { InternalServerError, NotVerifiedError } from '../../common/error/errors';
+import { InternalServerError } from '../../common/error/errors';
 import { InvalidCredentialsError } from '../../common/error/generated-graphql-admin-errors';
-import { InvalidCredentialsError as ShopInvalidCredentialsError } from '../../common/error/generated-graphql-shop-errors';
+import {
+    InvalidCredentialsError as ShopInvalidCredentialsError,
+    NotVerifiedError,
+} from '../../common/error/generated-graphql-shop-errors';
 import { AuthenticationStrategy } from '../../config/auth/authentication-strategy';
 import {
-    NATIVE_AUTH_STRATEGY_NAME,
     NativeAuthenticationData,
     NativeAuthenticationStrategy,
+    NATIVE_AUTH_STRATEGY_NAME,
 } from '../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../config/config.service';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
@@ -43,7 +46,7 @@ export class AuthService {
         apiType: ApiType,
         authenticationMethod: string,
         authenticationData: any,
-    ): Promise<AuthenticatedSession | InvalidCredentialsError> {
+    ): Promise<AuthenticatedSession | InvalidCredentialsError | NotVerifiedError> {
         this.eventBus.publish(
             new AttemptedLoginEvent(
                 ctx,
@@ -65,7 +68,7 @@ export class AuthService {
         ctx: RequestContext,
         user: User,
         authenticationStrategyName: string,
-    ): Promise<AuthenticatedSession> {
+    ): Promise<AuthenticatedSession | NotVerifiedError> {
         if (!user.roles || !user.roles[0]?.channels) {
             const userWithRoles = await this.connection
                 .getRepository(ctx, User)
@@ -78,7 +81,7 @@ export class AuthService {
         }
 
         if (this.configService.authOptions.requireVerification && !user.verified) {
-            throw new NotVerifiedError();
+            return new NotVerifiedError();
         }
         if (ctx.session && ctx.session.activeOrderId) {
             await this.sessionService.deleteSessionsByActiveOrderId(ctx, ctx.session.activeOrderId);

ファイルの差分が大きいため隠しています
+ 0 - 0
schema-shop.json


この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません