From 5d1c4b81e7772b1a51c4243e86c25ab91b2a2107 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Tue, 8 Mar 2022 19:50:51 +0100 Subject: [PATCH] feat: introduce authentication context --- .../src/components/ApolloProvider/index.tsx | 7 +++- .../web/src/components/LoginForm/index.tsx | 13 +++++-- packages/web/src/config/urls.ts | 4 +- packages/web/src/contexts/Authentication.tsx | 39 +++++++++++++++++++ packages/web/src/graphql/client.ts | 6 ++- packages/web/src/graphql/link.ts | 19 ++++++--- packages/web/src/hooks/useAuthentication.ts | 20 ++++++++-- packages/web/src/index.tsx | 21 +++++----- 8 files changed, 103 insertions(+), 26 deletions(-) create mode 100644 packages/web/src/contexts/Authentication.tsx diff --git a/packages/web/src/components/ApolloProvider/index.tsx b/packages/web/src/components/ApolloProvider/index.tsx index 00716ce2..5be9549a 100644 --- a/packages/web/src/components/ApolloProvider/index.tsx +++ b/packages/web/src/components/ApolloProvider/index.tsx @@ -1,7 +1,9 @@ import * as React from 'react'; import { ApolloProvider as BaseApolloProvider } from '@apollo/client'; import { useSnackbar } from 'notistack'; + import client, { setLink } from 'graphql/client'; +import useAuthentication from 'hooks/useAuthentication'; type ApolloProviderProps = { children: React.ReactNode; @@ -9,14 +11,15 @@ type ApolloProviderProps = { const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => { const { enqueueSnackbar } = useSnackbar(); + const authentication = useAuthentication(); const onError = React.useCallback((message) => { enqueueSnackbar(message, { variant: 'error' }); }, [enqueueSnackbar]); React.useEffect(() => { - setLink({ onError }) - }, [onError]); + setLink({ onError, token: authentication.token }) + }, [onError, authentication]); return ( diff --git a/packages/web/src/components/LoginForm/index.tsx b/packages/web/src/components/LoginForm/index.tsx index c7ea0cb6..2ed8189f 100644 --- a/packages/web/src/components/LoginForm/index.tsx +++ b/packages/web/src/components/LoginForm/index.tsx @@ -6,6 +6,7 @@ import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; import LoadingButton from '@mui/lab/LoadingButton'; +import useAuthentication from 'hooks/useAuthentication'; import * as URLS from 'config/urls'; import { setItem } from 'helpers/storage'; import { LOGIN } from 'graphql/mutations/login'; @@ -38,6 +39,7 @@ function renderFields(props: { loading: boolean }) { required fullWidth margin="dense" + autoComplete="current-password" /> { + if (authentication.isAuthenticated) { + navigate(URLS.DASHBOARD); + } + }, [authentication.isAuthenticated]); + const handleSubmit = async (values: any) => { const { data } = await login({ variables: { @@ -68,9 +77,7 @@ function LoginForm() { const { token } = data.login; - setItem('token', token); - - navigate(URLS.FLOWS); + authentication.updateToken(token); }; const render = React.useMemo(() => renderFields({ loading }), [loading]); diff --git a/packages/web/src/config/urls.ts b/packages/web/src/config/urls.ts index e476f2fa..9f608e4b 100644 --- a/packages/web/src/config/urls.ts +++ b/packages/web/src/config/urls.ts @@ -1,4 +1,3 @@ -export const DASHBOARD = '/dashboard'; export const CONNECTIONS = '/connections'; export const EXPLORE = '/explore'; @@ -26,3 +25,6 @@ export const FLOWS = '/flows'; // TODO: revert this back to /flows/:flowId once we have a proper single flow page export const FLOW = (flowId: string): string => `/editor/${flowId}`; export const FLOW_PATTERN = '/flows/:flowId'; + + +export const DASHBOARD = FLOWS; diff --git a/packages/web/src/contexts/Authentication.tsx b/packages/web/src/contexts/Authentication.tsx new file mode 100644 index 00000000..d4e4e421 --- /dev/null +++ b/packages/web/src/contexts/Authentication.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import { getItem, setItem } from 'helpers/storage'; + +export type AuthenticationContextParams = { + token: string | null; + updateToken: (token: string) => void; +}; + +export const AuthenticationContext = React.createContext({ + token: null, + updateToken: () => void 0, +}); + +type AuthenticationProviderProps = { + children: React.ReactNode; +}; + +export const AuthenticationProvider = (props: AuthenticationProviderProps): React.ReactElement => { + const { children } = props; + const [token, setToken] = React.useState(() => getItem('token')); + + const value = React.useMemo(() => { + return { + token, + updateToken: (newToken: string) => { + setToken(newToken); + setItem('token', newToken); + }, + }; + }, [token]); + + return ( + + {children} + + ); +}; \ No newline at end of file diff --git a/packages/web/src/graphql/client.ts b/packages/web/src/graphql/client.ts index 16b4807b..e321c9ba 100644 --- a/packages/web/src/graphql/client.ts +++ b/packages/web/src/graphql/client.ts @@ -5,6 +5,7 @@ import appConfig from 'config/app'; type CreateClientOptions = { onError?: (message: string) => void; + token?: string | null; }; const client = new ApolloClient({ @@ -12,8 +13,9 @@ const client = new ApolloClient({ link: createLink({ uri: appConfig.graphqlUrl }) }); -export function setLink({ onError }: CreateClientOptions): typeof client { - const link = createLink({ uri: appConfig.graphqlUrl, onError }); +export function setLink(options: CreateClientOptions): typeof client { + const { onError, token } = options; + const link = createLink({ uri: appConfig.graphqlUrl, token, onError }); client.setLink(link); diff --git a/packages/web/src/graphql/link.ts b/packages/web/src/graphql/link.ts index d49b883e..42a4e5c7 100644 --- a/packages/web/src/graphql/link.ts +++ b/packages/web/src/graphql/link.ts @@ -3,16 +3,17 @@ import type { ApolloLink } from '@apollo/client'; import { onError } from '@apollo/client/link/error'; import * as URLS from 'config/urls'; -import { getItem } from 'helpers/storage'; type CreateLinkOptions = { uri: string; + token?: string | null; onError?: (message: string) => void; }; -const createHttpLink = (uri: CreateLinkOptions['uri']): ApolloLink => { +const createHttpLink = (options: Pick): ApolloLink => { + const { uri, token } = options; const headers = { - authorization: getItem('token') || '', + authorization: token, }; return new HttpLink({ uri, headers }); } @@ -41,8 +42,16 @@ const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink => // eslint-disable-next-line @typescript-eslint/no-empty-function const noop = () => {}; -const createLink = ({ uri, onError = noop }: CreateLinkOptions): ApolloLink => { - return from([createErrorLink(onError), createHttpLink(uri)]); +const createLink = (options: CreateLinkOptions): ApolloLink => { + const { + uri, + onError = noop, + token, + } = options; + + const httpOptions = { uri, token }; + + return from([createErrorLink(onError), createHttpLink(httpOptions)]); }; export default createLink; diff --git a/packages/web/src/hooks/useAuthentication.ts b/packages/web/src/hooks/useAuthentication.ts index 502a8376..5ee93349 100644 --- a/packages/web/src/hooks/useAuthentication.ts +++ b/packages/web/src/hooks/useAuthentication.ts @@ -1,7 +1,19 @@ -import { getItem } from 'helpers/storage'; +import * as React from 'react'; +import { AuthenticationContext } from 'contexts/Authentication'; +import type { AuthenticationContextParams } from 'contexts/Authentication'; -export default function useAuthentication(): boolean { - const token = getItem('token'); +type UseAuthenticationReturn = { + isAuthenticated: boolean; + token: AuthenticationContextParams["token"]; + updateToken: AuthenticationContextParams["updateToken"]; +}; - return Boolean(token); +export default function useAuthentication(): UseAuthenticationReturn { + const authenticationContext = React.useContext(AuthenticationContext); + + return { + token: authenticationContext.token, + updateToken: authenticationContext.updateToken, + isAuthenticated: Boolean(authenticationContext.token), + }; } diff --git a/packages/web/src/index.tsx b/packages/web/src/index.tsx index 48da2b62..ac150301 100644 --- a/packages/web/src/index.tsx +++ b/packages/web/src/index.tsx @@ -4,21 +4,24 @@ import ThemeProvider from 'components/ThemeProvider'; import IntlProvider from 'components/IntlProvider'; import ApolloProvider from 'components/ApolloProvider'; import SnackbarProvider from 'components/SnackbarProvider'; +import { AuthenticationProvider } from 'contexts/Authentication'; import Router from 'components/Router'; import routes from 'routes'; import reportWebVitals from './reportWebVitals'; ReactDOM.render( - - - - - {routes} - - - - + + + + + + {routes} + + + + + , document.getElementById('root') )