diff --git a/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx b/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx
index 1d595c93cfd4..377e5ce0302d 100644
--- a/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/environment-variables.mdx
@@ -57,6 +57,7 @@ import TabItem from '@theme/TabItem';
['AUTH_GOOGLE_CALLBACK_URL', '', 'Google auth callback'],
['FRONT_AUTH_CALLBACK_URL', 'http://localhost:3001/verify ', 'Callback used for Login page'],
['IS_SIGN_UP_DISABLED', 'false', 'Disable sign-up'],
+ ['PASSWORD_RESET_TOKEN_EXPIRES_IN', '5m', 'Password reset token expiration time'],
]}>
### Email
diff --git a/packages/twenty-emails/src/common-style.ts b/packages/twenty-emails/src/common-style.ts
index 6a80ebeddb0d..bd550b544824 100644
--- a/packages/twenty-emails/src/common-style.ts
+++ b/packages/twenty-emails/src/common-style.ts
@@ -25,6 +25,7 @@ export const emailTheme = {
colors: {
highlighted: grayScale.gray60,
primary: grayScale.gray50,
+ tertiary: grayScale.gray40,
inverted: grayScale.gray0,
},
weight: {
diff --git a/packages/twenty-emails/src/components/Link.tsx b/packages/twenty-emails/src/components/Link.tsx
new file mode 100644
index 000000000000..5ebcc52f9781
--- /dev/null
+++ b/packages/twenty-emails/src/components/Link.tsx
@@ -0,0 +1,16 @@
+import * as React from 'react';
+import { Link as EmailLink } from '@react-email/components';
+import { emailTheme } from 'src/common-style';
+
+const linkStyle = {
+ color: emailTheme.font.colors.tertiary,
+ textDecoration: 'underline',
+};
+
+export const Link = ({ value, href }) => {
+ return (
+
+ {value}
+
+ );
+};
diff --git a/packages/twenty-emails/src/emails/password-reset-link.email.tsx b/packages/twenty-emails/src/emails/password-reset-link.email.tsx
new file mode 100644
index 000000000000..a8355b0857d1
--- /dev/null
+++ b/packages/twenty-emails/src/emails/password-reset-link.email.tsx
@@ -0,0 +1,29 @@
+import * as React from 'react';
+import { BaseEmail } from 'src/components/BaseEmail';
+import { CallToAction } from 'src/components/CallToAction';
+import { Link } from 'src/components/Link';
+import { MainText } from 'src/components/MainText';
+import { Title } from 'src/components/Title';
+
+type PasswordResetLinkEmailProps = {
+ duration: string;
+ link: string;
+};
+
+export const PasswordResetLinkEmail = ({
+ duration,
+ link,
+}: PasswordResetLinkEmailProps) => {
+ return (
+
+
+
+
+ This link is only valid for the next {duration}. If link does not work,
+ you can use the login verification link directly:
+
+
+
+
+ );
+};
diff --git a/packages/twenty-emails/src/emails/password-update-notify.email.tsx b/packages/twenty-emails/src/emails/password-update-notify.email.tsx
new file mode 100644
index 000000000000..6eaf77b99bad
--- /dev/null
+++ b/packages/twenty-emails/src/emails/password-update-notify.email.tsx
@@ -0,0 +1,38 @@
+import * as React from 'react';
+import { format } from 'date-fns';
+import { BaseEmail } from 'src/components/BaseEmail';
+import { CallToAction } from 'src/components/CallToAction';
+import { MainText } from 'src/components/MainText';
+import { Title } from 'src/components/Title';
+
+type PasswordUpdateNotifyEmailProps = {
+ userName: string;
+ email: string;
+ link: string;
+};
+
+export const PasswordUpdateNotifyEmail = ({
+ userName,
+ email,
+ link,
+}: PasswordUpdateNotifyEmailProps) => {
+ const helloString = userName?.length > 1 ? `Dear ${userName}` : 'Dear';
+ return (
+
+
+
+ {helloString},
+
+
+ This is a confirmation that password for your account ({email}) was
+ successfully changed on {format(new Date(), 'MMMM d, yyyy')}.
+
+
+ If you did not initiate this change, please contact your workspace owner
+ immediately.
+
+
+
+
+ );
+};
diff --git a/packages/twenty-emails/tsup.index.tsx b/packages/twenty-emails/tsup.index.tsx
index e816bb7363b1..ca2deeeaaab3 100644
--- a/packages/twenty-emails/tsup.index.tsx
+++ b/packages/twenty-emails/tsup.index.tsx
@@ -1,2 +1,4 @@
export * from './src/emails/clean-inactive-workspaces.email';
export * from './src/emails/delete-inactive-workspaces.email';
+export * from './src/emails/password-reset-link.email';
+export * from './src/emails/password-update-notify.email';
diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
index 0d9506317b5b..8f17069e20d6 100644
--- a/packages/twenty-front/src/App.tsx
+++ b/packages/twenty-front/src/App.tsx
@@ -10,6 +10,7 @@ import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect';
import { useDefaultHomePagePath } from '~/hooks/useDefaultHomePagePath';
import { CreateProfile } from '~/pages/auth/CreateProfile';
import { CreateWorkspace } from '~/pages/auth/CreateWorkspace';
+import { PasswordReset } from '~/pages/auth/PasswordReset';
import { PlanRequired } from '~/pages/auth/PlanRequired';
import { SignInUp } from '~/pages/auth/SignInUp';
import { VerifyEffect } from '~/pages/auth/VerifyEffect';
@@ -59,6 +60,7 @@ export const App = () => {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
index 7b0ee2434e2e..9a165f349f77 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
@@ -54,14 +54,14 @@ export const PageChangeEffect = () => {
}, [location, previousLocation]);
useEffect(() => {
- const isMachinOngoingUserCreationRoute =
+ const isMatchingOngoingUserCreationRoute =
isMatchingLocation(AppPath.SignUp) ||
isMatchingLocation(AppPath.SignIn) ||
isMatchingLocation(AppPath.Invite) ||
isMatchingLocation(AppPath.Verify);
const isMatchingOnboardingRoute =
- isMachinOngoingUserCreationRoute ||
+ isMatchingOngoingUserCreationRoute ||
isMatchingLocation(AppPath.CreateWorkspace) ||
isMatchingLocation(AppPath.CreateProfile) ||
isMatchingLocation(AppPath.PlanRequired);
@@ -75,7 +75,8 @@ export const PageChangeEffect = () => {
if (
onboardingStatus === OnboardingStatus.OngoingUserCreation &&
- !isMachinOngoingUserCreationRoute
+ !isMatchingOngoingUserCreationRoute &&
+ !isMatchingLocation(AppPath.ResetPassword)
) {
navigate(AppPath.SignIn);
} else if (
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index c647e4a6449e..d126a0f9ef61 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -495,6 +495,8 @@ export type User = {
id: Scalars['ID']['output'];
lastName: Scalars['String']['output'];
passwordHash?: Maybe;
+ passwordResetToken?: Maybe;
+ passwordResetTokenExpiresAt?: Maybe;
supportUserHash?: Maybe;
updatedAt: Scalars['DateTime']['output'];
workspaceMember: WorkspaceMember;
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 9d21cdb89973..6d49e69e5be6 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -97,6 +97,12 @@ export type CursorPaging = {
last?: InputMaybe;
};
+export type EmailPasswordResetLink = {
+ __typename?: 'EmailPasswordResetLink';
+ /** Boolean that confirms query was dispatched */
+ success: Scalars['Boolean'];
+};
+
export type FeatureFlag = {
__typename?: 'FeatureFlag';
id: Scalars['ID'];
@@ -199,6 +205,12 @@ export type IdFilterComparison = {
notLike?: InputMaybe;
};
+export type InvalidatePassword = {
+ __typename?: 'InvalidatePassword';
+ /** Boolean that confirms query was dispatched */
+ success: Scalars['Boolean'];
+};
+
export type LoginToken = {
__typename?: 'LoginToken';
loginToken: AuthToken;
@@ -213,12 +225,14 @@ export type Mutation = {
deleteCurrentWorkspace: Workspace;
deleteOneObject: ObjectDeleteResponse;
deleteUser: User;
+ emailPasswordResetLink: EmailPasswordResetLink;
generateApiKeyToken: ApiKeyToken;
generateTransientToken: TransientToken;
impersonate: Verify;
renewToken: AuthTokens;
signUp: LoginToken;
updateOneObject: Object;
+ updatePasswordViaResetToken: InvalidatePassword;
updateWorkspace: Workspace;
uploadFile: Scalars['String'];
uploadImage: Scalars['String'];
@@ -268,6 +282,12 @@ export type MutationSignUpArgs = {
};
+export type MutationUpdatePasswordViaResetTokenArgs = {
+ newPassword: Scalars['String'];
+ passwordResetToken: Scalars['String'];
+};
+
+
export type MutationUpdateWorkspaceArgs = {
data: UpdateWorkspaceInput;
};
@@ -362,6 +382,7 @@ export type Query = {
getTimelineThreadsFromPersonId: Array;
object: Object;
objects: ObjectConnection;
+ validatePasswordResetToken: ValidatePasswordResetToken;
};
@@ -389,6 +410,11 @@ export type QueryGetTimelineThreadsFromPersonIdArgs = {
personId: Scalars['String'];
};
+
+export type QueryValidatePasswordResetTokenArgs = {
+ passwordResetToken: Scalars['String'];
+};
+
export type RefreshToken = {
__typename?: 'RefreshToken';
createdAt: Scalars['DateTime'];
@@ -500,6 +526,8 @@ export type User = {
id: Scalars['ID'];
lastName: Scalars['String'];
passwordHash?: Maybe;
+ passwordResetToken?: Maybe;
+ passwordResetTokenExpiresAt?: Maybe;
supportUserHash?: Maybe;
updatedAt: Scalars['DateTime'];
workspaceMember: WorkspaceMember;
@@ -518,6 +546,12 @@ export type UserExists = {
exists: Scalars['Boolean'];
};
+export type ValidatePasswordResetToken = {
+ __typename?: 'ValidatePasswordResetToken';
+ email: Scalars['String'];
+ id: Scalars['String'];
+};
+
export type Verify = {
__typename?: 'Verify';
tokens: AuthTokenPair;
@@ -694,6 +728,11 @@ export type ChallengeMutationVariables = Exact<{
export type ChallengeMutation = { __typename?: 'Mutation', challenge: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
+export type EmailPasswordResetLinkMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
+
export type GenerateApiKeyTokenMutationVariables = Exact<{
apiKeyId: Scalars['String'];
expiresAt: Scalars['String'];
@@ -730,6 +769,14 @@ export type SignUpMutationVariables = Exact<{
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
+export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
+ token: Scalars['String'];
+ newPassword: Scalars['String'];
+}>;
+
+
+export type UpdatePasswordViaResetTokenMutation = { __typename?: 'Mutation', updatePasswordViaResetToken: { __typename?: 'InvalidatePassword', success: boolean } };
+
export type VerifyMutationVariables = Exact<{
loginToken: Scalars['String'];
}>;
@@ -744,6 +791,13 @@ export type CheckUserExistsQueryVariables = Exact<{
export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename?: 'UserExists', exists: boolean } };
+export type ValidatePasswordResetTokenQueryVariables = Exact<{
+ token: Scalars['String'];
+}>;
+
+
+export type ValidatePasswordResetTokenQuery = { __typename?: 'Query', validatePasswordResetToken: { __typename?: 'ValidatePasswordResetToken', id: string, email: string } };
+
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
@@ -1008,6 +1062,38 @@ export function useChallengeMutation(baseOptions?: Apollo.MutationHookOptions;
export type ChallengeMutationResult = Apollo.MutationResult;
export type ChallengeMutationOptions = Apollo.BaseMutationOptions;
+export const EmailPasswordResetLinkDocument = gql`
+ mutation EmailPasswordResetLink {
+ emailPasswordResetLink {
+ success
+ }
+}
+ `;
+export type EmailPasswordResetLinkMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useEmailPasswordResetLinkMutation__
+ *
+ * To run a mutation, you first call `useEmailPasswordResetLinkMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useEmailPasswordResetLinkMutation` 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 [emailPasswordResetLinkMutation, { data, loading, error }] = useEmailPasswordResetLinkMutation({
+ * variables: {
+ * },
+ * });
+ */
+export function useEmailPasswordResetLinkMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(EmailPasswordResetLinkDocument, options);
+ }
+export type EmailPasswordResetLinkMutationHookResult = ReturnType;
+export type EmailPasswordResetLinkMutationResult = Apollo.MutationResult;
+export type EmailPasswordResetLinkMutationOptions = Apollo.BaseMutationOptions;
export const GenerateApiKeyTokenDocument = gql`
mutation GenerateApiKeyToken($apiKeyId: String!, $expiresAt: String!) {
generateApiKeyToken(apiKeyId: $apiKeyId, expiresAt: $expiresAt) {
@@ -1191,6 +1277,43 @@ export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions;
export type SignUpMutationResult = Apollo.MutationResult;
export type SignUpMutationOptions = Apollo.BaseMutationOptions;
+export const UpdatePasswordViaResetTokenDocument = gql`
+ mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
+ updatePasswordViaResetToken(
+ passwordResetToken: $token
+ newPassword: $newPassword
+ ) {
+ success
+ }
+}
+ `;
+export type UpdatePasswordViaResetTokenMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useUpdatePasswordViaResetTokenMutation__
+ *
+ * To run a mutation, you first call `useUpdatePasswordViaResetTokenMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUpdatePasswordViaResetTokenMutation` 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 [updatePasswordViaResetTokenMutation, { data, loading, error }] = useUpdatePasswordViaResetTokenMutation({
+ * variables: {
+ * token: // value for 'token'
+ * newPassword: // value for 'newPassword'
+ * },
+ * });
+ */
+export function useUpdatePasswordViaResetTokenMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(UpdatePasswordViaResetTokenDocument, options);
+ }
+export type UpdatePasswordViaResetTokenMutationHookResult = ReturnType;
+export type UpdatePasswordViaResetTokenMutationResult = Apollo.MutationResult;
+export type UpdatePasswordViaResetTokenMutationOptions = Apollo.BaseMutationOptions;
export const VerifyDocument = gql`
mutation Verify($loginToken: String!) {
verify(loginToken: $loginToken) {
@@ -1265,6 +1388,42 @@ export function useCheckUserExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOp
export type CheckUserExistsQueryHookResult = ReturnType;
export type CheckUserExistsLazyQueryHookResult = ReturnType;
export type CheckUserExistsQueryResult = Apollo.QueryResult;
+export const ValidatePasswordResetTokenDocument = gql`
+ query validatePasswordResetToken($token: String!) {
+ validatePasswordResetToken(passwordResetToken: $token) {
+ id
+ email
+ }
+}
+ `;
+
+/**
+ * __useValidatePasswordResetTokenQuery__
+ *
+ * To run a query within a React component, call `useValidatePasswordResetTokenQuery` and pass it any options that fit your needs.
+ * When your component renders, `useValidatePasswordResetTokenQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useValidatePasswordResetTokenQuery({
+ * variables: {
+ * token: // value for 'token'
+ * },
+ * });
+ */
+export function useValidatePasswordResetTokenQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(ValidatePasswordResetTokenDocument, options);
+ }
+export function useValidatePasswordResetTokenLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(ValidatePasswordResetTokenDocument, options);
+ }
+export type ValidatePasswordResetTokenQueryHookResult = ReturnType;
+export type ValidatePasswordResetTokenLazyQueryHookResult = ReturnType;
+export type ValidatePasswordResetTokenQueryResult = Apollo.QueryResult;
export const GetClientConfigDocument = gql`
query GetClientConfig {
clientConfig {
diff --git a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
index 1aedc914fc86..cd2b62704f62 100644
--- a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
+++ b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
@@ -42,7 +42,8 @@ export const useApolloFactory = () => {
!isMatchingLocation(AppPath.Verify) &&
!isMatchingLocation(AppPath.SignIn) &&
!isMatchingLocation(AppPath.SignUp) &&
- !isMatchingLocation(AppPath.Invite)
+ !isMatchingLocation(AppPath.Invite) &&
+ !isMatchingLocation(AppPath.ResetPassword)
) {
navigate(AppPath.SignIn);
}
diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/emailPasswordResetLink.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/emailPasswordResetLink.ts
new file mode 100644
index 000000000000..7b9ebac99e56
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/graphql/mutations/emailPasswordResetLink.ts
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const EMAIL_PASSWORD_RESET_Link = gql`
+ mutation EmailPasswordResetLink {
+ emailPasswordResetLink {
+ success
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/updatePasswordViaResetToken.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/updatePasswordViaResetToken.ts
new file mode 100644
index 000000000000..2ae5ab168d51
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/graphql/mutations/updatePasswordViaResetToken.ts
@@ -0,0 +1,12 @@
+import { gql } from '@apollo/client';
+
+export const UPDATE_PASSWORD_VIA_RESET_TOKEN = gql`
+ mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
+ updatePasswordViaResetToken(
+ passwordResetToken: $token
+ newPassword: $newPassword
+ ) {
+ success
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/auth/graphql/queries/validatePasswordResetToken.ts b/packages/twenty-front/src/modules/auth/graphql/queries/validatePasswordResetToken.ts
new file mode 100644
index 000000000000..b6631e4bf755
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/graphql/queries/validatePasswordResetToken.ts
@@ -0,0 +1,10 @@
+import { gql } from '@apollo/client';
+
+export const VALIDATE_PASSWORD_RESET_TOKEN = gql`
+ query validatePasswordResetToken($token: String!) {
+ validatePasswordResetToken(passwordResetToken: $token) {
+ id
+ email
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx
new file mode 100644
index 000000000000..51743ac07e6f
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx
@@ -0,0 +1,45 @@
+import { H2Title } from '@/ui/display/typography/components/H2Title';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { Button } from '@/ui/input/button/components/Button';
+import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
+import { logError } from '~/utils/logError';
+
+export const ChangePassword = () => {
+ const { enqueueSnackBar } = useSnackBar();
+
+ const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
+
+ const handlePasswordResetClick = async () => {
+ try {
+ const { data } = await emailPasswordResetLink();
+ if (data?.emailPasswordResetLink?.success) {
+ enqueueSnackBar('Password reset link has been sent to the email', {
+ variant: 'success',
+ });
+ } else {
+ enqueueSnackBar('There was some issue', {
+ variant: 'error',
+ });
+ }
+ } catch (error) {
+ logError(error);
+ enqueueSnackBar((error as Error).message, {
+ variant: 'error',
+ });
+ }
+ };
+
+ return (
+ <>
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/types/AppPath.ts b/packages/twenty-front/src/modules/types/AppPath.ts
index b01080c59a36..e35563a1ef86 100644
--- a/packages/twenty-front/src/modules/types/AppPath.ts
+++ b/packages/twenty-front/src/modules/types/AppPath.ts
@@ -4,6 +4,7 @@ export enum AppPath {
SignIn = '/sign-in',
SignUp = '/sign-up',
Invite = '/invite/:workspaceInviteHash',
+ ResetPassword = '/reset-password/:passwordResetToken',
// Onboarding
CreateWorkspace = '/create/workspace',
diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx
new file mode 100644
index 000000000000..72d8816950f7
--- /dev/null
+++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx
@@ -0,0 +1,275 @@
+import { useState } from 'react';
+import { Controller, useForm } from 'react-hook-form';
+import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
+import { useNavigate, useParams } from 'react-router-dom';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { zodResolver } from '@hookform/resolvers/zod';
+import { motion } from 'framer-motion';
+import { useRecoilValue } from 'recoil';
+import { z } from 'zod';
+
+import { Logo } from '@/auth/components/Logo';
+import { Title } from '@/auth/components/Title';
+import { useAuth } from '@/auth/hooks/useAuth';
+import { useIsLogged } from '@/auth/hooks/useIsLogged';
+import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex';
+import { billingState } from '@/client-config/states/billingState';
+import { AppPath } from '@/types/AppPath';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { MainButton } from '@/ui/input/button/components/MainButton';
+import { TextInput } from '@/ui/input/components/TextInput';
+import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
+import {
+ useUpdatePasswordViaResetTokenMutation,
+ useValidatePasswordResetTokenQuery,
+} from '~/generated/graphql';
+import { logError } from '~/utils/logError';
+
+const validationSchema = z
+ .object({
+ passwordResetToken: z.string(),
+ newPassword: z
+ .string()
+ .regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
+ })
+ .required();
+
+type Form = z.infer;
+
+const StyledMainContainer = styled.div`
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ flex-direction: column;
+ width: 100%;
+`;
+
+const StyledContentContainer = styled.div`
+ margin-bottom: ${({ theme }) => theme.spacing(8)};
+ margin-top: ${({ theme }) => theme.spacing(4)};
+ width: 200px;
+`;
+
+const StyledForm = styled.form`
+ align-items: center;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+`;
+
+const StyledFullWidthMotionDiv = styled(motion.div)`
+ width: 100%;
+`;
+
+const StyledInputContainer = styled.div`
+ margin-bottom: ${({ theme }) => theme.spacing(3)};
+`;
+
+const StyledFooterContainer = styled.div`
+ align-items: center;
+ color: ${({ theme }) => theme.font.color.tertiary};
+ display: flex;
+ font-size: ${({ theme }) => theme.font.size.sm};
+ text-align: center;
+ max-width: 280px;
+`;
+
+export const PasswordReset = () => {
+ const { enqueueSnackBar } = useSnackBar();
+
+ const navigate = useNavigate();
+
+ const [email, setEmail] = useState('');
+
+ const theme = useTheme();
+
+ const passwordResetToken = useParams().passwordResetToken;
+
+ const isLoggedIn = useIsLogged();
+
+ const { control, handleSubmit } = useForm