diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index d089ceeed..06aec14f8 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -812,17 +812,6 @@ export enum HealthIndicatorId { worker = 'worker' } -export type IdFilter = { - eq?: InputMaybe; - gt?: InputMaybe; - gte?: InputMaybe; - in?: InputMaybe>; - is?: InputMaybe; - lt?: InputMaybe; - lte?: InputMaybe; - neq?: InputMaybe; -}; - export enum IdentityProviderType { OIDC = 'OIDC', SAML = 'SAML' @@ -1254,6 +1243,7 @@ export type MutationGetLoginTokenFromCredentialsArgs = { export type MutationGetLoginTokenFromEmailVerificationTokenArgs = { captchaToken?: InputMaybe; + email: Scalars['String']['input']; emailVerificationToken: Scalars['String']['input']; origin: Scalars['String']['input']; }; @@ -1552,7 +1542,7 @@ export type ObjectRecordFilterInput = { and?: InputMaybe>; createdAt?: InputMaybe; deletedAt?: InputMaybe; - id?: InputMaybe; + id?: InputMaybe; not?: InputMaybe; or?: InputMaybe>; updatedAt?: InputMaybe; @@ -2317,6 +2307,17 @@ export type TransientToken = { transientToken: AuthToken; }; +export type UuidFilter = { + eq?: InputMaybe; + gt?: InputMaybe; + gte?: InputMaybe; + in?: InputMaybe>; + is?: InputMaybe; + lt?: InputMaybe; + lte?: InputMaybe; + neq?: InputMaybe; +}; + export type UuidFilterComparison = { eq?: InputMaybe; gt?: InputMaybe; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index b60ca9fa0..5ca2f0c76 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1129,6 +1129,7 @@ export type MutationGetLoginTokenFromCredentialsArgs = { export type MutationGetLoginTokenFromEmailVerificationTokenArgs = { captchaToken?: InputMaybe; + 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; 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; excludedObjectNameSingulars?: InputMaybe | Scalars['String']>; includedObjectNameSingulars?: InputMaybe | Scalars['String']>; filter?: InputMaybe; @@ -3885,9 +3888,10 @@ export type GetLoginTokenFromCredentialsMutationHookResult = ReturnType; export type GetLoginTokenFromCredentialsMutationOptions = Apollo.BaseMutationOptions; 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; export type GetClientConfigQueryResult = Apollo.QueryResult; 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' diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index 7a3969a8f..23896d9b5 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -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); } }; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getLoginTokenFromEmailVerificationToken.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getLoginTokenFromEmailVerificationToken.ts index a3a4f2e2e..5fc0bb274 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/getLoginTokenFromEmailVerificationToken.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getLoginTokenFromEmailVerificationToken.ts @@ -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 ) { diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 4ffc49c5a..5149ab2b5 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts index dfaf64bdf..668499a05 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts @@ -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: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 0831dbd3d..0138dbfd0 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -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, + @InjectRepository(AppToken, 'core') + private readonly appTokenRepository: Repository, 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, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts index 333174e75..a2a923d86 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts @@ -9,6 +9,11 @@ export class GetLoginTokenFromEmailVerificationTokenInput { @IsString() emailVerificationToken: string; + @Field(() => String) + @IsNotEmpty() + @IsString() + email: string; + @Field(() => String, { nullable: true }) @IsString() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.spec.ts index 13a45ab55..791524c51 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.spec.ts @@ -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; + let userRepository: Repository; 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>( getRepositoryToken(AppToken, 'core'), ); + userRepository = module.get>( + getRepositoryToken(User, 'core'), + ); twentyConfigService = module.get(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', diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.ts index 3e350726e..ed452b06f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/email-verification-token.service.ts @@ -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, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, 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; } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts index bfca293d9..218f779a2 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification-exception-filter.util.ts @@ -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; diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts index 8cc641e8d..dd3f8281d 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.exception.ts @@ -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', } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts index c22faf26c..e8da2a485 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts @@ -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, diff --git a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts index 8a4afa07c..0d2025b0f 100644 --- a/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts +++ b/packages/twenty-server/src/engine/core-modules/graphql/utils/graphql-errors.util.ts @@ -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) { + super(message, ErrorCode.BAD_USER_INPUT, extensions); Object.defineProperty(this, 'name', { value: 'UserInputError' }); }