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
This commit is contained in:
@ -2286,6 +2286,7 @@ export type PipelineWhereUniqueInput = {
|
|||||||
|
|
||||||
export type Query = {
|
export type Query = {
|
||||||
__typename?: 'Query';
|
__typename?: 'Query';
|
||||||
|
checkUserExists: UserExists;
|
||||||
clientConfig: ClientConfig;
|
clientConfig: ClientConfig;
|
||||||
currentUser: User;
|
currentUser: User;
|
||||||
currentWorkspace: Workspace;
|
currentWorkspace: Workspace;
|
||||||
@ -2299,6 +2300,11 @@ export type Query = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type QueryCheckUserExistsArgs = {
|
||||||
|
email: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type QueryFindManyCommentThreadsArgs = {
|
export type QueryFindManyCommentThreadsArgs = {
|
||||||
cursor?: InputMaybe<CommentThreadWhereUniqueInput>;
|
cursor?: InputMaybe<CommentThreadWhereUniqueInput>;
|
||||||
distinct?: InputMaybe<Array<CommentThreadScalarFieldEnum>>;
|
distinct?: InputMaybe<Array<CommentThreadScalarFieldEnum>>;
|
||||||
@ -2498,6 +2504,11 @@ export type UserCreateWithoutWorkspaceMemberInput = {
|
|||||||
updatedAt?: InputMaybe<Scalars['DateTime']>;
|
updatedAt?: InputMaybe<Scalars['DateTime']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserExists = {
|
||||||
|
__typename?: 'UserExists';
|
||||||
|
exists: Scalars['Boolean'];
|
||||||
|
};
|
||||||
|
|
||||||
export type UserOrderByWithRelationInput = {
|
export type UserOrderByWithRelationInput = {
|
||||||
avatarUrl?: InputMaybe<SortOrder>;
|
avatarUrl?: InputMaybe<SortOrder>;
|
||||||
comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
|
comments?: InputMaybe<CommentOrderByRelationAggregateInput>;
|
||||||
@ -2789,6 +2800,13 @@ export type CreateEventMutationVariables = Exact<{
|
|||||||
|
|
||||||
export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } };
|
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<{
|
export type ChallengeMutationVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
password: Scalars['String'];
|
password: Scalars['String'];
|
||||||
@ -3116,6 +3134,41 @@ export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions<
|
|||||||
export type CreateEventMutationHookResult = ReturnType<typeof useCreateEventMutation>;
|
export type CreateEventMutationHookResult = ReturnType<typeof useCreateEventMutation>;
|
||||||
export type CreateEventMutationResult = Apollo.MutationResult<CreateEventMutation>;
|
export type CreateEventMutationResult = Apollo.MutationResult<CreateEventMutation>;
|
||||||
export type CreateEventMutationOptions = Apollo.BaseMutationOptions<CreateEventMutation, CreateEventMutationVariables>;
|
export type CreateEventMutationOptions = Apollo.BaseMutationOptions<CreateEventMutation, CreateEventMutationVariables>;
|
||||||
|
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<CheckUserExistsQuery, CheckUserExistsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useQuery<CheckUserExistsQuery, CheckUserExistsQueryVariables>(CheckUserExistsDocument, options);
|
||||||
|
}
|
||||||
|
export function useCheckUserExistsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<CheckUserExistsQuery, CheckUserExistsQueryVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useLazyQuery<CheckUserExistsQuery, CheckUserExistsQueryVariables>(CheckUserExistsDocument, options);
|
||||||
|
}
|
||||||
|
export type CheckUserExistsQueryHookResult = ReturnType<typeof useCheckUserExistsQuery>;
|
||||||
|
export type CheckUserExistsLazyQueryHookResult = ReturnType<typeof useCheckUserExistsLazyQuery>;
|
||||||
|
export type CheckUserExistsQueryResult = Apollo.QueryResult<CheckUserExistsQuery, CheckUserExistsQueryVariables>;
|
||||||
export const ChallengeDocument = gql`
|
export const ChallengeDocument = gql`
|
||||||
mutation Challenge($email: String!, $password: String!) {
|
mutation Challenge($email: String!, $password: String!) {
|
||||||
challenge(email: $email, password: $password) {
|
challenge(email: $email, password: $password) {
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
|
export * from './select';
|
||||||
export * from './update';
|
export * from './update';
|
||||||
|
|||||||
9
front/src/modules/auth/services/select.ts
Normal file
9
front/src/modules/auth/services/select.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CHECK_USER_EXISTS = gql`
|
||||||
|
query CheckUserExists($email: String!) {
|
||||||
|
checkUserExists(email: $email) {
|
||||||
|
exists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -12,6 +12,7 @@ import { Title } from '@/auth/components/ui/Title';
|
|||||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
||||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
import { authProvidersState } from '@/client-config/states/authProvidersState';
|
||||||
|
import { isDemoModeState } from '@/client-config/states/isDemoModeState';
|
||||||
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/hotkeys/hooks/useScopedHotkeys';
|
||||||
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysScope';
|
||||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
@ -35,7 +36,7 @@ export function Index() {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [, setMockMode] = useRecoilState(isMockModeState);
|
const [, setMockMode] = useRecoilState(isMockModeState);
|
||||||
const [authProviders] = useRecoilState(authProvidersState);
|
const [authProviders] = useRecoilState(authProvidersState);
|
||||||
const [demoMode] = useRecoilState(authProvidersState);
|
const [demoMode] = useRecoilState(isDemoModeState);
|
||||||
|
|
||||||
const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState(
|
const [authFlowUserEmail, setAuthFlowUserEmail] = useRecoilState(
|
||||||
authFlowUserEmailState,
|
authFlowUserEmailState,
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import { InternalHotkeysScope } from '@/hotkeys/types/internal/InternalHotkeysSc
|
|||||||
import { MainButton } from '@/ui/components/buttons/MainButton';
|
import { MainButton } from '@/ui/components/buttons/MainButton';
|
||||||
import { TextInput } from '@/ui/components/inputs/TextInput';
|
import { TextInput } from '@/ui/components/inputs/TextInput';
|
||||||
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
import { SubSectionTitle } from '@/ui/components/section-titles/SubSectionTitle';
|
||||||
|
import { useCheckUserExistsQuery } from '~/generated/graphql';
|
||||||
|
|
||||||
const StyledContentContainer = styled.div`
|
const StyledContentContainer = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -84,11 +85,20 @@ export function PasswordLogin() {
|
|||||||
[handleLogin],
|
[handleLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const { loading, data } = useCheckUserExistsQuery({
|
||||||
|
variables: {
|
||||||
|
email: authFlowUserEmail,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Logo />
|
<Logo />
|
||||||
<Title>Welcome to Twenty</Title>
|
<Title>Welcome to Twenty</Title>
|
||||||
<SubTitle>Enter your credentials to sign in</SubTitle>
|
<SubTitle>
|
||||||
|
Enter your credentials to sign{' '}
|
||||||
|
{data?.checkUserExists.exists ? 'in' : 'up'}
|
||||||
|
</SubTitle>
|
||||||
<StyledAnimatedContent>
|
<StyledAnimatedContent>
|
||||||
<StyledContentContainer>
|
<StyledContentContainer>
|
||||||
<StyledSectionContainer>
|
<StyledSectionContainer>
|
||||||
@ -115,7 +125,7 @@ export function PasswordLogin() {
|
|||||||
<MainButton
|
<MainButton
|
||||||
title="Continue"
|
title="Continue"
|
||||||
onClick={handleLogin}
|
onClick={handleLogin}
|
||||||
disabled={!authFlowUserEmail || !internalPassword}
|
disabled={!authFlowUserEmail || !internalPassword || loading}
|
||||||
fullWidth
|
fullWidth
|
||||||
/>
|
/>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
|
|||||||
@ -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 { AuthTokens } from './dto/token.entity';
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
import { RefreshTokenInput } from './dto/refresh-token.input';
|
import { RefreshTokenInput } from './dto/refresh-token.input';
|
||||||
@ -13,6 +13,8 @@ import {
|
|||||||
PrismaSelector,
|
PrismaSelector,
|
||||||
} from 'src/decorators/prisma-select.decorator';
|
} from 'src/decorators/prisma-select.decorator';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { UserExists } from './dto/user-exists.entity';
|
||||||
|
import { CheckUserExistsInput } from './dto/user-exists.input';
|
||||||
|
|
||||||
@Resolver()
|
@Resolver()
|
||||||
export class AuthResolver {
|
export class AuthResolver {
|
||||||
@ -21,6 +23,16 @@ export class AuthResolver {
|
|||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
|
@Query(() => UserExists)
|
||||||
|
async checkUserExists(
|
||||||
|
@Args() checkUserExistsInput: CheckUserExistsInput,
|
||||||
|
): Promise<UserExists> {
|
||||||
|
const { exists } = await this.authService.checkUserExists(
|
||||||
|
checkUserExistsInput.email,
|
||||||
|
);
|
||||||
|
return { exists };
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => LoginToken)
|
@Mutation(() => LoginToken)
|
||||||
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||||
const user = await this.authService.challenge(challengeInput);
|
const user = await this.authService.challenge(challengeInput);
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import { ArgsType, Field } from '@nestjs/graphql';
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
import {
|
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
IsEmail,
|
|
||||||
IsNotEmpty,
|
|
||||||
IsString,
|
|
||||||
Matches,
|
|
||||||
MinLength,
|
|
||||||
} from 'class-validator';
|
|
||||||
import { PASSWORD_REGEX } from '../auth.util';
|
|
||||||
|
|
||||||
@ArgsType()
|
@ArgsType()
|
||||||
export class ChallengeInput {
|
export class ChallengeInput {
|
||||||
@ -18,7 +11,5 @@ export class ChallengeInput {
|
|||||||
@Field(() => String)
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
|
||||||
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
|
|
||||||
password: string;
|
password: string;
|
||||||
}
|
}
|
||||||
|
|||||||
7
server/src/core/auth/dto/user-exists.entity.ts
Normal file
7
server/src/core/auth/dto/user-exists.entity.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class UserExists {
|
||||||
|
@Field(() => Boolean)
|
||||||
|
exists: boolean;
|
||||||
|
}
|
||||||
10
server/src/core/auth/dto/user-exists.input.ts
Normal file
10
server/src/core/auth/dto/user-exists.input.ts
Normal file
@ -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;
|
||||||
|
}
|
||||||
@ -11,6 +11,7 @@ import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util';
|
|||||||
import { Verify } from '../dto/verify.entity';
|
import { Verify } from '../dto/verify.entity';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
import { Prisma } from '@prisma/client';
|
import { Prisma } from '@prisma/client';
|
||||||
|
import { UserExists } from '../dto/user-exists.entity';
|
||||||
|
|
||||||
export type UserPayload = {
|
export type UserPayload = {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
@ -26,18 +27,16 @@ export class AuthService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async challenge(challengeInput: ChallengeInput) {
|
async challenge(challengeInput: ChallengeInput) {
|
||||||
assert(
|
|
||||||
PASSWORD_REGEX.test(challengeInput.password),
|
|
||||||
'Password too weak',
|
|
||||||
BadRequestException,
|
|
||||||
);
|
|
||||||
|
|
||||||
let user = await this.userService.findUnique({
|
let user = await this.userService.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email: challengeInput.email,
|
email: challengeInput.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const isPasswordValid = PASSWORD_REGEX.test(challengeInput.password);
|
||||||
|
|
||||||
|
assert(!!user || isPasswordValid, 'Password too weak', BadRequestException);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
const passwordHash = await hashPassword(challengeInput.password);
|
const passwordHash = await hashPassword(challengeInput.password);
|
||||||
|
|
||||||
@ -92,4 +91,14 @@ export class AuthService {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async checkUserExists(email: string): Promise<UserExists> {
|
||||||
|
const user = await this.userService.findUnique({
|
||||||
|
where: {
|
||||||
|
email,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return { exists: !!user };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user