mirror of
https://github.com/lingble/twenty.git
synced 2025-11-19 23:44:51 +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",
|
||||
"date-fns": "^2.30.0",
|
||||
"graphql": "^16.6.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"libphonenumber-js": "^1.10.26",
|
||||
"luxon": "^3.3.0",
|
||||
@@ -107,6 +108,7 @@
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/js-cookie": "^3.0.3",
|
||||
"@types/luxon": "^3.3.0",
|
||||
"@types/react-datepicker": "^4.11.2",
|
||||
"@types/scroll-into-view": "^1.16.0",
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { useRecoilState } from 'recoil';
|
||||
|
||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
|
||||
import { AppThemeProvider } from './providers/AppThemeProvider';
|
||||
import { AuthProvider } from './providers/AuthProvider';
|
||||
import { apiClient, mockClient } from './apollo';
|
||||
import { ApolloProvider } from './providers/apollo/ApolloProvider';
|
||||
import { AppThemeProvider } from './providers/theme/AppThemeProvider';
|
||||
import { App } from './App';
|
||||
|
||||
export function AppWrapper() {
|
||||
const [isMockMode] = useRecoilState(isMockModeState);
|
||||
return (
|
||||
<ApolloProvider client={isMockMode ? mockClient : apiClient}>
|
||||
<ApolloProvider>
|
||||
<BrowserRouter>
|
||||
<AuthProvider>
|
||||
<AppThemeProvider>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</AppThemeProvider>
|
||||
</AuthProvider>
|
||||
<AppThemeProvider>
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>
|
||||
</AppThemeProvider>
|
||||
</BrowserRouter>
|
||||
</ApolloProvider>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,34 @@
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
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 { App } from '~/App';
|
||||
import { AuthProvider } from '~/providers/AuthProvider';
|
||||
import { FullHeightStorybookLayout } from '~/testing/FullHeightStorybookLayout';
|
||||
import { mockedUsersData } from '~/testing/mock-data/users';
|
||||
import { mockedClient } from '~/testing/mockedClient';
|
||||
|
||||
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) => {
|
||||
const AppInStoryBook = (
|
||||
<FullHeightStorybookLayout>
|
||||
<AuthProvider>
|
||||
<MockedAuth>
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</MockedAuth>
|
||||
</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'];
|
||||
};
|
||||
|
||||
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 = {
|
||||
set?: InputMaybe<Scalars['Boolean']>;
|
||||
};
|
||||
@@ -681,8 +698,14 @@ export type JsonNullableFilter = {
|
||||
string_starts_with?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type LoginToken = {
|
||||
__typename?: 'LoginToken';
|
||||
loginToken: AuthToken;
|
||||
};
|
||||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
challenge: LoginToken;
|
||||
createOneComment: Comment;
|
||||
createOneCommentThread: CommentThread;
|
||||
createOneCompany: Company;
|
||||
@@ -691,10 +714,18 @@ export type Mutation = {
|
||||
deleteManyCompany: AffectedRows;
|
||||
deleteManyPerson: AffectedRows;
|
||||
deleteManyPipelineProgress: AffectedRows;
|
||||
renewToken: AuthTokens;
|
||||
updateOneCommentThread: CommentThread;
|
||||
updateOneCompany?: Maybe<Company>;
|
||||
updateOnePerson?: Maybe<Person>;
|
||||
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 = {
|
||||
data: CommentThreadUpdateInput;
|
||||
where: CommentThreadWhereUniqueInput;
|
||||
@@ -761,6 +797,11 @@ export type MutationUpdateOnePipelineProgressArgs = {
|
||||
where: PipelineProgressWhereUniqueInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationVerifyArgs = {
|
||||
loginToken: Scalars['String'];
|
||||
};
|
||||
|
||||
export type NestedBoolFilter = {
|
||||
equals?: InputMaybe<Scalars['Boolean']>;
|
||||
not?: InputMaybe<NestedBoolFilter>;
|
||||
@@ -1489,6 +1530,12 @@ export type UserWhereUniqueInput = {
|
||||
id?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type Verify = {
|
||||
__typename?: 'Verify';
|
||||
tokens: AuthTokenPair;
|
||||
user: User;
|
||||
};
|
||||
|
||||
export type Workspace = {
|
||||
__typename?: 'Workspace';
|
||||
commentThreads?: Maybe<Array<CommentThread>>;
|
||||
@@ -1519,6 +1566,28 @@ export type WorkspaceMember = {
|
||||
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<{
|
||||
commentId: 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 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`
|
||||
mutation CreateComment($commentId: String!, $commentText: String!, $authorId: String!, $commentThreadId: String!, $createdAt: DateTime!) {
|
||||
createOneComment(
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { hasAccessToken } from '../services/AuthService';
|
||||
import { useIsLogged } from '../hooks/useIsLogged';
|
||||
|
||||
const EmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -34,13 +34,15 @@ export function RequireAuth({
|
||||
}): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isLogged = useIsLogged();
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasAccessToken()) {
|
||||
if (!isLogged) {
|
||||
navigate('/auth');
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [isLogged, navigate]);
|
||||
|
||||
if (!hasAccessToken())
|
||||
if (!isLogged) {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<FadeInStyle>
|
||||
@@ -48,5 +50,7 @@ export function RequireAuth({
|
||||
</FadeInStyle>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { keyframes } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { hasAccessToken } from '../services/AuthService';
|
||||
import { useIsLogged } from '../hooks/useIsLogged';
|
||||
|
||||
const EmptyContainer = styled.div`
|
||||
align-items: center;
|
||||
@@ -34,13 +34,15 @@ export function RequireNotAuth({
|
||||
}): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const isLogged = useIsLogged();
|
||||
|
||||
useEffect(() => {
|
||||
if (hasAccessToken()) {
|
||||
if (isLogged) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate]);
|
||||
}, [isLogged, navigate]);
|
||||
|
||||
if (hasAccessToken())
|
||||
if (isLogged) {
|
||||
return (
|
||||
<EmptyContainer>
|
||||
<FadeInStyle>
|
||||
@@ -48,5 +50,7 @@ export function RequireNotAuth({
|
||||
</FadeInStyle>
|
||||
</EmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
export const hasAccessToken = () => {
|
||||
const accessToken = localStorage.getItem('accessToken');
|
||||
import { cookieStorage } from '@/utils/cookie-storage';
|
||||
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 = () => {
|
||||
const accessToken = localStorage.getItem('accessToken');
|
||||
const accessToken = cookieStorage.getItem('accessToken');
|
||||
if (!accessToken) {
|
||||
return null;
|
||||
}
|
||||
@@ -18,76 +86,3 @@ export const getUserIdFromToken: () => string | 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 {
|
||||
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',
|
||||
);
|
||||
});
|
||||
});
|
||||
import { getUserIdFromToken } from '../AuthService';
|
||||
|
||||
it('getUserIdFromToken returns null when the token is not present', async () => {
|
||||
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 () => {
|
||||
localStorage.setItem('accessToken', 'xxx-invalid-access');
|
||||
cookieStorage.setItem('accessToken', 'xxx-invalid-access');
|
||||
const userId = getUserIdFromToken();
|
||||
expect(userId).toBeNull();
|
||||
});
|
||||
|
||||
it('getUserIdFromToken returns the right userId when the token is valid', async () => {
|
||||
localStorage.setItem(
|
||||
cookieStorage.setItem(
|
||||
'accessToken',
|
||||
'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');
|
||||
});
|
||||
|
||||
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(() => {
|
||||
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 { useTheme } from '@emotion/react';
|
||||
|
||||
import { removeTokens } from '@/auth/services/AuthService';
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import {
|
||||
IconColorSwatch,
|
||||
IconLogout,
|
||||
@@ -15,11 +15,15 @@ import NavTitle from '@/ui/layout/navbar/NavTitle';
|
||||
import SubNavbarContainer from '@/ui/layout/navbar/sub-navbar/SubNavBarContainer';
|
||||
|
||||
export function SettingsNavbar() {
|
||||
const logout = useCallback(() => {
|
||||
removeTokens();
|
||||
window.location.href = '/';
|
||||
}, []);
|
||||
const theme = useTheme();
|
||||
|
||||
const { logout } = useAuth();
|
||||
|
||||
const handleLogout = useCallback(() => {
|
||||
logout();
|
||||
window.location.href = '/';
|
||||
}, [logout]);
|
||||
|
||||
return (
|
||||
<SubNavbarContainer backButtonTitle="Settings">
|
||||
<NavItemsContainer>
|
||||
@@ -63,7 +67,7 @@ export function SettingsNavbar() {
|
||||
<NavTitle label="Other" />
|
||||
<NavItem
|
||||
label="Logout"
|
||||
onClick={logout}
|
||||
onClick={handleLogout}
|
||||
icon={<IconLogout size={theme.iconSizeMedium} />}
|
||||
danger={true}
|
||||
/>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
import { useGetCurrentUserQuery as generatedUseGetCurrentUserQuery } from '~/generated/graphql';
|
||||
|
||||
export const GET_CURRENT_USER = gql`
|
||||
query GetCurrentUser($uuid: String) {
|
||||
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 { Modal } from '@/auth/components/ui/Modal';
|
||||
import { Title } from '@/auth/components/ui/Title';
|
||||
import { hasAccessToken } from '@/auth/services/AuthService';
|
||||
import { authFlowUserEmailState } from '@/auth/states/authFlowUserEmailState';
|
||||
import { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
import { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
||||
@@ -34,14 +33,6 @@ export function Index() {
|
||||
authFlowUserEmailState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMockMode(true);
|
||||
|
||||
if (hasAccessToken()) {
|
||||
navigate('/');
|
||||
}
|
||||
}, [navigate, setMockMode]);
|
||||
|
||||
const onGoogleLoginClick = useCallback(() => {
|
||||
window.location.href = process.env.REACT_APP_AUTH_URL + '/google' || '';
|
||||
}, []);
|
||||
@@ -62,6 +53,10 @@ export function Index() {
|
||||
[onPasswordLoginClick],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setMockMode(true);
|
||||
}, [navigate, setMockMode]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Companies />
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Logo } from '@/auth/components/ui/Logo';
|
||||
import { Modal } from '@/auth/components/ui/Modal';
|
||||
import { SubTitle } from '@/auth/components/ui/SubTitle';
|
||||
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 { isMockModeState } from '@/auth/states/isMockModeState';
|
||||
import { PrimaryButton } from '@/ui/components/buttons/PrimaryButton';
|
||||
@@ -47,48 +47,28 @@ export function PasswordLogin() {
|
||||
const [internalPassword, setInternalPassword] = useState(prefillPassword);
|
||||
const [formError, setFormError] = useState('');
|
||||
|
||||
const userLogin = useCallback(async () => {
|
||||
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,
|
||||
}),
|
||||
},
|
||||
);
|
||||
const { login } = useAuth();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
setFormError(errorData.message);
|
||||
return;
|
||||
const handleLogin = useCallback(async () => {
|
||||
try {
|
||||
await login(authFlowUserEmail, internalPassword);
|
||||
setMockMode(false);
|
||||
navigate('/');
|
||||
} catch (err: any) {
|
||||
setFormError(err.message);
|
||||
}
|
||||
const { loginToken } = await response.json();
|
||||
|
||||
if (!loginToken) {
|
||||
return;
|
||||
}
|
||||
|
||||
await getTokensFromLoginToken(loginToken.token);
|
||||
setMockMode(false);
|
||||
|
||||
navigate('/');
|
||||
}, [authFlowUserEmail, internalPassword, navigate, setMockMode]);
|
||||
}, [authFlowUserEmail, internalPassword, login, navigate, setMockMode]);
|
||||
|
||||
useHotkeys(
|
||||
'enter',
|
||||
() => {
|
||||
userLogin();
|
||||
handleLogin();
|
||||
},
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
[userLogin],
|
||||
[handleLogin],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -118,7 +98,7 @@ export function PasswordLogin() {
|
||||
type="password"
|
||||
/>
|
||||
<StyledButtonContainer>
|
||||
<PrimaryButton fullWidth onClick={userLogin}>
|
||||
<PrimaryButton fullWidth onClick={handleLogin}>
|
||||
Continue
|
||||
</PrimaryButton>
|
||||
</StyledButtonContainer>
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
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() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const loginToken = searchParams.get('loginToken');
|
||||
|
||||
const isLogged = useIsLogged();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { verify } = useAuth();
|
||||
|
||||
useEffect(() => {
|
||||
async function getTokens() {
|
||||
if (!loginToken) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
await getTokensFromLoginToken(loginToken);
|
||||
setIsLoading(false);
|
||||
await verify(loginToken);
|
||||
navigate('/');
|
||||
}
|
||||
|
||||
if (!isLoading) {
|
||||
if (!isLogged) {
|
||||
getTokens();
|
||||
}
|
||||
}, [isLoading, navigate, loginToken]);
|
||||
// Verify only needs to run once at mount
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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 { DefaultLayout } from '@/ui/layout/DefaultLayout';
|
||||
import { AuthProvider } from '~/providers/AuthProvider';
|
||||
|
||||
import { ComponentStorybookLayout } from './ComponentStorybookLayout';
|
||||
import { FullHeightStorybookLayout } from './FullHeightStorybookLayout';
|
||||
@@ -20,9 +19,7 @@ export function getRenderWrapperForPage(
|
||||
<ApolloProvider client={mockedClient}>
|
||||
<MemoryRouter initialEntries={[currentPath]}>
|
||||
<FullHeightStorybookLayout>
|
||||
<AuthProvider>
|
||||
<DefaultLayout>{children}</DefaultLayout>
|
||||
</AuthProvider>
|
||||
<DefaultLayout>{children}</DefaultLayout>
|
||||
</FullHeightStorybookLayout>
|
||||
</MemoryRouter>
|
||||
</ApolloProvider>
|
||||
|
||||
@@ -4892,6 +4892,11 @@
|
||||
jest-matcher-utils "^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":
|
||||
version "1.1.1"
|
||||
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"
|
||||
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:
|
||||
version "1.1.6"
|
||||
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 { GoogleAuthController } from './controllers/google-auth.controller';
|
||||
import { GoogleStrategy } from './strategies/google.auth.strategy';
|
||||
import { TokenController } from './controllers/token.controller';
|
||||
import { PrismaService } from 'src/database/prisma.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { AuthController } from './controllers/auth.controller';
|
||||
import { PasswordAuthController } from './controllers/password-auth.controller';
|
||||
import { VerifyAuthController } from './controllers/verify-auth.controller';
|
||||
|
||||
import { TokenService } from './services/token.service';
|
||||
import { AuthResolver } from './auth.resolver';
|
||||
|
||||
const jwtModule = JwtModule.registerAsync({
|
||||
useFactory: async (configService: ConfigService) => {
|
||||
@@ -28,18 +27,14 @@ const jwtModule = JwtModule.registerAsync({
|
||||
|
||||
@Module({
|
||||
imports: [jwtModule, ConfigModule.forRoot({}), UserModule],
|
||||
controllers: [
|
||||
GoogleAuthController,
|
||||
PasswordAuthController,
|
||||
TokenController,
|
||||
AuthController,
|
||||
],
|
||||
controllers: [GoogleAuthController, VerifyAuthController],
|
||||
providers: [
|
||||
AuthService,
|
||||
TokenService,
|
||||
JwtAuthStrategy,
|
||||
GoogleStrategy,
|
||||
PrismaService,
|
||||
AuthResolver,
|
||||
],
|
||||
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 { AuthController } from './auth.controller';
|
||||
import { VerifyAuthController } from './verify-auth.controller';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
describe('AuthController', () => {
|
||||
let controller: AuthController;
|
||||
describe('VerifyAuthController', () => {
|
||||
let controller: VerifyAuthController;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [AuthController],
|
||||
controllers: [VerifyAuthController],
|
||||
providers: [
|
||||
{
|
||||
provide: AuthService,
|
||||
@@ -21,7 +21,7 @@ describe('AuthController', () => {
|
||||
],
|
||||
}).compile();
|
||||
|
||||
controller = module.get<AuthController>(AuthController);
|
||||
controller = module.get<VerifyAuthController>(VerifyAuthController);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { AuthService } from '../services/auth.service';
|
||||
import { VerifyInput } from '../dto/verify.input';
|
||||
import { VerifyEntity } from '../dto/verify.entity';
|
||||
import { Verify } from '../dto/verify.entity';
|
||||
import { TokenService } from '../services/token.service';
|
||||
|
||||
@Controller('auth')
|
||||
export class AuthController {
|
||||
@Controller('auth/verify')
|
||||
export class VerifyAuthController {
|
||||
constructor(
|
||||
private readonly authService: AuthService,
|
||||
private readonly tokenService: TokenService,
|
||||
) {}
|
||||
|
||||
@Post('verify')
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<VerifyEntity> {
|
||||
@Post()
|
||||
async verify(@Body() verifyInput: VerifyInput): Promise<Verify> {
|
||||
const email = await this.tokenService.verifyLoginToken(
|
||||
verifyInput.loginToken,
|
||||
);
|
||||
@@ -1,10 +1,14 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class ChallengeInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
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 {
|
||||
loginToken: TokenEntity;
|
||||
@ObjectType()
|
||||
export class LoginToken {
|
||||
@Field(() => AuthToken)
|
||||
loginToken: AuthToken;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class RefreshTokenInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
refreshToken: string;
|
||||
|
||||
@@ -6,18 +6,23 @@ import {
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { PASSWORD_REGEX } from '../auth.util';
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
|
||||
@ArgsType()
|
||||
export class RegisterInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@MinLength(8)
|
||||
@Matches(PASSWORD_REGEX, { message: 'password too weak' })
|
||||
password: string;
|
||||
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
displayName: string;
|
||||
|
||||
@@ -1,4 +1,25 @@
|
||||
export class TokenEntity {
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class AuthToken {
|
||||
@Field(() => String)
|
||||
token: string;
|
||||
|
||||
@Field(() => 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 { User } from '@prisma/client';
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
import { AuthTokens } from './token.entity';
|
||||
import { User } from 'src/core/@generated/user/user.model';
|
||||
|
||||
export class VerifyEntity {
|
||||
user: Omit<User, 'passwordHash'>;
|
||||
|
||||
tokens: {
|
||||
accessToken: TokenEntity;
|
||||
refreshToken: TokenEntity;
|
||||
};
|
||||
@ObjectType()
|
||||
export class Verify extends AuthTokens {
|
||||
@Field(() => User)
|
||||
user: User;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ArgsType, Field } from '@nestjs/graphql';
|
||||
import { IsNotEmpty, IsString } from 'class-validator';
|
||||
|
||||
@ArgsType()
|
||||
export class VerifyInput {
|
||||
@Field(() => String)
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
loginToken: string;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { UserService } from 'src/core/user/user.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { RegisterInput } from '../dto/register.input';
|
||||
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';
|
||||
|
||||
export type UserPayload = {
|
||||
@@ -73,17 +73,17 @@ export class AuthService {
|
||||
return user;
|
||||
}
|
||||
|
||||
async verify(email: string): Promise<VerifyEntity> {
|
||||
const data = await this.userService.findUnique({
|
||||
async verify(email: string): Promise<Verify> {
|
||||
const user = await this.userService.findUnique({
|
||||
where: {
|
||||
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
|
||||
const { passwordHash: _, ...user } = data;
|
||||
// passwordHash is hidden for security reasons
|
||||
user.passwordHash = '';
|
||||
|
||||
const accessToken = await this.tokenService.generateAccessToken(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 { addMilliseconds } from 'date-fns';
|
||||
import ms from 'ms';
|
||||
import { TokenEntity } from '../dto/token.entity';
|
||||
import { AuthToken } from '../dto/token.entity';
|
||||
import { TokenExpiredError } from 'jsonwebtoken';
|
||||
|
||||
@Injectable()
|
||||
@@ -24,7 +24,7 @@ export class TokenService {
|
||||
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');
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
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 expiresIn = this.configService.get<string>(
|
||||
'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 expiresIn = this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN');
|
||||
assert(expiresIn, '', InternalServerErrorException);
|
||||
@@ -163,8 +163,8 @@ export class TokenService {
|
||||
}
|
||||
|
||||
async generateTokensFromRefreshToken(token: string): Promise<{
|
||||
accessToken: TokenEntity;
|
||||
refreshToken: TokenEntity;
|
||||
accessToken: AuthToken;
|
||||
refreshToken: AuthToken;
|
||||
}> {
|
||||
const {
|
||||
user,
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { WorkspaceService } from './services/workspace.service';
|
||||
import { WorkspaceMemberService } from './services/workspace-member.service';
|
||||
import { WorkspaceMemberResolver } from './resolvers/workspace-member.resolver';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
WorkspaceService,
|
||||
WorkspaceMemberService,
|
||||
WorkspaceMemberResolver,
|
||||
],
|
||||
providers: [WorkspaceService, WorkspaceMemberService],
|
||||
exports: [WorkspaceService, WorkspaceMemberService],
|
||||
})
|
||||
export class WorkspaceModule {}
|
||||
|
||||
Reference in New Issue
Block a user