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:
Antoine Moreaux
2025-05-23 18:24:26 +02:00
committed by GitHub
parent 8de85eea61
commit 5428348d7f
14 changed files with 206 additions and 43 deletions

View File

@ -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']>;

View File

@ -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'

View File

@ -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);
}
};

View File

@ -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
) {

View File

@ -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,