fix(email-verification): prevent double email validation (#12250)
Fix #12177 Fix #12171 --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -812,17 +812,6 @@ export enum HealthIndicatorId {
|
||||
worker = 'worker'
|
||||
}
|
||||
|
||||
export type IdFilter = {
|
||||
eq?: InputMaybe<Scalars['ID']['input']>;
|
||||
gt?: InputMaybe<Scalars['ID']['input']>;
|
||||
gte?: InputMaybe<Scalars['ID']['input']>;
|
||||
in?: InputMaybe<Array<Scalars['ID']['input']>>;
|
||||
is?: InputMaybe<FilterIs>;
|
||||
lt?: InputMaybe<Scalars['ID']['input']>;
|
||||
lte?: InputMaybe<Scalars['ID']['input']>;
|
||||
neq?: InputMaybe<Scalars['ID']['input']>;
|
||||
};
|
||||
|
||||
export enum IdentityProviderType {
|
||||
OIDC = 'OIDC',
|
||||
SAML = 'SAML'
|
||||
@ -1254,6 +1243,7 @@ export type MutationGetLoginTokenFromCredentialsArgs = {
|
||||
|
||||
export type MutationGetLoginTokenFromEmailVerificationTokenArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']['input']>;
|
||||
email: Scalars['String']['input'];
|
||||
emailVerificationToken: Scalars['String']['input'];
|
||||
origin: Scalars['String']['input'];
|
||||
};
|
||||
@ -1552,7 +1542,7 @@ export type ObjectRecordFilterInput = {
|
||||
and?: InputMaybe<Array<ObjectRecordFilterInput>>;
|
||||
createdAt?: InputMaybe<DateFilter>;
|
||||
deletedAt?: InputMaybe<DateFilter>;
|
||||
id?: InputMaybe<IdFilter>;
|
||||
id?: InputMaybe<UuidFilter>;
|
||||
not?: InputMaybe<ObjectRecordFilterInput>;
|
||||
or?: InputMaybe<Array<ObjectRecordFilterInput>>;
|
||||
updatedAt?: InputMaybe<DateFilter>;
|
||||
@ -2317,6 +2307,17 @@ export type TransientToken = {
|
||||
transientToken: AuthToken;
|
||||
};
|
||||
|
||||
export type UuidFilter = {
|
||||
eq?: InputMaybe<Scalars['UUID']['input']>;
|
||||
gt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
gte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
in?: InputMaybe<Array<Scalars['UUID']['input']>>;
|
||||
is?: InputMaybe<FilterIs>;
|
||||
lt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
lte?: InputMaybe<Scalars['UUID']['input']>;
|
||||
neq?: InputMaybe<Scalars['UUID']['input']>;
|
||||
};
|
||||
|
||||
export type UuidFilterComparison = {
|
||||
eq?: InputMaybe<Scalars['UUID']['input']>;
|
||||
gt?: InputMaybe<Scalars['UUID']['input']>;
|
||||
|
||||
@ -1129,6 +1129,7 @@ export type MutationGetLoginTokenFromCredentialsArgs = {
|
||||
|
||||
export type MutationGetLoginTokenFromEmailVerificationTokenArgs = {
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
email: Scalars['String'];
|
||||
emailVerificationToken: Scalars['String'];
|
||||
origin: Scalars['String'];
|
||||
};
|
||||
@ -2626,6 +2627,7 @@ export type GetLoginTokenFromCredentialsMutation = { __typename?: 'Mutation', ge
|
||||
|
||||
export type GetLoginTokenFromEmailVerificationTokenMutationVariables = Exact<{
|
||||
emailVerificationToken: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
captchaToken?: InputMaybe<Scalars['String']>;
|
||||
origin: Scalars['String'];
|
||||
}>;
|
||||
@ -2749,6 +2751,7 @@ export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typ
|
||||
export type SearchQueryVariables = Exact<{
|
||||
searchInput: Scalars['String'];
|
||||
limit: Scalars['Int'];
|
||||
after?: InputMaybe<Scalars['String']>;
|
||||
excludedObjectNameSingulars?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||
includedObjectNameSingulars?: InputMaybe<Array<Scalars['String']> | Scalars['String']>;
|
||||
filter?: InputMaybe<ObjectRecordFilterInput>;
|
||||
@ -3885,9 +3888,10 @@ export type GetLoginTokenFromCredentialsMutationHookResult = ReturnType<typeof u
|
||||
export type GetLoginTokenFromCredentialsMutationResult = Apollo.MutationResult<GetLoginTokenFromCredentialsMutation>;
|
||||
export type GetLoginTokenFromCredentialsMutationOptions = Apollo.BaseMutationOptions<GetLoginTokenFromCredentialsMutation, GetLoginTokenFromCredentialsMutationVariables>;
|
||||
export const GetLoginTokenFromEmailVerificationTokenDocument = gql`
|
||||
mutation GetLoginTokenFromEmailVerificationToken($emailVerificationToken: String!, $captchaToken: String, $origin: String!) {
|
||||
mutation GetLoginTokenFromEmailVerificationToken($emailVerificationToken: String!, $email: String!, $captchaToken: String, $origin: String!) {
|
||||
getLoginTokenFromEmailVerificationToken(
|
||||
emailVerificationToken: $emailVerificationToken
|
||||
email: $email
|
||||
captchaToken: $captchaToken
|
||||
origin: $origin
|
||||
) {
|
||||
@ -3917,6 +3921,7 @@ export type GetLoginTokenFromEmailVerificationTokenMutationFn = Apollo.MutationF
|
||||
* const [getLoginTokenFromEmailVerificationTokenMutation, { data, loading, error }] = useGetLoginTokenFromEmailVerificationTokenMutation({
|
||||
* variables: {
|
||||
* emailVerificationToken: // value for 'emailVerificationToken'
|
||||
* email: // value for 'email'
|
||||
* captchaToken: // value for 'captchaToken'
|
||||
* origin: // value for 'origin'
|
||||
* },
|
||||
@ -4641,10 +4646,11 @@ export type GetClientConfigQueryHookResult = ReturnType<typeof useGetClientConfi
|
||||
export type GetClientConfigLazyQueryHookResult = ReturnType<typeof useGetClientConfigLazyQuery>;
|
||||
export type GetClientConfigQueryResult = Apollo.QueryResult<GetClientConfigQuery, GetClientConfigQueryVariables>;
|
||||
export const SearchDocument = gql`
|
||||
query Search($searchInput: String!, $limit: Int!, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) {
|
||||
query Search($searchInput: String!, $limit: Int!, $after: String, $excludedObjectNameSingulars: [String!], $includedObjectNameSingulars: [String!], $filter: ObjectRecordFilterInput) {
|
||||
search(
|
||||
searchInput: $searchInput
|
||||
limit: $limit
|
||||
after: $after
|
||||
excludedObjectNameSingulars: $excludedObjectNameSingulars
|
||||
includedObjectNameSingulars: $includedObjectNameSingulars
|
||||
filter: $filter
|
||||
@ -4682,6 +4688,7 @@ export const SearchDocument = gql`
|
||||
* variables: {
|
||||
* searchInput: // value for 'searchInput'
|
||||
* limit: // value for 'limit'
|
||||
* after: // value for 'after'
|
||||
* excludedObjectNameSingulars: // value for 'excludedObjectNameSingulars'
|
||||
* includedObjectNameSingulars: // value for 'includedObjectNameSingulars'
|
||||
* filter: // value for 'filter'
|
||||
|
||||
@ -2,6 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { AppPath } from '@/types/AppPath';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
|
||||
import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
|
||||
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
|
||||
@ -41,7 +42,10 @@ export const VerifyEmailEffect = () => {
|
||||
|
||||
try {
|
||||
const { loginToken, workspaceUrls } =
|
||||
await getLoginTokenFromEmailVerificationToken(emailVerificationToken);
|
||||
await getLoginTokenFromEmailVerificationToken(
|
||||
emailVerificationToken,
|
||||
email,
|
||||
);
|
||||
|
||||
enqueueSnackBar(t`Email verified.`, {
|
||||
dedupeKey: 'email-verification-dedupe-key',
|
||||
@ -56,10 +60,23 @@ export const VerifyEmailEffect = () => {
|
||||
}
|
||||
verifyLoginToken(loginToken.token);
|
||||
} catch (error) {
|
||||
enqueueSnackBar(t`Email verification failed.`, {
|
||||
const message: string =
|
||||
error instanceof ApolloError
|
||||
? error.message
|
||||
: 'Email verification failed';
|
||||
|
||||
enqueueSnackBar(t`${message}`, {
|
||||
dedupeKey: 'email-verification-error-dedupe-key',
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
if (
|
||||
error instanceof ApolloError &&
|
||||
error.graphQLErrors[0].extensions?.subCode ===
|
||||
'EMAIL_ALREADY_VERIFIED'
|
||||
) {
|
||||
navigate(AppPath.SignInUp);
|
||||
}
|
||||
|
||||
setIsError(true);
|
||||
}
|
||||
};
|
||||
|
||||
@ -3,11 +3,13 @@ import { gql } from '@apollo/client';
|
||||
export const GET_LOGIN_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql`
|
||||
mutation GetLoginTokenFromEmailVerificationToken(
|
||||
$emailVerificationToken: String!
|
||||
$email: String!
|
||||
$captchaToken: String
|
||||
$origin: String!
|
||||
) {
|
||||
getLoginTokenFromEmailVerificationToken(
|
||||
emailVerificationToken: $emailVerificationToken
|
||||
email: $email
|
||||
captchaToken: $captchaToken
|
||||
origin: $origin
|
||||
) {
|
||||
|
||||
@ -210,9 +210,14 @@ export const useAuth = () => {
|
||||
);
|
||||
|
||||
const handleGetLoginTokenFromEmailVerificationToken = useCallback(
|
||||
async (emailVerificationToken: string, captchaToken?: string) => {
|
||||
async (
|
||||
emailVerificationToken: string,
|
||||
email: string,
|
||||
captchaToken?: string,
|
||||
) => {
|
||||
const loginTokenResult = await getLoginTokenFromEmailVerificationToken({
|
||||
variables: {
|
||||
email,
|
||||
emailVerificationToken,
|
||||
captchaToken,
|
||||
origin,
|
||||
|
||||
@ -9,10 +9,10 @@ import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/service
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { SSOService } from 'src/engine/core-modules/sso/services/sso.service';
|
||||
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
@ -34,7 +34,7 @@ describe('AuthResolver', () => {
|
||||
providers: [
|
||||
AuthResolver,
|
||||
{
|
||||
provide: getRepositoryToken(Workspace, 'core'),
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
|
||||
@ -53,6 +53,7 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { SettingPermissionType } from 'src/engine/metadata-modules/permissions/constants/setting-permission-type.constants';
|
||||
import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
|
||||
import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input';
|
||||
import { GetLoginTokenFromCredentialsInput } from './dto/get-login-token-from-credentials.input';
|
||||
@ -75,6 +76,8 @@ export class AuthResolver {
|
||||
constructor(
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
@InjectRepository(AppToken, 'core')
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
private authService: AuthService,
|
||||
private renewTokenService: RenewTokenService,
|
||||
private userService: UserService,
|
||||
@ -168,21 +171,24 @@ export class AuthResolver {
|
||||
getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput,
|
||||
@Args('origin') origin: string,
|
||||
) {
|
||||
const user =
|
||||
const appToken =
|
||||
await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow(
|
||||
getLoginTokenFromEmailVerificationTokenInput.emailVerificationToken,
|
||||
getLoginTokenFromEmailVerificationTokenInput,
|
||||
);
|
||||
|
||||
const workspace =
|
||||
(await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
|
||||
origin,
|
||||
)) ??
|
||||
(await this.userWorkspaceService.findFirstWorkspaceByUserId(user.id));
|
||||
(await this.userWorkspaceService.findFirstWorkspaceByUserId(
|
||||
appToken.user.id,
|
||||
));
|
||||
|
||||
await this.userService.markEmailAsVerified(user.id);
|
||||
await this.userService.markEmailAsVerified(appToken.user.id);
|
||||
await this.appTokenRepository.remove(appToken);
|
||||
|
||||
const loginToken = await this.loginTokenService.generateLoginToken(
|
||||
user.email,
|
||||
appToken.user.email,
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
|
||||
@ -9,6 +9,11 @@ export class GetLoginTokenFromEmailVerificationTokenInput {
|
||||
@IsString()
|
||||
emailVerificationToken: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
email: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
|
||||
@ -14,12 +14,14 @@ import {
|
||||
EmailVerificationExceptionCode,
|
||||
} from 'src/engine/core-modules/email-verification/email-verification.exception';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
import { EmailVerificationTokenService } from './email-verification-token.service';
|
||||
|
||||
describe('EmailVerificationTokenService', () => {
|
||||
let service: EmailVerificationTokenService;
|
||||
let appTokenRepository: Repository<AppToken>;
|
||||
let userRepository: Repository<User>;
|
||||
let twentyConfigService: TwentyConfigService;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -30,6 +32,12 @@ describe('EmailVerificationTokenService', () => {
|
||||
provide: getRepositoryToken(AppToken, 'core'),
|
||||
useClass: Repository,
|
||||
},
|
||||
{
|
||||
provide: getRepositoryToken(User, 'core'),
|
||||
useValue: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: TwentyConfigService,
|
||||
useValue: {
|
||||
@ -45,6 +53,9 @@ describe('EmailVerificationTokenService', () => {
|
||||
appTokenRepository = module.get<Repository<AppToken>>(
|
||||
getRepositoryToken(AppToken, 'core'),
|
||||
);
|
||||
userRepository = module.get<Repository<User>>(
|
||||
getRepositoryToken(User, 'core'),
|
||||
);
|
||||
twentyConfigService = module.get<TwentyConfigService>(TwentyConfigService);
|
||||
});
|
||||
|
||||
@ -92,14 +103,14 @@ describe('EmailVerificationTokenService', () => {
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOne')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'remove')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
const result =
|
||||
await service.validateEmailVerificationTokenOrThrow(plainToken);
|
||||
const result = await service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: plainToken,
|
||||
email: 'test@example.com',
|
||||
});
|
||||
|
||||
expect(result).toEqual(mockUser);
|
||||
expect(result).toEqual(mockAppToken);
|
||||
expect(appTokenRepository.findOne).toHaveBeenCalledWith({
|
||||
where: {
|
||||
value: hashedToken,
|
||||
@ -107,14 +118,17 @@ describe('EmailVerificationTokenService', () => {
|
||||
},
|
||||
relations: ['user'],
|
||||
});
|
||||
expect(appTokenRepository.remove).toHaveBeenCalledWith(mockAppToken);
|
||||
});
|
||||
|
||||
it('should throw exception for invalid token', async () => {
|
||||
jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow('invalid-token'),
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'invalid-token',
|
||||
email: 'test@twenty.com',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Invalid email verification token',
|
||||
@ -123,6 +137,63 @@ describe('EmailVerificationTokenService', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception for already validated token', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
isEmailVerified: true,
|
||||
};
|
||||
|
||||
jest.spyOn(appTokenRepository, 'findOne').mockResolvedValue(null);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(mockUser as User);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'invalid-token',
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Email already verified',
|
||||
EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception when email does not match appToken email', async () => {
|
||||
const mockUser = {
|
||||
id: 'user-id',
|
||||
email: 'test@example.com',
|
||||
isEmailVerified: false,
|
||||
};
|
||||
|
||||
const mockAppToken = {
|
||||
type: AppTokenType.EmailVerificationToken,
|
||||
expiresAt: new Date(Date.now() + 86400000), // 24h from now
|
||||
context: { email: 'other-email@example.com' },
|
||||
user: {
|
||||
email: 'other-email@example.com',
|
||||
},
|
||||
};
|
||||
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOne')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'valid-token',
|
||||
email: mockUser.email,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Email does not match token',
|
||||
EmailVerificationExceptionCode.INVALID_EMAIL,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw exception for wrong token type', async () => {
|
||||
const mockAppToken = {
|
||||
type: AppTokenType.PasswordResetToken,
|
||||
@ -132,9 +203,13 @@ describe('EmailVerificationTokenService', () => {
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOne')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow('wrong-type-token'),
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'wrong-type-token',
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Invalid email verification token type',
|
||||
@ -152,9 +227,13 @@ describe('EmailVerificationTokenService', () => {
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOne')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow('expired-token'),
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'expired-token',
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Email verification token expired',
|
||||
@ -173,9 +252,13 @@ describe('EmailVerificationTokenService', () => {
|
||||
jest
|
||||
.spyOn(appTokenRepository, 'findOne')
|
||||
.mockResolvedValue(mockAppToken as AppToken);
|
||||
jest.spyOn(userRepository, 'findOne').mockResolvedValue(null);
|
||||
|
||||
await expect(
|
||||
service.validateEmailVerificationTokenOrThrow('valid-token'),
|
||||
service.validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken: 'valid-token',
|
||||
email: 'test@example.com',
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
new EmailVerificationException(
|
||||
'Email missing in email verification token context',
|
||||
|
||||
@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { Repository } from 'typeorm';
|
||||
@ -17,12 +18,15 @@ import {
|
||||
EmailVerificationExceptionCode,
|
||||
} from 'src/engine/core-modules/email-verification/email-verification.exception';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Injectable()
|
||||
export class EmailVerificationTokenService {
|
||||
constructor(
|
||||
@InjectRepository(AppToken, 'core')
|
||||
private readonly appTokenRepository: Repository<AppToken>,
|
||||
@InjectRepository(User, 'core')
|
||||
private readonly userRepository: Repository<User>,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
) {}
|
||||
|
||||
@ -54,7 +58,27 @@ export class EmailVerificationTokenService {
|
||||
};
|
||||
}
|
||||
|
||||
async validateEmailVerificationTokenOrThrow(emailVerificationToken: string) {
|
||||
async validateEmailVerificationTokenOrThrow({
|
||||
emailVerificationToken,
|
||||
email,
|
||||
}: {
|
||||
emailVerificationToken: string;
|
||||
email: string;
|
||||
}) {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
email,
|
||||
isEmailVerified: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(user)) {
|
||||
throw new EmailVerificationException(
|
||||
'Email already verified',
|
||||
EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED,
|
||||
);
|
||||
}
|
||||
|
||||
const hashedToken = crypto
|
||||
.createHash('sha256')
|
||||
.update(emailVerificationToken)
|
||||
@ -96,8 +120,13 @@ export class EmailVerificationTokenService {
|
||||
);
|
||||
}
|
||||
|
||||
await this.appTokenRepository.remove(appToken);
|
||||
if (appToken.context?.email !== email) {
|
||||
throw new EmailVerificationException(
|
||||
'Email does not match token',
|
||||
EmailVerificationExceptionCode.INVALID_EMAIL,
|
||||
);
|
||||
}
|
||||
|
||||
return appToken.user;
|
||||
return appToken;
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,11 +17,16 @@ export class EmailVerificationExceptionFilter implements ExceptionFilter {
|
||||
case EmailVerificationExceptionCode.INVALID_APP_TOKEN_TYPE:
|
||||
case EmailVerificationExceptionCode.TOKEN_EXPIRED:
|
||||
case EmailVerificationExceptionCode.RATE_LIMIT_EXCEEDED:
|
||||
throw new ForbiddenError(exception.message);
|
||||
throw new ForbiddenError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
case EmailVerificationExceptionCode.EMAIL_MISSING:
|
||||
case EmailVerificationExceptionCode.EMAIL_ALREADY_VERIFIED:
|
||||
case EmailVerificationExceptionCode.INVALID_EMAIL:
|
||||
case EmailVerificationExceptionCode.EMAIL_VERIFICATION_NOT_REQUIRED:
|
||||
throw new UserInputError(exception.message);
|
||||
throw new UserInputError(exception.message, {
|
||||
subCode: exception.code,
|
||||
});
|
||||
default: {
|
||||
const _exhaustiveCheck: never = exception.code;
|
||||
|
||||
|
||||
@ -14,5 +14,6 @@ export enum EmailVerificationExceptionCode {
|
||||
TOKEN_EXPIRED = 'TOKEN_EXPIRED',
|
||||
EMAIL_MISSING = 'EMAIL_MISSING',
|
||||
EMAIL_ALREADY_VERIFIED = 'EMAIL_ALREADY_VERIFIED',
|
||||
INVALID_EMAIL = 'INVALID_EMAIL',
|
||||
RATE_LIMIT_EXCEEDED = 'RATE_LIMIT_EXCEEDED',
|
||||
}
|
||||
|
||||
@ -10,10 +10,11 @@ import { EmailModule } from 'src/engine/core-modules/email/email.module';
|
||||
import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module';
|
||||
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
|
||||
import { UserModule } from 'src/engine/core-modules/user/user.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([AppToken], 'core'),
|
||||
TypeOrmModule.forFeature([AppToken, User], 'core'),
|
||||
EmailModule,
|
||||
TwentyConfigModule,
|
||||
DomainManagerModule,
|
||||
|
||||
@ -150,8 +150,9 @@ export class PersistedQueryNotSupportedError extends BaseGraphQLError {
|
||||
}
|
||||
|
||||
export class UserInputError extends BaseGraphQLError {
|
||||
constructor(message: string) {
|
||||
super(message, ErrorCode.BAD_USER_INPUT);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(message: string, extensions?: Record<string, any>) {
|
||||
super(message, ErrorCode.BAD_USER_INPUT, extensions);
|
||||
|
||||
Object.defineProperty(this, 'name', { value: 'UserInputError' });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user