Fetch jwt token from hasura-auth with refresh_token

This commit is contained in:
Charles Bochet
2023-04-21 14:07:02 +02:00
parent f98f0e942e
commit c5f2850a3b
26 changed files with 212 additions and 61 deletions

32
front/src/apollo.tsx Normal file
View File

@ -0,0 +1,32 @@
import { ApolloClient, InMemoryCache, createHttpLink } from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { RestLink } from 'apollo-link-rest';
const apiLink = createHttpLink({
uri: `${process.env.REACT_APP_API_URL}/v1/graphql`,
});
const withAuthHeadersLink = setContext((_, { headers }) => {
const token = localStorage.getItem('accessToken');
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : '',
},
};
});
export const apiClient = new ApolloClient({
link: withAuthHeadersLink.concat(apiLink),
cache: new InMemoryCache(),
});
const authLink = new RestLink({
uri: `${process.env.REACT_APP_AUTH_URL}`,
credentials: 'same-origin',
});
export const authClient = new ApolloClient({
link: authLink,
cache: new InMemoryCache(),
});

View File

@ -1,5 +1,4 @@
import { useRef } from 'react';
import TableHeader from '../../components/table/table-header/TableHeader';
import { render, fireEvent } from '@testing-library/react';
import { useOutsideAlerter } from '../useOutsideAlerter';
import { act } from 'react-dom/test-utils';
@ -17,9 +16,7 @@ function TestComponent() {
);
}
export default TableHeader;
test('clicking the button toggles an answer on/off', async () => {
test('useOutsideAlerter hook works properly', async () => {
const { getByText } = render(<TestComponent />);
const inside = getByText('Inside');
const outside = getByText('Outside');

View File

@ -0,0 +1,56 @@
import { render, waitFor } from '@testing-library/react';
import { useRefreshToken } from '../useRefreshToken';
const localStorageMock = (function () {
let store: { [key: string]: string } = {};
return {
getItem: function (key: string) {
return store[key];
},
setItem: function (key: string, value: string) {
store[key] = value.toString();
},
clear: function () {
store = {};
},
removeItem: function (key: string) {
delete store[key];
},
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
function TestComponent() {
const { loading } = useRefreshToken();
return <div>{!loading && <div>Refreshed</div>}</div>;
}
jest.mock('@apollo/client', () => {
return {
__esModule: true,
...jest.requireActual('@apollo/client'),
useQuery: () => ({
data: {
token: {
accessToken: 'test-access-token',
},
},
isLoading: false,
error: {},
}),
};
});
test('useRefreshToken works properly', async () => {
localStorage.setItem('refreshToken', 'test-refresh-token');
render(<TestComponent />);
await waitFor(() => {
expect(localStorageMock.getItem('accessToken')).toBe('test-access-token');
});
});
afterEach(() => {
jest.clearAllMocks();
});

View File

@ -0,0 +1,3 @@
export const useHasAccessToken = () => {
return false;
};

View File

@ -0,0 +1,3 @@
export const redirectToSignIn = () => {
return false;
};

View File

@ -0,0 +1,32 @@
import { gql, useQuery } from '@apollo/client';
import { useEffect } from 'react';
import { authClient } from '../../apollo';
export const GET_TOKEN = gql`
fragment Payload on REST {
refreshToken: String
}
query jwt($input: Payload) {
token(input: $input) @rest(type: "string", path: "/token", method: "POST") {
accessToken
}
}
`;
export const useRefreshToken = () => {
const refreshToken = localStorage.getItem('refreshToken');
const { data, loading } = useQuery(GET_TOKEN, {
client: authClient,
variables: { input: { refreshToken } },
});
useEffect(() => {
if (!loading) {
const accessToken = data.token.accessToken;
if (refreshToken && accessToken) {
localStorage.setItem('accessToken', accessToken || '');
}
}
}, [data, refreshToken, loading]);
return { loading };
};

View File

@ -3,36 +3,16 @@ import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { BrowserRouter } from 'react-router-dom';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
} from '@apollo/client';
import { setContext } from '@apollo/client/link/context';
import { ApolloProvider } from '@apollo/client';
import '@emotion/react';
import { ThemeType } from './layout/styles/themes';
const httpLink = createHttpLink({
uri: `${process.env.REACT_APP_API_URL}/v1/graphql`,
});
const authLink = setContext((_, { headers }) => {
return {
headers: { ...headers, 'x-hasura-admin-secret': 'secret' },
};
});
const client = new ApolloClient({
link: authLink.concat(httpLink),
cache: new InMemoryCache(),
});
import { apiClient } from './apollo';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement,
);
root.render(
<ApolloProvider client={client}>
<ApolloProvider client={apiClient}>
<BrowserRouter>
<App />
</BrowserRouter>

View File

@ -1,15 +1,18 @@
import { useEffect } from 'react';
import { useSearchParams, useNavigate } from 'react-router-dom';
import { useRefreshToken } from '../../hooks/auth/useRefreshToken';
import { useEffect } from 'react';
function Callback() {
const [searchParams] = useSearchParams();
const refreshToken = searchParams.get('refreshToken');
localStorage.setItem('refreshToken', refreshToken || '');
const { loading } = useRefreshToken();
const navigate = useNavigate();
useEffect(() => {
navigate('/');
}, [navigate]);
if (!loading) {
navigate('/');
}
}, [navigate, loading]);
return <></>;
}

View File

@ -6,7 +6,8 @@ function Login() {
const navigate = useNavigate();
useEffect(() => {
if (!refreshToken) {
window.location.href = process.env.REACT_APP_LOGIN_PROVIDER_URL || '';
window.location.href =
process.env.REACT_APP_AUTH_URL + '/signin/provider/google' || '';
}
navigate('/');
}, [refreshToken, navigate]);

View File

@ -2,6 +2,14 @@ import { render } from '@testing-library/react';
import { CallbackDefault } from '../__stories__/Callback.stories';
jest.mock('../../../hooks/auth/useRefreshToken', () => ({
useRefreshToken: () => ({ loading: false }),
}));
it('Checks the Callback page render', () => {
render(<CallbackDefault />);
});
afterEach(() => {
jest.clearAllMocks();
});

View File

@ -14,8 +14,8 @@ const StyledPeopleContainer = styled.div`
`;
export const GET_PEOPLE = gql`
query GetPeople($orderBy: [person_order_by!]) {
person(order_by: $orderBy) {
query GetPeople($orderBy: [persons_order_by!]) {
persons(order_by: $orderBy) {
id
phone
email
@ -57,7 +57,7 @@ function People() {
setOrderBy(sorts.length ? reduceSortsToOrderBy(sorts) : defaultOrderBy);
};
const { data } = useQuery<{ person: GraphqlPerson[] }>(GET_PEOPLE, {
const { data } = useQuery<{ persons: GraphqlPerson[] }>(GET_PEOPLE, {
variables: { orderBy: orderBy },
});
@ -66,7 +66,7 @@ function People() {
<StyledPeopleContainer>
{
<Table
data={data ? data.person.map(mapPerson) : []}
data={data ? data.persons.map(mapPerson) : []}
columns={peopleColumns}
viewName="All People"
viewIcon={faList}

View File

@ -22,7 +22,7 @@ const mocks = [
},
result: {
data: {
person: defaultData,
persons: defaultData,
},
},
},