Generate Token through Auth0
This commit is contained in:
@ -2,16 +2,61 @@ import React from 'react';
|
||||
import Inbox from './pages/inbox/Inbox';
|
||||
import Contacts from './pages/Contacts';
|
||||
import Insights from './pages/Insights';
|
||||
import AuthCallback from './pages/AuthCallback';
|
||||
import AppLayout from './layout/AppLayout';
|
||||
import RequireAuth from './components/RequireAuth';
|
||||
import { Routes, Route } from 'react-router-dom';
|
||||
import { useQuery, gql } from '@apollo/client';
|
||||
|
||||
const GET_USER_PROFILE = gql`
|
||||
query GetUserProfile {
|
||||
users {
|
||||
id
|
||||
email
|
||||
first_name
|
||||
last_name
|
||||
tenant {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function App() {
|
||||
const { data } = useQuery(GET_USER_PROFILE, {
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
const user = data?.users[0];
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppLayout user={user}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Inbox />} />
|
||||
<Route path="/contacts" element={<Contacts />} />
|
||||
<Route path="/insights" element={<Insights />} />
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Inbox />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/contacts"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Contacts />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/insights"
|
||||
element={
|
||||
<RequireAuth>
|
||||
<Insights />
|
||||
</RequireAuth>
|
||||
}
|
||||
/>
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
</Routes>
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
10
front/src/components/RequireAuth.tsx
Normal file
10
front/src/components/RequireAuth.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import AuthService from '../hooks/AuthenticationHooks';
|
||||
|
||||
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
||||
const { redirectIfNotLoggedIn } = AuthService;
|
||||
redirectIfNotLoggedIn();
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
||||
14
front/src/components/__stories__/RequireAuth.stories.tsx
Normal file
14
front/src/components/__stories__/RequireAuth.stories.tsx
Normal file
@ -0,0 +1,14 @@
|
||||
import RequireAuth from '../RequireAuth';
|
||||
|
||||
import Navbar from '../RequireAuth';
|
||||
|
||||
export default {
|
||||
title: 'RequireAuth',
|
||||
component: Navbar,
|
||||
};
|
||||
|
||||
export const RequireAuthWithHelloChild = () => (
|
||||
<RequireAuth>
|
||||
<div>Hello</div>
|
||||
</RequireAuth>
|
||||
);
|
||||
9
front/src/components/__tests__/RequireAuth.test.tsx
Normal file
9
front/src/components/__tests__/RequireAuth.test.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
import { RequireAuthWithHelloChild } from '../__stories__/RequireAuth.stories';
|
||||
|
||||
it('Checks the Require Auth renders', () => {
|
||||
const { getAllByText } = render(<RequireAuthWithHelloChild />);
|
||||
|
||||
expect(getAllByText('Hello')).toBeTruthy();
|
||||
});
|
||||
39
front/src/hooks/AuthenticationHooks.ts
Normal file
39
front/src/hooks/AuthenticationHooks.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const useIsNotLoggedIn = () => {
|
||||
const { isAuthenticated, isLoading } = useAuth0();
|
||||
const hasAccessToken = localStorage.getItem('accessToken');
|
||||
return (!isAuthenticated || !hasAccessToken) && !isLoading;
|
||||
};
|
||||
|
||||
const redirectIfNotLoggedIn = () => {
|
||||
const isNotLoggedIn = useIsNotLoggedIn();
|
||||
const { loginWithRedirect } = useAuth0();
|
||||
if (isNotLoggedIn) {
|
||||
loginWithRedirect();
|
||||
}
|
||||
};
|
||||
|
||||
const useGetAccessToken = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [token, setToken] = useState('');
|
||||
const { getAccessTokenSilently } = useAuth0();
|
||||
|
||||
useEffect(() => {
|
||||
const fetchToken = async () => {
|
||||
setLoading(true);
|
||||
const accessToken = await getAccessTokenSilently();
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
|
||||
setLoading(false);
|
||||
setToken(accessToken);
|
||||
};
|
||||
|
||||
fetchToken();
|
||||
}, []);
|
||||
|
||||
return { loading, token };
|
||||
};
|
||||
|
||||
export default { useIsNotLoggedIn, useGetAccessToken, redirectIfNotLoggedIn };
|
||||
106
front/src/hooks/__tests__/AuthenticationHooks.test.tsx
Normal file
106
front/src/hooks/__tests__/AuthenticationHooks.test.tsx
Normal file
@ -0,0 +1,106 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import AuthenticationHooks from '../AuthenticationHooks';
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
jest.mock('@auth0/auth0-react');
|
||||
const mockedUseAuth0 = mocked(useAuth0, true);
|
||||
|
||||
const user = {
|
||||
email: 'johndoe@me.com',
|
||||
email_verified: true,
|
||||
sub: 'google-oauth2|12345678901234',
|
||||
};
|
||||
|
||||
describe('useIsNotLoggedIn', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('returns false if auth0 is loading', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: true,
|
||||
});
|
||||
|
||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false if token is not there', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if token is there but user is not connected on auth0', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: false,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
window.localStorage.setItem('accessToken', 'token');
|
||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false if token is there and user is connected on auth0', () => {
|
||||
mockedUseAuth0.mockReturnValue({
|
||||
isAuthenticated: true,
|
||||
user,
|
||||
logout: jest.fn(),
|
||||
loginWithRedirect: jest.fn(),
|
||||
getAccessTokenWithPopup: jest.fn(),
|
||||
getAccessTokenSilently: jest.fn(),
|
||||
getIdTokenClaims: jest.fn(),
|
||||
loginWithPopup: jest.fn(),
|
||||
handleRedirectCallback: jest.fn(),
|
||||
isLoading: false,
|
||||
});
|
||||
|
||||
window.localStorage.setItem('accessToken', 'token');
|
||||
const { useIsNotLoggedIn } = AuthenticationHooks;
|
||||
const { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(false);
|
||||
});
|
||||
});
|
||||
@ -3,12 +3,48 @@ import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { Auth0Provider } from '@auth0/auth0-react';
|
||||
import {
|
||||
ApolloClient,
|
||||
InMemoryCache,
|
||||
ApolloProvider,
|
||||
createHttpLink,
|
||||
} from '@apollo/client';
|
||||
import { setContext } from '@apollo/client/link/context';
|
||||
|
||||
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const client = new ApolloClient({
|
||||
link: authLink.concat(httpLink),
|
||||
cache: new InMemoryCache(),
|
||||
});
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement,
|
||||
);
|
||||
root.render(
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>,
|
||||
<Auth0Provider
|
||||
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
|
||||
clientId={process.env.REACT_APP_AUTH0_CLIENT_ID || ''}
|
||||
authorizationParams={{
|
||||
redirect_uri: process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
|
||||
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
|
||||
}}
|
||||
>
|
||||
<ApolloProvider client={client}>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ApolloProvider>
|
||||
</Auth0Provider>,
|
||||
);
|
||||
|
||||
@ -9,12 +9,18 @@ const StyledLayout = styled.div`
|
||||
|
||||
type OwnProps = {
|
||||
children: JSX.Element;
|
||||
user?: {
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
tenant: { id: string; name: string };
|
||||
};
|
||||
};
|
||||
|
||||
function AppLayout({ children }: OwnProps) {
|
||||
function AppLayout({ children, user }: OwnProps) {
|
||||
return (
|
||||
<StyledLayout>
|
||||
<Navbar />
|
||||
<Navbar user={user} />
|
||||
<div>{children}</div>
|
||||
</StyledLayout>
|
||||
);
|
||||
|
||||
@ -1,50 +1,68 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useMatch, useResolvedPath } from 'react-router-dom';
|
||||
import NavItem from './NavItem';
|
||||
import ProfileContainer from './ProfileContainer';
|
||||
|
||||
const NavbarContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: stretch;
|
||||
justify-content: space-between;
|
||||
padding-left: 12px;
|
||||
height: 58px;
|
||||
border-bottom: 2px solid #eaecee;
|
||||
`;
|
||||
|
||||
function Navbar() {
|
||||
const NavItemsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
type OwnProps = {
|
||||
user?: {
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
tenant: { id: string; name: string };
|
||||
};
|
||||
};
|
||||
|
||||
function Navbar({ user }: OwnProps) {
|
||||
return (
|
||||
<>
|
||||
<NavbarContainer>
|
||||
<NavItem
|
||||
label="Inbox"
|
||||
to="/"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavItem
|
||||
label="Contacts"
|
||||
to="/contacts"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/contacts').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavItem
|
||||
label="Insights"
|
||||
to="/insights"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/insights').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavItemsContainer>
|
||||
<NavItem
|
||||
label="Inbox"
|
||||
to="/"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavItem
|
||||
label="Contacts"
|
||||
to="/contacts"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/contacts').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<NavItem
|
||||
label="Insights"
|
||||
to="/insights"
|
||||
active={
|
||||
!!useMatch({
|
||||
path: useResolvedPath('/insights').pathname,
|
||||
end: true,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</NavItemsContainer>
|
||||
<ProfileContainer user={user} />
|
||||
</NavbarContainer>
|
||||
</>
|
||||
);
|
||||
|
||||
74
front/src/layout/navbar/ProfileContainer.tsx
Normal file
74
front/src/layout/navbar/ProfileContainer.tsx
Normal file
@ -0,0 +1,74 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
type OwnProps = {
|
||||
user?: {
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
tenant: { id: string; name: string };
|
||||
};
|
||||
};
|
||||
|
||||
const StyledContainer = styled.button`
|
||||
display: flex;
|
||||
height: 60px;
|
||||
background: inherit;
|
||||
align-items: center;
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
margin-left: 10px;
|
||||
margin-right: 10px;
|
||||
font-size: 14px;
|
||||
margin-bottom: -2px;
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
`;
|
||||
|
||||
const StyledInfoContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const StyledEmail = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const StyledTenant = styled.div`
|
||||
display: flex;
|
||||
text-transform: capitalize;
|
||||
font-weight: bold;
|
||||
`;
|
||||
|
||||
const StyledAvatar = styled.div`
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 40px;
|
||||
background: black;
|
||||
font-size: 20px;
|
||||
color: white;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
`;
|
||||
|
||||
function ProfileContainer({ user }: OwnProps) {
|
||||
return (
|
||||
<StyledContainer>
|
||||
<StyledAvatar>
|
||||
{user?.first_name
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('')}
|
||||
</StyledAvatar>
|
||||
<StyledInfoContainer>
|
||||
<StyledEmail>{user?.email}</StyledEmail>
|
||||
<StyledTenant>{user?.tenant.name}</StyledTenant>
|
||||
</StyledInfoContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export default ProfileContainer;
|
||||
@ -9,6 +9,13 @@ export default {
|
||||
|
||||
export const NavbarOnInsights = () => (
|
||||
<MemoryRouter initialEntries={['/insights']}>
|
||||
<Navbar />
|
||||
<Navbar
|
||||
user={{
|
||||
email: 'charles@twenty.com',
|
||||
first_name: 'Charles',
|
||||
last_name: 'Bochet',
|
||||
tenant: { id: '1', name: 'Twenty' },
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
18
front/src/pages/AuthCallback.tsx
Normal file
18
front/src/pages/AuthCallback.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import AuthService from '../hooks/AuthenticationHooks';
|
||||
|
||||
function AuthCallback() {
|
||||
const { useGetAccessToken } = AuthService;
|
||||
|
||||
const { token } = useGetAccessToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default AuthCallback;
|
||||
@ -1,5 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
function PluginHistory() {
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user