Simplify infrastructure to one container
This commit is contained in:
@ -1,65 +1,29 @@
|
||||
import { Auth0Provider } from '@auth0/auth0-react';
|
||||
import React, { useEffect } from 'react';
|
||||
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 { useGetProfile } from './hooks/profile/useGetProfile';
|
||||
import { useGetTenantByDomain } from './hooks/tenant/useGetTenantByDomain';
|
||||
|
||||
function App() {
|
||||
const { tenant } = useGetTenantByDomain();
|
||||
const { user } = useGetProfile();
|
||||
const user = {
|
||||
id: 1,
|
||||
email: 'charles@twenty.com',
|
||||
first_name: 'Charles',
|
||||
last_name: 'Bochet',
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{tenant && (
|
||||
<Auth0Provider
|
||||
domain={process.env.REACT_APP_AUTH0_DOMAIN || ''}
|
||||
clientId={tenant?.auth0_client_id || ''}
|
||||
authorizationParams={{
|
||||
redirect_uri:
|
||||
window.location.protocol +
|
||||
'//' +
|
||||
window.location.host +
|
||||
process.env.REACT_APP_AUTH0_CALLBACK_URL || '',
|
||||
audience: process.env.REACT_APP_AUTH0_AUDIENCE || '',
|
||||
}}
|
||||
>
|
||||
<AppLayout user={user}>
|
||||
<Routes>
|
||||
<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>
|
||||
</Auth0Provider>
|
||||
)}
|
||||
{
|
||||
<AppLayout user={user}>
|
||||
<Routes>
|
||||
<Route path="/" element={<Inbox />} />
|
||||
<Route path="/contacts" element={<Contacts />} />
|
||||
<Route path="/insights" element={<Insights />} />
|
||||
</Routes>
|
||||
</AppLayout>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
import { redirectIfNotLoggedIn } from '../hooks/AuthenticationHooks';
|
||||
|
||||
function RequireAuth({ children }: { children: JSX.Element }): JSX.Element {
|
||||
redirectIfNotLoggedIn();
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
export default RequireAuth;
|
||||
@ -1,14 +0,0 @@
|
||||
import RequireAuth from '../RequireAuth';
|
||||
|
||||
import Navbar from '../RequireAuth';
|
||||
|
||||
export default {
|
||||
title: 'RequireAuth',
|
||||
component: Navbar,
|
||||
};
|
||||
|
||||
export const RequireAuthWithHelloChild = () => (
|
||||
<RequireAuth>
|
||||
<div>Hello</div>
|
||||
</RequireAuth>
|
||||
);
|
||||
@ -1,9 +0,0 @@
|
||||
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();
|
||||
});
|
||||
@ -1,57 +0,0 @@
|
||||
import { useAuth0 } from '@auth0/auth0-react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import jwt from 'jwt-decode';
|
||||
import { TokenPayload } from '../interfaces/TokenPayload.interface';
|
||||
|
||||
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 useGetUserEmailFromToken = (): string | undefined => {
|
||||
const token = localStorage.getItem('accessToken');
|
||||
|
||||
const payload: TokenPayload | undefined = token ? jwt(token) : undefined;
|
||||
if (!payload) {
|
||||
return;
|
||||
}
|
||||
|
||||
return payload['https://hasura.io/jwt/claims']['x-hasura-user-email'];
|
||||
};
|
||||
|
||||
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 {
|
||||
useIsNotLoggedIn,
|
||||
useGetAccessToken,
|
||||
redirectIfNotLoggedIn,
|
||||
useGetUserEmailFromToken,
|
||||
};
|
||||
@ -1,129 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import {
|
||||
useIsNotLoggedIn,
|
||||
useGetUserEmailFromToken,
|
||||
} 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 { 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 { 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 { 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 { result } = renderHook(() => useIsNotLoggedIn());
|
||||
const isNotLoggedIn = result.current;
|
||||
|
||||
expect(isNotLoggedIn).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useGetUserEmailFromToken', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear();
|
||||
jest.resetModules();
|
||||
});
|
||||
|
||||
it('returns undefined if token is not there', () => {
|
||||
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||
const email = result.current;
|
||||
|
||||
expect(email).toBe(undefined);
|
||||
});
|
||||
|
||||
it('returns email if token is there', () => {
|
||||
window.localStorage.setItem(
|
||||
'accessToken',
|
||||
'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1FQXZiR0dFNjJ4S25mTFNxNHQ0dCJ9.eyJodHRwczovL2hhc3VyYS5pby9qd3QvY2xhaW1zIjp7IngtaGFzdXJhLWFsbG93ZWQtcm9sZXMiOlsidXNlciJdLCJ4LWhhc3VyYS1kZWZhdWx0LXJvbGUiOiJ1c2VyIiwieC1oYXN1cmEtdXNlci1lbWFpbCI6ImNoYXJsZXNAb3VpaGVscC50d2VudHkuY29tIiwieC1oYXN1cmEtdXNlci1pZCI6Imdvb2dsZS1vYXV0aDJ8MTE4MjM1ODk3NDQ2OTIwNTQ3NzMzIn0sImlzcyI6Imh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vIiwic3ViIjoiZ29vZ2xlLW9hdXRoMnwxMTgyMzU4OTc0NDY5MjA1NDc3MzMiLCJhdWQiOlsiaGFzdXJhLWRldiIsImh0dHBzOi8vdHdlbnR5LWRldi5ldS5hdXRoMC5jb20vdXNlcmluZm8iXSwiaWF0IjoxNjc1MzYyNzY0LCJleHAiOjE2NzU0NDkxNjQsImF6cCI6IlM2ZXoyUFdUdUFsRncydjdxTFBWb2hmVXRseHc4QlBhIiwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBlbWFpbCJ9.DseeSqYzNlYVQfuicoK8fK1Z6b-TYNvCkRoXXYOhg1X3HDSejowUTudyrJGErkT65xMCfx8K5quof9eV8BZQixCPr670r5gAIHxHuGY_KNfHTOALe8E5VyQaoekRyDr99Qo3QxliOOlJxtmckA8FTeD6JanfVmcrqghUOIsSXXDOOzJV6eME7JErEjTQHpfxveSVbPlCmIqZ3fqDaFdKfAlUDZFhVQM8XbfubNmG4VcoMyB7H47yLdGkYvVfPO1lVg0efywQo4IfbtiqFv5CjOEqO6PG78Wfkd24bcilkf6ZuGXsA-w-0xlU089GhKF99lNI1PxvNWAaLFbqanxiEw',
|
||||
);
|
||||
const { result } = renderHook(() => useGetUserEmailFromToken());
|
||||
|
||||
expect(result.current).toBe('charles@ouihelp.twenty.com');
|
||||
});
|
||||
});
|
||||
@ -1,25 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQuery, QueryResult } from '@apollo/client';
|
||||
import { useGetProfile } from '../useGetProfile';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useGetUserEmailFromToken', () => {
|
||||
beforeEach(() => {
|
||||
const result: Partial<QueryResult<any>> = {
|
||||
data: { users: [{ email: 'test@twenty.com' }] },
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
|
||||
});
|
||||
|
||||
it('returns profile', () => {
|
||||
const { result } = renderHook(() => useGetProfile());
|
||||
const email = result.current.user?.email;
|
||||
expect(email).toEqual(result.current.user?.email);
|
||||
expect(useQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -1,33 +0,0 @@
|
||||
import { ApolloError, useQuery } from '@apollo/client';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { User } from '../../interfaces/user.interface';
|
||||
import { useGetUserEmailFromToken } from '../AuthenticationHooks';
|
||||
|
||||
const GET_USER_PROFILE = gql`
|
||||
query GetUserProfile($email: String!) {
|
||||
users(where: { email: { _eq: $email } }, limit: 1) {
|
||||
id
|
||||
email
|
||||
first_name
|
||||
last_name
|
||||
tenant {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type ProfileResult = {
|
||||
loading: boolean;
|
||||
error?: ApolloError;
|
||||
user?: User;
|
||||
};
|
||||
|
||||
export const useGetProfile = (): ProfileResult => {
|
||||
const email = useGetUserEmailFromToken();
|
||||
const { loading, error, data } = useQuery(GET_USER_PROFILE, {
|
||||
variables: { email },
|
||||
});
|
||||
return { loading, error, user: data?.users[0] };
|
||||
};
|
||||
@ -1,25 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useQuery, QueryResult } from '@apollo/client';
|
||||
import { useGetTenantByDomain } from '../useGetTenantByDomain';
|
||||
|
||||
jest.mock('@apollo/client', () => ({
|
||||
useQuery: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useGetTenantByDomain', () => {
|
||||
beforeEach(() => {
|
||||
const result: Partial<QueryResult<any>> = {
|
||||
data: { tenants: [{ domain: 'pilot.twenty.com' }] },
|
||||
loading: false,
|
||||
error: undefined,
|
||||
};
|
||||
(useQuery as jest.Mock).mockImplementation(() => result as QueryResult);
|
||||
});
|
||||
|
||||
it('returns tenant by domain', () => {
|
||||
const { result } = renderHook(() => useGetTenantByDomain());
|
||||
const domain = result.current.tenant?.domain;
|
||||
expect(domain).toEqual(result.current.tenant?.domain);
|
||||
expect(useQuery).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -1,31 +0,0 @@
|
||||
import { ApolloError, useQuery } from '@apollo/client';
|
||||
import { gql } from 'graphql-tag';
|
||||
import { Tenant } from '../../interfaces/tenant.interface';
|
||||
|
||||
const GET_TENANT_BY_DOMAIN = gql`
|
||||
query GetTenantByDomain($domain: String!) {
|
||||
tenants(where: { domain: { _eq: $domain } }, limit: 1) {
|
||||
auth0_client_id
|
||||
domain
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
type TenantResult = {
|
||||
loading: boolean;
|
||||
error?: ApolloError;
|
||||
tenant?: Tenant;
|
||||
};
|
||||
|
||||
export const useGetTenantByDomain = (): TenantResult => {
|
||||
const domain = window.location.hostname;
|
||||
const { loading, error, data } = useQuery(GET_TENANT_BY_DOMAIN, {
|
||||
variables: { domain },
|
||||
context: {
|
||||
headers: {
|
||||
'x-hasura-default-role': 'public',
|
||||
},
|
||||
},
|
||||
});
|
||||
return { loading, error, tenant: data?.tenants[0] };
|
||||
};
|
||||
@ -14,17 +14,8 @@ import { setContext } from '@apollo/client/link/context';
|
||||
const httpLink = createHttpLink({ uri: process.env.REACT_APP_API_URL });
|
||||
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
const requestHeaders = { ...headers };
|
||||
const token = localStorage.getItem('accessToken');
|
||||
const headerContainsPublicRole =
|
||||
requestHeaders.hasOwnProperty('x-hasura-default-role') &&
|
||||
requestHeaders['x-hasura-default-role'] === 'public';
|
||||
if (!headerContainsPublicRole && token) {
|
||||
requestHeaders['authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
return {
|
||||
headers: requestHeaders,
|
||||
headers: headers,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@ -1,6 +0,0 @@
|
||||
export interface TokenPayload {
|
||||
'https://hasura.io/jwt/claims': {
|
||||
'x-hasura-user-email': string;
|
||||
'x-hasura-user-id': string;
|
||||
};
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
export interface Tenant {
|
||||
id: number;
|
||||
name: string;
|
||||
domain: string;
|
||||
auth0_client_id: string;
|
||||
}
|
||||
@ -1,9 +1,6 @@
|
||||
import { Tenant } from './tenant.interface';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
email: string;
|
||||
first_name: string;
|
||||
last_name: string;
|
||||
tenant?: Tenant;
|
||||
}
|
||||
|
||||
@ -29,12 +29,6 @@ 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;
|
||||
@ -61,7 +55,6 @@ function ProfileContainer({ user }: OwnProps) {
|
||||
</StyledAvatar>
|
||||
<StyledInfoContainer>
|
||||
<StyledEmail>{user?.email}</StyledEmail>
|
||||
<StyledTenant>{user?.tenant?.name}</StyledTenant>
|
||||
</StyledInfoContainer>
|
||||
</StyledContainer>
|
||||
);
|
||||
|
||||
@ -15,12 +15,6 @@ export const NavbarOnInsights = () => (
|
||||
email: 'charles@twenty.com',
|
||||
first_name: 'Charles',
|
||||
last_name: 'Bochet',
|
||||
tenant: {
|
||||
id: 1,
|
||||
name: 'Twenty',
|
||||
domain: 'pilot.twenty.com',
|
||||
auth0_client_id: 'auth0_client_id',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { useGetAccessToken } from '../hooks/AuthenticationHooks';
|
||||
|
||||
function AuthCallback() {
|
||||
const { token } = useGetAccessToken();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
window.location.href = '/';
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
return <div></div>;
|
||||
}
|
||||
|
||||
export default AuthCallback;
|
||||
Reference in New Issue
Block a user