diff --git a/front/package-lock.json b/front/package-lock.json index 9c0f857e9..ad5d1f6de 100644 --- a/front/package-lock.json +++ b/front/package-lock.json @@ -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", diff --git a/front/package.json b/front/package.json index 5e4943a07..f239d1e53 100644 --- a/front/package.json +++ b/front/package.json @@ -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", diff --git a/front/src/apollo.tsx b/front/src/apollo.tsx new file mode 100644 index 000000000..3c3c73684 --- /dev/null +++ b/front/src/apollo.tsx @@ -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(), +}); diff --git a/front/src/hooks/__tests__/useOutsideAlerter.test.tsx b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx index 9f970b56a..830fb4f25 100644 --- a/front/src/hooks/__tests__/useOutsideAlerter.test.tsx +++ b/front/src/hooks/__tests__/useOutsideAlerter.test.tsx @@ -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(); const inside = getByText('Inside'); const outside = getByText('Outside'); diff --git a/front/src/hooks/auth/__tests__/useRefreshToken.test.tsx b/front/src/hooks/auth/__tests__/useRefreshToken.test.tsx new file mode 100644 index 000000000..b89081773 --- /dev/null +++ b/front/src/hooks/auth/__tests__/useRefreshToken.test.tsx @@ -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
{!loading &&
Refreshed
}
; +} + +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(); + + await waitFor(() => { + expect(localStorageMock.getItem('accessToken')).toBe('test-access-token'); + }); +}); + +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/front/src/hooks/auth/useHasAccessToken.tsx b/front/src/hooks/auth/useHasAccessToken.tsx new file mode 100644 index 000000000..91ee21b71 --- /dev/null +++ b/front/src/hooks/auth/useHasAccessToken.tsx @@ -0,0 +1,3 @@ +export const useHasAccessToken = () => { + return false; +}; diff --git a/front/src/hooks/auth/useRedirectToSignIn.tsx b/front/src/hooks/auth/useRedirectToSignIn.tsx new file mode 100644 index 000000000..5aa69d04a --- /dev/null +++ b/front/src/hooks/auth/useRedirectToSignIn.tsx @@ -0,0 +1,3 @@ +export const redirectToSignIn = () => { + return false; +}; diff --git a/front/src/hooks/auth/useRefreshToken.tsx b/front/src/hooks/auth/useRefreshToken.tsx new file mode 100644 index 000000000..c4a0b3d60 --- /dev/null +++ b/front/src/hooks/auth/useRefreshToken.tsx @@ -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 }; +}; diff --git a/front/src/index.tsx b/front/src/index.tsx index 2a3c1c11a..ec84e70d6 100644 --- a/front/src/index.tsx +++ b/front/src/index.tsx @@ -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( - + diff --git a/front/src/pages/auth/Callback.tsx b/front/src/pages/auth/Callback.tsx index f30c8e4e8..16d63ac0f 100644 --- a/front/src/pages/auth/Callback.tsx +++ b/front/src/pages/auth/Callback.tsx @@ -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 <>; } diff --git a/front/src/pages/auth/Login.tsx b/front/src/pages/auth/Login.tsx index b57c12efb..fb98fe083 100644 --- a/front/src/pages/auth/Login.tsx +++ b/front/src/pages/auth/Login.tsx @@ -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]); diff --git a/front/src/pages/auth/__tests__/Callback.test.tsx b/front/src/pages/auth/__tests__/Callback.test.tsx index 3b22994e1..ab5f92494 100644 --- a/front/src/pages/auth/__tests__/Callback.test.tsx +++ b/front/src/pages/auth/__tests__/Callback.test.tsx @@ -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(); }); + +afterEach(() => { + jest.clearAllMocks(); +}); diff --git a/front/src/pages/people/People.tsx b/front/src/pages/people/People.tsx index 04e36c52b..9de38894a 100644 --- a/front/src/pages/people/People.tsx +++ b/front/src/pages/people/People.tsx @@ -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() { {