mirror of
https://github.com/twentyhq/twenty.git
synced 2024-11-30 13:42:01 +03:00
GH-3652 Add forgot password on sign-in page (#3789)
* Remove auth guard from password reset email endpoint * Add arg for GQL mutation and update its usage * Add forgot password button on sign-in page * Generate automated graphql queries * Move utils to dedicated hook * Remove useless hook function * Split simple hook methods * Split workspace hook * Split signInWithGoogle hook * Split useSignInUpForm * Fix error in logs * Add Link Button UI Component * Add storybook doc --------- Co-authored-by: martmull <martmull@hotmail.fr>
This commit is contained in:
parent
917fc5bd4d
commit
3cbf958a1c
@ -24,6 +24,11 @@ export type Scalars = {
|
|||||||
Upload: { input: any; output: any; }
|
Upload: { input: any; output: any; }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ApiKeyToken = {
|
||||||
|
__typename?: 'ApiKeyToken';
|
||||||
|
token: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthProviders = {
|
export type AuthProviders = {
|
||||||
__typename?: 'AuthProviders';
|
__typename?: 'AuthProviders';
|
||||||
google: Scalars['Boolean']['output'];
|
google: Scalars['Boolean']['output'];
|
||||||
@ -43,6 +48,11 @@ export type AuthTokenPair = {
|
|||||||
refreshToken: AuthToken;
|
refreshToken: AuthToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthTokens = {
|
||||||
|
__typename?: 'AuthTokens';
|
||||||
|
tokens: AuthTokenPair;
|
||||||
|
};
|
||||||
|
|
||||||
export type Billing = {
|
export type Billing = {
|
||||||
__typename?: 'Billing';
|
__typename?: 'Billing';
|
||||||
billingUrl: Scalars['String']['output'];
|
billingUrl: Scalars['String']['output'];
|
||||||
@ -136,6 +146,12 @@ export type DeleteOneRelationInput = {
|
|||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type EmailPasswordResetLink = {
|
||||||
|
__typename?: 'EmailPasswordResetLink';
|
||||||
|
/** Boolean that confirms query was dispatched */
|
||||||
|
success: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldConnection = {
|
export type FieldConnection = {
|
||||||
__typename?: 'FieldConnection';
|
__typename?: 'FieldConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@ -214,8 +230,20 @@ export type IdFilterComparison = {
|
|||||||
notLike?: InputMaybe<Scalars['ID']['input']>;
|
notLike?: InputMaybe<Scalars['ID']['input']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type InvalidatePassword = {
|
||||||
|
__typename?: 'InvalidatePassword';
|
||||||
|
/** Boolean that confirms query was dispatched */
|
||||||
|
success: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LoginToken = {
|
||||||
|
__typename?: 'LoginToken';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
|
challenge: LoginToken;
|
||||||
createOneField: Field;
|
createOneField: Field;
|
||||||
createOneObject: Object;
|
createOneObject: Object;
|
||||||
createOneRelation: Relation;
|
createOneRelation: Relation;
|
||||||
@ -223,11 +251,25 @@ export type Mutation = {
|
|||||||
deleteOneObject: Object;
|
deleteOneObject: Object;
|
||||||
deleteOneRelation: RelationDeleteResponse;
|
deleteOneRelation: RelationDeleteResponse;
|
||||||
deleteUser: User;
|
deleteUser: User;
|
||||||
|
emailPasswordResetLink: EmailPasswordResetLink;
|
||||||
|
generateApiKeyToken: ApiKeyToken;
|
||||||
|
generateTransientToken: TransientToken;
|
||||||
|
impersonate: Verify;
|
||||||
|
renewToken: AuthTokens;
|
||||||
|
signUp: LoginToken;
|
||||||
updateOneField: Field;
|
updateOneField: Field;
|
||||||
updateOneObject: Object;
|
updateOneObject: Object;
|
||||||
|
updatePasswordViaResetToken: InvalidatePassword;
|
||||||
uploadFile: Scalars['String']['output'];
|
uploadFile: Scalars['String']['output'];
|
||||||
uploadImage: Scalars['String']['output'];
|
uploadImage: Scalars['String']['output'];
|
||||||
uploadProfilePicture: Scalars['String']['output'];
|
uploadProfilePicture: Scalars['String']['output'];
|
||||||
|
verify: Verify;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationChallengeArgs = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
password: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -261,6 +303,34 @@ export type MutationDeleteOneRelationArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationEmailPasswordResetLinkArgs = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
|
apiKeyId: Scalars['String']['input'];
|
||||||
|
expiresAt: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationImpersonateArgs = {
|
||||||
|
userId: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRenewTokenArgs = {
|
||||||
|
refreshToken: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationSignUpArgs = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
password: Scalars['String']['input'];
|
||||||
|
workspaceInviteHash?: InputMaybe<Scalars['String']['input']>;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneFieldArgs = {
|
export type MutationUpdateOneFieldArgs = {
|
||||||
input: UpdateOneFieldMetadataInput;
|
input: UpdateOneFieldMetadataInput;
|
||||||
};
|
};
|
||||||
@ -271,6 +341,12 @@ export type MutationUpdateOneObjectArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationUpdatePasswordViaResetTokenArgs = {
|
||||||
|
newPassword: Scalars['String']['input'];
|
||||||
|
passwordResetToken: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUploadFileArgs = {
|
export type MutationUploadFileArgs = {
|
||||||
file: Scalars['Upload']['input'];
|
file: Scalars['Upload']['input'];
|
||||||
fileFolder?: InputMaybe<FileFolder>;
|
fileFolder?: InputMaybe<FileFolder>;
|
||||||
@ -287,6 +363,11 @@ export type MutationUploadProfilePictureArgs = {
|
|||||||
file: Scalars['Upload']['input'];
|
file: Scalars['Upload']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationVerifyArgs = {
|
||||||
|
loginToken: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type ObjectConnection = {
|
export type ObjectConnection = {
|
||||||
__typename?: 'ObjectConnection';
|
__typename?: 'ObjectConnection';
|
||||||
/** Array of edges. */
|
/** Array of edges. */
|
||||||
@ -321,13 +402,27 @@ export type PageInfo = {
|
|||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
|
checkUserExists: UserExists;
|
||||||
|
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
field: Field;
|
field: Field;
|
||||||
fields: FieldConnection;
|
fields: FieldConnection;
|
||||||
|
findWorkspaceFromInviteHash: Workspace;
|
||||||
object: Object;
|
object: Object;
|
||||||
objects: ObjectConnection;
|
objects: ObjectConnection;
|
||||||
relation: Relation;
|
relation: Relation;
|
||||||
relations: RelationConnection;
|
relations: RelationConnection;
|
||||||
|
validatePasswordResetToken: ValidatePasswordResetToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryCheckUserExistsArgs = {
|
||||||
|
email: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryCheckWorkspaceInviteHashIsValidArgs = {
|
||||||
|
inviteHash: Scalars['String']['input'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@ -342,6 +437,11 @@ export type QueryFieldsArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryFindWorkspaceFromInviteHashArgs = {
|
||||||
|
inviteHash: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryObjectArgs = {
|
export type QueryObjectArgs = {
|
||||||
id: Scalars['ID']['input'];
|
id: Scalars['ID']['input'];
|
||||||
};
|
};
|
||||||
@ -362,6 +462,11 @@ export type QueryRelationsArgs = {
|
|||||||
paging?: CursorPaging;
|
paging?: CursorPaging;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryValidatePasswordResetTokenArgs = {
|
||||||
|
passwordResetToken: Scalars['String']['input'];
|
||||||
|
};
|
||||||
|
|
||||||
export type RefreshToken = {
|
export type RefreshToken = {
|
||||||
__typename?: 'RefreshToken';
|
__typename?: 'RefreshToken';
|
||||||
createdAt: Scalars['DateTime']['output'];
|
createdAt: Scalars['DateTime']['output'];
|
||||||
@ -424,6 +529,19 @@ export type Telemetry = {
|
|||||||
enabled: Scalars['Boolean']['output'];
|
enabled: Scalars['Boolean']['output'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TimelineThread = {
|
||||||
|
__typename?: 'TimelineThread';
|
||||||
|
firstParticipant: TimelineThreadParticipant;
|
||||||
|
id: Scalars['ID']['output'];
|
||||||
|
lastMessageBody: Scalars['String']['output'];
|
||||||
|
lastMessageReceivedAt: Scalars['DateTime']['output'];
|
||||||
|
lastTwoParticipants: Array<TimelineThreadParticipant>;
|
||||||
|
numberOfMessagesInThread: Scalars['Float']['output'];
|
||||||
|
participantCount: Scalars['Float']['output'];
|
||||||
|
read: Scalars['Boolean']['output'];
|
||||||
|
subject: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type TimelineThreadParticipant = {
|
export type TimelineThreadParticipant = {
|
||||||
__typename?: 'TimelineThreadParticipant';
|
__typename?: 'TimelineThreadParticipant';
|
||||||
avatarUrl: Scalars['String']['output'];
|
avatarUrl: Scalars['String']['output'];
|
||||||
@ -435,6 +553,11 @@ export type TimelineThreadParticipant = {
|
|||||||
workspaceMemberId?: Maybe<Scalars['ID']['output']>;
|
workspaceMemberId?: Maybe<Scalars['ID']['output']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TransientToken = {
|
||||||
|
__typename?: 'TransientToken';
|
||||||
|
transientToken: AuthToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type UpdateFieldInput = {
|
export type UpdateFieldInput = {
|
||||||
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
defaultValue?: InputMaybe<Scalars['JSON']['input']>;
|
||||||
description?: InputMaybe<Scalars['String']['input']>;
|
description?: InputMaybe<Scalars['String']['input']>;
|
||||||
@ -502,6 +625,23 @@ export type UserEdge = {
|
|||||||
node: User;
|
node: User;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserExists = {
|
||||||
|
__typename?: 'UserExists';
|
||||||
|
exists: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ValidatePasswordResetToken = {
|
||||||
|
__typename?: 'ValidatePasswordResetToken';
|
||||||
|
email: Scalars['String']['output'];
|
||||||
|
id: Scalars['String']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Verify = {
|
||||||
|
__typename?: 'Verify';
|
||||||
|
tokens: AuthTokenPair;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
__typename?: 'Workspace';
|
__typename?: 'Workspace';
|
||||||
allowImpersonation: Scalars['Boolean']['output'];
|
allowImpersonation: Scalars['Boolean']['output'];
|
||||||
@ -524,6 +664,11 @@ export type WorkspaceEdge = {
|
|||||||
node: Workspace;
|
node: Workspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type WorkspaceInviteHashValid = {
|
||||||
|
__typename?: 'WorkspaceInviteHashValid';
|
||||||
|
isValid: Scalars['Boolean']['output'];
|
||||||
|
};
|
||||||
|
|
||||||
export type WorkspaceMember = {
|
export type WorkspaceMember = {
|
||||||
__typename?: 'WorkspaceMember';
|
__typename?: 'WorkspaceMember';
|
||||||
avatarUrl?: Maybe<Scalars['String']['output']>;
|
avatarUrl?: Maybe<Scalars['String']['output']>;
|
||||||
|
@ -265,6 +265,11 @@ export type MutationDeleteOneObjectArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationEmailPasswordResetLinkArgs = {
|
||||||
|
email: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationGenerateApiKeyTokenArgs = {
|
export type MutationGenerateApiKeyTokenArgs = {
|
||||||
apiKeyId: Scalars['String'];
|
apiKeyId: Scalars['String'];
|
||||||
expiresAt: Scalars['String'];
|
expiresAt: Scalars['String'];
|
||||||
@ -752,7 +757,9 @@ export type ChallengeMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type ChallengeMutation = { __typename?: 'Mutation', challenge: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
export type ChallengeMutation = { __typename?: 'Mutation', challenge: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } };
|
||||||
|
|
||||||
export type EmailPasswordResetLinkMutationVariables = Exact<{ [key: string]: never; }>;
|
export type EmailPasswordResetLinkMutationVariables = Exact<{
|
||||||
|
email: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
export type EmailPasswordResetLinkMutation = { __typename?: 'Mutation', emailPasswordResetLink: { __typename?: 'EmailPasswordResetLink', success: boolean } };
|
||||||
@ -1143,8 +1150,8 @@ export type ChallengeMutationHookResult = ReturnType<typeof useChallengeMutation
|
|||||||
export type ChallengeMutationResult = Apollo.MutationResult<ChallengeMutation>;
|
export type ChallengeMutationResult = Apollo.MutationResult<ChallengeMutation>;
|
||||||
export type ChallengeMutationOptions = Apollo.BaseMutationOptions<ChallengeMutation, ChallengeMutationVariables>;
|
export type ChallengeMutationOptions = Apollo.BaseMutationOptions<ChallengeMutation, ChallengeMutationVariables>;
|
||||||
export const EmailPasswordResetLinkDocument = gql`
|
export const EmailPasswordResetLinkDocument = gql`
|
||||||
mutation EmailPasswordResetLink {
|
mutation EmailPasswordResetLink($email: String!) {
|
||||||
emailPasswordResetLink {
|
emailPasswordResetLink(email: $email) {
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1164,6 +1171,7 @@ export type EmailPasswordResetLinkMutationFn = Apollo.MutationFunction<EmailPass
|
|||||||
* @example
|
* @example
|
||||||
* const [emailPasswordResetLinkMutation, { data, loading, error }] = useEmailPasswordResetLinkMutation({
|
* const [emailPasswordResetLinkMutation, { data, loading, error }] = useEmailPasswordResetLinkMutation({
|
||||||
* variables: {
|
* variables: {
|
||||||
|
* email: // value for 'email'
|
||||||
* },
|
* },
|
||||||
* });
|
* });
|
||||||
*/
|
*/
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
export const EMAIL_PASSWORD_RESET_Link = gql`
|
export const EMAIL_PASSWORD_RESET_Link = gql`
|
||||||
mutation EmailPasswordResetLink {
|
mutation EmailPasswordResetLink($email: String!) {
|
||||||
emailPasswordResetLink {
|
emailPasswordResetLink(email: $email) {
|
||||||
success
|
success
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,20 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { Controller } from 'react-hook-form';
|
import { Controller } from 'react-hook-form';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword.ts';
|
||||||
|
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm.ts';
|
||||||
|
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle.ts';
|
||||||
|
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash.ts';
|
||||||
|
import { authProvidersState } from '@/client-config/states/authProvidersState.ts';
|
||||||
import { IconGoogle } from '@/ui/display/icon/components/IconGoogle';
|
import { IconGoogle } from '@/ui/display/icon/components/IconGoogle';
|
||||||
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
import { Loader } from '@/ui/feedback/loader/components/Loader';
|
||||||
import { MainButton } from '@/ui/input/button/components/MainButton';
|
import { MainButton } from '@/ui/input/button/components/MainButton';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||||
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn';
|
||||||
|
|
||||||
import { Logo } from '../../components/Logo';
|
import { Logo } from '../../components/Logo';
|
||||||
@ -43,24 +50,20 @@ const StyledInputContainer = styled.div`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export const SignInUpForm = () => {
|
export const SignInUpForm = () => {
|
||||||
|
const [authProviders] = useRecoilState(authProvidersState);
|
||||||
|
const [showErrors, setShowErrors] = useState(false);
|
||||||
|
const { handleResetPassword } = useHandleResetPassword();
|
||||||
|
const workspace = useWorkspaceFromInviteHash();
|
||||||
|
const { signInWithGoogle } = useSignInWithGoogle();
|
||||||
|
const { form } = useSignInUpForm();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
authProviders,
|
|
||||||
signInWithGoogle,
|
|
||||||
signInUpStep,
|
signInUpStep,
|
||||||
signInUpMode,
|
signInUpMode,
|
||||||
showErrors,
|
|
||||||
setShowErrors,
|
|
||||||
continueWithCredentials,
|
continueWithCredentials,
|
||||||
continueWithEmail,
|
continueWithEmail,
|
||||||
submitCredentials,
|
submitCredentials,
|
||||||
form: {
|
} = useSignInUp(form);
|
||||||
control,
|
|
||||||
watch,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isSubmitting },
|
|
||||||
},
|
|
||||||
workspace,
|
|
||||||
} = useSignInUp();
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
@ -72,7 +75,7 @@ export const SignInUpForm = () => {
|
|||||||
continueWithCredentials();
|
continueWithCredentials();
|
||||||
} else if (signInUpStep === SignInUpStep.Password) {
|
} else if (signInUpStep === SignInUpStep.Password) {
|
||||||
setShowErrors(true);
|
setShowErrors(true);
|
||||||
handleSubmit(submitCredentials)();
|
form.handleSubmit(submitCredentials)();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -88,10 +91,10 @@ export const SignInUpForm = () => {
|
|||||||
|
|
||||||
return signInUpMode === SignInUpMode.SignIn
|
return signInUpMode === SignInUpMode.SignIn
|
||||||
? 'Sign in'
|
? 'Sign in'
|
||||||
: isSubmitting
|
: form.formState.isSubmitting
|
||||||
? 'Creating workspace'
|
? 'Creating workspace'
|
||||||
: 'Sign up';
|
: 'Sign up';
|
||||||
}, [signInUpMode, signInUpStep, isSubmitting]);
|
}, [signInUpMode, signInUpStep, form.formState.isSubmitting]);
|
||||||
|
|
||||||
const title = useMemo(() => {
|
const title = useMemo(() => {
|
||||||
if (signInUpMode === SignInUpMode.Invite) {
|
if (signInUpMode === SignInUpMode.Invite) {
|
||||||
@ -141,7 +144,7 @@ export const SignInUpForm = () => {
|
|||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="email"
|
name="email"
|
||||||
control={control}
|
control={form.control}
|
||||||
render={({
|
render={({
|
||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
@ -180,7 +183,7 @@ export const SignInUpForm = () => {
|
|||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="password"
|
name="password"
|
||||||
control={control}
|
control={form.control}
|
||||||
render={({
|
render={({
|
||||||
field: { onChange, onBlur, value },
|
field: { onChange, onBlur, value },
|
||||||
fieldState: { error },
|
fieldState: { error },
|
||||||
@ -218,24 +221,32 @@ export const SignInUpForm = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setShowErrors(true);
|
setShowErrors(true);
|
||||||
handleSubmit(submitCredentials)();
|
form.handleSubmit(submitCredentials)();
|
||||||
}}
|
}}
|
||||||
Icon={() => isSubmitting && <Loader />}
|
Icon={() => form.formState.isSubmitting && <Loader />}
|
||||||
disabled={
|
disabled={
|
||||||
SignInUpStep.Init
|
SignInUpStep.Init
|
||||||
? false
|
? false
|
||||||
: signInUpStep === SignInUpStep.Email
|
: signInUpStep === SignInUpStep.Email
|
||||||
? !watch('email')
|
? !form.watch('email')
|
||||||
: !watch('email') || !watch('password') || isSubmitting
|
: !form.watch('email') ||
|
||||||
|
!form.watch('password') ||
|
||||||
|
form.formState.isSubmitting
|
||||||
}
|
}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
|
{signInUpStep === SignInUpStep.Password ? (
|
||||||
|
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>
|
||||||
|
Forgot your password?
|
||||||
|
</ActionLink>
|
||||||
|
) : (
|
||||||
<StyledFooterNote>
|
<StyledFooterNote>
|
||||||
By using Twenty, you agree to the Terms of Service and Data Processing
|
By using Twenty, you agree to the Terms of Service and Data Processing
|
||||||
Agreement.
|
Agreement.
|
||||||
</StyledFooterNote>
|
</StyledFooterNote>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar.tsx';
|
||||||
|
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql.tsx';
|
||||||
|
|
||||||
|
export const useHandleResetPassword = () => {
|
||||||
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
|
||||||
|
|
||||||
|
const handleResetPassword = useCallback(
|
||||||
|
(email: string) => {
|
||||||
|
return async () => {
|
||||||
|
if (!email) {
|
||||||
|
enqueueSnackBar('Invalid email', {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await emailPasswordResetLink({
|
||||||
|
variables: { email },
|
||||||
|
});
|
||||||
|
|
||||||
|
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) {
|
||||||
|
enqueueSnackBar((error as Error).message, {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
[enqueueSnackBar, emailPasswordResetLink],
|
||||||
|
);
|
||||||
|
|
||||||
|
return { handleResetPassword };
|
||||||
|
};
|
@ -1,22 +1,17 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form';
|
import { SubmitHandler, UseFormReturn } from 'react-hook-form';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { useRecoilValue } from 'recoil';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { Form } from '@/auth/sign-in-up/hooks/useSignInUpForm.ts';
|
||||||
import { billingState } from '@/client-config/states/billingState';
|
import { billingState } from '@/client-config/states/billingState';
|
||||||
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
|
|
||||||
import { AppPath } from '@/types/AppPath';
|
import { AppPath } from '@/types/AppPath';
|
||||||
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
import { PageHotkeyScope } from '@/types/PageHotkeyScope';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql';
|
|
||||||
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
import { useIsMatchingLocation } from '~/hooks/useIsMatchingLocation';
|
||||||
|
|
||||||
import { useAuth } from '../../hooks/useAuth';
|
import { useAuth } from '../../hooks/useAuth';
|
||||||
import { PASSWORD_REGEX } from '../../utils/passwordRegex';
|
|
||||||
|
|
||||||
export enum SignInUpMode {
|
export enum SignInUpMode {
|
||||||
SignIn = 'sign-in',
|
SignIn = 'sign-in',
|
||||||
@ -30,27 +25,11 @@ export enum SignInUpStep {
|
|||||||
Password = 'password',
|
Password = 'password',
|
||||||
}
|
}
|
||||||
|
|
||||||
const validationSchema = z
|
export const useSignInUp = (form: UseFormReturn<Form>) => {
|
||||||
.object({
|
|
||||||
exist: z.boolean(),
|
|
||||||
email: z.string().trim().email('Email must be a valid email'),
|
|
||||||
password: z
|
|
||||||
.string()
|
|
||||||
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
|
||||||
})
|
|
||||||
.required();
|
|
||||||
|
|
||||||
type Form = z.infer<typeof validationSchema>;
|
|
||||||
|
|
||||||
export const useSignInUp = () => {
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
const isMatchingLocation = useIsMatchingLocation();
|
const isMatchingLocation = useIsMatchingLocation();
|
||||||
|
|
||||||
const [authProviders] = useRecoilState(authProvidersState);
|
|
||||||
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
|
|
||||||
const billing = useRecoilValue(billingState);
|
const billing = useRecoilValue(billingState);
|
||||||
|
|
||||||
const workspaceInviteHash = useParams().workspaceInviteHash;
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
const [signInUpStep, setSignInUpStep] = useState<SignInUpStep>(
|
||||||
SignInUpStep.Init,
|
SignInUpStep.Init,
|
||||||
@ -64,31 +43,9 @@ export const useSignInUp = () => {
|
|||||||
? SignInUpMode.SignIn
|
? SignInUpMode.SignIn
|
||||||
: SignInUpMode.SignUp;
|
: SignInUpMode.SignUp;
|
||||||
});
|
});
|
||||||
const [showErrors, setShowErrors] = useState(false);
|
|
||||||
|
|
||||||
const { data: workspaceFromInviteHash } = useGetWorkspaceFromInviteHashQuery({
|
|
||||||
variables: { inviteHash: workspaceInviteHash || '' },
|
|
||||||
});
|
|
||||||
|
|
||||||
const form = useForm<Form>({
|
|
||||||
mode: 'onChange',
|
|
||||||
defaultValues: {
|
|
||||||
exist: false,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(validationSchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isSignInPrefilled) {
|
|
||||||
form.setValue('email', 'tim@apple.dev');
|
|
||||||
form.setValue('password', 'Applecar2025');
|
|
||||||
}
|
|
||||||
}, [form, isSignInPrefilled]);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
signInWithCredentials,
|
signInWithCredentials,
|
||||||
signUpWithCredentials,
|
signUpWithCredentials,
|
||||||
signInWithGoogle,
|
|
||||||
checkUserExists: { checkUserExistsQuery },
|
checkUserExists: { checkUserExistsQuery },
|
||||||
} = useAuth();
|
} = useAuth();
|
||||||
|
|
||||||
@ -169,10 +126,6 @@ export const useSignInUp = () => {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const goBackToEmailStep = useCallback(() => {
|
|
||||||
setSignInUpStep(SignInUpStep.Email);
|
|
||||||
}, [setSignInUpStep]);
|
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
@ -193,17 +146,10 @@ export const useSignInUp = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authProviders,
|
|
||||||
signInWithGoogle: () => signInWithGoogle(workspaceInviteHash),
|
|
||||||
signInUpStep,
|
signInUpStep,
|
||||||
signInUpMode,
|
signInUpMode,
|
||||||
showErrors,
|
|
||||||
setShowErrors,
|
|
||||||
continueWithCredentials,
|
continueWithCredentials,
|
||||||
continueWithEmail,
|
continueWithEmail,
|
||||||
goBackToEmailStep,
|
|
||||||
submitCredentials,
|
submitCredentials,
|
||||||
form,
|
|
||||||
workspace: workspaceFromInviteHash?.findWorkspaceFromInviteHash,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { PASSWORD_REGEX } from '@/auth/utils/passwordRegex.ts';
|
||||||
|
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState.ts';
|
||||||
|
|
||||||
|
const validationSchema = z
|
||||||
|
.object({
|
||||||
|
exist: z.boolean(),
|
||||||
|
email: z.string().trim().email('Email must be a valid email'),
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
export type Form = z.infer<typeof validationSchema>;
|
||||||
|
export const useSignInUpForm = () => {
|
||||||
|
const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
|
||||||
|
const form = useForm<Form>({
|
||||||
|
mode: 'onChange',
|
||||||
|
defaultValues: {
|
||||||
|
exist: false,
|
||||||
|
},
|
||||||
|
resolver: zodResolver(validationSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignInPrefilled) {
|
||||||
|
form.setValue('email', 'tim@apple.dev');
|
||||||
|
form.setValue('password', 'Applecar2025');
|
||||||
|
}
|
||||||
|
}, [form, isSignInPrefilled]);
|
||||||
|
return { form: form };
|
||||||
|
};
|
@ -0,0 +1,9 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useAuth } from '@/auth/hooks/useAuth.ts';
|
||||||
|
|
||||||
|
export const useSignInWithGoogle = () => {
|
||||||
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
|
const { signInWithGoogle } = useAuth();
|
||||||
|
return { signInWithGoogle: () => signInWithGoogle(workspaceInviteHash) };
|
||||||
|
};
|
@ -0,0 +1,11 @@
|
|||||||
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { useGetWorkspaceFromInviteHashQuery } from '~/generated/graphql.tsx';
|
||||||
|
|
||||||
|
export const useWorkspaceFromInviteHash = () => {
|
||||||
|
const workspaceInviteHash = useParams().workspaceInviteHash;
|
||||||
|
const { data: workspaceFromInviteHash } = useGetWorkspaceFromInviteHashQuery({
|
||||||
|
variables: { inviteHash: workspaceInviteHash || '' },
|
||||||
|
});
|
||||||
|
return workspaceFromInviteHash?.findWorkspaceFromInviteHash;
|
||||||
|
};
|
@ -1,17 +1,32 @@
|
|||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
import { H2Title } from '@/ui/display/typography/components/H2Title';
|
||||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
import { useEmailPasswordResetLinkMutation } from '~/generated/graphql';
|
||||||
import { logError } from '~/utils/logError';
|
|
||||||
|
|
||||||
export const ChangePassword = () => {
|
export const ChangePassword = () => {
|
||||||
const { enqueueSnackBar } = useSnackBar();
|
const { enqueueSnackBar } = useSnackBar();
|
||||||
|
|
||||||
|
const currentUser = useRecoilValue(currentUserState);
|
||||||
|
|
||||||
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
|
const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation();
|
||||||
|
|
||||||
const handlePasswordResetClick = async () => {
|
const handlePasswordResetClick = async () => {
|
||||||
|
if (!currentUser?.email) {
|
||||||
|
enqueueSnackBar('Invalid email', {
|
||||||
|
variant: 'error',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data } = await emailPasswordResetLink();
|
const { data } = await emailPasswordResetLink({
|
||||||
|
variables: {
|
||||||
|
email: currentUser.email,
|
||||||
|
},
|
||||||
|
});
|
||||||
if (data?.emailPasswordResetLink?.success) {
|
if (data?.emailPasswordResetLink?.success) {
|
||||||
enqueueSnackBar('Password reset link has been sent to the email', {
|
enqueueSnackBar('Password reset link has been sent to the email', {
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
@ -22,7 +37,6 @@ export const ChangePassword = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
|
||||||
enqueueSnackBar((error as Error).message, {
|
enqueueSnackBar((error as Error).message, {
|
||||||
variant: 'error',
|
variant: 'error',
|
||||||
});
|
});
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
const StyledButtonLink = styled.a`
|
||||||
|
align-items: center;
|
||||||
|
color: ${({ theme }) => theme.font.color.light};
|
||||||
|
display: flex;
|
||||||
|
font-size: ${({ theme }) => theme.font.size.sm};
|
||||||
|
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||||
|
gap: ${({ theme }) => theme.spacing(1)};
|
||||||
|
padding: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
|
text-decoration: none;
|
||||||
|
|
||||||
|
:hover {
|
||||||
|
color: ${({ theme }) => theme.font.color.tertiary};
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ActionLink = (props: React.ComponentProps<'a'>) => {
|
||||||
|
return (
|
||||||
|
<StyledButtonLink
|
||||||
|
href={props.href}
|
||||||
|
onClick={props.onClick}
|
||||||
|
target={props.target}
|
||||||
|
rel={props.rel}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</StyledButtonLink>
|
||||||
|
);
|
||||||
|
};
|
@ -1,33 +1,18 @@
|
|||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
|
||||||
|
|
||||||
import { IconBrandGithub } from '@/ui/display/icon';
|
import { IconBrandGithub } from '@/ui/display/icon';
|
||||||
|
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||||
|
|
||||||
import packageJson from '../../../../../../package.json';
|
import packageJson from '../../../../../../package.json';
|
||||||
import { githubLink } from '../constants';
|
import { githubLink } from '../constants';
|
||||||
|
|
||||||
const StyledVersionLink = styled.a`
|
|
||||||
align-items: center;
|
|
||||||
color: ${({ theme }) => theme.font.color.light};
|
|
||||||
display: flex;
|
|
||||||
font-size: ${({ theme }) => theme.font.size.sm};
|
|
||||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
|
||||||
gap: ${({ theme }) => theme.spacing(1)};
|
|
||||||
padding: 0 ${({ theme }) => theme.spacing(1)};
|
|
||||||
text-decoration: none;
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
color: ${({ theme }) => theme.font.color.tertiary};
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
|
|
||||||
export const GithubVersionLink = () => {
|
export const GithubVersionLink = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledVersionLink href={githubLink} target="_blank" rel="noreferrer">
|
<ActionLink href={githubLink} target="_blank" rel="noreferrer">
|
||||||
<IconBrandGithub size={theme.icon.size.md} />
|
<IconBrandGithub size={theme.icon.size.md} />
|
||||||
{packageJson.version}
|
{packageJson.version}
|
||||||
</StyledVersionLink>
|
</ActionLink>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,27 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ActionLink } from '@/ui/navigation/link/components/ActionLink.tsx';
|
||||||
|
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator.tsx';
|
||||||
|
|
||||||
|
const meta: Meta<typeof ActionLink> = {
|
||||||
|
title: 'UI/navigation/link/ActionLink',
|
||||||
|
component: ActionLink,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof ActionLink>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
args: {
|
||||||
|
children: 'Need to reset your password?',
|
||||||
|
onClick: () => alert('Action link clicked'),
|
||||||
|
target: undefined,
|
||||||
|
rel: undefined,
|
||||||
|
},
|
||||||
|
argTypes: {
|
||||||
|
href: { control: false },
|
||||||
|
target: { type: 'string' },
|
||||||
|
rel: { type: 'string' },
|
||||||
|
},
|
||||||
|
decorators: [ComponentDecorator],
|
||||||
|
};
|
@ -27,6 +27,7 @@ export * from './src/modules/ui/input/button/components/FloatingButtonGroup'
|
|||||||
export * from './src/modules/ui/input/button/components/FloatingIconButton'
|
export * from './src/modules/ui/input/button/components/FloatingIconButton'
|
||||||
export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'
|
export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'
|
||||||
export * from './src/modules/ui/input/button/components/LightButton'
|
export * from './src/modules/ui/input/button/components/LightButton'
|
||||||
|
export * from '@/ui/navigation/link/components/ActionLink.tsx'
|
||||||
export * from './src/modules/ui/input/button/components/LightIconButton'
|
export * from './src/modules/ui/input/button/components/LightIconButton'
|
||||||
export * from './src/modules/ui/input/button/components/MainButton'
|
export * from './src/modules/ui/input/button/components/MainButton'
|
||||||
export * from './src/modules/ui/input/button/components/RoundedIconButton'
|
export * from './src/modules/ui/input/button/components/RoundedIconButton'
|
||||||
|
@ -24,6 +24,7 @@ import { ValidatePasswordResetTokenInput } from 'src/core/auth/dto/validate-pass
|
|||||||
import { UpdatePasswordViaResetTokenInput } from 'src/core/auth/dto/update-password-via-reset-token.input';
|
import { UpdatePasswordViaResetTokenInput } from 'src/core/auth/dto/update-password-via-reset-token.input';
|
||||||
import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity';
|
import { EmailPasswordResetLink } from 'src/core/auth/dto/email-password-reset-link.entity';
|
||||||
import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity';
|
import { InvalidatePassword } from 'src/core/auth/dto/invalidate-password.entity';
|
||||||
|
import { EmailPasswordResetLinkInput } from 'src/core/auth/dto/email-password-reset-link.input';
|
||||||
|
|
||||||
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
import { ApiKeyToken, AuthTokens } from './dto/token.entity';
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
@ -162,17 +163,17 @@ export class AuthResolver {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
|
||||||
@Mutation(() => EmailPasswordResetLink)
|
@Mutation(() => EmailPasswordResetLink)
|
||||||
async emailPasswordResetLink(
|
async emailPasswordResetLink(
|
||||||
@AuthUser() { email }: User,
|
@Args() emailPasswordResetInput: EmailPasswordResetLinkInput,
|
||||||
): Promise<EmailPasswordResetLink> {
|
): Promise<EmailPasswordResetLink> {
|
||||||
const resetToken =
|
const resetToken = await this.tokenService.generatePasswordResetToken(
|
||||||
await this.tokenService.generatePasswordResetToken(email);
|
emailPasswordResetInput.email,
|
||||||
|
);
|
||||||
|
|
||||||
return await this.tokenService.sendEmailPasswordResetLink(
|
return await this.tokenService.sendEmailPasswordResetLink(
|
||||||
resetToken,
|
resetToken,
|
||||||
email,
|
emailPasswordResetInput.email,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,11 @@
|
|||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
import { IsEmail, IsNotEmpty } from 'class-validator';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
|
export class EmailPasswordResetLinkInput {
|
||||||
|
@Field(() => String)
|
||||||
|
@IsNotEmpty()
|
||||||
|
@IsEmail()
|
||||||
|
email: string;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user