From 9cd5f7c05791c4ce1eee45927b1178c03c1a3c34 Mon Sep 17 00:00:00 2001 From: Emilien Chauvet Date: Sat, 8 Jul 2023 15:02:39 -0700 Subject: [PATCH] Feat/navigate to signup if email does not exist (#540) * Add userExists route * Fix demo mode for login * Improve sign in/up flow * Remove redundant password length constraint * Fix test --- front/src/generated/graphql.tsx | 53 +++++++++++++++++++ front/src/modules/auth/services/index.ts | 1 + front/src/modules/auth/services/select.ts | 9 ++++ front/src/pages/auth/Index.tsx | 3 +- front/src/pages/auth/PasswordLogin.tsx | 14 ++++- server/src/core/auth/auth.resolver.ts | 14 ++++- server/src/core/auth/dto/challenge.input.ts | 11 +--- .../src/core/auth/dto/user-exists.entity.ts | 7 +++ server/src/core/auth/dto/user-exists.input.ts | 10 ++++ server/src/core/auth/services/auth.service.ts | 21 +++++--- 10 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 front/src/modules/auth/services/select.ts create mode 100644 server/src/core/auth/dto/user-exists.entity.ts create mode 100644 server/src/core/auth/dto/user-exists.input.ts diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx index 11d23a30b..2265106f7 100644 --- a/front/src/generated/graphql.tsx +++ b/front/src/generated/graphql.tsx @@ -2286,6 +2286,7 @@ export type PipelineWhereUniqueInput = { export type Query = { __typename?: 'Query'; + checkUserExists: UserExists; clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; @@ -2299,6 +2300,11 @@ export type Query = { }; +export type QueryCheckUserExistsArgs = { + email: Scalars['String']; +}; + + export type QueryFindManyCommentThreadsArgs = { cursor?: InputMaybe; distinct?: InputMaybe>; @@ -2498,6 +2504,11 @@ export type UserCreateWithoutWorkspaceMemberInput = { updatedAt?: InputMaybe; }; +export type UserExists = { + __typename?: 'UserExists'; + exists: Scalars['Boolean']; +}; + export type UserOrderByWithRelationInput = { avatarUrl?: InputMaybe; comments?: InputMaybe; @@ -2789,6 +2800,13 @@ export type CreateEventMutationVariables = Exact<{ export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } }; +export type CheckUserExistsQueryVariables = Exact<{ + email: Scalars['String']; +}>; + + +export type CheckUserExistsQuery = { __typename?: 'Query', checkUserExists: { __typename?: 'UserExists', exists: boolean } }; + export type ChallengeMutationVariables = Exact<{ email: Scalars['String']; password: Scalars['String']; @@ -3116,6 +3134,41 @@ export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions< export type CreateEventMutationHookResult = ReturnType; export type CreateEventMutationResult = Apollo.MutationResult; export type CreateEventMutationOptions = Apollo.BaseMutationOptions; +export const CheckUserExistsDocument = gql` + query CheckUserExists($email: String!) { + checkUserExists(email: $email) { + exists + } +} + `; + +/** + * __useCheckUserExistsQuery__ + * + * To run a query within a React component, call `useCheckUserExistsQuery` and pass it any options that fit your needs. + * When your component renders, `useCheckUserExistsQuery` 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 } = useCheckUserExistsQuery({ + * variables: { + * email: // value for 'email' + * }, + * }); + */ +export function useCheckUserExistsQuery(baseOptions: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(CheckUserExistsDocument, options); + } +export function useCheckUserExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(CheckUserExistsDocument, options); + } +export type CheckUserExistsQueryHookResult = ReturnType; +export type CheckUserExistsLazyQueryHookResult = ReturnType; +export type CheckUserExistsQueryResult = Apollo.QueryResult; export const ChallengeDocument = gql` mutation Challenge($email: String!, $password: String!) { challenge(email: $email, password: $password) { diff --git a/front/src/modules/auth/services/index.ts b/front/src/modules/auth/services/index.ts index c37c258c7..18c6c2f7d 100644 --- a/front/src/modules/auth/services/index.ts +++ b/front/src/modules/auth/services/index.ts @@ -1 +1,2 @@ +export * from './select'; export * from './update'; diff --git a/front/src/modules/auth/services/select.ts b/front/src/modules/auth/services/select.ts new file mode 100644 index 000000000..2b35abb13 --- /dev/null +++ b/front/src/modules/auth/services/select.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const CHECK_USER_EXISTS = gql` + query CheckUserExists($email: String!) { + checkUserExists(email: $email) { + exists + } + } +`; diff --git a/front/src/pages/auth/Index.tsx b/front/src/pages/auth/Index.tsx index 895c8a546..f6e399152 100644 --- a/front/src/pages/auth/Index.tsx +++ b/front/src/pages/auth/Index.tsx @@ -12,6 +12,7 @@ import { Title } from '@/auth/components/ui/Title'; import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState'; import { isMockModeState } from '@/auth/states/isMockModeState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; +import { isDemoModeState } from '@/client-config/states/isDemoModeState'; import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys'; import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope'; import { MainButton } from '@/ui/components/buttons/MainButton'; @@ -35,7 +36,7 @@ export function Index() { const theme = useTheme(); const [, setMockMode] = useRecoilState(isMockModeState); const [authProviders] = useRecoilState(authProvidersState); - const [demoMode] = useRecoilState(authProvidersState); + const [demoMode] = useRecoilState(isDemoModeState); const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState( authFlowUserEmailState, diff --git a/front/src/pages/auth/PasswordLogin.tsx b/front/src/pages/auth/PasswordLogin.tsx index 3040c19d3..3e08e0da0 100644 --- a/front/src/pages/auth/PasswordLogin.tsx +++ b/front/src/pages/auth/PasswordLogin.tsx @@ -16,6 +16,7 @@ import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysSc import { MainButton } from '@/ui/components/buttons/MainButton'; import { TextInput } from '@/ui/components/inputs/TextInput'; import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle'; +import { useCheckUserExistsQuery } from '~/generated/graphql'; const StyledContentContainer = styled.div` width: 100%; @@ -84,11 +85,20 @@ export function PasswordLogin() { [handleLogin], ); + const { loading, data } = useCheckUserExistsQuery({ + variables: { + email: authFlowUserEmail, + }, + }); + return ( <> Welcome to Twenty - Enter your credentials to sign in + + Enter your credentials to sign{' '} + {data?.checkUserExists.exists ? 'in' : 'up'} + @@ -115,7 +125,7 @@ export function PasswordLogin() { diff --git a/server/src/core/auth/auth.resolver.ts b/server/src/core/auth/auth.resolver.ts index d4eea4f46..a2b02961b 100644 --- a/server/src/core/auth/auth.resolver.ts +++ b/server/src/core/auth/auth.resolver.ts @@ -1,4 +1,4 @@ -import { Args, Mutation, Resolver } from '@nestjs/graphql'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { AuthTokens } from './dto/token.entity'; import { TokenService } from './services/token.service'; import { RefreshTokenInput } from './dto/refresh-token.input'; @@ -13,6 +13,8 @@ import { PrismaSelector, } from 'src/decorators/prisma-select.decorator'; import { Prisma } from '@prisma/client'; +import { UserExists } from './dto/user-exists.entity'; +import { CheckUserExistsInput } from './dto/user-exists.input'; @Resolver() export class AuthResolver { @@ -21,6 +23,16 @@ export class AuthResolver { private tokenService: TokenService, ) {} + @Query(() => UserExists) + async checkUserExists( + @Args() checkUserExistsInput: CheckUserExistsInput, + ): Promise { + const { exists } = await this.authService.checkUserExists( + checkUserExistsInput.email, + ); + return { exists }; + } + @Mutation(() => LoginToken) async challenge(@Args() challengeInput: ChallengeInput): Promise { const user = await this.authService.challenge(challengeInput); diff --git a/server/src/core/auth/dto/challenge.input.ts b/server/src/core/auth/dto/challenge.input.ts index 6a00b2eb6..cf012d729 100644 --- a/server/src/core/auth/dto/challenge.input.ts +++ b/server/src/core/auth/dto/challenge.input.ts @@ -1,12 +1,5 @@ import { ArgsType, Field } from '@nestjs/graphql'; -import { - IsEmail, - IsNotEmpty, - IsString, - Matches, - MinLength, -} from 'class-validator'; -import { PASSWORD_REGEX } from '../auth.util'; +import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; @ArgsType() export class ChallengeInput { @@ -18,7 +11,5 @@ export class ChallengeInput { @Field(() => String) @IsNotEmpty() @IsString() - @MinLength(8) - @Matches(PASSWORD_REGEX, { message: 'password too weak' }) password: string; } diff --git a/server/src/core/auth/dto/user-exists.entity.ts b/server/src/core/auth/dto/user-exists.entity.ts new file mode 100644 index 000000000..b4b70d0af --- /dev/null +++ b/server/src/core/auth/dto/user-exists.entity.ts @@ -0,0 +1,7 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class UserExists { + @Field(() => Boolean) + exists: boolean; +} diff --git a/server/src/core/auth/dto/user-exists.input.ts b/server/src/core/auth/dto/user-exists.input.ts new file mode 100644 index 000000000..86b6a5203 --- /dev/null +++ b/server/src/core/auth/dto/user-exists.input.ts @@ -0,0 +1,10 @@ +import { ArgsType, Field } from '@nestjs/graphql'; +import { IsNotEmpty, IsString } from 'class-validator'; + +@ArgsType() +export class CheckUserExistsInput { + @Field(() => String) + @IsString() + @IsNotEmpty() + email: string; +} diff --git a/server/src/core/auth/services/auth.service.ts b/server/src/core/auth/services/auth.service.ts index e2a7abfdb..f4a6b614c 100644 --- a/server/src/core/auth/services/auth.service.ts +++ b/server/src/core/auth/services/auth.service.ts @@ -11,6 +11,7 @@ import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util'; import { Verify } from '../dto/verify.entity'; import { TokenService } from './token.service'; import { Prisma } from '@prisma/client'; +import { UserExists } from '../dto/user-exists.entity'; export type UserPayload = { firstName: string; @@ -26,18 +27,16 @@ export class AuthService { ) {} async challenge(challengeInput: ChallengeInput) { - assert( - PASSWORD_REGEX.test(challengeInput.password), - 'Password too weak', - BadRequestException, - ); - let user = await this.userService.findUnique({ where: { email: challengeInput.email, }, }); + const isPasswordValid = PASSWORD_REGEX.test(challengeInput.password); + + assert(!!user || isPasswordValid, 'Password too weak', BadRequestException); + if (!user) { const passwordHash = await hashPassword(challengeInput.password); @@ -92,4 +91,14 @@ export class AuthService { }, }; } + + async checkUserExists(email: string): Promise { + const user = await this.userService.findUnique({ + where: { + email, + }, + }); + + return { exists: !!user }; + } }