mirror of
https://github.com/lingble/twenty.git
synced 2025-11-26 19:04:58 +00:00
feat: rewrite auth (#364)
* feat: wip rewrite auth * feat: restructure folders and fix stories and tests * feat: remove auth provider and fix tests
This commit is contained in:
1
front/.nvmrc
Normal file
1
front/.nvmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
18.6.0
|
||||||
@@ -19,6 +19,7 @@
|
|||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^2.30.0",
|
"date-fns": "^2.30.0",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"libphonenumber-js": "^1.10.26",
|
"libphonenumber-js": "^1.10.26",
|
||||||
"luxon": "^3.3.0",
|
"luxon": "^3.3.0",
|
||||||
@@ -107,6 +108,7 @@
|
|||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/user-event": "^13.5.0",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^27.5.2",
|
||||||
|
"@types/js-cookie": "^3.0.3",
|
||||||
"@types/luxon": "^3.3.0",
|
"@types/luxon": "^3.3.0",
|
||||||
"@types/react-datepicker": "^4.11.2",
|
"@types/react-datepicker": "^4.11.2",
|
||||||
"@types/scroll-into-view": "^1.16.0",
|
"@types/scroll-into-view": "^1.16.0",
|
||||||
|
|||||||
@@ -1,27 +1,19 @@
|
|||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
import { ApolloProvider } from './providers/apollo/ApolloProvider';
|
||||||
|
import { AppThemeProvider } from './providers/theme/AppThemeProvider';
|
||||||
import { AppThemeProvider } from './providers/AppThemeProvider';
|
|
||||||
import { AuthProvider } from './providers/AuthProvider';
|
|
||||||
import { apiClient, mockClient } from './apollo';
|
|
||||||
import { App } from './App';
|
import { App } from './App';
|
||||||
|
|
||||||
export function AppWrapper() {
|
export function AppWrapper() {
|
||||||
const [isMockMode] = useRecoilState(isMockModeState);
|
|
||||||
return (
|
return (
|
||||||
<ApolloProvider client={isMockMode ? mockClient : apiClient}>
|
<ApolloProvider>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AppThemeProvider>
|
||||||
<AppThemeProvider>
|
<StrictMode>
|
||||||
<StrictMode>
|
<App />
|
||||||
<App />
|
</StrictMode>
|
||||||
</StrictMode>
|
</AppThemeProvider>
|
||||||
</AppThemeProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { ApolloProvider } from '@apollo/client';
|
import { ApolloProvider } from '@apollo/client';
|
||||||
import { ThemeProvider } from '@emotion/react';
|
import { ThemeProvider } from '@emotion/react';
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot, useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { isAuthenticatingState } from '@/auth/states/isAuthenticatingState';
|
||||||
import { darkTheme } from '@/ui/layout/styles/themes';
|
import { darkTheme } from '@/ui/layout/styles/themes';
|
||||||
import { App } from '~/App';
|
import { App } from '~/App';
|
||||||
import { AuthProvider } from '~/providers/AuthProvider';
|
|
||||||
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
|
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
|
||||||
|
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||||
import { mockedClient } from '~/testing/mockedClient';
|
import { mockedClient } from '~/testing/mockedClient';
|
||||||
|
|
||||||
export const render = () => renderWithDarkMode(false);
|
export const render = () => renderWithDarkMode(false);
|
||||||
|
|
||||||
|
const MockedAuth: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||||
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
|
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||||
|
|
||||||
|
setCurrentUser(mockedUsersData[0]);
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
|
||||||
|
return <>{children}</>;
|
||||||
|
};
|
||||||
|
|
||||||
export const renderWithDarkMode = (forceDarkMode?: boolean) => {
|
export const renderWithDarkMode = (forceDarkMode?: boolean) => {
|
||||||
const AppInStoryBook = (
|
const AppInStoryBook = (
|
||||||
<FullHeightStorybookLayout>
|
<FullHeightStorybookLayout>
|
||||||
<AuthProvider>
|
<MockedAuth>
|
||||||
<App />
|
<App />
|
||||||
</AuthProvider>
|
</MockedAuth>
|
||||||
</FullHeightStorybookLayout>
|
</FullHeightStorybookLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,125 +0,0 @@
|
|||||||
import {
|
|
||||||
ApolloClient,
|
|
||||||
ApolloLink,
|
|
||||||
createHttpLink,
|
|
||||||
from,
|
|
||||||
InMemoryCache,
|
|
||||||
Observable,
|
|
||||||
} from '@apollo/client';
|
|
||||||
import { setContext } from '@apollo/client/link/context';
|
|
||||||
import { onError } from '@apollo/client/link/error';
|
|
||||||
import { RestLink } from 'apollo-link-rest';
|
|
||||||
|
|
||||||
import { CommentThreadTarget } from './generated/graphql';
|
|
||||||
import { getTokensFromRefreshToken } from './modules/auth/services/AuthService';
|
|
||||||
import { mockedCompaniesData } from './testing/mock-data/companies';
|
|
||||||
import { mockedUsersData } from './testing/mock-data/users';
|
|
||||||
|
|
||||||
const apiLink = createHttpLink({
|
|
||||||
uri: `${process.env.REACT_APP_API_URL}`,
|
|
||||||
});
|
|
||||||
|
|
||||||
const withAuthHeadersLink = setContext((_, { headers }) => {
|
|
||||||
const token = localStorage.getItem('accessToken');
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
...headers,
|
|
||||||
authorization: token ? `Bearer ${token}` : '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const errorLink = onError(({ graphQLErrors, operation, forward }) => {
|
|
||||||
if (graphQLErrors) {
|
|
||||||
for (const err of graphQLErrors) {
|
|
||||||
switch (err.extensions.code) {
|
|
||||||
case 'UNAUTHENTICATED':
|
|
||||||
return new Observable((observer) => {
|
|
||||||
(async () => {
|
|
||||||
try {
|
|
||||||
await getTokensFromRefreshToken();
|
|
||||||
|
|
||||||
const oldHeaders = operation.getContext().headers;
|
|
||||||
|
|
||||||
operation.setContext({
|
|
||||||
headers: {
|
|
||||||
...oldHeaders,
|
|
||||||
authorization: `Bearer ${localStorage.getItem(
|
|
||||||
'accessToken',
|
|
||||||
)}`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const subscriber = {
|
|
||||||
next: observer.next.bind(observer),
|
|
||||||
error: observer.error.bind(observer),
|
|
||||||
complete: observer.complete.bind(observer),
|
|
||||||
};
|
|
||||||
|
|
||||||
forward(operation).subscribe(subscriber);
|
|
||||||
} catch (error) {
|
|
||||||
observer.error(error);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export const apiClient = new ApolloClient({
|
|
||||||
link: from([errorLink, withAuthHeadersLink, apiLink]),
|
|
||||||
cache: new InMemoryCache({
|
|
||||||
typePolicies: {
|
|
||||||
CommentThread: {
|
|
||||||
fields: {
|
|
||||||
commentThreadTargets: {
|
|
||||||
merge(
|
|
||||||
existing: CommentThreadTarget[] = [],
|
|
||||||
incoming: CommentThreadTarget[],
|
|
||||||
) {
|
|
||||||
return [...incoming];
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
defaultOptions: {
|
|
||||||
query: {
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const authLink = new RestLink({
|
|
||||||
uri: `${process.env.REACT_APP_AUTH_URL}`,
|
|
||||||
credentials: 'same-origin',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const authClient = new ApolloClient({
|
|
||||||
link: authLink,
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockLink = new ApolloLink((operation, forward) => {
|
|
||||||
return forward(operation).map((response) => {
|
|
||||||
if (operation.operationName === 'GetCompanies') {
|
|
||||||
return { data: { companies: mockedCompaniesData } };
|
|
||||||
}
|
|
||||||
if (operation.operationName === 'GetCurrentUser') {
|
|
||||||
return { data: { users: [mockedUsersData[0]] } };
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
export const mockClient = new ApolloClient({
|
|
||||||
link: from([mockLink, apiLink]),
|
|
||||||
cache: new InMemoryCache(),
|
|
||||||
defaultOptions: {
|
|
||||||
query: {
|
|
||||||
fetchPolicy: 'cache-first',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -22,6 +22,23 @@ export type AffectedRows = {
|
|||||||
count: Scalars['Int'];
|
count: Scalars['Int'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type AuthToken = {
|
||||||
|
__typename?: 'AuthToken';
|
||||||
|
expiresAt: Scalars['DateTime'];
|
||||||
|
token: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthTokenPair = {
|
||||||
|
__typename?: 'AuthTokenPair';
|
||||||
|
accessToken: AuthToken;
|
||||||
|
refreshToken: AuthToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AuthTokens = {
|
||||||
|
__typename?: 'AuthTokens';
|
||||||
|
tokens: AuthTokenPair;
|
||||||
|
};
|
||||||
|
|
||||||
export type BoolFieldUpdateOperationsInput = {
|
export type BoolFieldUpdateOperationsInput = {
|
||||||
set?: InputMaybe<Scalars['Boolean']>;
|
set?: InputMaybe<Scalars['Boolean']>;
|
||||||
};
|
};
|
||||||
@@ -681,8 +698,14 @@ export type JsonNullableFilter = {
|
|||||||
string_starts_with?: InputMaybe<Scalars['String']>;
|
string_starts_with?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type LoginToken = {
|
||||||
|
__typename?: 'LoginToken';
|
||||||
|
loginToken: AuthToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type Mutation = {
|
export type Mutation = {
|
||||||
__typename?: 'Mutation';
|
__typename?: 'Mutation';
|
||||||
|
challenge: LoginToken;
|
||||||
createOneComment: Comment;
|
createOneComment: Comment;
|
||||||
createOneCommentThread: CommentThread;
|
createOneCommentThread: CommentThread;
|
||||||
createOneCompany: Company;
|
createOneCompany: Company;
|
||||||
@@ -691,10 +714,18 @@ export type Mutation = {
|
|||||||
deleteManyCompany: AffectedRows;
|
deleteManyCompany: AffectedRows;
|
||||||
deleteManyPerson: AffectedRows;
|
deleteManyPerson: AffectedRows;
|
||||||
deleteManyPipelineProgress: AffectedRows;
|
deleteManyPipelineProgress: AffectedRows;
|
||||||
|
renewToken: AuthTokens;
|
||||||
updateOneCommentThread: CommentThread;
|
updateOneCommentThread: CommentThread;
|
||||||
updateOneCompany?: Maybe<Company>;
|
updateOneCompany?: Maybe<Company>;
|
||||||
updateOnePerson?: Maybe<Person>;
|
updateOnePerson?: Maybe<Person>;
|
||||||
updateOnePipelineProgress?: Maybe<PipelineProgress>;
|
updateOnePipelineProgress?: Maybe<PipelineProgress>;
|
||||||
|
verify: Verify;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationChallengeArgs = {
|
||||||
|
email: Scalars['String'];
|
||||||
|
password: Scalars['String'];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -738,6 +769,11 @@ export type MutationDeleteManyPipelineProgressArgs = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationRenewTokenArgs = {
|
||||||
|
refreshToken: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
export type MutationUpdateOneCommentThreadArgs = {
|
export type MutationUpdateOneCommentThreadArgs = {
|
||||||
data: CommentThreadUpdateInput;
|
data: CommentThreadUpdateInput;
|
||||||
where: CommentThreadWhereUniqueInput;
|
where: CommentThreadWhereUniqueInput;
|
||||||
@@ -761,6 +797,11 @@ export type MutationUpdateOnePipelineProgressArgs = {
|
|||||||
where: PipelineProgressWhereUniqueInput;
|
where: PipelineProgressWhereUniqueInput;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export type MutationVerifyArgs = {
|
||||||
|
loginToken: Scalars['String'];
|
||||||
|
};
|
||||||
|
|
||||||
export type NestedBoolFilter = {
|
export type NestedBoolFilter = {
|
||||||
equals?: InputMaybe<Scalars['Boolean']>;
|
equals?: InputMaybe<Scalars['Boolean']>;
|
||||||
not?: InputMaybe<NestedBoolFilter>;
|
not?: InputMaybe<NestedBoolFilter>;
|
||||||
@@ -1489,6 +1530,12 @@ export type UserWhereUniqueInput = {
|
|||||||
id?: InputMaybe<Scalars['String']>;
|
id?: InputMaybe<Scalars['String']>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Verify = {
|
||||||
|
__typename?: 'Verify';
|
||||||
|
tokens: AuthTokenPair;
|
||||||
|
user: User;
|
||||||
|
};
|
||||||
|
|
||||||
export type Workspace = {
|
export type Workspace = {
|
||||||
__typename?: 'Workspace';
|
__typename?: 'Workspace';
|
||||||
commentThreads?: Maybe<Array<CommentThread>>;
|
commentThreads?: Maybe<Array<CommentThread>>;
|
||||||
@@ -1519,6 +1566,28 @@ export type WorkspaceMember = {
|
|||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ChallengeMutationVariables = Exact<{
|
||||||
|
email: Scalars['String'];
|
||||||
|
password: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type ChallengeMutation = { __typename?: 'Mutation', challenge: { __typename?: 'LoginToken', loginToken: { __typename?: 'AuthToken', expiresAt: string, token: string } } };
|
||||||
|
|
||||||
|
export type VerifyMutationVariables = Exact<{
|
||||||
|
loginToken: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, email: string, displayName: string, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, workspace: { __typename?: 'Workspace', id: string, domainName: string, displayName: string, logo?: string | null } } | null }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
|
refreshToken: Scalars['String'];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
|
||||||
|
export type RenewTokenMutation = { __typename?: 'Mutation', renewToken: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', expiresAt: string, token: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CreateCommentMutationVariables = Exact<{
|
export type CreateCommentMutationVariables = Exact<{
|
||||||
commentId: Scalars['String'];
|
commentId: Scalars['String'];
|
||||||
commentText: Scalars['String'];
|
commentText: Scalars['String'];
|
||||||
@@ -1730,6 +1799,141 @@ export type GetUsersQueryVariables = Exact<{ [key: string]: never; }>;
|
|||||||
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string, email: string, displayName: string }> };
|
export type GetUsersQuery = { __typename?: 'Query', findManyUser: Array<{ __typename?: 'User', id: string, email: string, displayName: string }> };
|
||||||
|
|
||||||
|
|
||||||
|
export const ChallengeDocument = gql`
|
||||||
|
mutation Challenge($email: String!, $password: String!) {
|
||||||
|
challenge(email: $email, password: $password) {
|
||||||
|
loginToken {
|
||||||
|
expiresAt
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type ChallengeMutationFn = Apollo.MutationFunction<ChallengeMutation, ChallengeMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useChallengeMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useChallengeMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useChallengeMutation` 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 [challengeMutation, { data, loading, error }] = useChallengeMutation({
|
||||||
|
* variables: {
|
||||||
|
* email: // value for 'email'
|
||||||
|
* password: // value for 'password'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useChallengeMutation(baseOptions?: Apollo.MutationHookOptions<ChallengeMutation, ChallengeMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<ChallengeMutation, ChallengeMutationVariables>(ChallengeDocument, options);
|
||||||
|
}
|
||||||
|
export type ChallengeMutationHookResult = ReturnType<typeof useChallengeMutation>;
|
||||||
|
export type ChallengeMutationResult = Apollo.MutationResult<ChallengeMutation>;
|
||||||
|
export type ChallengeMutationOptions = Apollo.BaseMutationOptions<ChallengeMutation, ChallengeMutationVariables>;
|
||||||
|
export const VerifyDocument = gql`
|
||||||
|
mutation Verify($loginToken: String!) {
|
||||||
|
verify(loginToken: $loginToken) {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
displayName
|
||||||
|
workspaceMember {
|
||||||
|
id
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
domainName
|
||||||
|
displayName
|
||||||
|
logo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens {
|
||||||
|
accessToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type VerifyMutationFn = Apollo.MutationFunction<VerifyMutation, VerifyMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useVerifyMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useVerifyMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useVerifyMutation` 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 [verifyMutation, { data, loading, error }] = useVerifyMutation({
|
||||||
|
* variables: {
|
||||||
|
* loginToken: // value for 'loginToken'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useVerifyMutation(baseOptions?: Apollo.MutationHookOptions<VerifyMutation, VerifyMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<VerifyMutation, VerifyMutationVariables>(VerifyDocument, options);
|
||||||
|
}
|
||||||
|
export type VerifyMutationHookResult = ReturnType<typeof useVerifyMutation>;
|
||||||
|
export type VerifyMutationResult = Apollo.MutationResult<VerifyMutation>;
|
||||||
|
export type VerifyMutationOptions = Apollo.BaseMutationOptions<VerifyMutation, VerifyMutationVariables>;
|
||||||
|
export const RenewTokenDocument = gql`
|
||||||
|
mutation RenewToken($refreshToken: String!) {
|
||||||
|
renewToken(refreshToken: $refreshToken) {
|
||||||
|
tokens {
|
||||||
|
accessToken {
|
||||||
|
expiresAt
|
||||||
|
token
|
||||||
|
}
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export type RenewTokenMutationFn = Apollo.MutationFunction<RenewTokenMutation, RenewTokenMutationVariables>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* __useRenewTokenMutation__
|
||||||
|
*
|
||||||
|
* To run a mutation, you first call `useRenewTokenMutation` within a React component and pass it any options that fit your needs.
|
||||||
|
* When your component renders, `useRenewTokenMutation` 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 [renewTokenMutation, { data, loading, error }] = useRenewTokenMutation({
|
||||||
|
* variables: {
|
||||||
|
* refreshToken: // value for 'refreshToken'
|
||||||
|
* },
|
||||||
|
* });
|
||||||
|
*/
|
||||||
|
export function useRenewTokenMutation(baseOptions?: Apollo.MutationHookOptions<RenewTokenMutation, RenewTokenMutationVariables>) {
|
||||||
|
const options = {...defaultOptions, ...baseOptions}
|
||||||
|
return Apollo.useMutation<RenewTokenMutation, RenewTokenMutationVariables>(RenewTokenDocument, options);
|
||||||
|
}
|
||||||
|
export type RenewTokenMutationHookResult = ReturnType<typeof useRenewTokenMutation>;
|
||||||
|
export type RenewTokenMutationResult = Apollo.MutationResult<RenewTokenMutation>;
|
||||||
|
export type RenewTokenMutationOptions = Apollo.BaseMutationOptions<RenewTokenMutation, RenewTokenMutationVariables>;
|
||||||
export const CreateCommentDocument = gql`
|
export const CreateCommentDocument = gql`
|
||||||
mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $commentThreadId: String!, $createdAt: DateTime!) {
|
mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $commentThreadId: String!, $createdAt: DateTime!) {
|
||||||
createOneComment(
|
createOneComment(
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { keyframes } from '@emotion/react';
|
import { keyframes } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { hasAccessToken } from '../services/AuthService';
|
import { useIsLogged } from '../hooks/useIsLogged';
|
||||||
|
|
||||||
const EmptyContainer = styled.div`
|
const EmptyContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -34,13 +34,15 @@ export function RequireAuth({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isLogged = useIsLogged();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasAccessToken()) {
|
if (!isLogged) {
|
||||||
navigate('/auth');
|
navigate('/auth');
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [isLogged, navigate]);
|
||||||
|
|
||||||
if (!hasAccessToken())
|
if (!isLogged) {
|
||||||
return (
|
return (
|
||||||
<EmptyContainer>
|
<EmptyContainer>
|
||||||
<FadeInStyle>
|
<FadeInStyle>
|
||||||
@@ -48,5 +50,7 @@ export function RequireAuth({
|
|||||||
</FadeInStyle>
|
</FadeInStyle>
|
||||||
</EmptyContainer>
|
</EmptyContainer>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
|||||||
import { keyframes } from '@emotion/react';
|
import { keyframes } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
import { hasAccessToken } from '../services/AuthService';
|
import { useIsLogged } from '../hooks/useIsLogged';
|
||||||
|
|
||||||
const EmptyContainer = styled.div`
|
const EmptyContainer = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -34,13 +34,15 @@ export function RequireNotAuth({
|
|||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const isLogged = useIsLogged();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasAccessToken()) {
|
if (isLogged) {
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
}, [navigate]);
|
}, [isLogged, navigate]);
|
||||||
|
|
||||||
if (hasAccessToken())
|
if (isLogged) {
|
||||||
return (
|
return (
|
||||||
<EmptyContainer>
|
<EmptyContainer>
|
||||||
<FadeInStyle>
|
<FadeInStyle>
|
||||||
@@ -48,5 +50,7 @@ export function RequireNotAuth({
|
|||||||
</FadeInStyle>
|
</FadeInStyle>
|
||||||
</EmptyContainer>
|
</EmptyContainer>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|||||||
82
front/src/modules/auth/hooks/useAuth.ts
Normal file
82
front/src/modules/auth/hooks/useAuth.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { useChallengeMutation, useVerifyMutation } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { tokenService } from '../services/TokenService';
|
||||||
|
import { currentUserState } from '../states/currentUserState';
|
||||||
|
import { isAuthenticatingState } from '../states/isAuthenticatingState';
|
||||||
|
|
||||||
|
export function useAuth() {
|
||||||
|
const [, setCurrentUser] = useRecoilState(currentUserState);
|
||||||
|
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
||||||
|
|
||||||
|
const [challenge] = useChallengeMutation();
|
||||||
|
const [verify] = useVerifyMutation();
|
||||||
|
|
||||||
|
const handleChallenge = useCallback(
|
||||||
|
async (email: string, password: string) => {
|
||||||
|
const challengeResult = await challenge({
|
||||||
|
variables: {
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (challengeResult.errors) {
|
||||||
|
throw challengeResult.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!challengeResult.data?.challenge) {
|
||||||
|
throw new Error('No login token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return challengeResult.data.challenge;
|
||||||
|
},
|
||||||
|
[challenge],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVerify = useCallback(
|
||||||
|
async (loginToken: string) => {
|
||||||
|
const verifyResult = await verify({
|
||||||
|
variables: { loginToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (verifyResult.errors) {
|
||||||
|
throw verifyResult.errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!verifyResult.data?.verify) {
|
||||||
|
throw new Error('No verify result');
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenService.setTokenPair(verifyResult.data?.verify.tokens);
|
||||||
|
|
||||||
|
setIsAuthenticating(false);
|
||||||
|
setCurrentUser(verifyResult.data?.verify.user);
|
||||||
|
|
||||||
|
return verifyResult.data?.verify;
|
||||||
|
},
|
||||||
|
[setCurrentUser, setIsAuthenticating, verify],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLogin = useCallback(
|
||||||
|
async (email: string, password: string) => {
|
||||||
|
const { loginToken } = await handleChallenge(email, password);
|
||||||
|
|
||||||
|
await handleVerify(loginToken.token);
|
||||||
|
},
|
||||||
|
[handleChallenge, handleVerify],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleLogout = useCallback(() => {
|
||||||
|
tokenService.removeTokenPair();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
challenge: handleChallenge,
|
||||||
|
verify: handleVerify,
|
||||||
|
login: handleLogin,
|
||||||
|
logout: handleLogout,
|
||||||
|
};
|
||||||
|
}
|
||||||
21
front/src/modules/auth/hooks/useIsLogged.ts
Normal file
21
front/src/modules/auth/hooks/useIsLogged.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import { cookieStorage } from '@/utils/cookie-storage';
|
||||||
|
|
||||||
|
export function useIsLogged(): boolean {
|
||||||
|
const [value, setValue] = useState<string | undefined>(
|
||||||
|
cookieStorage.getItem('accessToken'),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateValue = (newValue: string | undefined) => setValue(newValue);
|
||||||
|
|
||||||
|
cookieStorage.addEventListener('accessToken', updateValue);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cookieStorage.removeEventListener('accessToken', updateValue);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return !!value;
|
||||||
|
}
|
||||||
@@ -1,13 +1,81 @@
|
|||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
ApolloLink,
|
||||||
|
HttpLink,
|
||||||
|
InMemoryCache,
|
||||||
|
UriFunction,
|
||||||
|
} from '@apollo/client';
|
||||||
import jwt from 'jwt-decode';
|
import jwt from 'jwt-decode';
|
||||||
|
|
||||||
export const hasAccessToken = () => {
|
import { cookieStorage } from '@/utils/cookie-storage';
|
||||||
const accessToken = localStorage.getItem('accessToken');
|
import {
|
||||||
|
RenewTokenDocument,
|
||||||
|
RenewTokenMutation,
|
||||||
|
RenewTokenMutationVariables,
|
||||||
|
} from '~/generated/graphql';
|
||||||
|
import { loggerLink } from '~/providers/apollo/logger';
|
||||||
|
|
||||||
return accessToken ? true : false;
|
import { tokenService } from './TokenService';
|
||||||
|
|
||||||
|
const logger = loggerLink(() => 'Twenty-Refresh');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renew token mutation with custom apollo client
|
||||||
|
* @param uri string | UriFunction | undefined
|
||||||
|
* @param refreshToken string
|
||||||
|
* @returns RenewTokenMutation
|
||||||
|
*/
|
||||||
|
const renewTokenMutation = async (
|
||||||
|
uri: string | UriFunction | undefined,
|
||||||
|
refreshToken: string,
|
||||||
|
) => {
|
||||||
|
const httpLink = new HttpLink({ uri });
|
||||||
|
|
||||||
|
// Create new client to call refresh token graphql mutation
|
||||||
|
const client = new ApolloClient({
|
||||||
|
link: ApolloLink.from([logger, httpLink]),
|
||||||
|
cache: new InMemoryCache({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data, errors } = await client.mutate<
|
||||||
|
RenewTokenMutation,
|
||||||
|
RenewTokenMutationVariables
|
||||||
|
>({
|
||||||
|
mutation: RenewTokenDocument,
|
||||||
|
variables: {
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'network-only',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (errors || !data) {
|
||||||
|
throw new Error('Something went wrong during token renewal');
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renew token and update cookie storage
|
||||||
|
* @param uri string | UriFunction | undefined
|
||||||
|
* @returns TokenPair
|
||||||
|
*/
|
||||||
|
export const renewToken = async (uri: string | UriFunction | undefined) => {
|
||||||
|
const tokenPair = tokenService.getTokenPair();
|
||||||
|
|
||||||
|
if (!tokenPair) {
|
||||||
|
throw new Error('Refresh token is not defined');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await renewTokenMutation(uri, tokenPair.refreshToken);
|
||||||
|
|
||||||
|
tokenService.setTokenPair(data.renewToken.tokens);
|
||||||
|
|
||||||
|
return data.renewToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getUserIdFromToken: () => string | null = () => {
|
export const getUserIdFromToken: () => string | null = () => {
|
||||||
const accessToken = localStorage.getItem('accessToken');
|
const accessToken = cookieStorage.getItem('accessToken');
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -18,76 +86,3 @@ export const getUserIdFromToken: () => string | null = () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasRefreshToken = () => {
|
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
|
||||||
|
|
||||||
return refreshToken ? true : false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTokensFromLoginToken = async (loginToken: string) => {
|
|
||||||
if (!loginToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
process.env.REACT_APP_AUTH_URL + '/verify' || '',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ loginToken }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const { tokens } = await response.json();
|
|
||||||
if (!tokens) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('accessToken', tokens.accessToken.token);
|
|
||||||
localStorage.setItem('refreshToken', tokens.refreshToken.token);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getTokensFromRefreshToken = async () => {
|
|
||||||
const refreshToken = localStorage.getItem('refreshToken');
|
|
||||||
if (!refreshToken) {
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(
|
|
||||||
process.env.REACT_APP_AUTH_URL + '/token' || '',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ refreshToken }),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
const { tokens } = await response.json();
|
|
||||||
|
|
||||||
if (!tokens) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
localStorage.setItem('accessToken', tokens.accessToken.token);
|
|
||||||
localStorage.setItem('refreshToken', tokens.refreshToken.token);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeTokens = () => {
|
|
||||||
localStorage.removeItem('refreshToken');
|
|
||||||
localStorage.removeItem('accessToken');
|
|
||||||
};
|
|
||||||
|
|||||||
34
front/src/modules/auth/services/TokenService.ts
Normal file
34
front/src/modules/auth/services/TokenService.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { cookieStorage } from '@/utils/cookie-storage';
|
||||||
|
import { AuthTokenPair } from '~/generated/graphql';
|
||||||
|
|
||||||
|
export class TokenService {
|
||||||
|
getTokenPair() {
|
||||||
|
const accessToken = cookieStorage.getItem('accessToken');
|
||||||
|
const refreshToken = cookieStorage.getItem('refreshToken');
|
||||||
|
|
||||||
|
if (!accessToken || !refreshToken) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setTokenPair(tokens: AuthTokenPair) {
|
||||||
|
cookieStorage.setItem('accessToken', tokens.accessToken.token, {
|
||||||
|
secure: true,
|
||||||
|
});
|
||||||
|
cookieStorage.setItem('refreshToken', tokens.refreshToken.token, {
|
||||||
|
secure: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeTokenPair() {
|
||||||
|
cookieStorage.removeItem('accessToken');
|
||||||
|
cookieStorage.removeItem('refreshToken');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const tokenService = new TokenService();
|
||||||
@@ -1,109 +1,6 @@
|
|||||||
import { waitFor } from '@testing-library/react';
|
import { cookieStorage } from '@/utils/cookie-storage';
|
||||||
|
|
||||||
import {
|
import { getUserIdFromToken } from '../AuthService';
|
||||||
getTokensFromLoginToken,
|
|
||||||
getTokensFromRefreshToken,
|
|
||||||
getUserIdFromToken,
|
|
||||||
hasAccessToken,
|
|
||||||
hasRefreshToken,
|
|
||||||
} from '../AuthService';
|
|
||||||
|
|
||||||
const validTokensPayload = {
|
|
||||||
accessToken: {
|
|
||||||
token:
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6MTY4Njk5MzQ4Mn0.F_FD6nJ5fssR_47v2XFhtzqjr-wrEQpqaWVq8iIlLJw',
|
|
||||||
expiresAt: '2023-06-17T09:18:02.942Z',
|
|
||||||
},
|
|
||||||
refreshToken: {
|
|
||||||
token:
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6OTQ2Mjk5MzE4MiwianRpIjoiNzBmMWNhMjctOTYxYi00ZGZlLWEwOTUtMTY2OWEwOGViMTVjIn0.xEdX9dOGzrPHrPsivQYB9ipYGJH-mJ7GSIVPacmIzfY',
|
|
||||||
expiresAt: '2023-09-15T09:13:02.952Z',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockFetch = async (
|
|
||||||
input: RequestInfo | URL,
|
|
||||||
init?: RequestInit,
|
|
||||||
): Promise<Response> => {
|
|
||||||
if (input.toString().match(/\/auth\/token$/g)) {
|
|
||||||
const refreshToken = init?.body
|
|
||||||
? JSON.parse(init.body.toString()).refreshToken
|
|
||||||
: null;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
tokens:
|
|
||||||
refreshToken === 'xxx-valid-refresh' ? validTokensPayload : null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (input.toString().match(/\/auth\/verify$/g)) {
|
|
||||||
const loginToken = init?.body
|
|
||||||
? JSON.parse(init.body.toString()).loginToken
|
|
||||||
: null;
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
resolve(
|
|
||||||
new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
tokens:
|
|
||||||
loginToken === 'xxx-valid-login' ? validTokensPayload : null,
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return new Promise(() => new Response());
|
|
||||||
};
|
|
||||||
|
|
||||||
global.fetch = mockFetch;
|
|
||||||
|
|
||||||
it('hasAccessToken is true when token is present', () => {
|
|
||||||
localStorage.setItem('accessToken', 'xxx');
|
|
||||||
expect(hasAccessToken()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hasAccessToken is false when token is not', () => {
|
|
||||||
expect(hasAccessToken()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hasRefreshToken is true when token is present', () => {
|
|
||||||
localStorage.setItem('refreshToken', 'xxx');
|
|
||||||
expect(hasRefreshToken()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hasRefreshToken is true when token is not', () => {
|
|
||||||
expect(hasRefreshToken()).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refreshToken does not refresh the token if refresh token is missing', () => {
|
|
||||||
getTokensFromRefreshToken();
|
|
||||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refreshToken does not refreh the token if refresh token is invalid', () => {
|
|
||||||
localStorage.setItem('refreshToken', 'xxx-invalid-refresh');
|
|
||||||
getTokensFromRefreshToken();
|
|
||||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refreshToken does not refreh the token if refresh token is empty', () => {
|
|
||||||
getTokensFromRefreshToken();
|
|
||||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('refreshToken refreshes the token if refresh token is valid', async () => {
|
|
||||||
localStorage.setItem('refreshToken', 'xxx-valid-refresh');
|
|
||||||
getTokensFromRefreshToken();
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(localStorage.getItem('accessToken')).toBe(
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6MTY4Njk5MzQ4Mn0.F_FD6nJ5fssR_47v2XFhtzqjr-wrEQpqaWVq8iIlLJw',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getUserIdFromToken returns null when the token is not present', async () => {
|
it('getUserIdFromToken returns null when the token is not present', async () => {
|
||||||
const userId = getUserIdFromToken();
|
const userId = getUserIdFromToken();
|
||||||
@@ -111,13 +8,13 @@ it('getUserIdFromToken returns null when the token is not present', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('getUserIdFromToken returns null when the token is not valid', async () => {
|
it('getUserIdFromToken returns null when the token is not valid', async () => {
|
||||||
localStorage.setItem('accessToken', 'xxx-invalid-access');
|
cookieStorage.setItem('accessToken', 'xxx-invalid-access');
|
||||||
const userId = getUserIdFromToken();
|
const userId = getUserIdFromToken();
|
||||||
expect(userId).toBeNull();
|
expect(userId).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getUserIdFromToken returns the right userId when the token is valid', async () => {
|
it('getUserIdFromToken returns the right userId when the token is valid', async () => {
|
||||||
localStorage.setItem(
|
cookieStorage.setItem(
|
||||||
'accessToken',
|
'accessToken',
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTI0ODgsImV4cCI6MTY4Njk5Mjc4OH0.IO7U5G14IrrQriw3JjrKVxmZgd6XKL6yUIwuNe_R55E',
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTI0ODgsImV4cCI6MTY4Njk5Mjc4OH0.IO7U5G14IrrQriw3JjrKVxmZgd6XKL6yUIwuNe_R55E',
|
||||||
);
|
);
|
||||||
@@ -125,28 +22,6 @@ it('getUserIdFromToken returns the right userId when the token is valid', async
|
|||||||
expect(userId).toBe('374fe3a5-df1e-4119-afe0-2a62a2ba481e');
|
expect(userId).toBe('374fe3a5-df1e-4119-afe0-2a62a2ba481e');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getTokensFromLoginToken does nothing if loginToken is empty', async () => {
|
|
||||||
await getTokensFromLoginToken('');
|
|
||||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
|
||||||
expect(localStorage.getItem('refreshToken')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getTokensFromLoginToken does nothing if loginToken is not valid', async () => {
|
|
||||||
await getTokensFromLoginToken('xxx-invalid-login');
|
|
||||||
expect(localStorage.getItem('accessToken')).toBeNull();
|
|
||||||
expect(localStorage.getItem('refreshToken')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getTokensFromLoginToken does nothing if loginToken is not valid', async () => {
|
|
||||||
await getTokensFromLoginToken('xxx-valid-login');
|
|
||||||
expect(localStorage.getItem('accessToken')).toBe(
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6MTY4Njk5MzQ4Mn0.F_FD6nJ5fssR_47v2XFhtzqjr-wrEQpqaWVq8iIlLJw',
|
|
||||||
);
|
|
||||||
expect(localStorage.getItem('refreshToken')).toBe(
|
|
||||||
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6OTQ2Mjk5MzE4MiwianRpIjoiNzBmMWNhMjctOTYxYi00ZGZlLWEwOTUtMTY2OWEwOGViMTVjIn0.xEdX9dOGzrPHrPsivQYB9ipYGJH-mJ7GSIVPacmIzfY',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
localStorage.clear();
|
cookieStorage.clear();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
import { tokenService } from '../TokenService';
|
||||||
|
|
||||||
|
const tokenPair = {
|
||||||
|
accessToken: {
|
||||||
|
token:
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJ3b3Jrc3BhY2VJZCI6InR3ZW50eS03ZWQ5ZDIxMi0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6MTY4Njk5MzQ4Mn0.F_FD6nJ5fssR_47v2XFhtzqjr-wrEQpqaWVq8iIlLJw',
|
||||||
|
expiresAt: '2023-06-17T09:18:02.942Z',
|
||||||
|
},
|
||||||
|
refreshToken: {
|
||||||
|
token:
|
||||||
|
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIzNzRmZTNhNS1kZjFlLTQxMTktYWZlMC0yYTYyYTJiYTQ4MWUiLCJpYXQiOjE2ODY5OTMxODIsImV4cCI6OTQ2Mjk5MzE4MiwianRpIjoiNzBmMWNhMjctOTYxYi00ZGZlLWEwOTUtMTY2OWEwOGViMTVjIn0.xEdX9dOGzrPHrPsivQYB9ipYGJH-mJ7GSIVPacmIzfY',
|
||||||
|
expiresAt: '2023-09-15T09:13:02.952Z',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
it('getTokenPair is fullfiled when token is present', () => {
|
||||||
|
tokenService.setTokenPair(tokenPair);
|
||||||
|
|
||||||
|
// Otherwise the test will fail because Cookies-js seems to be async but functions aren't promises
|
||||||
|
setTimeout(() => {
|
||||||
|
expect(tokenService.getTokenPair()).toBe({
|
||||||
|
accessToken: tokenPair.accessToken,
|
||||||
|
refreshToken: tokenPair.refreshToken,
|
||||||
|
});
|
||||||
|
}, 10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getTokenPair is null when token is not set', () => {
|
||||||
|
expect(tokenService.getTokenPair()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removeTokenPair clean cookie storage', () => {
|
||||||
|
tokenService.setTokenPair(tokenPair);
|
||||||
|
tokenService.removeTokenPair();
|
||||||
|
expect(tokenService.getTokenPair()).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
Cookies.remove('accessToken');
|
||||||
|
Cookies.remove('refreshToken');
|
||||||
|
});
|
||||||
1
front/src/modules/auth/services/index.ts
Normal file
1
front/src/modules/auth/services/index.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export * from './index';
|
||||||
60
front/src/modules/auth/services/update.ts
Normal file
60
front/src/modules/auth/services/update.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const CHALLENGE = gql`
|
||||||
|
mutation Challenge($email: String!, $password: String!) {
|
||||||
|
challenge(email: $email, password: $password) {
|
||||||
|
loginToken {
|
||||||
|
expiresAt
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const VERIFY = gql`
|
||||||
|
mutation Verify($loginToken: String!) {
|
||||||
|
verify(loginToken: $loginToken) {
|
||||||
|
user {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
displayName
|
||||||
|
workspaceMember {
|
||||||
|
id
|
||||||
|
workspace {
|
||||||
|
id
|
||||||
|
domainName
|
||||||
|
displayName
|
||||||
|
logo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens {
|
||||||
|
accessToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const RENEW_TOKEN = gql`
|
||||||
|
mutation RenewToken($refreshToken: String!) {
|
||||||
|
renewToken(refreshToken: $refreshToken) {
|
||||||
|
tokens {
|
||||||
|
accessToken {
|
||||||
|
expiresAt
|
||||||
|
token
|
||||||
|
}
|
||||||
|
refreshToken {
|
||||||
|
token
|
||||||
|
expiresAt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
|
|||||||
import { useMatch, useResolvedPath } from 'react-router-dom';
|
import { useMatch, useResolvedPath } from 'react-router-dom';
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
|
|
||||||
import { removeTokens } from '@/auth/services/AuthService';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
import {
|
import {
|
||||||
IconColorSwatch,
|
IconColorSwatch,
|
||||||
IconLogout,
|
IconLogout,
|
||||||
@@ -15,11 +15,15 @@ import NavTitle from '@/ui/layout/navbar/NavTitle';
|
|||||||
import SubNavbarContainer from '@/ui/layout/navbar/sub-navbar/SubNavBarContainer';
|
import SubNavbarContainer from '@/ui/layout/navbar/sub-navbar/SubNavBarContainer';
|
||||||
|
|
||||||
export function SettingsNavbar() {
|
export function SettingsNavbar() {
|
||||||
const logout = useCallback(() => {
|
|
||||||
removeTokens();
|
|
||||||
window.location.href = '/';
|
|
||||||
}, []);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const { logout } = useAuth();
|
||||||
|
|
||||||
|
const handleLogout = useCallback(() => {
|
||||||
|
logout();
|
||||||
|
window.location.href = '/';
|
||||||
|
}, [logout]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SubNavbarContainer backButtonTitle="Settings">
|
<SubNavbarContainer backButtonTitle="Settings">
|
||||||
<NavItemsContainer>
|
<NavItemsContainer>
|
||||||
@@ -63,7 +67,7 @@ export function SettingsNavbar() {
|
|||||||
<NavTitle label="Other" />
|
<NavTitle label="Other" />
|
||||||
<NavItem
|
<NavItem
|
||||||
label="Logout"
|
label="Logout"
|
||||||
onClick={logout}
|
onClick={handleLogout}
|
||||||
icon={<IconLogout size={theme.iconSizeMedium} />}
|
icon={<IconLogout size={theme.iconSizeMedium} />}
|
||||||
danger={true}
|
danger={true}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
import { useGetCurrentUserQuery as generatedUseGetCurrentUserQuery } from '~/generated/graphql';
|
|
||||||
|
|
||||||
export const GET_CURRENT_USER = gql`
|
export const GET_CURRENT_USER = gql`
|
||||||
query GetCurrentUser($uuid: String) {
|
query GetCurrentUser($uuid: String) {
|
||||||
users: findManyUser(where: { id: { equals: $uuid } }) {
|
users: findManyUser(where: { id: { equals: $uuid } }) {
|
||||||
@@ -30,11 +28,3 @@ export const GET_USERS = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function useGetCurrentUserQuery(userId: string | null) {
|
|
||||||
return generatedUseGetCurrentUserQuery({
|
|
||||||
variables: {
|
|
||||||
uuid: userId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
3
front/src/modules/utils/assert.ts
Normal file
3
front/src/modules/utils/assert.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function assertNotNull<T>(item: T): item is NonNullable<T> {
|
||||||
|
return item !== null && item !== undefined;
|
||||||
|
}
|
||||||
62
front/src/modules/utils/cookie-storage.ts
Normal file
62
front/src/modules/utils/cookie-storage.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import Cookies, { CookieAttributes } from 'js-cookie';
|
||||||
|
|
||||||
|
type Listener = (
|
||||||
|
newValue: string | undefined,
|
||||||
|
oldValue: string | undefined,
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
class CookieStorage {
|
||||||
|
private listeners: Record<string, Listener[]> = {};
|
||||||
|
private keys: Set<string> = new Set();
|
||||||
|
|
||||||
|
getItem(key: string): string | undefined {
|
||||||
|
return Cookies.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
setItem(key: string, value: string, attributes?: CookieAttributes): void {
|
||||||
|
const oldValue = this.getItem(key);
|
||||||
|
|
||||||
|
this.keys.add(key);
|
||||||
|
Cookies.set(key, value, attributes);
|
||||||
|
this.dispatch(key, value, oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeItem(key: string): void {
|
||||||
|
const oldValue = this.getItem(key);
|
||||||
|
|
||||||
|
this.keys.delete(key);
|
||||||
|
Cookies.remove(key);
|
||||||
|
this.dispatch(key, undefined, oldValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
this.keys.forEach((key) => this.removeItem(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
private dispatch(
|
||||||
|
key: string,
|
||||||
|
newValue: string | undefined,
|
||||||
|
oldValue: string | undefined,
|
||||||
|
): void {
|
||||||
|
if (this.listeners[key]) {
|
||||||
|
this.listeners[key].forEach((callback) => callback(newValue, oldValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(key: string, callback: Listener): void {
|
||||||
|
if (!this.listeners[key]) {
|
||||||
|
this.listeners[key] = [];
|
||||||
|
}
|
||||||
|
this.listeners[key].push(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(key: string, callback: Listener): void {
|
||||||
|
if (this.listeners[key]) {
|
||||||
|
this.listeners[key] = this.listeners[key].filter(
|
||||||
|
(listener) => listener !== callback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const cookieStorage = new CookieStorage();
|
||||||
16
front/src/modules/utils/promise-to-observable.ts
Normal file
16
front/src/modules/utils/promise-to-observable.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { Observable } from '@apollo/client';
|
||||||
|
|
||||||
|
export const promiseToObservable = <T>(promise: Promise<T>) =>
|
||||||
|
new Observable<T>((subscriber) => {
|
||||||
|
promise.then(
|
||||||
|
(value) => {
|
||||||
|
if (subscriber.closed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.next(value);
|
||||||
|
subscriber.complete();
|
||||||
|
},
|
||||||
|
(err) => subscriber.error(err),
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -10,7 +10,6 @@ import { HorizontalSeparator } from '@/auth/components/ui/HorizontalSeparator';
|
|||||||
import { Logo } from '@/auth/components/ui/Logo';
|
import { Logo } from '@/auth/components/ui/Logo';
|
||||||
import { Modal } from '@/auth/components/ui/Modal';
|
import { Modal } from '@/auth/components/ui/Modal';
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/ui/Title';
|
||||||
import { hasAccessToken } from '@/auth/services/AuthService';
|
|
||||||
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 { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
import { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
||||||
@@ -34,14 +33,6 @@ export function Index() {
|
|||||||
authFlowUserEmailState,
|
authFlowUserEmailState,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setMockMode(true);
|
|
||||||
|
|
||||||
if (hasAccessToken()) {
|
|
||||||
navigate('/');
|
|
||||||
}
|
|
||||||
}, [navigate, setMockMode]);
|
|
||||||
|
|
||||||
const onGoogleLoginClick = useCallback(() => {
|
const onGoogleLoginClick = useCallback(() => {
|
||||||
window.location.href = process.env.REACT_APP_AUTH_URL + '/google' || '';
|
window.location.href = process.env.REACT_APP_AUTH_URL + '/google' || '';
|
||||||
}, []);
|
}, []);
|
||||||
@@ -62,6 +53,10 @@ export function Index() {
|
|||||||
[onPasswordLoginClick],
|
[onPasswordLoginClick],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMockMode(true);
|
||||||
|
}, [navigate, setMockMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Companies />
|
<Companies />
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { Logo } from '@/auth/components/ui/Logo';
|
|||||||
import { Modal } from '@/auth/components/ui/Modal';
|
import { Modal } from '@/auth/components/ui/Modal';
|
||||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
||||||
import { Title } from '@/auth/components/ui/Title';
|
import { Title } from '@/auth/components/ui/Title';
|
||||||
import { getTokensFromLoginToken } from '@/auth/services/AuthService';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
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 { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
import { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
||||||
@@ -47,48 +47,28 @@ export function PasswordLogin() {
|
|||||||
const [internalPassword, setInternalPassword] = useState(prefillPassword);
|
const [internalPassword, setInternalPassword] = useState(prefillPassword);
|
||||||
const [formError, setFormError] = useState('');
|
const [formError, setFormError] = useState('');
|
||||||
|
|
||||||
const userLogin = useCallback(async () => {
|
const { login } = useAuth();
|
||||||
const response = await fetch(
|
|
||||||
process.env.REACT_APP_AUTH_URL + '/password' || '',
|
|
||||||
{
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
email: authFlowUserEmail,
|
|
||||||
password: internalPassword,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
const handleLogin = useCallback(async () => {
|
||||||
const errorData = await response.json();
|
try {
|
||||||
setFormError(errorData.message);
|
await login(authFlowUserEmail, internalPassword);
|
||||||
return;
|
setMockMode(false);
|
||||||
|
navigate('/');
|
||||||
|
} catch (err: any) {
|
||||||
|
setFormError(err.message);
|
||||||
}
|
}
|
||||||
const { loginToken } = await response.json();
|
}, [authFlowUserEmail, internalPassword, login, navigate, setMockMode]);
|
||||||
|
|
||||||
if (!loginToken) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await getTokensFromLoginToken(loginToken.token);
|
|
||||||
setMockMode(false);
|
|
||||||
|
|
||||||
navigate('/');
|
|
||||||
}, [authFlowUserEmail, internalPassword, navigate, setMockMode]);
|
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'enter',
|
'enter',
|
||||||
() => {
|
() => {
|
||||||
userLogin();
|
handleLogin();
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableOnContentEditable: true,
|
enableOnContentEditable: true,
|
||||||
enableOnFormTags: true,
|
enableOnFormTags: true,
|
||||||
},
|
},
|
||||||
[userLogin],
|
[handleLogin],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -118,7 +98,7 @@ export function PasswordLogin() {
|
|||||||
type="password"
|
type="password"
|
||||||
/>
|
/>
|
||||||
<StyledButtonContainer>
|
<StyledButtonContainer>
|
||||||
<PrimaryButton fullWidth onClick={userLogin}>
|
<PrimaryButton fullWidth onClick={handleLogin}>
|
||||||
Continue
|
Continue
|
||||||
</PrimaryButton>
|
</PrimaryButton>
|
||||||
</StyledButtonContainer>
|
</StyledButtonContainer>
|
||||||
|
|||||||
@@ -1,30 +1,33 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { getTokensFromLoginToken } from '@/auth/services/AuthService';
|
import { useAuth } from '@/auth/hooks/useAuth';
|
||||||
|
import { useIsLogged } from '@/auth/hooks/useIsLogged';
|
||||||
|
|
||||||
export function Verify() {
|
export function Verify() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
|
|
||||||
const loginToken = searchParams.get('loginToken');
|
const loginToken = searchParams.get('loginToken');
|
||||||
|
|
||||||
|
const isLogged = useIsLogged();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const { verify } = useAuth();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function getTokens() {
|
async function getTokens() {
|
||||||
if (!loginToken) {
|
if (!loginToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
await verify(loginToken);
|
||||||
await getTokensFromLoginToken(loginToken);
|
|
||||||
setIsLoading(false);
|
|
||||||
navigate('/');
|
navigate('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoading) {
|
if (!isLogged) {
|
||||||
getTokens();
|
getTokens();
|
||||||
}
|
}
|
||||||
}, [isLoading, navigate, loginToken]);
|
// Verify only needs to run once at mount
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { useRecoilState } from 'recoil';
|
|
||||||
|
|
||||||
import { getUserIdFromToken } from '@/auth/services/AuthService';
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
|
||||||
import { isAuthenticatingState } from '@/auth/states/isAuthenticatingState';
|
|
||||||
import { useGetCurrentUserQuery } from '@/users/services';
|
|
||||||
|
|
||||||
type OwnProps = {
|
|
||||||
children: JSX.Element;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AuthProvider({ children }: OwnProps) {
|
|
||||||
const [, setCurrentUser] = useRecoilState(currentUserState);
|
|
||||||
const [, setIsAuthenticating] = useRecoilState(isAuthenticatingState);
|
|
||||||
|
|
||||||
const userIdFromToken = getUserIdFromToken();
|
|
||||||
|
|
||||||
const { data } = useGetCurrentUserQuery(userIdFromToken);
|
|
||||||
const user = data?.users?.[0];
|
|
||||||
useEffect(() => {
|
|
||||||
if (user) {
|
|
||||||
setCurrentUser(user);
|
|
||||||
setIsAuthenticating(false);
|
|
||||||
}
|
|
||||||
}, [user, setCurrentUser, setIsAuthenticating]);
|
|
||||||
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
19
front/src/providers/apollo/ApolloProvider.tsx
Normal file
19
front/src/providers/apollo/ApolloProvider.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { ApolloProvider as ApolloProviderBase } from '@apollo/client';
|
||||||
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||||
|
|
||||||
|
import { apolloClient } from './apollo-client';
|
||||||
|
import { mockClient } from './mock-client';
|
||||||
|
|
||||||
|
export const ApolloProvider: React.FC<React.PropsWithChildren> = ({
|
||||||
|
children,
|
||||||
|
}) => {
|
||||||
|
const [isMockMode] = useRecoilState(isMockModeState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ApolloProviderBase client={isMockMode ? mockClient : apolloClient}>
|
||||||
|
{children}
|
||||||
|
</ApolloProviderBase>
|
||||||
|
);
|
||||||
|
};
|
||||||
36
front/src/providers/apollo/apollo-client.ts
Normal file
36
front/src/providers/apollo/apollo-client.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { InMemoryCache } from '@apollo/client';
|
||||||
|
|
||||||
|
import { tokenService } from '@/auth/services/TokenService';
|
||||||
|
import { CommentThreadTarget } from '~/generated/graphql';
|
||||||
|
|
||||||
|
import { ApolloFactory } from './apollo.factory';
|
||||||
|
|
||||||
|
const apollo = new ApolloFactory({
|
||||||
|
uri: `${process.env.REACT_APP_API_URL}`,
|
||||||
|
cache: new InMemoryCache({
|
||||||
|
typePolicies: {
|
||||||
|
CommentThread: {
|
||||||
|
fields: {
|
||||||
|
commentThreadTargets: {
|
||||||
|
merge(
|
||||||
|
existing: CommentThreadTarget[] = [],
|
||||||
|
incoming: CommentThreadTarget[],
|
||||||
|
) {
|
||||||
|
return [...incoming];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
defaultOptions: {
|
||||||
|
query: {
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUnauthenticatedError() {
|
||||||
|
tokenService.removeTokenPair();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apolloClient = apollo.getClient();
|
||||||
167
front/src/providers/apollo/apollo.factory.ts
Normal file
167
front/src/providers/apollo/apollo.factory.ts
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
/* eslint-disable no-loop-func */
|
||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
ApolloClientOptions,
|
||||||
|
ApolloLink,
|
||||||
|
createHttpLink,
|
||||||
|
ServerError,
|
||||||
|
ServerParseError,
|
||||||
|
} from '@apollo/client';
|
||||||
|
import { GraphQLErrors } from '@apollo/client/errors';
|
||||||
|
import { setContext } from '@apollo/client/link/context';
|
||||||
|
import { onError } from '@apollo/client/link/error';
|
||||||
|
import { RetryLink } from '@apollo/client/link/retry';
|
||||||
|
import { Observable } from '@apollo/client/utilities';
|
||||||
|
|
||||||
|
import { renewToken } from '@/auth/services/AuthService';
|
||||||
|
import { tokenService } from '@/auth/services/TokenService';
|
||||||
|
|
||||||
|
import { assertNotNull } from '../../modules/utils/assert';
|
||||||
|
import { promiseToObservable } from '../../modules/utils/promise-to-observable';
|
||||||
|
|
||||||
|
import { ApolloManager } from './interfaces/apollo-manager.interface';
|
||||||
|
import { loggerLink } from './logger';
|
||||||
|
|
||||||
|
const logger = loggerLink(() => 'Twenty');
|
||||||
|
|
||||||
|
let isRefreshing = false;
|
||||||
|
let pendingRequests: (() => void)[] = [];
|
||||||
|
|
||||||
|
const resolvePendingRequests = () => {
|
||||||
|
pendingRequests.map((callback) => callback());
|
||||||
|
pendingRequests = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface Options<TCacheShape> extends ApolloClientOptions<TCacheShape> {
|
||||||
|
onError?: (err: GraphQLErrors | undefined) => void;
|
||||||
|
onNetworkError?: (err: Error | ServerParseError | ServerError) => void;
|
||||||
|
onUnauthenticatedError?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ApolloFactory<TCacheShape> implements ApolloManager<TCacheShape> {
|
||||||
|
private client: ApolloClient<TCacheShape>;
|
||||||
|
|
||||||
|
constructor(opts: Options<TCacheShape>) {
|
||||||
|
const {
|
||||||
|
uri,
|
||||||
|
onError: onErrorCb,
|
||||||
|
onNetworkError,
|
||||||
|
onUnauthenticatedError,
|
||||||
|
...options
|
||||||
|
} = opts;
|
||||||
|
|
||||||
|
const buildApolloLink = (): ApolloLink => {
|
||||||
|
const httpLink = createHttpLink({
|
||||||
|
uri,
|
||||||
|
});
|
||||||
|
|
||||||
|
const authLink = setContext(async (_, { headers }) => {
|
||||||
|
const credentials = tokenService.getTokenPair();
|
||||||
|
|
||||||
|
return {
|
||||||
|
headers: {
|
||||||
|
...headers,
|
||||||
|
authorization: credentials?.accessToken
|
||||||
|
? `Bearer ${credentials?.accessToken}`
|
||||||
|
: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const retryLink = new RetryLink({
|
||||||
|
delay: {
|
||||||
|
initial: 100,
|
||||||
|
},
|
||||||
|
attempts: {
|
||||||
|
max: 2,
|
||||||
|
retryIf: (error) => !!error,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const errorLink = onError(
|
||||||
|
({ graphQLErrors, networkError, forward, operation }) => {
|
||||||
|
if (graphQLErrors) {
|
||||||
|
onErrorCb?.(graphQLErrors);
|
||||||
|
|
||||||
|
for (const graphQLError of graphQLErrors) {
|
||||||
|
switch (graphQLError?.extensions?.code) {
|
||||||
|
case 'UNAUTHENTICATED': {
|
||||||
|
// error code is set to UNAUTHENTICATED
|
||||||
|
// when AuthenticationError thrown in resolver
|
||||||
|
let forward$: Observable<boolean>;
|
||||||
|
|
||||||
|
if (!isRefreshing) {
|
||||||
|
isRefreshing = true;
|
||||||
|
forward$ = promiseToObservable(
|
||||||
|
renewToken(uri)
|
||||||
|
.then(() => {
|
||||||
|
resolvePendingRequests();
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
pendingRequests = [];
|
||||||
|
onUnauthenticatedError?.();
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isRefreshing = false;
|
||||||
|
}),
|
||||||
|
).filter((value) => Boolean(value));
|
||||||
|
} else {
|
||||||
|
// Will only emit once the Promise is resolved
|
||||||
|
forward$ = promiseToObservable(
|
||||||
|
new Promise<boolean>((resolve) => {
|
||||||
|
pendingRequests.push(() => resolve(true));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return forward$.flatMap(() => forward(operation));
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn(
|
||||||
|
`[GraphQL error]: Message: ${
|
||||||
|
graphQLError.message
|
||||||
|
}, Location: ${
|
||||||
|
graphQLError.locations
|
||||||
|
? JSON.stringify(graphQLError.locations)
|
||||||
|
: graphQLError.locations
|
||||||
|
}, Path: ${graphQLError.path}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (networkError) {
|
||||||
|
if (process.env.NODE_ENV === 'development') {
|
||||||
|
console.warn(`[Network error]: ${networkError}`);
|
||||||
|
}
|
||||||
|
onNetworkError?.(networkError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApolloLink.from(
|
||||||
|
[
|
||||||
|
errorLink,
|
||||||
|
authLink,
|
||||||
|
// Only show logger in dev mode
|
||||||
|
process.env.NODE_ENV !== 'production' ? logger : null,
|
||||||
|
retryLink,
|
||||||
|
httpLink,
|
||||||
|
].filter(assertNotNull),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
this.client = new ApolloClient({
|
||||||
|
...options,
|
||||||
|
link: buildApolloLink(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getClient() {
|
||||||
|
return this.client;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ApolloClient } from '@apollo/client';
|
||||||
|
|
||||||
|
export interface ApolloManager<TCacheShape> {
|
||||||
|
getClient(): ApolloClient<TCacheShape>;
|
||||||
|
}
|
||||||
45
front/src/providers/apollo/logger/format-title.ts
Normal file
45
front/src/providers/apollo/logger/format-title.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { OperationType } from './operation-type';
|
||||||
|
|
||||||
|
const operationTypeColors = {
|
||||||
|
query: '#03A9F4',
|
||||||
|
mutation: '#61A600',
|
||||||
|
subscription: '#61A600',
|
||||||
|
error: '#F51818',
|
||||||
|
default: '#61A600',
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOperationColor = (operationType: OperationType) => {
|
||||||
|
return operationTypeColors[operationType] ?? operationTypeColors.default;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatTitle = (
|
||||||
|
operationType: OperationType,
|
||||||
|
schemaName: string,
|
||||||
|
queryName: string,
|
||||||
|
time: string | number,
|
||||||
|
) => {
|
||||||
|
const headerCss = [
|
||||||
|
'color: gray; font-weight: lighter', // title
|
||||||
|
`color: ${getOperationColor(operationType)}; font-weight: bold;`, // operationType
|
||||||
|
'color: gray; font-weight: lighter;', // schemaName
|
||||||
|
'color: black; font-weight: bold;', // queryName
|
||||||
|
];
|
||||||
|
|
||||||
|
const parts = [
|
||||||
|
'%c apollo',
|
||||||
|
`%c${operationType}`,
|
||||||
|
`%c${schemaName}::%c${queryName}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (operationType !== OperationType.Subscription) {
|
||||||
|
parts.push(`%c(in ${time} ms)`);
|
||||||
|
headerCss.push('color: gray; font-weight: lighter;'); // time
|
||||||
|
} else {
|
||||||
|
parts.push(`%c(@ ${time})`);
|
||||||
|
headerCss.push('color: gray; font-weight: lighter;'); // time
|
||||||
|
}
|
||||||
|
|
||||||
|
return [parts.join(' '), ...headerCss];
|
||||||
|
};
|
||||||
|
|
||||||
|
export default formatTitle;
|
||||||
102
front/src/providers/apollo/logger/index.ts
Normal file
102
front/src/providers/apollo/logger/index.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { ApolloLink, gql, Operation } from '@apollo/client';
|
||||||
|
|
||||||
|
import formatTitle from './format-title';
|
||||||
|
|
||||||
|
const getGroup = (collapsed: boolean) =>
|
||||||
|
collapsed
|
||||||
|
? console.groupCollapsed.bind(console)
|
||||||
|
: console.group.bind(console);
|
||||||
|
|
||||||
|
const parseQuery = (queryString: string) => {
|
||||||
|
const queryObj = gql`
|
||||||
|
${queryString}
|
||||||
|
`;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const { name } = queryObj.definitions[0] as any;
|
||||||
|
return [name ? name.value : 'Generic', queryString.trim()];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loggerLink = (getSchemaName: (operation: Operation) => string) =>
|
||||||
|
new ApolloLink((operation, forward) => {
|
||||||
|
const schemaName = getSchemaName(operation);
|
||||||
|
operation.setContext({ start: Date.now() });
|
||||||
|
|
||||||
|
const { variables } = operation;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const operationType = (operation.query.definitions[0] as any).operation;
|
||||||
|
const headers = operation.getContext().headers;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
const [queryName, query] = parseQuery(operation.query.loc!.source.body);
|
||||||
|
|
||||||
|
if (operationType === 'subscription') {
|
||||||
|
const date = new Date().toLocaleTimeString();
|
||||||
|
|
||||||
|
const titleArgs = formatTitle(operationType, schemaName, queryName, date);
|
||||||
|
|
||||||
|
console.groupCollapsed(...titleArgs);
|
||||||
|
|
||||||
|
if (variables && Object.keys(variables).length !== 0) {
|
||||||
|
console.log('VARIABLES', variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('QUERY', query);
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
|
||||||
|
return forward(operation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return forward(operation).map((result) => {
|
||||||
|
const time = Date.now() - operation.getContext().start;
|
||||||
|
const errors = result.errors ?? result.data?.[queryName]?.errors;
|
||||||
|
const hasError = Boolean(errors);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const titleArgs = formatTitle(
|
||||||
|
operationType,
|
||||||
|
schemaName,
|
||||||
|
queryName,
|
||||||
|
time,
|
||||||
|
);
|
||||||
|
|
||||||
|
getGroup(!hasError)(...titleArgs);
|
||||||
|
|
||||||
|
if (errors) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
errors.forEach((err: any) => {
|
||||||
|
console.log(
|
||||||
|
`%c${err.message}`,
|
||||||
|
'color: #F51818; font-weight: lighter',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('HEADERS: ', headers);
|
||||||
|
|
||||||
|
if (variables && Object.keys(variables).length !== 0) {
|
||||||
|
console.log('VARIABLES', variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('QUERY', query);
|
||||||
|
|
||||||
|
if (result.data) {
|
||||||
|
console.log('RESULT', result.data);
|
||||||
|
}
|
||||||
|
if (errors) {
|
||||||
|
console.log('ERRORS', errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.groupEnd();
|
||||||
|
} catch {
|
||||||
|
// this may happen if console group is not supported
|
||||||
|
console.log(
|
||||||
|
`${operationType} ${schemaName}::${queryName} (in ${time} ms)`,
|
||||||
|
);
|
||||||
|
if (errors) {
|
||||||
|
console.error(errors);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
});
|
||||||
6
front/src/providers/apollo/logger/operation-type.ts
Normal file
6
front/src/providers/apollo/logger/operation-type.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export enum OperationType {
|
||||||
|
Query = 'query',
|
||||||
|
Mutation = 'mutation',
|
||||||
|
Subscription = 'subscription',
|
||||||
|
Error = 'error',
|
||||||
|
}
|
||||||
36
front/src/providers/apollo/mock-client.ts
Normal file
36
front/src/providers/apollo/mock-client.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
ApolloClient,
|
||||||
|
ApolloLink,
|
||||||
|
createHttpLink,
|
||||||
|
from,
|
||||||
|
InMemoryCache,
|
||||||
|
} from '@apollo/client';
|
||||||
|
|
||||||
|
import { mockedCompaniesData } from '~/testing/mock-data/companies';
|
||||||
|
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||||
|
|
||||||
|
const apiLink = createHttpLink({
|
||||||
|
uri: `${process.env.REACT_APP_API_URL}`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockLink = new ApolloLink((operation, forward) => {
|
||||||
|
return forward(operation).map((response) => {
|
||||||
|
if (operation.operationName === 'GetCompanies') {
|
||||||
|
return { data: { companies: mockedCompaniesData } };
|
||||||
|
}
|
||||||
|
if (operation.operationName === 'Verify') {
|
||||||
|
return { data: { user: [mockedUsersData[0]], tokens: {} } };
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
export const mockClient = new ApolloClient({
|
||||||
|
link: from([mockLink, apiLink]),
|
||||||
|
cache: new InMemoryCache(),
|
||||||
|
defaultOptions: {
|
||||||
|
query: {
|
||||||
|
fetchPolicy: 'cache-first',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -4,7 +4,6 @@ import { ApolloProvider } from '@apollo/client';
|
|||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
import { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||||
import { AuthProvider } from '~/providers/AuthProvider';
|
|
||||||
|
|
||||||
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
||||||
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
||||||
@@ -20,9 +19,7 @@ export function getRenderWrapperForPage(
|
|||||||
<ApolloProvider client={mockedClient}>
|
<ApolloProvider client={mockedClient}>
|
||||||
<MemoryRouter initialEntries={[currentPath]}>
|
<MemoryRouter initialEntries={[currentPath]}>
|
||||||
<FullHeightStorybookLayout>
|
<FullHeightStorybookLayout>
|
||||||
<AuthProvider>
|
<DefaultLayout>{children}</DefaultLayout>
|
||||||
<DefaultLayout>{children}</DefaultLayout>
|
|
||||||
</AuthProvider>
|
|
||||||
</FullHeightStorybookLayout>
|
</FullHeightStorybookLayout>
|
||||||
</MemoryRouter>
|
</MemoryRouter>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
|||||||
@@ -4892,6 +4892,11 @@
|
|||||||
jest-matcher-utils "^27.0.0"
|
jest-matcher-utils "^27.0.0"
|
||||||
pretty-format "^27.0.0"
|
pretty-format "^27.0.0"
|
||||||
|
|
||||||
|
"@types/js-cookie@^3.0.3":
|
||||||
|
version "3.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/js-cookie/-/js-cookie-3.0.3.tgz#d6bfbbdd0c187354ca555213d1962f6d0691ff4e"
|
||||||
|
integrity sha512-Xe7IImK09HP1sv2M/aI+48a20VX+TdRJucfq4vfRVy6nWN8PYPOEnlMRSgxJAgYQIXJVL8dZ4/ilAM7dWNaOww==
|
||||||
|
|
||||||
"@types/js-levenshtein@^1.1.1":
|
"@types/js-levenshtein@^1.1.1":
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
|
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
|
||||||
@@ -11937,6 +11942,11 @@ jose@^4.11.4:
|
|||||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca"
|
resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.4.tgz#59e09204e2670c3164ee24cbfe7115c6f8bff9ca"
|
||||||
integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==
|
integrity sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==
|
||||||
|
|
||||||
|
js-cookie@^3.0.5:
|
||||||
|
version "3.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-3.0.5.tgz#0b7e2fd0c01552c58ba86e0841f94dc2557dcdbc"
|
||||||
|
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
|
||||||
|
|
||||||
js-levenshtein@^1.1.6:
|
js-levenshtein@^1.1.6:
|
||||||
version "1.1.6"
|
version "1.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
|
|||||||
import { AuthService } from './services/auth.service';
|
import { AuthService } from './services/auth.service';
|
||||||
import { GoogleAuthController } from './controllers/google-auth.controller';
|
import { GoogleAuthController } from './controllers/google-auth.controller';
|
||||||
import { GoogleStrategy } from './strategies/google.auth.strategy';
|
import { GoogleStrategy } from './strategies/google.auth.strategy';
|
||||||
import { TokenController } from './controllers/token.controller';
|
|
||||||
import { PrismaService } from 'src/database/prisma.service';
|
import { PrismaService } from 'src/database/prisma.service';
|
||||||
import { UserModule } from '../user/user.module';
|
import { UserModule } from '../user/user.module';
|
||||||
import { AuthController } from './controllers/auth.controller';
|
import { VerifyAuthController } from './controllers/verify-auth.controller';
|
||||||
import { PasswordAuthController } from './controllers/password-auth.controller';
|
|
||||||
|
|
||||||
import { TokenService } from './services/token.service';
|
import { TokenService } from './services/token.service';
|
||||||
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
|
||||||
const jwtModule = JwtModule.registerAsync({
|
const jwtModule = JwtModule.registerAsync({
|
||||||
useFactory: async (configService: ConfigService) => {
|
useFactory: async (configService: ConfigService) => {
|
||||||
@@ -28,18 +27,14 @@ const jwtModule = JwtModule.registerAsync({
|
|||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
|
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
|
||||||
controllers: [
|
controllers: [GoogleAuthController, VerifyAuthController],
|
||||||
GoogleAuthController,
|
|
||||||
PasswordAuthController,
|
|
||||||
TokenController,
|
|
||||||
AuthController,
|
|
||||||
],
|
|
||||||
providers: [
|
providers: [
|
||||||
AuthService,
|
AuthService,
|
||||||
TokenService,
|
TokenService,
|
||||||
JwtAuthStrategy,
|
JwtAuthStrategy,
|
||||||
GoogleStrategy,
|
GoogleStrategy,
|
||||||
PrismaService,
|
PrismaService,
|
||||||
|
AuthResolver,
|
||||||
],
|
],
|
||||||
exports: [jwtModule],
|
exports: [jwtModule],
|
||||||
})
|
})
|
||||||
|
|||||||
30
server/src/core/auth/auth.resolver.spec.ts
Normal file
30
server/src/core/auth/auth.resolver.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AuthResolver } from './auth.resolver';
|
||||||
|
import { TokenService } from './services/token.service';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
|
||||||
|
describe('AuthResolver', () => {
|
||||||
|
let resolver: AuthResolver;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AuthResolver,
|
||||||
|
{
|
||||||
|
provide: AuthService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: TokenService,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
resolver = module.get<AuthResolver>(AuthResolver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(resolver).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
49
server/src/core/auth/auth.resolver.ts
Normal file
49
server/src/core/auth/auth.resolver.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||||
|
import { AuthTokens } from './dto/token.entity';
|
||||||
|
import { TokenService } from './services/token.service';
|
||||||
|
import { RefreshTokenInput } from './dto/refresh-token.input';
|
||||||
|
import { BadRequestException } from '@nestjs/common';
|
||||||
|
import { Verify } from './dto/verify.entity';
|
||||||
|
import { VerifyInput } from './dto/verify.input';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { LoginToken } from './dto/login-token.entity';
|
||||||
|
import { ChallengeInput } from './dto/challenge.input';
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class AuthResolver {
|
||||||
|
constructor(
|
||||||
|
private authService: AuthService,
|
||||||
|
private tokenService: TokenService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
@Mutation(() => LoginToken)
|
||||||
|
async challenge(@Args() challengeInput: ChallengeInput): Promise<LoginToken> {
|
||||||
|
const user = await this.authService.challenge(challengeInput);
|
||||||
|
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
||||||
|
|
||||||
|
return { loginToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => Verify)
|
||||||
|
async verify(@Args() verifyInput: VerifyInput): Promise<Verify> {
|
||||||
|
const email = await this.tokenService.verifyLoginToken(
|
||||||
|
verifyInput.loginToken,
|
||||||
|
);
|
||||||
|
const result = await this.authService.verify(email);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Mutation(() => AuthTokens)
|
||||||
|
async renewToken(@Args() args: RefreshTokenInput): Promise<AuthTokens> {
|
||||||
|
if (!args.refreshToken) {
|
||||||
|
throw new BadRequestException('Refresh token is mendatory');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
||||||
|
args.refreshToken,
|
||||||
|
);
|
||||||
|
|
||||||
|
return { tokens: tokens };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
|
||||||
import { PasswordAuthController } from './password-auth.controller';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
|
||||||
import { TokenService } from '../services/token.service';
|
|
||||||
|
|
||||||
describe('PasswordAuthController', () => {
|
|
||||||
let controller: PasswordAuthController;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
|
||||||
controllers: [PasswordAuthController],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: AuthService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: TokenService,
|
|
||||||
useValue: {},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).compile();
|
|
||||||
|
|
||||||
controller = module.get<PasswordAuthController>(PasswordAuthController);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be defined', () => {
|
|
||||||
expect(controller).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { Body, Controller, Post } from '@nestjs/common';
|
|
||||||
import { ChallengeInput } from '../dto/challenge.input';
|
|
||||||
import { AuthService } from '../services/auth.service';
|
|
||||||
import { LoginTokenEntity } from '../dto/login-token.entity';
|
|
||||||
import { TokenService } from '../services/token.service';
|
|
||||||
|
|
||||||
@Controller('auth/password')
|
|
||||||
export class PasswordAuthController {
|
|
||||||
constructor(
|
|
||||||
private readonly authService: AuthService,
|
|
||||||
private readonly tokenService: TokenService,
|
|
||||||
) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
async challenge(
|
|
||||||
@Body() challengeInput: ChallengeInput,
|
|
||||||
): Promise<LoginTokenEntity> {
|
|
||||||
const user = await this.authService.challenge(challengeInput);
|
|
||||||
const loginToken = await this.tokenService.generateLoginToken(user.email);
|
|
||||||
|
|
||||||
return { loginToken };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import { BadRequestException, Body, Controller, Post } from '@nestjs/common';
|
|
||||||
import { RefreshTokenInput } from '../dto/refresh-token.input';
|
|
||||||
import { TokenService } from '../services/token.service';
|
|
||||||
|
|
||||||
@Controller('auth/token')
|
|
||||||
export class TokenController {
|
|
||||||
constructor(private tokenService: TokenService) {}
|
|
||||||
|
|
||||||
@Post()
|
|
||||||
async generateAccessToken(@Body() body: RefreshTokenInput) {
|
|
||||||
if (!body.refreshToken) {
|
|
||||||
throw new BadRequestException('Refresh token is mendatory');
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokens = await this.tokenService.generateTokensFromRefreshToken(
|
|
||||||
body.refreshToken,
|
|
||||||
);
|
|
||||||
|
|
||||||
return { tokens: tokens };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { AuthController } from './auth.controller';
|
import { VerifyAuthController } from './verify-auth.controller';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { TokenService } from '../services/token.service';
|
import { TokenService } from '../services/token.service';
|
||||||
|
|
||||||
describe('AuthController', () => {
|
describe('VerifyAuthController', () => {
|
||||||
let controller: AuthController;
|
let controller: VerifyAuthController;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const module: TestingModule = await Test.createTestingModule({
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
controllers: [AuthController],
|
controllers: [VerifyAuthController],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: AuthService,
|
provide: AuthService,
|
||||||
@@ -21,7 +21,7 @@ describe('AuthController', () => {
|
|||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
controller = module.get<AuthController>(AuthController);
|
controller = module.get<VerifyAuthController>(VerifyAuthController);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should be defined', () => {
|
it('should be defined', () => {
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Body, Controller, Post } from '@nestjs/common';
|
import { Body, Controller, Post } from '@nestjs/common';
|
||||||
import { AuthService } from '../services/auth.service';
|
import { AuthService } from '../services/auth.service';
|
||||||
import { VerifyInput } from '../dto/verify.input';
|
import { VerifyInput } from '../dto/verify.input';
|
||||||
import { VerifyEntity } from '../dto/verify.entity';
|
import { Verify } from '../dto/verify.entity';
|
||||||
import { TokenService } from '../services/token.service';
|
import { TokenService } from '../services/token.service';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth/verify')
|
||||||
export class AuthController {
|
export class VerifyAuthController {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly authService: AuthService,
|
private readonly authService: AuthService,
|
||||||
private readonly tokenService: TokenService,
|
private readonly tokenService: TokenService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('verify')
|
@Post()
|
||||||
async verify(@Body() verifyInput: VerifyInput): Promise<VerifyEntity> {
|
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
|
||||||
const email = await this.tokenService.verifyLoginToken(
|
const email = await this.tokenService.verifyLoginToken(
|
||||||
verifyInput.loginToken,
|
verifyInput.loginToken,
|
||||||
);
|
);
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
export class ChallengeInput {
|
export class ChallengeInput {
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
password: string;
|
password: string;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { TokenEntity } from './token.entity';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
import { AuthToken } from './token.entity';
|
||||||
|
|
||||||
export class LoginTokenEntity {
|
@ObjectType()
|
||||||
loginToken: TokenEntity;
|
export class LoginToken {
|
||||||
|
@Field(() => AuthToken)
|
||||||
|
loginToken: AuthToken;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
export class RefreshTokenInput {
|
export class RefreshTokenInput {
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
|
|||||||
@@ -6,18 +6,23 @@ import {
|
|||||||
MinLength,
|
MinLength,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { PASSWORD_REGEX } from '../auth.util';
|
import { PASSWORD_REGEX } from '../auth.util';
|
||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
export class RegisterInput {
|
export class RegisterInput {
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsEmail()
|
@IsEmail()
|
||||||
email: string;
|
email: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
|
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
|
||||||
password: string;
|
password: string;
|
||||||
|
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
displayName: string;
|
displayName: string;
|
||||||
|
|||||||
@@ -1,4 +1,25 @@
|
|||||||
export class TokenEntity {
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AuthToken {
|
||||||
|
@Field(() => String)
|
||||||
token: string;
|
token: string;
|
||||||
|
|
||||||
|
@Field(() => Date)
|
||||||
expiresAt: Date;
|
expiresAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AuthTokenPair {
|
||||||
|
@Field(() => AuthToken)
|
||||||
|
accessToken: AuthToken;
|
||||||
|
|
||||||
|
@Field(() => AuthToken)
|
||||||
|
refreshToken: AuthToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
export class AuthTokens {
|
||||||
|
@Field(() => AuthTokenPair)
|
||||||
|
tokens: AuthTokenPair;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { TokenEntity } from './token.entity';
|
import { Field, ObjectType } from '@nestjs/graphql';
|
||||||
import { User } from '@prisma/client';
|
import { AuthTokens } from './token.entity';
|
||||||
|
import { User } from 'src/core/@generated/user/user.model';
|
||||||
|
|
||||||
export class VerifyEntity {
|
@ObjectType()
|
||||||
user: Omit<User, 'passwordHash'>;
|
export class Verify extends AuthTokens {
|
||||||
|
@Field(() => User)
|
||||||
tokens: {
|
user: User;
|
||||||
accessToken: TokenEntity;
|
|
||||||
refreshToken: TokenEntity;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import { ArgsType, Field } from '@nestjs/graphql';
|
||||||
import { IsNotEmpty, IsString } from 'class-validator';
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
@ArgsType()
|
||||||
export class VerifyInput {
|
export class VerifyInput {
|
||||||
|
@Field(() => String)
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@IsString()
|
@IsString()
|
||||||
loginToken: string;
|
loginToken: string;
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { UserService } from 'src/core/user/user.service';
|
|||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
import { RegisterInput } from '../dto/register.input';
|
import { RegisterInput } from '../dto/register.input';
|
||||||
import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util';
|
import { PASSWORD_REGEX, compareHash, hashPassword } from '../auth.util';
|
||||||
import { VerifyEntity } from '../dto/verify.entity';
|
import { Verify } from '../dto/verify.entity';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
export type UserPayload = {
|
export type UserPayload = {
|
||||||
@@ -73,17 +73,17 @@ export class AuthService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(email: string): Promise<VerifyEntity> {
|
async verify(email: string): Promise<Verify> {
|
||||||
const data = await this.userService.findUnique({
|
const user = await this.userService.findUnique({
|
||||||
where: {
|
where: {
|
||||||
email,
|
email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
assert(data, "This user doesn't exist", NotFoundException);
|
assert(user, "This user doesn't exist", NotFoundException);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// passwordHash is hidden for security reasons
|
||||||
const { passwordHash: _, ...user } = data;
|
user.passwordHash = '';
|
||||||
|
|
||||||
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||||
const refreshToken = await this.tokenService.generateRefreshToken(user.id);
|
const refreshToken = await this.tokenService.generateRefreshToken(user.id);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { PrismaService } from 'src/database/prisma.service';
|
|||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
import { addMilliseconds } from 'date-fns';
|
import { addMilliseconds } from 'date-fns';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { TokenEntity } from '../dto/token.entity';
|
import { AuthToken } from '../dto/token.entity';
|
||||||
import { TokenExpiredError } from 'jsonwebtoken';
|
import { TokenExpiredError } from 'jsonwebtoken';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -24,7 +24,7 @@ export class TokenService {
|
|||||||
private readonly prismaService: PrismaService,
|
private readonly prismaService: PrismaService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async generateAccessToken(userId: string): Promise<TokenEntity> {
|
async generateAccessToken(userId: string): Promise<AuthToken> {
|
||||||
const expiresIn = this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN');
|
const expiresIn = this.configService.get<string>('ACCESS_TOKEN_EXPIRES_IN');
|
||||||
assert(expiresIn, '', InternalServerErrorException);
|
assert(expiresIn, '', InternalServerErrorException);
|
||||||
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
const expiresAt = addMilliseconds(new Date().getTime(), ms(expiresIn));
|
||||||
@@ -55,7 +55,7 @@ export class TokenService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateRefreshToken(userId: string): Promise<TokenEntity> {
|
async generateRefreshToken(userId: string): Promise<AuthToken> {
|
||||||
const secret = this.configService.get('REFRESH_TOKEN_SECRET');
|
const secret = this.configService.get('REFRESH_TOKEN_SECRET');
|
||||||
const expiresIn = this.configService.get<string>(
|
const expiresIn = this.configService.get<string>(
|
||||||
'REFRESH_TOKEN_EXPIRES_IN',
|
'REFRESH_TOKEN_EXPIRES_IN',
|
||||||
@@ -86,7 +86,7 @@ export class TokenService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async generateLoginToken(email: string): Promise<TokenEntity> {
|
async generateLoginToken(email: string): Promise<AuthToken> {
|
||||||
const secret = this.configService.get('LOGIN_TOKEN_SECRET');
|
const secret = this.configService.get('LOGIN_TOKEN_SECRET');
|
||||||
const expiresIn = this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN');
|
const expiresIn = this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN');
|
||||||
assert(expiresIn, '', InternalServerErrorException);
|
assert(expiresIn, '', InternalServerErrorException);
|
||||||
@@ -163,8 +163,8 @@ export class TokenService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateTokensFromRefreshToken(token: string): Promise<{
|
async generateTokensFromRefreshToken(token: string): Promise<{
|
||||||
accessToken: TokenEntity;
|
accessToken: AuthToken;
|
||||||
refreshToken: TokenEntity;
|
refreshToken: AuthToken;
|
||||||
}> {
|
}> {
|
||||||
const {
|
const {
|
||||||
user,
|
user,
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { WorkspaceService } from './services/workspace.service';
|
import { WorkspaceService } from './services/workspace.service';
|
||||||
import { WorkspaceMemberService } from './services/workspace-member.service';
|
import { WorkspaceMemberService } from './services/workspace-member.service';
|
||||||
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [
|
providers: [WorkspaceService, WorkspaceMemberService],
|
||||||
WorkspaceService,
|
|
||||||
WorkspaceMemberService,
|
|
||||||
WorkspaceMemberResolver,
|
|
||||||
],
|
|
||||||
exports: [WorkspaceService, WorkspaceMemberService],
|
exports: [WorkspaceService, WorkspaceMemberService],
|
||||||
})
|
})
|
||||||
export class WorkspaceModule {}
|
export class WorkspaceModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user