feat: introduce authentication context
This commit is contained in:

committed by
Ömer Faruk Aydın

parent
030d886cf7
commit
5d1c4b81e7
@@ -1,7 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { ApolloProvider as BaseApolloProvider } from '@apollo/client';
|
import { ApolloProvider as BaseApolloProvider } from '@apollo/client';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
|
|
||||||
import client, { setLink } from 'graphql/client';
|
import client, { setLink } from 'graphql/client';
|
||||||
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
|
|
||||||
type ApolloProviderProps = {
|
type ApolloProviderProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -9,14 +11,15 @@ type ApolloProviderProps = {
|
|||||||
|
|
||||||
const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
const ApolloProvider = (props: ApolloProviderProps): React.ReactElement => {
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const authentication = useAuthentication();
|
||||||
|
|
||||||
const onError = React.useCallback((message) => {
|
const onError = React.useCallback((message) => {
|
||||||
enqueueSnackbar(message, { variant: 'error' });
|
enqueueSnackbar(message, { variant: 'error' });
|
||||||
}, [enqueueSnackbar]);
|
}, [enqueueSnackbar]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setLink({ onError })
|
setLink({ onError, token: authentication.token })
|
||||||
}, [onError]);
|
}, [onError, authentication]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BaseApolloProvider client={client} {...props} />
|
<BaseApolloProvider client={client} {...props} />
|
||||||
|
@@ -6,6 +6,7 @@ import Paper from '@mui/material/Paper';
|
|||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import LoadingButton from '@mui/lab/LoadingButton';
|
import LoadingButton from '@mui/lab/LoadingButton';
|
||||||
|
|
||||||
|
import useAuthentication from 'hooks/useAuthentication';
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { setItem } from 'helpers/storage';
|
import { setItem } from 'helpers/storage';
|
||||||
import { LOGIN } from 'graphql/mutations/login';
|
import { LOGIN } from 'graphql/mutations/login';
|
||||||
@@ -38,6 +39,7 @@ function renderFields(props: { loading: boolean }) {
|
|||||||
required
|
required
|
||||||
fullWidth
|
fullWidth
|
||||||
margin="dense"
|
margin="dense"
|
||||||
|
autoComplete="current-password"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<LoadingButton
|
<LoadingButton
|
||||||
@@ -57,8 +59,15 @@ function renderFields(props: { loading: boolean }) {
|
|||||||
|
|
||||||
function LoginForm() {
|
function LoginForm() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const authentication = useAuthentication();
|
||||||
const [login, { loading }] = useMutation(LOGIN);
|
const [login, { loading }] = useMutation(LOGIN);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (authentication.isAuthenticated) {
|
||||||
|
navigate(URLS.DASHBOARD);
|
||||||
|
}
|
||||||
|
}, [authentication.isAuthenticated]);
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
const handleSubmit = async (values: any) => {
|
||||||
const { data } = await login({
|
const { data } = await login({
|
||||||
variables: {
|
variables: {
|
||||||
@@ -68,9 +77,7 @@ function LoginForm() {
|
|||||||
|
|
||||||
const { token } = data.login;
|
const { token } = data.login;
|
||||||
|
|
||||||
setItem('token', token);
|
authentication.updateToken(token);
|
||||||
|
|
||||||
navigate(URLS.FLOWS);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const render = React.useMemo(() => renderFields({ loading }), [loading]);
|
const render = React.useMemo(() => renderFields({ loading }), [loading]);
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
export const DASHBOARD = '/dashboard';
|
|
||||||
export const CONNECTIONS = '/connections';
|
export const CONNECTIONS = '/connections';
|
||||||
export const EXPLORE = '/explore';
|
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
|
// 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 = (flowId: string): string => `/editor/${flowId}`;
|
||||||
export const FLOW_PATTERN = '/flows/:flowId';
|
export const FLOW_PATTERN = '/flows/:flowId';
|
||||||
|
|
||||||
|
|
||||||
|
export const DASHBOARD = FLOWS;
|
||||||
|
39
packages/web/src/contexts/Authentication.tsx
Normal file
39
packages/web/src/contexts/Authentication.tsx
Normal file
@@ -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<AuthenticationContextParams>({
|
||||||
|
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 (
|
||||||
|
<AuthenticationContext.Provider
|
||||||
|
value={value}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthenticationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
@@ -5,6 +5,7 @@ import appConfig from 'config/app';
|
|||||||
|
|
||||||
type CreateClientOptions = {
|
type CreateClientOptions = {
|
||||||
onError?: (message: string) => void;
|
onError?: (message: string) => void;
|
||||||
|
token?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const client = new ApolloClient({
|
const client = new ApolloClient({
|
||||||
@@ -12,8 +13,9 @@ const client = new ApolloClient({
|
|||||||
link: createLink({ uri: appConfig.graphqlUrl })
|
link: createLink({ uri: appConfig.graphqlUrl })
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setLink({ onError }: CreateClientOptions): typeof client {
|
export function setLink(options: CreateClientOptions): typeof client {
|
||||||
const link = createLink({ uri: appConfig.graphqlUrl, onError });
|
const { onError, token } = options;
|
||||||
|
const link = createLink({ uri: appConfig.graphqlUrl, token, onError });
|
||||||
|
|
||||||
client.setLink(link);
|
client.setLink(link);
|
||||||
|
|
||||||
|
@@ -3,16 +3,17 @@ import type { ApolloLink } from '@apollo/client';
|
|||||||
import { onError } from '@apollo/client/link/error';
|
import { onError } from '@apollo/client/link/error';
|
||||||
|
|
||||||
import * as URLS from 'config/urls';
|
import * as URLS from 'config/urls';
|
||||||
import { getItem } from 'helpers/storage';
|
|
||||||
|
|
||||||
type CreateLinkOptions = {
|
type CreateLinkOptions = {
|
||||||
uri: string;
|
uri: string;
|
||||||
|
token?: string | null;
|
||||||
onError?: (message: string) => void;
|
onError?: (message: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const createHttpLink = (uri: CreateLinkOptions['uri']): ApolloLink => {
|
const createHttpLink = (options: Pick<CreateLinkOptions, 'uri' | 'token'>): ApolloLink => {
|
||||||
|
const { uri, token } = options;
|
||||||
const headers = {
|
const headers = {
|
||||||
authorization: getItem('token') || '',
|
authorization: token,
|
||||||
};
|
};
|
||||||
return new HttpLink({ uri, headers });
|
return new HttpLink({ uri, headers });
|
||||||
}
|
}
|
||||||
@@ -41,8 +42,16 @@ const createErrorLink = (callback: CreateLinkOptions['onError']): ApolloLink =>
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
const noop = () => {};
|
const noop = () => {};
|
||||||
|
|
||||||
const createLink = ({ uri, onError = noop }: CreateLinkOptions): ApolloLink => {
|
const createLink = (options: CreateLinkOptions): ApolloLink => {
|
||||||
return from([createErrorLink(onError), createHttpLink(uri)]);
|
const {
|
||||||
|
uri,
|
||||||
|
onError = noop,
|
||||||
|
token,
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const httpOptions = { uri, token };
|
||||||
|
|
||||||
|
return from([createErrorLink(onError), createHttpLink(httpOptions)]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createLink;
|
export default createLink;
|
||||||
|
@@ -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 {
|
type UseAuthenticationReturn = {
|
||||||
const token = getItem('token');
|
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),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -4,12 +4,14 @@ import ThemeProvider from 'components/ThemeProvider';
|
|||||||
import IntlProvider from 'components/IntlProvider';
|
import IntlProvider from 'components/IntlProvider';
|
||||||
import ApolloProvider from 'components/ApolloProvider';
|
import ApolloProvider from 'components/ApolloProvider';
|
||||||
import SnackbarProvider from 'components/SnackbarProvider';
|
import SnackbarProvider from 'components/SnackbarProvider';
|
||||||
|
import { AuthenticationProvider } from 'contexts/Authentication';
|
||||||
import Router from 'components/Router';
|
import Router from 'components/Router';
|
||||||
import routes from 'routes';
|
import routes from 'routes';
|
||||||
import reportWebVitals from './reportWebVitals';
|
import reportWebVitals from './reportWebVitals';
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<SnackbarProvider>
|
<SnackbarProvider>
|
||||||
|
<AuthenticationProvider>
|
||||||
<ApolloProvider>
|
<ApolloProvider>
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
@@ -19,6 +21,7 @@ ReactDOM.render(
|
|||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</ApolloProvider>
|
</ApolloProvider>
|
||||||
|
</AuthenticationProvider>
|
||||||
</SnackbarProvider>,
|
</SnackbarProvider>,
|
||||||
document.getElementById('root')
|
document.getElementById('root')
|
||||||
)
|
)
|
||||||
|
Reference in New Issue
Block a user