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

View File

@ -19,6 +19,7 @@
"@types/node": "^16.18.4",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"apollo-link-rest": "^0.9.0",
"graphql": "^16.6.0",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",
@ -8666,6 +8667,16 @@
"node": ">= 8"
}
},
"node_modules/apollo-link-rest": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/apollo-link-rest/-/apollo-link-rest-0.9.0.tgz",
"integrity": "sha512-kuXjR56Y12w0TZcqwVaONKlipB6g3Ya1dAy4NMCaylPpNXq6tO+qzQFPUyDJC7B0JoJPIFjxPV2rAet4uGM4UQ==",
"peerDependencies": {
"@apollo/client": ">=3",
"graphql": ">=0.11",
"qs": ">=6"
}
},
"node_modules/app-root-dir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/app-root-dir/-/app-root-dir-1.0.2.tgz",
@ -9966,7 +9977,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz",
"integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"get-intrinsic": "^1.0.2"
@ -13973,7 +13983,6 @@
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.0.tgz",
"integrity": "sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q==",
"dev": true,
"dependencies": {
"function-bind": "^1.1.1",
"has": "^1.0.3",
@ -14406,7 +14415,6 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
"integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
@ -19997,7 +20005,6 @@
"version": "1.12.3",
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz",
"integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==",
"dev": true,
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -22226,7 +22233,6 @@
"version": "6.11.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.1.tgz",
"integrity": "sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==",
"dev": true,
"dependencies": {
"side-channel": "^1.0.4"
},
@ -24020,7 +24026,6 @@
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz",
"integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.0",
"get-intrinsic": "^1.0.2",

View File

@ -14,6 +14,7 @@
"@types/node": "^16.18.4",
"@types/react": "^18.0.25",
"@types/react-dom": "^18.0.9",
"apollo-link-rest": "^0.9.0",
"graphql": "^16.6.0",
"jwt-decode": "^3.1.2",
"libphonenumber-js": "^1.10.26",

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,
},
},
},

View File

@ -0,0 +1,14 @@
table:
name: companies
schema: public
select_permissions:
- role: user
permission:
columns:
- id
- workspace_id
- company_domain
- company_name
- created_at
- updated_at
filter: {}

View File

@ -1,3 +0,0 @@
table:
name: company
schema: public

View File

@ -1,10 +0,0 @@
table:
name: person
schema: public
object_relationships:
- name: company
using:
foreign_key_constraint_on: company_id
- name: workspace
using:
foreign_key_constraint_on: workspace_id

View File

@ -0,0 +1,25 @@
table:
name: persons
schema: public
object_relationships:
- name: company
using:
foreign_key_constraint_on: company_id
- name: workspace
using:
foreign_key_constraint_on: workspace_id
select_permissions:
- role: user
permission:
columns:
- company_id
- id
- workspace_id
- city
- email
- firstname
- lastname
- phone
- created_at
- updated_at
filter: {}

View File

@ -6,6 +6,6 @@
- "!include auth_user_roles.yaml"
- "!include auth_user_security_keys.yaml"
- "!include auth_users.yaml"
- "!include public_company.yaml"
- "!include public_person.yaml"
- "!include public_companies.yaml"
- "!include public_persons.yaml"
- "!include public_workspaces.yaml"

View File

@ -0,0 +1 @@
alter table "public"."companies" rename to "company";

View File

@ -0,0 +1 @@
alter table "public"."company" rename to "companies";

View File

@ -0,0 +1 @@
alter table "public"."persons" rename to "person";

View File

@ -0,0 +1 @@
alter table "public"."person" rename to "persons";

View File

@ -3,4 +3,4 @@ HASURA_AUTH_CLIENT_URL: http://localhost:3001/auth/callback
HASURA_AUTH_PROVIDER_GOOGLE_CLIENT_ID: REPLACE_ME
HASURA_AUTH_PROVIDER_GOOGLE_CLIENT_SECRET: REPLACE_ME
FRONT_REACT_APP_API_URL=http://localhost:8080
FRONT_REACT_APP_LOGIN_PROVIDER_URL=http://localhost:4000/signin/provider/google
FRONT_REACT_APP_AUTH_URL=http://localhost:4000

View File

@ -9,7 +9,7 @@ services:
- "6006:6006"
environment:
- REACT_APP_API_URL=${FRONT_REACT_APP_API_URL}
- REACT_APP_LOGIN_PROVIDER_URL=${FRONT_REACT_APP_LOGIN_PROVIDER_URL}
- REACT_APP_AUTH_URL=${FRONT_REACT_APP_AUTH_URL}
volumes:
- ../../front:/app/front
- twenty_node_modules_front:/app/front/node_modules

View File

@ -2,7 +2,7 @@ FROM node:18.16.0-alpine as front
ARG FONTAWESOME_NPM_AUTH_TOKEN
ARG REACT_APP_API_URL
ARG REACT_APP_LOGIN_PROVIDER_URL
ARG REACT_APP_AUTH_URL
WORKDIR /app/front
COPY ./front .