Generate Token through Auth0

This commit is contained in:
Charles Bochet
2023-01-27 12:12:04 +01:00
parent 54acb16db8
commit 8e0dc44bf6
21 changed files with 3616 additions and 2344 deletions

View File

@ -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>
);

View 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;

View 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>
);

View 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();
});

View 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 };

View 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);
});
});

View File

@ -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>,
);

View File

@ -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>
);

View File

@ -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>
</>
);

View 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;

View File

@ -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>
);

View 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;

View File

@ -1,5 +1,3 @@
import styled from '@emotion/styled';
function PluginHistory() {
return <div></div>;
}