Skip to content

Commit

Permalink
feat(core): Add NotVerifiedError to AuthenticationResult
Browse files Browse the repository at this point in the history
Closes #500
  • Loading branch information
michaelbromley committed Oct 16, 2020
1 parent 6204acf commit ee39263
Show file tree
Hide file tree
Showing 11 changed files with 77 additions and 73 deletions.
3 changes: 2 additions & 1 deletion packages/admin-ui/src/lib/core/src/common/generated-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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; }>;
Expand Down Expand Up @@ -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'>
Expand Down
38 changes: 14 additions & 24 deletions packages/common/src/generated-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -2142,7 +2152,6 @@ export type SearchResponse = {
items: Array<SearchResult>;
totalItems: Scalars['Int'];
facetValues: Array<FacetValueResult>;
prices: SearchResponsePriceData;
};

/**
Expand Down Expand Up @@ -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']>;
Expand Down
35 changes: 13 additions & 22 deletions packages/core/e2e/graphql/generated-e2e-shop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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'];
Expand Down Expand Up @@ -2056,7 +2065,6 @@ export type SearchResponse = {
items: Array<SearchResult>;
totalItems: Scalars['Int'];
facetValues: Array<FacetValueResult>;
prices: SearchResponsePriceData;
};

/**
Expand Down Expand Up @@ -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']>;
Expand Down
8 changes: 7 additions & 1 deletion packages/core/e2e/shop-auth.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/api/resolvers/shop/shop-auth.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
13 changes: 11 additions & 2 deletions packages/core/src/api/schema/shop-api/shop.api.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
14 changes: 0 additions & 14 deletions packages/core/src/common/error/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
12 changes: 11 additions & 1 deletion packages/core/src/common/error/generated-graphql-shop-errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]",
Expand Down Expand Up @@ -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",
Expand Down
15 changes: 9 additions & 6 deletions packages/core/src/service/services/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion schema-shop.json

Large diffs are not rendered by default.

0 comments on commit ee39263

Please sign in to comment.