diff --git a/packages/twenty-chrome-extension/src/background/index.ts b/packages/twenty-chrome-extension/src/background/index.ts index 33992a6316..d4d284753c 100644 --- a/packages/twenty-chrome-extension/src/background/index.ts +++ b/packages/twenty-chrome-extension/src/background/index.ts @@ -62,7 +62,7 @@ const setTokenStateFromCookie = (cookie: string) => { if (isDefined(tokenPair)) { chrome.storage.local.set({ isAuthenticated: true, - accessToken: tokenPair.accessToken, + accessToken: tokenPair.accessOrWorkspaceAgnosticToken, refreshToken: tokenPair.refreshToken, }); } diff --git a/packages/twenty-chrome-extension/src/db/types/auth.types.ts b/packages/twenty-chrome-extension/src/db/types/auth.types.ts index 68d339c254..82853ac5e3 100644 --- a/packages/twenty-chrome-extension/src/db/types/auth.types.ts +++ b/packages/twenty-chrome-extension/src/db/types/auth.types.ts @@ -11,7 +11,7 @@ export type ExchangeAuthCodeInput = { export type Tokens = { loginToken: AuthToken; - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; refreshToken: AuthToken; }; diff --git a/packages/twenty-chrome-extension/src/generated/graphql.tsx b/packages/twenty-chrome-extension/src/generated/graphql.tsx index 6ef88dc33d..64ebea47f5 100644 --- a/packages/twenty-chrome-extension/src/generated/graphql.tsx +++ b/packages/twenty-chrome-extension/src/generated/graphql.tsx @@ -826,7 +826,7 @@ export type AuthToken = { }; export type AuthTokenPair = { - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; refreshToken: AuthToken; }; diff --git a/packages/twenty-chrome-extension/src/graphql/auth/mutations.ts b/packages/twenty-chrome-extension/src/graphql/auth/mutations.ts index 470d3826f3..45804f7069 100644 --- a/packages/twenty-chrome-extension/src/graphql/auth/mutations.ts +++ b/packages/twenty-chrome-extension/src/graphql/auth/mutations.ts @@ -15,7 +15,7 @@ export const EXCHANGE_AUTHORIZATION_CODE = gql` token expiresAt } - accessToken { + accessOrWorkspaceAgnosticToken { token expiresAt } @@ -31,7 +31,7 @@ export const RENEW_TOKEN = gql` mutation RenewToken($appToken: String!) { renewToken(appToken: $appToken) { tokens { - accessToken { + accessOrWorkspaceAgnosticToken { token expiresAt } diff --git a/packages/twenty-e2e-testing/lib/utils/getAuthToken.ts b/packages/twenty-e2e-testing/lib/utils/getAuthToken.ts index 38534fdf23..3e6ff53299 100644 --- a/packages/twenty-e2e-testing/lib/utils/getAuthToken.ts +++ b/packages/twenty-e2e-testing/lib/utils/getAuthToken.ts @@ -8,7 +8,7 @@ export const getAuthToken = async (page: Page) => { if (!authCookie) { throw new Error('No auth cookie found'); } - const token = JSON.parse(decodeURIComponent(authCookie.value)).accessToken + const token = JSON.parse(decodeURIComponent(authCookie.value)).accessOrWorkspaceAgnosticToken .token; return { authToken: token }; diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index b869153f44..145269cab7 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -74,7 +74,7 @@ const preview: Preview = { }, }, cookie: { - tokenPair: `{%22accessToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`, + tokenPair: `{%22accessOrWorkspaceAgnosticToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-07-18T15:06:40.704Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22refreshToken%22:{%22token%22:%22${mockedUserJWT}%22%2C%22expiresAt%22:%222023-10-15T15:06:41.558Z%22%2C%22__typename%22:%22AuthToken%22}%2C%22__typename%22:%22AuthTokenPair%22}`, }, }, }; diff --git a/packages/twenty-front/lingui.config.ts b/packages/twenty-front/lingui.config.ts index e60c85cc34..debcc9c7ac 100644 --- a/packages/twenty-front/lingui.config.ts +++ b/packages/twenty-front/lingui.config.ts @@ -3,7 +3,7 @@ import { formatter } from '@lingui/format-po'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; export default defineConfig({ - sourceLocale: 'en', + sourceLocale: SOURCE_LOCALE, locales: Object.values(APP_LOCALES), pseudoLocale: 'pseudo-en', fallbackLocales: { diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 5379c6aba8..baa1311b49 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -161,7 +161,7 @@ export type AuthToken = { export type AuthTokenPair = { __typename?: 'AuthTokenPair'; - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; refreshToken: AuthToken; }; @@ -1168,6 +1168,7 @@ export type Mutation = { getAuthorizationUrlForSSO: GetAuthorizationUrlForSsoOutput; getLoginTokenFromCredentials: LoginToken; getLoginTokenFromEmailVerificationToken: GetLoginTokenFromEmailVerificationTokenOutput; + getWorkspaceAgnosticTokenFromEmailVerificationToken: AvailableWorkspacesAndAccessTokensOutput; impersonate: ImpersonateOutput; initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; @@ -1461,8 +1462,10 @@ export type MutationGetAuthorizationUrlForSsoArgs = { export type MutationGetLoginTokenFromCredentialsArgs = { captchaToken?: InputMaybe; email: Scalars['String']; + locale?: InputMaybe; origin: Scalars['String']; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; @@ -1474,6 +1477,13 @@ export type MutationGetLoginTokenFromEmailVerificationTokenArgs = { }; +export type MutationGetWorkspaceAgnosticTokenFromEmailVerificationTokenArgs = { + captchaToken?: InputMaybe; + email: Scalars['String']; + emailVerificationToken: Scalars['String']; +}; + + export type MutationImpersonateArgs = { userId: Scalars['String']; workspaceId: Scalars['String']; @@ -1538,14 +1548,18 @@ export type MutationSendInvitationsArgs = { export type MutationSignInArgs = { captchaToken?: InputMaybe; email: Scalars['String']; + locale?: InputMaybe; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; export type MutationSignUpArgs = { captchaToken?: InputMaybe; email: Scalars['String']; + locale?: InputMaybe; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; @@ -1554,7 +1568,7 @@ export type MutationSignUpInWorkspaceArgs = { email: Scalars['String']; locale?: InputMaybe; password: Scalars['String']; - verifyEmailNextPath?: InputMaybe; + verifyEmailRedirectPath?: InputMaybe; workspaceId?: InputMaybe; workspaceInviteHash?: InputMaybe; workspacePersonalInviteToken?: InputMaybe; @@ -3152,7 +3166,7 @@ export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: { __ty export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string }; -export type AuthTokensFragmentFragment = { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } }; +export type AuthTokenPairFragmentFragment = { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } }; export type AvailableWorkspaceFragmentFragment = { __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }; @@ -3190,6 +3204,14 @@ export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: nev export type GenerateTransientTokenMutation = { __typename?: 'Mutation', generateTransientToken: { __typename?: 'TransientToken', transientToken: { __typename?: 'AuthToken', token: string } } }; +export type GetAuthTokensFromLoginTokenMutationVariables = Exact<{ + loginToken: Scalars['String']; + origin: Scalars['String']; +}>; + + +export type GetAuthTokensFromLoginTokenMutation = { __typename?: 'Mutation', getAuthTokensFromLoginToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; + export type GetAuthTokensFromOtpMutationVariables = Exact<{ loginToken: Scalars['String']; otp: Scalars['String']; @@ -3198,15 +3220,7 @@ export type GetAuthTokensFromOtpMutationVariables = Exact<{ }>; -export type GetAuthTokensFromOtpMutation = { __typename?: 'Mutation', getAuthTokensFromOTP: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; - -export type GetAuthTokensFromLoginTokenMutationVariables = Exact<{ - loginToken: Scalars['String']; - origin: Scalars['String']; -}>; - - -export type GetAuthTokensFromLoginTokenMutation = { __typename?: 'Mutation', getAuthTokensFromLoginToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type GetAuthTokensFromOtpMutation = { __typename?: 'Mutation', getAuthTokensFromOTP: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type GetAuthorizationUrlForSsoMutationVariables = Exact<{ input: GetAuthorizationUrlForSsoInput; @@ -3235,6 +3249,15 @@ export type GetLoginTokenFromEmailVerificationTokenMutationVariables = Exact<{ export type GetLoginTokenFromEmailVerificationTokenMutation = { __typename?: 'Mutation', getLoginTokenFromEmailVerificationToken: { __typename?: 'GetLoginTokenFromEmailVerificationTokenOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null } } }; +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutationVariables = Exact<{ + emailVerificationToken: Scalars['String']; + email: Scalars['String']; + captchaToken?: InputMaybe; +}>; + + +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation = { __typename?: 'Mutation', getWorkspaceAgnosticTokenFromEmailVerificationToken: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; + export type ImpersonateMutationVariables = Exact<{ userId: Scalars['String']; workspaceId: Scalars['String']; @@ -3261,7 +3284,7 @@ export type RenewTokenMutationVariables = Exact<{ }>; -export type RenewTokenMutation = { __typename?: 'Mutation', renewToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type RenewTokenMutation = { __typename?: 'Mutation', renewToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type ResendEmailVerificationTokenMutationVariables = Exact<{ email: Scalars['String']; @@ -3285,16 +3308,18 @@ export type SignInMutationVariables = Exact<{ }>; -export type SignInMutation = { __typename?: 'Mutation', signIn: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type SignInMutation = { __typename?: 'Mutation', signIn: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type SignUpMutationVariables = Exact<{ email: Scalars['String']; password: Scalars['String']; captchaToken?: InputMaybe; + locale?: InputMaybe; + verifyEmailRedirectPath?: InputMaybe; }>; -export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; +export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'AvailableWorkspacesAndAccessTokensOutput', availableWorkspaces: { __typename?: 'AvailableWorkspaces', availableWorkspacesForSignIn: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }>, availableWorkspacesForSignUp: Array<{ __typename?: 'AvailableWorkspace', id: string, displayName?: string | null, loginToken?: string | null, inviteHash?: string | null, personalInviteToken?: string | null, logo?: string | null, workspaceUrls: { __typename?: 'WorkspaceUrls', subdomainUrl: string, customUrl?: string | null }, sso: Array<{ __typename?: 'SSOConnection', type: IdentityProviderType, id: string, issuer: string, name: string, status: SsoIdentityProviderStatus }> }> }, tokens: { __typename?: 'AuthTokenPair', accessOrWorkspaceAgnosticToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } }; export type SignUpInNewWorkspaceMutationVariables = Exact<{ [key: string]: never; }>; @@ -3309,7 +3334,7 @@ export type SignUpInWorkspaceMutationVariables = Exact<{ captchaToken?: InputMaybe; workspaceId?: InputMaybe; locale?: InputMaybe; - verifyEmailNextPath?: InputMaybe; + verifyEmailRedirectPath?: InputMaybe; }>; @@ -4045,9 +4070,9 @@ export const AuthTokenFragmentFragmentDoc = gql` expiresAt } `; -export const AuthTokensFragmentFragmentDoc = gql` - fragment AuthTokensFragment on AuthTokenPair { - accessToken { +export const AuthTokenPairFragmentFragmentDoc = gql` + fragment AuthTokenPairFragment on AuthTokenPair { + accessOrWorkspaceAgnosticToken { ...AuthTokenFragment } refreshToken { @@ -4830,6 +4855,42 @@ export function useGenerateTransientTokenMutation(baseOptions?: Apollo.MutationH export type GenerateTransientTokenMutationHookResult = ReturnType; export type GenerateTransientTokenMutationResult = Apollo.MutationResult; export type GenerateTransientTokenMutationOptions = Apollo.BaseMutationOptions; +export const GetAuthTokensFromLoginTokenDocument = gql` + mutation getAuthTokensFromLoginToken($loginToken: String!, $origin: String!) { + getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) { + tokens { + ...AuthTokenPairFragment + } + } +} + ${AuthTokenPairFragmentFragmentDoc}`; +export type GetAuthTokensFromLoginTokenMutationFn = Apollo.MutationFunction; + +/** + * __useGetAuthTokensFromLoginTokenMutation__ + * + * To run a mutation, you first call `useGetAuthTokensFromLoginTokenMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetAuthTokensFromLoginTokenMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [getAuthTokensFromLoginTokenMutation, { data, loading, error }] = useGetAuthTokensFromLoginTokenMutation({ + * variables: { + * loginToken: // value for 'loginToken' + * origin: // value for 'origin' + * }, + * }); + */ +export function useGetAuthTokensFromLoginTokenMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GetAuthTokensFromLoginTokenDocument, options); + } +export type GetAuthTokensFromLoginTokenMutationHookResult = ReturnType; +export type GetAuthTokensFromLoginTokenMutationResult = Apollo.MutationResult; +export type GetAuthTokensFromLoginTokenMutationOptions = Apollo.BaseMutationOptions; export const GetAuthTokensFromOtpDocument = gql` mutation getAuthTokensFromOTP($loginToken: String!, $otp: String!, $captchaToken: String, $origin: String!) { getAuthTokensFromOTP( @@ -4839,11 +4900,11 @@ export const GetAuthTokensFromOtpDocument = gql` origin: $origin ) { tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } - ${AuthTokensFragmentFragmentDoc}`; + ${AuthTokenPairFragmentFragmentDoc}`; export type GetAuthTokensFromOtpMutationFn = Apollo.MutationFunction; /** @@ -4873,42 +4934,6 @@ export function useGetAuthTokensFromOtpMutation(baseOptions?: Apollo.MutationHoo export type GetAuthTokensFromOtpMutationHookResult = ReturnType; export type GetAuthTokensFromOtpMutationResult = Apollo.MutationResult; export type GetAuthTokensFromOtpMutationOptions = Apollo.BaseMutationOptions; -export const GetAuthTokensFromLoginTokenDocument = gql` - mutation GetAuthTokensFromLoginToken($loginToken: String!, $origin: String!) { - getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) { - tokens { - ...AuthTokensFragment - } - } -} - ${AuthTokensFragmentFragmentDoc}`; -export type GetAuthTokensFromLoginTokenMutationFn = Apollo.MutationFunction; - -/** - * __useGetAuthTokensFromLoginTokenMutation__ - * - * To run a mutation, you first call `useGetAuthTokensFromLoginTokenMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useGetAuthTokensFromLoginTokenMutation` returns a tuple that includes: - * - A mutate function that you can call at any time to execute the mutation - * - An object with fields that represent the current status of the mutation's execution - * - * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; - * - * @example - * const [getAuthTokensFromLoginTokenMutation, { data, loading, error }] = useGetAuthTokensFromLoginTokenMutation({ - * variables: { - * loginToken: // value for 'loginToken' - * origin: // value for 'origin' - * }, - * }); - */ -export function useGetAuthTokensFromLoginTokenMutation(baseOptions?: Apollo.MutationHookOptions) { - const options = {...defaultOptions, ...baseOptions} - return Apollo.useMutation(GetAuthTokensFromLoginTokenDocument, options); - } -export type GetAuthTokensFromLoginTokenMutationHookResult = ReturnType; -export type GetAuthTokensFromLoginTokenMutationResult = Apollo.MutationResult; -export type GetAuthTokensFromLoginTokenMutationOptions = Apollo.BaseMutationOptions; export const GetAuthorizationUrlForSsoDocument = gql` mutation GetAuthorizationUrlForSSO($input: GetAuthorizationUrlForSSOInput!) { getAuthorizationUrlForSSO(input: $input) { @@ -5034,6 +5059,51 @@ export function useGetLoginTokenFromEmailVerificationTokenMutation(baseOptions?: export type GetLoginTokenFromEmailVerificationTokenMutationHookResult = ReturnType; export type GetLoginTokenFromEmailVerificationTokenMutationResult = Apollo.MutationResult; export type GetLoginTokenFromEmailVerificationTokenMutationOptions = Apollo.BaseMutationOptions; +export const GetWorkspaceAgnosticTokenFromEmailVerificationTokenDocument = gql` + mutation GetWorkspaceAgnosticTokenFromEmailVerificationToken($emailVerificationToken: String!, $email: String!, $captchaToken: String) { + getWorkspaceAgnosticTokenFromEmailVerificationToken( + emailVerificationToken: $emailVerificationToken + email: $email + captchaToken: $captchaToken + ) { + availableWorkspaces { + ...AvailableWorkspacesFragment + } + tokens { + ...AuthTokenPairFragment + } + } +} + ${AvailableWorkspacesFragmentFragmentDoc} +${AuthTokenPairFragmentFragmentDoc}`; +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutationFn = Apollo.MutationFunction; + +/** + * __useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation__ + * + * To run a mutation, you first call `useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation` returns a tuple that includes: + * - A mutate function that you can call at any time to execute the mutation + * - An object with fields that represent the current status of the mutation's execution + * + * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; + * + * @example + * const [getWorkspaceAgnosticTokenFromEmailVerificationTokenMutation, { data, loading, error }] = useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation({ + * variables: { + * emailVerificationToken: // value for 'emailVerificationToken' + * email: // value for 'email' + * captchaToken: // value for 'captchaToken' + * }, + * }); + */ +export function useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation(baseOptions?: Apollo.MutationHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useMutation(GetWorkspaceAgnosticTokenFromEmailVerificationTokenDocument, options); + } +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutationHookResult = ReturnType; +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutationResult = Apollo.MutationResult; +export type GetWorkspaceAgnosticTokenFromEmailVerificationTokenMutationOptions = Apollo.BaseMutationOptions; export const ImpersonateDocument = gql` mutation Impersonate($userId: String!, $workspaceId: String!) { impersonate(userId: $userId, workspaceId: $workspaceId) { @@ -5147,11 +5217,11 @@ export const RenewTokenDocument = gql` mutation RenewToken($appToken: String!) { renewToken(appToken: $appToken) { tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } - ${AuthTokensFragmentFragmentDoc}`; + ${AuthTokenPairFragmentFragmentDoc}`; export type RenewTokenMutationFn = Apollo.MutationFunction; /** @@ -5254,12 +5324,12 @@ export const SignInDocument = gql` ...AvailableWorkspacesFragment } tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } ${AvailableWorkspacesFragmentFragmentDoc} -${AuthTokensFragmentFragmentDoc}`; +${AuthTokenPairFragmentFragmentDoc}`; export type SignInMutationFn = Apollo.MutationFunction; /** @@ -5289,18 +5359,24 @@ export type SignInMutationHookResult = ReturnType; export type SignInMutationResult = Apollo.MutationResult; export type SignInMutationOptions = Apollo.BaseMutationOptions; export const SignUpDocument = gql` - mutation SignUp($email: String!, $password: String!, $captchaToken: String) { - signUp(email: $email, password: $password, captchaToken: $captchaToken) { + mutation SignUp($email: String!, $password: String!, $captchaToken: String, $locale: String, $verifyEmailRedirectPath: String) { + signUp( + email: $email + password: $password + captchaToken: $captchaToken + locale: $locale + verifyEmailRedirectPath: $verifyEmailRedirectPath + ) { availableWorkspaces { ...AvailableWorkspacesFragment } tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } ${AvailableWorkspacesFragmentFragmentDoc} -${AuthTokensFragmentFragmentDoc}`; +${AuthTokenPairFragmentFragmentDoc}`; export type SignUpMutationFn = Apollo.MutationFunction; /** @@ -5319,6 +5395,8 @@ export type SignUpMutationFn = Apollo.MutationFunction; export type SignUpInNewWorkspaceMutationOptions = Apollo.BaseMutationOptions; export const SignUpInWorkspaceDocument = gql` - mutation SignUpInWorkspace($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailNextPath: String) { + mutation SignUpInWorkspace($email: String!, $password: String!, $workspaceInviteHash: String, $workspacePersonalInviteToken: String = null, $captchaToken: String, $workspaceId: String, $locale: String, $verifyEmailRedirectPath: String) { signUpInWorkspace( email: $email password: $password @@ -5380,7 +5458,7 @@ export const SignUpInWorkspaceDocument = gql` captchaToken: $captchaToken workspaceId: $workspaceId locale: $locale - verifyEmailNextPath: $verifyEmailNextPath + verifyEmailRedirectPath: $verifyEmailRedirectPath ) { loginToken { ...AuthTokenFragment @@ -5417,7 +5495,7 @@ export type SignUpInWorkspaceMutationFn = Apollo.MutationFunction; email: Scalars['String']; + locale?: InputMaybe; origin: Scalars['String']; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; @@ -1405,6 +1408,13 @@ export type MutationGetLoginTokenFromEmailVerificationTokenArgs = { }; +export type MutationGetWorkspaceAgnosticTokenFromEmailVerificationTokenArgs = { + captchaToken?: InputMaybe; + email: Scalars['String']; + emailVerificationToken: Scalars['String']; +}; + + export type MutationImpersonateArgs = { userId: Scalars['String']; workspaceId: Scalars['String']; @@ -1469,14 +1479,18 @@ export type MutationSendInvitationsArgs = { export type MutationSignInArgs = { captchaToken?: InputMaybe; email: Scalars['String']; + locale?: InputMaybe; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; export type MutationSignUpArgs = { captchaToken?: InputMaybe; email: Scalars['String']; + locale?: InputMaybe; password: Scalars['String']; + verifyEmailRedirectPath?: InputMaybe; }; @@ -1485,7 +1499,7 @@ export type MutationSignUpInWorkspaceArgs = { email: Scalars['String']; locale?: InputMaybe; password: Scalars['String']; - verifyEmailNextPath?: InputMaybe; + verifyEmailRedirectPath?: InputMaybe; workspaceId?: InputMaybe; workspaceInviteHash?: InputMaybe; workspacePersonalInviteToken?: InputMaybe; diff --git a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts index 24879a8aa5..057817ffa1 100644 --- a/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts +++ b/packages/twenty-front/src/hooks/__tests__/usePageChangeEffectNavigateLocation.test.ts @@ -66,12 +66,12 @@ const setupMockUseParams = (objectNamePlural?: string) => { jest.mock('recoil'); const setupMockRecoil = ( objectNamePlural?: string, - verifyEmailNextPath?: string, + verifyEmailRedirectPath?: string, ) => { jest .mocked(useRecoilValue) .mockReturnValueOnce([{ namePlural: objectNamePlural ?? '' }]) - .mockReturnValueOnce(verifyEmailNextPath); + .mockReturnValueOnce(verifyEmailRedirectPath); }; // prettier-ignore @@ -83,7 +83,7 @@ const testCases: { res: string | undefined; objectNamePluralFromParams?: string; objectNamePluralFromMetadata?: string; - verifyEmailNextPath?: string; + verifyEmailRedirectPath?: string; }[] = [ { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, { loc: AppPath.Verify, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' }, @@ -126,9 +126,9 @@ const testCases: { { loc: AppPath.ResetPassword, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.COMPLETED, res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, res: AppPath.PlanRequired }, - { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailNextPath: '/nextPath?key=value', res: '/nextPath?key=value' }, + { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PLAN_REQUIRED, verifyEmailRedirectPath: '/nextPath?key=value', res: '/nextPath?key=value' }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: true, onboardingStatus: OnboardingStatus.COMPLETED, res: '/settings/billing' }, - { loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailNextPath: '/nextPath?key=value', res: undefined }, + { loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, verifyEmailRedirectPath: '/nextPath?key=value', res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: false, isWorkspaceSuspended: false, onboardingStatus: undefined, res: undefined }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.WORKSPACE_ACTIVATION, res: AppPath.CreateWorkspace }, { loc: AppPath.VerifyEmail, isLoggedIn: true, isWorkspaceSuspended: false, onboardingStatus: OnboardingStatus.PROFILE_CREATION, res: AppPath.CreateProfile }, @@ -330,7 +330,7 @@ describe('usePageChangeEffectNavigateLocation', () => { isLoggedIn, objectNamePluralFromParams, objectNamePluralFromMetadata, - verifyEmailNextPath, + verifyEmailRedirectPath, res, }) => { setupMockIsMatchingLocation(loc); @@ -338,7 +338,7 @@ describe('usePageChangeEffectNavigateLocation', () => { setupMockIsWorkspaceActivationStatusEqualsTo(isWorkspaceSuspended); setupMockIsLogged(isLoggedIn); setupMockUseParams(objectNamePluralFromParams); - setupMockRecoil(objectNamePluralFromMetadata, verifyEmailNextPath); + setupMockRecoil(objectNamePluralFromMetadata, verifyEmailRedirectPath); expect(usePageChangeEffectNavigateLocation()).toEqual(res); }, @@ -351,7 +351,8 @@ describe('usePageChangeEffectNavigateLocation', () => { ['isWorkspaceSuspended:true', 'isWorkspaceSuspended:false'] .length) + ['nonExistingObjectInParam', 'existingObjectInParam:false'].length + - ['caseWithRedirectionToVerifyEmailNextPath', 'caseWithout'].length, + ['caseWithRedirectionToVerifyEmailRedirectPath', 'caseWithout'] + .length, ); }); }); diff --git a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts index 95173056b6..d5c9a71bf7 100644 --- a/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts +++ b/packages/twenty-front/src/hooks/usePageChangeEffectNavigateLocation.ts @@ -1,4 +1,4 @@ -import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; +import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState'; import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; @@ -48,7 +48,7 @@ export const usePageChangeEffectNavigateLocation = () => { const objectMetadataItem = objectMetadataItems?.find( (objectMetadataItem) => objectMetadataItem.namePlural === objectNamePlural, ); - const verifyEmailNextPath = useRecoilValue(verifyEmailNextPathState); + const verifyEmailRedirectPath = useRecoilValue(verifyEmailRedirectPathState); if ( (!isLoggedIn || (isLoggedIn && !isOnAWorkspace)) && @@ -71,9 +71,9 @@ export const usePageChangeEffectNavigateLocation = () => { ) { if ( isMatchingLocation(location, AppPath.VerifyEmail) && - isDefined(verifyEmailNextPath) + isDefined(verifyEmailRedirectPath) ) { - return verifyEmailNextPath; + return verifyEmailRedirectPath; } return AppPath.PlanRequired; } diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts index 9a8b9c4b63..13e38d3e49 100644 --- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts +++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts @@ -12,7 +12,10 @@ jest.mock('@/auth/services/AuthService', () => { ...initialAuthService, renewToken: jest.fn().mockReturnValue( Promise.resolve({ - accessToken: { token: 'newAccessToken', expiresAt: '' }, + accessOrWorkspaceAgnosticToken: { + token: 'newAccessToken', + expiresAt: '', + }, refreshToken: { token: 'newRefreshToken', expiresAt: '' }, }), ), diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index 788b555ecb..d3dfffe179 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -109,8 +109,8 @@ export class ApolloFactory implements ApolloManager { headers: { ...headers, ...options.headers, - authorization: tokenPair.accessToken.token - ? `Bearer ${tokenPair.accessToken.token}` + authorization: tokenPair.accessOrWorkspaceAgnosticToken.token + ? `Bearer ${tokenPair.accessOrWorkspaceAgnosticToken.token}` : '', ...(this.currentWorkspaceMember?.locale ? { 'x-locale': this.currentWorkspaceMember.locale } diff --git a/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts b/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts index b6dbc0d6a3..60b16e4856 100644 --- a/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts +++ b/packages/twenty-front/src/modules/apollo/utils/hasTokenPair.ts @@ -3,5 +3,8 @@ import { isDefined } from 'twenty-shared/utils'; export const hasTokenPair = () => { const tokenPair = getTokenPair(); - return isDefined(tokenPair) && isDefined(tokenPair.accessToken?.token); + return ( + isDefined(tokenPair) && + isDefined(tokenPair.accessOrWorkspaceAgnosticToken?.token) + ); }; diff --git a/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts b/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts deleted file mode 100644 index 71882210b6..0000000000 --- a/packages/twenty-front/src/modules/app/states/verifyEmailNextPathState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui/utilities'; - -export const verifyEmailNextPathState = createState({ - key: 'verifyEmailNextPathState', - defaultValue: undefined, -}); diff --git a/packages/twenty-front/src/modules/app/states/verifyEmailRedirectPathState.ts b/packages/twenty-front/src/modules/app/states/verifyEmailRedirectPathState.ts new file mode 100644 index 0000000000..9cee32b0e0 --- /dev/null +++ b/packages/twenty-front/src/modules/app/states/verifyEmailRedirectPathState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; + +export const verifyEmailRedirectPathState = createState({ + key: 'verifyEmailRedirectPathState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx index fad0b7d578..bcbe1d24cf 100644 --- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx +++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx @@ -3,7 +3,7 @@ import { AppPath } from '@/types/AppPath'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { ApolloError } from '@apollo/client'; -import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; +import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState'; import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin'; import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; import { Modal } from '@/ui/layout/modal/components/Modal'; @@ -15,24 +15,31 @@ import { isDefined } from 'twenty-shared/utils'; import { useNavigateApp } from '~/hooks/useNavigateApp'; import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl'; import { EmailVerificationSent } from '../sign-in-up/components/EmailVerificationSent'; +import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace'; export const VerifyEmailEffect = () => { - const { getLoginTokenFromEmailVerificationToken } = useAuth(); + const { + getLoginTokenFromEmailVerificationToken, + getWorkspaceAgnosticTokenFromEmailVerificationToken, + } = useAuth(); const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar(); const [searchParams] = useSearchParams(); const [isError, setIsError] = useState(false); - const setVerifyEmailNextPath = useSetRecoilState(verifyEmailNextPathState); + const setVerifyEmailRedirectPath = useSetRecoilState( + verifyEmailRedirectPathState, + ); const email = searchParams.get('email'); const emailVerificationToken = searchParams.get('emailVerificationToken'); - const verifyEmailNextPath = searchParams.get('nextPath'); + const verifyEmailRedirectPath = searchParams.get('nextPath'); const navigate = useNavigateApp(); const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const { verifyLoginToken } = useVerifyLogin(); + const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace(); const { t } = useLingui(); useEffect(() => { @@ -47,19 +54,30 @@ export const VerifyEmailEffect = () => { return navigate(AppPath.SignInUp); } + const successSnackbarParams = { + message: t`Email verified.`, + options: { + dedupeKey: 'email-verification-dedupe-key', + }, + }; + try { + if (!isOnAWorkspace) { + await getWorkspaceAgnosticTokenFromEmailVerificationToken( + emailVerificationToken, + email, + ); + + return enqueueSuccessSnackBar(successSnackbarParams); + } + const { loginToken, workspaceUrls } = await getLoginTokenFromEmailVerificationToken( emailVerificationToken, email, ); - enqueueSuccessSnackBar({ - message: t`Email verified.`, - options: { - dedupeKey: 'email-verification-dedupe-key', - }, - }); + enqueueSuccessSnackBar(successSnackbarParams); const workspaceUrl = getWorkspaceUrl(workspaceUrls); if (workspaceUrl.slice(0, -1) !== window.location.origin) { @@ -68,11 +86,11 @@ export const VerifyEmailEffect = () => { }); } - if (isDefined(verifyEmailNextPath)) { - setVerifyEmailNextPath(verifyEmailNextPath); + if (isDefined(verifyEmailRedirectPath)) { + setVerifyEmailRedirectPath(verifyEmailRedirectPath); } - verifyLoginToken(loginToken.token); + return verifyLoginToken(loginToken.token); } catch (error) { enqueueErrorSnackBar({ ...(error instanceof ApolloError @@ -95,7 +113,6 @@ export const VerifyEmailEffect = () => { }; verifyEmailToken(); - // Verify email only needs to run once at mount // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/packages/twenty-front/src/modules/auth/graphql/fragments/authFragments.ts b/packages/twenty-front/src/modules/auth/graphql/fragments/authFragments.ts index 9896f91439..14d5068b4b 100644 --- a/packages/twenty-front/src/modules/auth/graphql/fragments/authFragments.ts +++ b/packages/twenty-front/src/modules/auth/graphql/fragments/authFragments.ts @@ -7,9 +7,9 @@ export const AUTH_TOKEN = gql` } `; -export const AUTH_TOKENS = gql` - fragment AuthTokensFragment on AuthTokenPair { - accessToken { +export const AUTH_TOKEN_PAIR = gql` + fragment AuthTokenPairFragment on AuthTokenPair { + accessOrWorkspaceAgnosticToken { ...AuthTokenFragment } refreshToken { diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokensFromLoginToken.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getAccessTokensFromLoginToken.ts similarity index 69% rename from packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokensFromLoginToken.ts rename to packages/twenty-front/src/modules/auth/graphql/mutations/getAccessTokensFromLoginToken.ts index 8d01b46152..7be14d35d1 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokensFromLoginToken.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getAccessTokensFromLoginToken.ts @@ -1,10 +1,10 @@ import { gql } from '@apollo/client'; export const GET_AUTH_TOKENS_FROM_LOGIN_TOKEN = gql` - mutation GetAuthTokensFromLoginToken($loginToken: String!, $origin: String!) { + mutation getAuthTokensFromLoginToken($loginToken: String!, $origin: String!) { getAuthTokensFromLoginToken(loginToken: $loginToken, origin: $origin) { tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokenFromOTP.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokenFromOTP.ts index 2a3a586d33..f11e0e5a11 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokenFromOTP.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getAuthTokenFromOTP.ts @@ -14,7 +14,7 @@ export const GET_AUTH_TOKENS_FROM_OTP = gql` origin: $origin ) { tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/getWorkspaceAgnosticTokenFromEmailVerificationToken.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/getWorkspaceAgnosticTokenFromEmailVerificationToken.ts new file mode 100644 index 0000000000..bff7821e84 --- /dev/null +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/getWorkspaceAgnosticTokenFromEmailVerificationToken.ts @@ -0,0 +1,22 @@ +import { gql } from '@apollo/client'; + +export const GET_WORKSPACE_AGNOSTIC_TOKEN_FROM_EMAIL_VERIFICATION_TOKEN = gql` + mutation GetWorkspaceAgnosticTokenFromEmailVerificationToken( + $emailVerificationToken: String! + $email: String! + $captchaToken: String + ) { + getWorkspaceAgnosticTokenFromEmailVerificationToken( + emailVerificationToken: $emailVerificationToken + email: $email + captchaToken: $captchaToken + ) { + availableWorkspaces { + ...AvailableWorkspacesFragment + } + tokens { + ...AuthTokenPairFragment + } + } + } +`; diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/renewToken.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/renewToken.ts index 46bf00ffe4..6525f6a562 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/renewToken.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/renewToken.ts @@ -4,7 +4,7 @@ export const RENEW_TOKEN = gql` mutation RenewToken($appToken: String!) { renewToken(appToken: $appToken) { tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signIn.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signIn.ts index 99f4953b09..81a889d438 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signIn.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signIn.ts @@ -7,7 +7,7 @@ export const SIGN_IN = gql` ...AvailableWorkspacesFragment } tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts index 437211565b..f2c9ccfdb3 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signUp.ts @@ -1,13 +1,25 @@ import { gql } from '@apollo/client'; export const SIGN_UP = gql` - mutation SignUp($email: String!, $password: String!, $captchaToken: String) { - signUp(email: $email, password: $password, captchaToken: $captchaToken) { + mutation SignUp( + $email: String! + $password: String! + $captchaToken: String + $locale: String + $verifyEmailRedirectPath: String + ) { + signUp( + email: $email + password: $password + captchaToken: $captchaToken + locale: $locale + verifyEmailRedirectPath: $verifyEmailRedirectPath + ) { availableWorkspaces { ...AvailableWorkspacesFragment } tokens { - ...AuthTokensFragment + ...AuthTokenPairFragment } } } diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/signUpInWorkspace.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/signUpInWorkspace.ts index 49d25882ca..9e05a2ecb1 100644 --- a/packages/twenty-front/src/modules/auth/graphql/mutations/signUpInWorkspace.ts +++ b/packages/twenty-front/src/modules/auth/graphql/mutations/signUpInWorkspace.ts @@ -9,7 +9,7 @@ export const SIGN_UP_IN_WORKSPACE = gql` $captchaToken: String $workspaceId: String $locale: String - $verifyEmailNextPath: String + $verifyEmailRedirectPath: String ) { signUpInWorkspace( email: $email @@ -19,7 +19,7 @@ export const SIGN_UP_IN_WORKSPACE = gql` captchaToken: $captchaToken workspaceId: $workspaceId locale: $locale - verifyEmailNextPath: $verifyEmailNextPath + verifyEmailRedirectPath: $verifyEmailRedirectPath ) { loginToken { ...AuthTokenFragment diff --git a/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts index 8833fadda0..1f7ca9de5a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/__mocks__/useAuth.ts @@ -1,9 +1,9 @@ import { - GetAuthTokensFromLoginTokenDocument, - GetCurrentUserDocument, - GetLoginTokenFromCredentialsDocument, - SignUpDocument, - SignUpInWorkspaceDocument, + GetAuthTokensFromLoginTokenDocument, + GetCurrentUserDocument, + GetLoginTokenFromCredentialsDocument, + SignUpDocument, + SignUpInWorkspaceDocument, } from '~/generated-metadata/graphql'; export const queries = { @@ -51,7 +51,7 @@ export const results = { }, getAuthTokensFromLoginToken: { tokens: { - accessToken: { token, expiresAt: 'expiresAt' }, + accessOrWorkspaceAgnosticToken: { token, expiresAt: 'expiresAt' }, refreshToken: { token, expiresAt: 'expiresAt' }, }, }, diff --git a/packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts b/packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts index cd9487bade..4f88277a7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts +++ b/packages/twenty-front/src/modules/auth/hooks/__tests__/useIsLogged.test.ts @@ -30,7 +30,7 @@ describe('useIsLogged', () => { await act(async () => { result.current.setTokenPair({ - accessToken: { + accessOrWorkspaceAgnosticToken: { expiresAt: '', token: 'testToken', }, diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 030b8f89e0..f586367f6d 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -24,6 +24,7 @@ import { useGetCurrentUserLazyQuery, useGetLoginTokenFromCredentialsMutation, useGetLoginTokenFromEmailVerificationTokenMutation, + useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation, useSignInMutation, useSignUpInWorkspaceMutation, useSignUpMutation, @@ -72,7 +73,7 @@ import { useRefreshObjectMetadataItems } from '@/object-metadata/hooks/useRefres import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { i18n } from '@lingui/core'; import { useNavigate, useSearchParams } from 'react-router-dom'; -import { APP_LOCALES } from 'twenty-shared/translations'; +import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { isDefined } from 'twenty-shared/utils'; import { iconsState } from 'twenty-ui/display'; import { AuthToken } from '~/generated/graphql'; @@ -117,6 +118,8 @@ export const useAuth = () => { useGetAuthTokensFromLoginTokenMutation(); const [getLoginTokenFromEmailVerificationToken] = useGetLoginTokenFromEmailVerificationTokenMutation(); + const [getWorkspaceAgnosticTokenFromEmailVerificationToken] = + useGetWorkspaceAgnosticTokenFromEmailVerificationTokenMutation(); const [getCurrentUser] = useGetCurrentUserLazyQuery(); const [getAuthTokensFromOtp] = useGetAuthTokensFromOtpMutation(); @@ -204,6 +207,111 @@ export const useAuth = () => { [navigate, client, goToRecoilSnapshot, setLastAuthenticateWorkspaceDomain], ); + const loadCurrentUser = useCallback(async () => { + const currentUserResult = await getCurrentUser({ + fetchPolicy: 'network-only', + }); + + if (isDefined(currentUserResult.error)) { + throw new Error(currentUserResult.error.message); + } + + const user = currentUserResult.data?.currentUser; + + if (!user) { + throw new Error('No current user result'); + } + + let workspaceMember = null; + + setCurrentUser(user); + + if (isDefined(user.workspaceMembers)) { + const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({ + ...workspaceMember, + colorScheme: workspaceMember.colorScheme as ColorScheme, + locale: workspaceMember.locale ?? SOURCE_LOCALE, + })); + + setCurrentWorkspaceMembers(workspaceMembers); + } + + if (isDefined(user.availableWorkspaces)) { + setAvailableWorkspaces(user.availableWorkspaces); + } + + if (isDefined(user.currentUserWorkspace)) { + setCurrentUserWorkspace(user.currentUserWorkspace); + } + + if (isDefined(user.workspaceMember)) { + workspaceMember = { + ...user.workspaceMember, + colorScheme: user.workspaceMember?.colorScheme as ColorScheme, + locale: user.workspaceMember?.locale ?? SOURCE_LOCALE, + }; + + setCurrentWorkspaceMember(workspaceMember); + + // TODO: factorize with UserProviderEffect + setDateTimeFormat({ + timeZone: + workspaceMember.timeZone && workspaceMember.timeZone !== 'system' + ? workspaceMember.timeZone + : detectTimeZone(), + dateFormat: isDefined(user.workspaceMember.dateFormat) + ? getDateFormatFromWorkspaceDateFormat( + user.workspaceMember.dateFormat, + ) + : DateFormat[detectDateFormat()], + timeFormat: isDefined(user.workspaceMember.timeFormat) + ? getTimeFormatFromWorkspaceTimeFormat( + user.workspaceMember.timeFormat, + ) + : TimeFormat[detectTimeFormat()], + }); + dynamicActivate( + (workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE, + ); + } + + const workspace = user.currentWorkspace ?? null; + + setCurrentWorkspace(workspace); + + if (isDefined(workspace) && isOnAWorkspace) { + setLastAuthenticateWorkspaceDomain({ + workspaceId: workspace.id, + workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls), + }); + } + + return { + user, + workspaceMember, + workspace, + }; + }, [ + getCurrentUser, + isOnAWorkspace, + setCurrentUser, + setCurrentUserWorkspace, + setCurrentWorkspace, + setCurrentWorkspaceMember, + setCurrentWorkspaceMembers, + setDateTimeFormat, + setLastAuthenticateWorkspaceDomain, + setAvailableWorkspaces, + ]); + + const handleSetAuthTokens = useCallback( + (tokens: AuthTokenPair) => { + setTokenPair(tokens); + cookieStorage.setItem('tokenPair', JSON.stringify(tokens)); + }, + [setTokenPair], + ); + const handleGetLoginTokenFromCredentials = useCallback( async (email: string, password: string, captchaToken?: string) => { try { @@ -268,109 +376,48 @@ export const useAuth = () => { [getLoginTokenFromEmailVerificationToken, origin], ); - const loadCurrentUser = useCallback(async () => { - const currentUserResult = await getCurrentUser({ - fetchPolicy: 'network-only', - }); + const handleGetWorkspaceAgnosticTokenFromEmailVerificationToken = useCallback( + async ( + emailVerificationToken: string, + email: string, + captchaToken?: string, + ) => { + const { data, errors } = + await getWorkspaceAgnosticTokenFromEmailVerificationToken({ + variables: { + email, + emailVerificationToken, + captchaToken, + }, + }); - if (isDefined(currentUserResult.error)) { - throw new Error(currentUserResult.error.message); - } + if (isDefined(errors)) { + throw errors; + } - const user = currentUserResult.data?.currentUser; + if (!data?.getWorkspaceAgnosticTokenFromEmailVerificationToken) { + throw new Error('No workspace agnostic token in result'); + } - if (!user) { - throw new Error('No current user result'); - } - - let workspaceMember = null; - - setCurrentUser(user); - - if (isDefined(user.workspaceMembers)) { - const workspaceMembers = user.workspaceMembers.map((workspaceMember) => ({ - ...workspaceMember, - colorScheme: workspaceMember.colorScheme as ColorScheme, - locale: workspaceMember.locale ?? 'en', - })); - - setCurrentWorkspaceMembers(workspaceMembers); - } - - if (isDefined(user.availableWorkspaces)) { - setAvailableWorkspaces(user.availableWorkspaces); - } - - if (isDefined(user.currentUserWorkspace)) { - setCurrentUserWorkspace(user.currentUserWorkspace); - } - - if (isDefined(user.workspaceMember)) { - workspaceMember = { - ...user.workspaceMember, - colorScheme: user.workspaceMember?.colorScheme as ColorScheme, - locale: user.workspaceMember?.locale ?? 'en', - }; - - setCurrentWorkspaceMember(workspaceMember); - - // TODO: factorize with UserProviderEffect - setDateTimeFormat({ - timeZone: - workspaceMember.timeZone && workspaceMember.timeZone !== 'system' - ? workspaceMember.timeZone - : detectTimeZone(), - dateFormat: isDefined(user.workspaceMember.dateFormat) - ? getDateFormatFromWorkspaceDateFormat( - user.workspaceMember.dateFormat, - ) - : DateFormat[detectDateFormat()], - timeFormat: isDefined(user.workspaceMember.timeFormat) - ? getTimeFormatFromWorkspaceTimeFormat( - user.workspaceMember.timeFormat, - ) - : TimeFormat[detectTimeFormat()], - }); - dynamicActivate( - (workspaceMember.locale as keyof typeof APP_LOCALES) ?? 'en', + handleSetAuthTokens( + data.getWorkspaceAgnosticTokenFromEmailVerificationToken.tokens, ); - } - const workspace = user.currentWorkspace ?? null; + const { user } = await loadCurrentUser(); - setCurrentWorkspace(workspace); + if (countAvailableWorkspaces(user.availableWorkspaces) === 0) { + return await createWorkspace({ newTab: false }); + } - if (isDefined(workspace) && isOnAWorkspace) { - setLastAuthenticateWorkspaceDomain({ - workspaceId: workspace.id, - workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls), - }); - } - - return { - user, - workspaceMember, - workspace, - }; - }, [ - getCurrentUser, - isOnAWorkspace, - setCurrentUser, - setCurrentUserWorkspace, - setCurrentWorkspace, - setCurrentWorkspaceMember, - setCurrentWorkspaceMembers, - setDateTimeFormat, - setLastAuthenticateWorkspaceDomain, - setAvailableWorkspaces, - ]); - - const handleSetAuthTokens = useCallback( - (tokens: AuthTokenPair) => { - setTokenPair(tokens); - cookieStorage.setItem('tokenPair', JSON.stringify(tokens)); + setSignInUpStep(SignInUpStep.WorkspaceSelection); }, - [setTokenPair], + [ + createWorkspace, + getWorkspaceAgnosticTokenFromEmailVerificationToken, + handleSetAuthTokens, + loadCurrentUser, + setSignInUpStep, + ], ); const handleSetLoginToken = useCallback( @@ -507,13 +554,24 @@ export const useAuth = () => { const handleCredentialsSignUp = useCallback( async (email: string, password: string, captchaToken?: string) => { const signUpResult = await signUp({ - variables: { email, password, captchaToken }, + variables: { + email, + password, + captchaToken, + locale: i18n.locale ?? SOURCE_LOCALE, + }, }); if (isDefined(signUpResult.errors)) { throw signUpResult.errors; } + if (isEmailVerificationRequired) { + setSearchParams({ email }); + setSignInUpStep(SignInUpStep.EmailVerification); + return null; + } + if (!signUpResult.data?.signUp) { throw new Error('No signUp result'); } @@ -529,6 +587,8 @@ export const useAuth = () => { setSignInUpStep(SignInUpStep.WorkspaceSelection); }, [ + isEmailVerificationRequired, + setSearchParams, handleSetAuthTokens, signUp, loadCurrentUser, @@ -561,14 +621,14 @@ export const useAuth = () => { workspaceInviteHash, workspacePersonalInviteToken, captchaToken, - verifyEmailNextPath, + verifyEmailRedirectPath, }: { email: string; password: string; workspaceInviteHash?: string; workspacePersonalInviteToken?: string; captchaToken?: string; - verifyEmailNextPath?: string; + verifyEmailRedirectPath?: string; }) => { const signUpInWorkspaceResult = await signUpInWorkspace({ variables: { @@ -577,11 +637,11 @@ export const useAuth = () => { workspaceInviteHash, workspacePersonalInviteToken, captchaToken, - locale: i18n.locale ?? 'en', + locale: i18n.locale ?? SOURCE_LOCALE, ...(workspacePublicData?.id ? { workspaceId: workspacePublicData.id } : {}), - verifyEmailNextPath, + verifyEmailRedirectPath, }, }); @@ -712,7 +772,7 @@ export const useAuth = () => { } if (!getAuthTokensFromOtpResult.data?.getAuthTokensFromOTP) { - throw new Error('No getAuthTokensFromLoginToken result'); + throw new Error('No getAuthTokensFromOTP result'); } await handleLoadWorkspaceAfterAuthentication( @@ -724,6 +784,8 @@ export const useAuth = () => { return { getLoginTokenFromCredentials: handleGetLoginTokenFromCredentials, + getWorkspaceAgnosticTokenFromEmailVerificationToken: + handleGetWorkspaceAgnosticTokenFromEmailVerificationToken, getLoginTokenFromEmailVerificationToken: handleGetLoginTokenFromEmailVerificationToken, getAuthTokensFromLoginToken: handleGetAuthTokensFromLoginToken, diff --git a/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts b/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts index d8c6a319d2..bb2b4ec0b0 100644 --- a/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts +++ b/packages/twenty-front/src/modules/auth/services/__tests__/AuthService.test.ts @@ -6,7 +6,7 @@ import { renewToken } from '@/auth/services/AuthService'; enableFetchMocks(); const tokens = { - accessToken: { + accessOrWorkspaceAgnosticToken: { token: 'accessToken', expiresAt: 'expiresAt', }, diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts index a2f5d6e33b..fa6a413e54 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.ts @@ -130,7 +130,7 @@ export const useSignInUp = (form: UseFormReturn
) => { ); } - const verifyEmailNextPath = buildAppPathWithQueryParams( + const verifyEmailRedirectPath = buildAppPathWithQueryParams( AppPath.PlanRequired, await buildSearchParamsFromUrlSyncedStates(), ); @@ -141,7 +141,7 @@ export const useSignInUp = (form: UseFormReturn) => { workspaceInviteHash, workspacePersonalInviteToken, captchaToken: token, - verifyEmailNextPath, + verifyEmailRedirectPath, }); } catch (error: any) { enqueueErrorSnackBar({ diff --git a/packages/twenty-front/src/modules/auth/states/tokenPairState.ts b/packages/twenty-front/src/modules/auth/states/tokenPairState.ts index 0e13c7abee..0c75277b3f 100644 --- a/packages/twenty-front/src/modules/auth/states/tokenPairState.ts +++ b/packages/twenty-front/src/modules/auth/states/tokenPairState.ts @@ -1,6 +1,6 @@ +import { createState } from 'twenty-ui/utilities'; import { AuthTokenPair } from '~/generated/graphql'; import { cookieStorageEffect } from '~/utils/recoil-effects'; -import { createState } from 'twenty-ui/utilities'; export const tokenPairState = createState({ key: 'tokenPairState', @@ -11,7 +11,7 @@ export const tokenPairState = createState({ {}, { validateInitFn: (payload: AuthTokenPair) => - Boolean(payload['accessToken']), + Boolean(payload['accessOrWorkspaceAgnosticToken']), }, ), ], diff --git a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useOnboardingStatus.test.ts b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useOnboardingStatus.test.ts index c9323829b5..4686e37b56 100644 --- a/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useOnboardingStatus.test.ts +++ b/packages/twenty-front/src/modules/onboarding/hooks/__tests__/useOnboardingStatus.test.ts @@ -8,7 +8,10 @@ import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus'; import { OnboardingStatus } from '~/generated/graphql'; const tokenPair = { - accessToken: { token: 'accessToken', expiresAt: 'expiresAt' }, + accessOrWorkspaceAgnosticToken: { + token: 'accessToken', + expiresAt: 'expiresAt', + }, refreshToken: { token: 'refreshToken', expiresAt: 'expiresAt' }, }; const currentUser = { diff --git a/packages/twenty-front/src/modules/subscription/hooks/useOnDbEvent.ts b/packages/twenty-front/src/modules/subscription/hooks/useOnDbEvent.ts index 1712ad9308..5c38ab8686 100644 --- a/packages/twenty-front/src/modules/subscription/hooks/useOnDbEvent.ts +++ b/packages/twenty-front/src/modules/subscription/hooks/useOnDbEvent.ts @@ -1,9 +1,9 @@ -import { useEffect, useMemo } from 'react'; -import { createClient } from 'graphql-sse'; -import { ON_DB_EVENT } from '@/subscription/graphql/subscriptions/onDbEvent'; -import { Subscription, SubscriptionOnDbEventArgs } from '~/generated/graphql'; -import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { getTokenPair } from '@/apollo/utils/getTokenPair'; +import { ON_DB_EVENT } from '@/subscription/graphql/subscriptions/onDbEvent'; +import { createClient } from 'graphql-sse'; +import { useEffect, useMemo } from 'react'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; +import { Subscription, SubscriptionOnDbEventArgs } from '~/generated/graphql'; type OnDbEventArgs = SubscriptionOnDbEventArgs & { skip?: boolean; @@ -25,12 +25,12 @@ export const useOnDbEvent = ({ return createClient({ url: `${REACT_APP_SERVER_BASE_URL}/graphql`, headers: { - Authorization: tokenPair?.accessToken.token - ? `Bearer ${tokenPair?.accessToken.token}` + Authorization: tokenPair?.accessOrWorkspaceAgnosticToken.token + ? `Bearer ${tokenPair?.accessOrWorkspaceAgnosticToken.token}` : '', }, }); - }, [tokenPair?.accessToken.token]); + }, [tokenPair?.accessOrWorkspaceAgnosticToken.token]); useEffect(() => { if (skip === true) { diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 90101d8b27..1e71a795be 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -1,4 +1,4 @@ -import { verifyEmailNextPathState } from '@/app/states/verifyEmailNextPathState'; +import { verifyEmailRedirectPathState } from '@/app/states/verifyEmailRedirectPathState'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { useAuth } from '@/auth/hooks/useAuth'; @@ -101,11 +101,11 @@ export const ChooseYourPlan = () => { const calendarBookingPageId = useRecoilValue(calendarBookingPageIdState); - const [verifyEmailNextPath, setVerifyEmailNextPath] = useRecoilState( - verifyEmailNextPathState, + const [verifyEmailRedirectPath, setVerifyEmailRedirectPath] = useRecoilState( + verifyEmailRedirectPathState, ); - if (isDefined(verifyEmailNextPath)) { - setVerifyEmailNextPath(undefined); + if (isDefined(verifyEmailRedirectPath)) { + setVerifyEmailRedirectPath(undefined); } const { data: plans } = useBillingBaseProductPricesQuery(); diff --git a/packages/twenty-server/lingui.config.ts b/packages/twenty-server/lingui.config.ts index 6ca360991b..78ed72e7e6 100644 --- a/packages/twenty-server/lingui.config.ts +++ b/packages/twenty-server/lingui.config.ts @@ -3,7 +3,7 @@ import { formatter } from '@lingui/format-po'; import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; export default defineConfig({ - sourceLocale: 'en', + sourceLocale: SOURCE_LOCALE, locales: Object.values(APP_LOCALES), pseudoLocale: 'pseudo-en', fallbackLocales: { diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts index 0e8297f8d5..86efeb4bc3 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts @@ -77,10 +77,10 @@ export class ApprovedAccessDomainService { lastName: sender.name.lastName, }, serverUrl: this.twentyConfigService.get('SERVER_URL'), - locale: 'en', + locale: sender.locale, }); - const html = await render(emailTemplate); - const text = await render(emailTemplate, { + const html = render(emailTemplate); + const text = render(emailTemplate, { plainText: true, }); 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 3467cbc89f..5fac9141e2 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 @@ -27,9 +27,9 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { AvailableWorkspacesAndAccessTokensOutput } from 'src/engine/core-modules/auth/dto/available-workspaces-and-access-tokens.output'; +import { GetAuthTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-auth-token-from-email-verification-token.input'; import { GetAuthorizationUrlForSSOInput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.input'; import { GetAuthorizationUrlForSSOOutput } from 'src/engine/core-modules/auth/dto/get-authorization-url-for-sso.output'; -import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input'; import { GetLoginTokenFromEmailVerificationTokenOutput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.output'; import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output'; import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service'; @@ -51,6 +51,7 @@ import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/re import { I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { TwoFactorAuthenticationVerificationInput } from 'src/engine/core-modules/two-factor-authentication/dto/two-factor-authentication-verification.input'; +import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication-exception.filter'; import { TwoFactorAuthenticationService } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; @@ -67,7 +68,6 @@ import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication-exception.filter'; import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input'; import { LoginToken } from './dto/login-token.entity'; @@ -213,7 +213,7 @@ export class AuthResolver { AuthProviderEnum.Password, ), tokens: { - accessToken: + accessOrWorkspaceAgnosticToken: await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken( { userId: user.id, @@ -233,13 +233,13 @@ export class AuthResolver { @UseGuards(PublicEndpointGuard) async getLoginTokenFromEmailVerificationToken( @Args() - getLoginTokenFromEmailVerificationTokenInput: GetLoginTokenFromEmailVerificationTokenInput, + getAuthTokenFromEmailVerificationTokenInput: GetAuthTokenFromEmailVerificationTokenInput, @Args('origin') origin: string, @AuthProvider() authProvider: AuthProviderEnum, ) { const appToken = await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow( - getLoginTokenFromEmailVerificationTokenInput, + getAuthTokenFromEmailVerificationTokenInput, ); const workspace = @@ -264,6 +264,50 @@ export class AuthResolver { return { loginToken, workspaceUrls }; } + @Mutation(() => AvailableWorkspacesAndAccessTokensOutput) + @UseGuards(PublicEndpointGuard) + async getWorkspaceAgnosticTokenFromEmailVerificationToken( + @Args() + getAuthTokenFromEmailVerificationTokenInput: GetAuthTokenFromEmailVerificationTokenInput, + @AuthProvider() authProvider: AuthProviderEnum, + ) { + const appToken = + await this.emailVerificationTokenService.validateEmailVerificationTokenOrThrow( + getAuthTokenFromEmailVerificationTokenInput, + ); + + await this.userService.markEmailAsVerified(appToken.user.id); + await this.appTokenRepository.remove(appToken); + + const availableWorkspaces = + await this.userWorkspaceService.findAvailableWorkspacesByEmail( + appToken.user.email, + ); + + return { + availableWorkspaces: + await this.userWorkspaceService.setLoginTokenToAvailableWorkspacesWhenAuthProviderMatch( + availableWorkspaces, + appToken.user, + authProvider, + ), + tokens: { + accessOrWorkspaceAgnosticToken: + await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken( + { + userId: appToken.user.id, + authProvider: AuthProviderEnum.Password, + }, + ), + refreshToken: await this.refreshTokenService.generateRefreshToken({ + userId: appToken.user.id, + authProvider: AuthProviderEnum.Password, + targetedTokenType: JwtTokenTypeEnum.WORKSPACE_AGNOSTIC, + }), + }, + }; + } + @Mutation(() => AuthTokens) @UseGuards(CaptchaGuard, PublicEndpointGuard) async getAuthTokensFromOTP( @@ -321,6 +365,14 @@ export class AuthResolver { user.email, ); + await this.emailVerificationService.sendVerificationEmail( + user.id, + user.email, + undefined, + signUpInput.locale ?? SOURCE_LOCALE, + signUpInput.verifyEmailRedirectPath, + ); + return { availableWorkspaces: await this.userWorkspaceService.setLoginTokenToAvailableWorkspacesWhenAuthProviderMatch( @@ -329,7 +381,7 @@ export class AuthResolver { AuthProviderEnum.Password, ), tokens: { - accessToken: + accessOrWorkspaceAgnosticToken: await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken( { userId: user.id, @@ -402,7 +454,7 @@ export class AuthResolver { user.email, workspace, signUpInput.locale ?? SOURCE_LOCALE, - signUpInput.verifyEmailNextPath, + signUpInput.verifyEmailRedirectPath, ); const loginToken = await this.loginTokenService.generateLoginToken( @@ -508,6 +560,8 @@ export class AuthResolver { const user = await this.userService.getUserByEmail(email); + await this.authService.checkIsEmailVerified(user.isEmailVerified); + const currentUserWorkspace = await this.userWorkspaceService.getUserWorkspaceForUserOrThrow({ userId: user.id, diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/exchange-auth-code.entity.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/exchange-auth-code.entity.ts index cc2be51457..321f12144b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/exchange-auth-code.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/exchange-auth-code.entity.ts @@ -5,7 +5,7 @@ import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity'; @ObjectType() export class ExchangeAuthCode { @Field(() => AuthToken) - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; @Field(() => AuthToken) refreshToken: AuthToken; 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-auth-token-from-email-verification-token.input.ts similarity index 86% rename from packages/twenty-server/src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input.ts rename to packages/twenty-server/src/engine/core-modules/auth/dto/get-auth-token-from-email-verification-token.input.ts index a2a923d86e..8130dca7f1 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-auth-token-from-email-verification-token.input.ts @@ -3,7 +3,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsNotEmpty, IsString, IsOptional } from 'class-validator'; @ArgsType() -export class GetLoginTokenFromEmailVerificationTokenInput { +export class GetAuthTokenFromEmailVerificationTokenInput { @Field(() => String) @IsNotEmpty() @IsString() diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts index 21f1cb51fc..1a6e3221f2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/sign-up.input.ts @@ -43,5 +43,5 @@ export class SignUpInput { @Field(() => String, { nullable: true }) @IsString() @IsOptional() - verifyEmailNextPath?: string; + verifyEmailRedirectPath?: string; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/token.entity.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/token.entity.ts index ca3a5d4ac9..002c256e7a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/token.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/token.entity.ts @@ -18,7 +18,7 @@ export class ApiKeyToken { @ObjectType() export class AuthTokenPair { @Field(() => AuthToken) - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; @Field(() => AuthToken) refreshToken: AuthToken; diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/user-credentials.input.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/user-credentials.input.ts index dcda6dcb9c..2dd64212ff 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/dto/user-credentials.input.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/user-credentials.input.ts @@ -1,6 +1,7 @@ import { ArgsType, Field } from '@nestjs/graphql'; import { IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { APP_LOCALES } from 'twenty-shared/translations'; @ArgsType() export class UserCredentialsInput { @@ -18,4 +19,14 @@ export class UserCredentialsInput { @IsString() @IsOptional() captchaToken?: string; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + locale?: keyof typeof APP_LOCALES; + + @Field(() => String, { nullable: true }) + @IsString() + @IsOptional() + verifyEmailRedirectPath?: string; } diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index 30587d1a67..53a192a6d4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -174,18 +174,22 @@ export class AuthService { ); } + await this.checkIsEmailVerified(user.isEmailVerified); + + return user; + } + + async checkIsEmailVerified(isEmailVerified: boolean) { const isEmailVerificationRequired = this.twentyConfigService.get( 'IS_EMAIL_VERIFICATION_REQUIRED', ); - if (isEmailVerificationRequired && !user.isEmailVerified) { + if (isEmailVerificationRequired && !isEmailVerified) { throw new AuthException( 'Email is not verified', AuthExceptionCode.EMAIL_NOT_VERIFIED, ); } - - return user; } private async validatePassword( @@ -296,7 +300,7 @@ export class AuthService { return { tokens: { - accessToken, + accessOrWorkspaceAgnosticToken: accessToken, refreshToken, }, }; @@ -474,12 +478,12 @@ export class AuthService { locale: firstUserWorkspace.locale, }); - const html = await render(emailTemplate, { pretty: true }); - const text = await render(emailTemplate, { plainText: true }); + const html = render(emailTemplate, { pretty: true }); + const text = render(emailTemplate, { plainText: true }); i18n.activate(firstUserWorkspace.locale); - this.emailService.send({ + await this.emailService.send({ from: `${this.twentyConfigService.get( 'EMAIL_FROM_NAME', )} <${this.twentyConfigService.get('EMAIL_FROM_ADDRESS')}>`, @@ -731,7 +735,7 @@ export class AuthService { pathname: '/welcome', searchParams: { tokenPair: JSON.stringify({ - accessToken: + accessOrWorkspaceAgnosticToken: await this.workspaceAgnosticTokenService.generateWorkspaceAgnosticToken( { userId: user.id, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts index 997c0f7c56..4c7b616abe 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts @@ -158,8 +158,8 @@ export class ResetPasswordService { const emailTemplate = PasswordResetLinkEmail(emailData); - const html = await render(emailTemplate, { pretty: true }); - const text = await render(emailTemplate, { plainText: true }); + const html = render(emailTemplate, { pretty: true }); + const text = render(emailTemplate, { plainText: true }); i18n.activate(locale); diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.spec.ts index 04371f2445..4a304e3e0c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.spec.ts @@ -3,13 +3,13 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; -import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service'; +import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { User } from 'src/engine/core-modules/user/user.entity'; import { RenewTokenService } from './renew-token.service'; @@ -101,7 +101,7 @@ describe('RenewTokenService', () => { await service.generateTokensFromRefreshToken(mockRefreshToken); expect(result).toEqual({ - accessToken: mockAccessToken, + accessOrWorkspaceAgnosticToken: mockAccessToken, refreshToken: mockNewRefreshToken, }); expect(refreshTokenService.verifyRefreshToken).toHaveBeenCalledWith( diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts index be6f3cad2a..66ec505cf9 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/renew-token.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { @@ -11,8 +11,8 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { AuthToken } from 'src/engine/core-modules/auth/dto/token.entity'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; -import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service'; import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; +import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service'; import { JwtTokenTypeEnum } from 'src/engine/core-modules/auth/types/auth-context.type'; @Injectable() @@ -26,7 +26,7 @@ export class RenewTokenService { ) {} async generateTokensFromRefreshToken(token: string): Promise<{ - accessToken: AuthToken; + accessOrWorkspaceAgnosticToken: AuthToken; refreshToken: AuthToken; }> { if (!token) { @@ -80,7 +80,7 @@ export class RenewTokenService { }); return { - accessToken, + accessOrWorkspaceAgnosticToken: accessToken, refreshToken, }; } 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 dd3f8281db..0bb31f6dc0 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 @@ -2,8 +2,12 @@ import { CustomException } from 'src/utils/custom-exception'; export class EmailVerificationException extends CustomException { declare code: EmailVerificationExceptionCode; - constructor(message: string, code: EmailVerificationExceptionCode) { - super(message, code); + constructor( + message: string, + code: EmailVerificationExceptionCode, + { userFriendlyMessage }: { userFriendlyMessage?: string } = {}, + ) { + super(message, code, userFriendlyMessage ?? message); } } diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index 274aeb8566..2f563bb1c2 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -46,7 +46,7 @@ export class EmailVerificationService { | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType | undefined, locale: keyof typeof APP_LOCALES, - verifyEmailNextPath?: string, + verifyEmailRedirectPath?: string, ) { if (!this.twentyConfigService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { return { success: false }; @@ -60,8 +60,8 @@ export class EmailVerificationService { searchParams: { emailVerificationToken, email, - ...(isDefined(verifyEmailNextPath) - ? { nextPath: verifyEmailNextPath } + ...(isDefined(verifyEmailRedirectPath) + ? { nextPath: verifyEmailRedirectPath } : {}), }, }; @@ -79,13 +79,12 @@ export class EmailVerificationService { const emailTemplate = SendEmailVerificationLinkEmail(emailData); - const html = await render(emailTemplate); - const text = await render(emailTemplate, { + const html = render(emailTemplate); + const text = render(emailTemplate, { plainText: true, }); i18n.activate(locale); - await this.emailService.send({ from: `${this.twentyConfigService.get( 'EMAIL_FROM_NAME', diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts index 6897503f9a..823d234df3 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.entity.ts @@ -2,7 +2,7 @@ import { Field, ObjectType, registerEnumType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; import { PermissionsOnAllObjectRecords } from 'twenty-shared/constants'; -import { APP_LOCALES } from 'twenty-shared/translations'; +import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations'; import { Column, CreateDateColumn, @@ -73,7 +73,7 @@ export class UserWorkspace { defaultAvatarUrl: string; @Field(() => String, { nullable: false }) - @Column({ nullable: false, default: 'en', type: 'varchar' }) + @Column({ nullable: false, default: SOURCE_LOCALE, type: 'varchar' }) locale: keyof typeof APP_LOCALES; @Field() diff --git a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts index 32976643be..0d15a90cb2 100644 --- a/packages/twenty-server/src/engine/core-modules/user/user.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/user/user.entity.ts @@ -14,6 +14,7 @@ import { Relation, UpdateDateColumn, } from 'typeorm'; +import { SOURCE_LOCALE } from 'twenty-shared/translations'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; @@ -94,7 +95,7 @@ export class User { deletedAt: Date; @Field(() => String, { nullable: false }) - @Column({ nullable: false, default: 'en' }) + @Column({ nullable: false, default: SOURCE_LOCALE }) locale: string; @OneToMany(() => AppToken, (appToken) => appToken.user, { diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 3890775965..8429ce917f 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -301,8 +301,8 @@ export class WorkspaceInvitationService { }; const emailTemplate = SendInviteLinkEmail(emailData); - const html = await render(emailTemplate); - const text = await render(emailTemplate, { + const html = render(emailTemplate); + const text = render(emailTemplate, { plainText: true, }); diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts index cb26c1bb22..14a3b23ac2 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-cleaner/services/cleaner.workspace-service.ts @@ -122,8 +122,8 @@ export class CleanerWorkspaceService { locale: workspaceMember.locale, }; const emailTemplate = WarnSuspendedWorkspaceEmail(emailData); - const html = await render(emailTemplate, { pretty: true }); - const text = await render(emailTemplate, { plainText: true }); + const html = render(emailTemplate, { pretty: true }); + const text = render(emailTemplate, { plainText: true }); i18n.activate(workspaceMember.locale); @@ -198,8 +198,8 @@ export class CleanerWorkspaceService { locale: workspaceMember.locale, }; const emailTemplate = CleanSuspendedWorkspaceEmail(emailData); - const html = await render(emailTemplate, { pretty: true }); - const text = await render(emailTemplate, { plainText: true }); + const html = render(emailTemplate, { pretty: true }); + const text = render(emailTemplate, { plainText: true }); this.emailService.send({ to: workspaceMember.userEmail, diff --git a/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts index 027f38ceba..34c8f0cefe 100644 --- a/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts @@ -58,7 +58,7 @@ describe('AuthResolve (integration)', () => { mutation GetAuthTokensFromLoginToken { getAuthTokensFromLoginToken(loginToken: "${loginToken}", origin: "${ORIGIN.toString()}") { tokens { - accessToken { + accessOrWorkspaceAgnosticToken { token } } @@ -82,7 +82,7 @@ describe('AuthResolve (integration)', () => { expect(data).toBeDefined(); expect(data.tokens).toBeDefined(); - const accessToken = data.tokens.accessToken; + const accessToken = data.tokens.accessOrWorkspaceAgnosticToken; expect(accessToken).toBeDefined(); expect(accessToken.token).toBeDefined();