From 06139d87f6e8ec20f7da42d42f3036822c0826f0 Mon Sep 17 00:00:00 2001 From: Liam Stevens <8955671+liamstevens111@users.noreply.github.com> Date: Tue, 7 Mar 2023 10:41:25 +0700 Subject: [PATCH] [#25] Include user profile details in localstorage with token information --- src/components/PrivateRoutes/index.tsx | 21 +++++-------- src/helpers/localStorage.test.ts | 43 ++++++++++++++++++++++++++ src/helpers/localStorage.ts | 13 ++++++++ src/helpers/userToken.test.ts | 43 -------------------------- src/helpers/userToken.ts | 11 ------- src/lib/requestManager.ts | 24 +++++++------- src/screens/Home/index.tsx | 3 +- src/screens/Login/index.test.tsx | 38 +++++++++++++++++++---- src/screens/Login/index.tsx | 18 ++++++----- 9 files changed, 118 insertions(+), 96 deletions(-) create mode 100644 src/helpers/localStorage.test.ts create mode 100644 src/helpers/localStorage.ts delete mode 100644 src/helpers/userToken.test.ts delete mode 100644 src/helpers/userToken.ts diff --git a/src/components/PrivateRoutes/index.tsx b/src/components/PrivateRoutes/index.tsx index 6adbad1..74d261e 100644 --- a/src/components/PrivateRoutes/index.tsx +++ b/src/components/PrivateRoutes/index.tsx @@ -1,8 +1,7 @@ import { useState, useEffect } from 'react'; import { Navigate, Outlet, useOutletContext } from 'react-router-dom'; -import AuthAdapter from 'adapters/authAdapter'; -import { getToken } from 'helpers/userToken'; +import { getItem } from 'helpers/localStorage'; import type { User } from 'types/User'; type ContextType = User; @@ -15,24 +14,18 @@ function PrivateRoutes() { useEffect(() => { const fetchCurrentUser = async () => { - const token = getToken().access_token; - - if (!token) { - setLoading(false); - } else { - const response = await AuthAdapter.getUser(); - - const data = await response.data; - - setUser(data.attributes); - setLoading(false); + const userProfile = getItem('UserProfile'); + if (userProfile?.user) { + setUser({ ...userProfile.user }); } + + setLoading(false); }; fetchCurrentUser(); }, []); if (loading) { - return null; + return

Loading...

; } return user ? : ; diff --git a/src/helpers/localStorage.test.ts b/src/helpers/localStorage.test.ts new file mode 100644 index 0000000..323c6f7 --- /dev/null +++ b/src/helpers/localStorage.test.ts @@ -0,0 +1,43 @@ +import { setItem, getItem, clearItem } from './localStorage'; + +/* eslint-disable camelcase */ +describe('setItem', () => { + test('Given a valid UserToken, sets it in LocalStorage', () => { + const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; + + setItem('UserProfile', testToken); + + expect(JSON.parse(localStorage.getItem('UserProfile') as string)).toStrictEqual(testToken); + }); +}); + +describe('getItem', () => { + test('Given an existing UserToken in LocalStorage, returns the value', () => { + const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; + + localStorage.setItem('UserProfile', JSON.stringify(testToken)); + + expect(getItem('UserProfile')).toStrictEqual(testToken); + + localStorage.clear(); + }); + + test('Given a NON-existing UserToken in LocalStorage, returns null', () => { + expect(getItem('UserProfile')).toBeNull(); + }); +}); + +describe('clearItem', () => { + test('Given an existing UserToken in LocalStorage, removes the value', () => { + const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; + + localStorage.setItem('UserProfile', JSON.stringify(testToken)); + + expect(JSON.parse(localStorage.getItem('UserProfile') as string)).toStrictEqual(testToken); + + clearItem('UserProfile'); + + expect(JSON.parse(localStorage.getItem('UserProfile') as string)).toBeNull(); + }); +}); +/* eslint-enable camelcase */ diff --git a/src/helpers/localStorage.ts b/src/helpers/localStorage.ts new file mode 100644 index 0000000..36e1ce5 --- /dev/null +++ b/src/helpers/localStorage.ts @@ -0,0 +1,13 @@ +export const getItem = (key: string) => { + const value = localStorage.getItem(key); + + return value && JSON.parse(value); +}; + +export const setItem = (key: string, value: object) => { + localStorage.setItem(key, JSON.stringify(value)); +}; + +export const clearItem = (key: string) => { + localStorage.removeItem(key); +}; diff --git a/src/helpers/userToken.test.ts b/src/helpers/userToken.test.ts deleted file mode 100644 index 04bcf72..0000000 --- a/src/helpers/userToken.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { setToken, getToken, clearToken } from './userToken'; - -/* eslint-disable camelcase */ -describe('setToken', () => { - test('Given a valid UserToken, sets it in LocalStorage', () => { - const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; - - setToken(testToken); - - expect(JSON.parse(localStorage.getItem('UserToken') as string)).toStrictEqual(testToken); - }); -}); - -describe('getToken', () => { - test('Given an existing UserToken in LocalStorage, returns the value', () => { - const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; - - localStorage.setItem('UserToken', JSON.stringify(testToken)); - - expect(getToken()).toStrictEqual(testToken); - - localStorage.clear(); - }); - - test('Given a NON-existing UserToken in LocalStorage, returns an empty object', () => { - expect(getToken()).toEqual({}); - }); -}); - -describe('clearToken', () => { - test('Given an existing UserToken in LocalStorage, removes the value', () => { - const testToken = { access_token: 'access_token', refresh_token: 'refesh_token' }; - - localStorage.setItem('UserToken', JSON.stringify(testToken)); - - expect(JSON.parse(localStorage.getItem('UserToken') as string)).toStrictEqual(testToken); - - clearToken(); - - expect(JSON.parse(localStorage.getItem('UserToken') as string)).toBeNull(); - }); -}); -/* eslint-enable camelcase */ diff --git a/src/helpers/userToken.ts b/src/helpers/userToken.ts deleted file mode 100644 index a3abd6c..0000000 --- a/src/helpers/userToken.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const getToken = () => { - return JSON.parse(localStorage.getItem('UserToken') || '{}'); -}; - -export const setToken = (token: { access_token: string; refresh_token: string }) => { - localStorage.setItem('UserToken', JSON.stringify(token)); -}; - -export const clearToken = () => { - localStorage.removeItem('UserToken'); -}; diff --git a/src/lib/requestManager.ts b/src/lib/requestManager.ts index fbecdde..3f8de0e 100644 --- a/src/lib/requestManager.ts +++ b/src/lib/requestManager.ts @@ -1,7 +1,7 @@ import axios, { Method as HTTPMethod, ResponseType, AxiosRequestConfig, AxiosResponse } from 'axios'; import AuthAdapter from 'adapters/authAdapter'; -import { setToken, getToken, clearToken } from 'helpers/userToken'; +import { setItem, getItem, clearItem } from 'helpers/localStorage'; import { LOGIN_URL } from '../constants'; @@ -35,10 +35,10 @@ const requestManager = ( axios.interceptors.request.use( function (config) { - const userToken = getToken(); + const userProfile = getItem('UserProfile'); - if (userToken?.access_token) { - config.headers.Authorization = `Bearer ${userToken.access_token}`; + if (userProfile?.auth?.access_token) { + config.headers.Authorization = `Bearer ${userProfile.auth.access_token}`; } return config; }, @@ -53,23 +53,21 @@ const requestManager = ( }, async function (error) { if (error.response?.status === 401) { - const userToken = getToken(); + const userProfile = getItem('UserProfile'); - clearToken(); + clearItem('UserProfile'); - if (userToken?.refresh_token) { + if (userProfile?.auth?.refresh_token) { try { - const response = await AuthAdapter.loginWithRefreshToken(userToken.refresh_token); + const response = await AuthAdapter.loginWithRefreshToken(userProfile.auth.refresh_token); - const { - attributes: { access_token: accessToken, refresh_token: refreshToken }, - } = await response.data; + const { attributes: authInfo } = await response.data; /* eslint-disable camelcase */ - setToken({ access_token: accessToken, refresh_token: refreshToken }); + setItem('UserProfile', { ...userProfile, auth: authInfo }); /* eslint-enable camelcase */ - error.config.headers.Authorization = `Bearer ${accessToken}`; + error.config.headers.Authorization = `Bearer ${authInfo.accessToken}`; return axios.request(error.config); } catch { window.location.href = LOGIN_URL; diff --git a/src/screens/Home/index.tsx b/src/screens/Home/index.tsx index 9dfc31d..6cc0710 100644 --- a/src/screens/Home/index.tsx +++ b/src/screens/Home/index.tsx @@ -8,7 +8,8 @@ const HomeScreen = (): JSX.Element => {
Home Screen
-
{user && user.email}
+ {/* TODO: Remove when header implemented in #19 */} +
{`${user?.name} - ${user?.email} - ${user?.avatarUrl}`}
); }; diff --git a/src/screens/Login/index.test.tsx b/src/screens/Login/index.test.tsx index f312d59..49113bb 100644 --- a/src/screens/Login/index.test.tsx +++ b/src/screens/Login/index.test.tsx @@ -19,6 +19,15 @@ const commonLoginParams = { const mockTokenData = { access_token: 'test_access_token', refresh_token: 'test_refresh_token', + token_type: 'Bearer', + expires_in: 7200, + created_at: 1677045997, +}; + +const mockUserProfileData = { + email: 'testemail@gmail.com', + name: 'TestName', + avatar_url: 'https://secure.gravatar.com/avatar/6733d09432e89459dba795de8312ac2d', }; const commonLoginResponse = { @@ -26,14 +35,23 @@ const commonLoginResponse = { id: '18339', type: 'token', attributes: { - token_type: 'Bearer', - expires_in: 7200, - created_at: 1677045997, ...mockTokenData, }, }, }; +const commonUserProfileResponse = { + data: { + id: '1', + type: 'user', + attributes: { + email: 'testemail@gmail.com', + name: 'TestName', + avatar_url: 'https://secure.gravatar.com/avatar/6733d09432e89459dba795de8312ac2d', + }, + }, +}; + const testCredentials = { email: 'testemail@gmail.com', password: 'password123', @@ -80,6 +98,7 @@ describe('LoginScreen', () => { .defaultReplyHeaders({ 'access-control-allow-origin': '*', 'access-control-allow-credentials': 'true', + 'access-control-allow-headers': 'Authorization', }) .post('/oauth/token', { ...commonLoginParams, @@ -87,7 +106,11 @@ describe('LoginScreen', () => { }) .reply(200, { ...commonLoginResponse, - }); + }) + .options('/me') + .reply(204) + .get('/me') + .reply(200, { ...commonUserProfileResponse }); render(, { wrapper: BrowserRouter }); @@ -105,7 +128,10 @@ describe('LoginScreen', () => { expect(submitButton).toHaveAttribute('disabled'); await waitFor(() => { - expect(localStorage.setItem).toHaveBeenLastCalledWith('UserToken', JSON.stringify({ ...mockTokenData })); + expect(localStorage.setItem).toHaveBeenLastCalledWith( + 'UserProfile', + JSON.stringify({ auth: mockTokenData, user: mockUserProfileData }) + ); }); await waitFor(() => { @@ -148,7 +174,7 @@ describe('LoginScreen', () => { expect(mockLogin).toBeCalled(); await waitFor(() => { - expect(localStorage.setItem).not.toHaveBeenLastCalledWith('UserToken', JSON.stringify({ ...mockTokenData })); + expect(localStorage.setItem).not.toHaveBeenLastCalledWith('UserProfile', JSON.stringify({ ...mockTokenData })); }); await waitFor(() => { diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 4bbce98..a891d12 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -8,7 +8,7 @@ import AuthAdapter from 'adapters/authAdapter'; import logo from 'assets/images/logo.svg'; import Button from 'components/Button'; import Input from 'components/Input'; -import { setToken } from 'helpers/userToken'; +import { setItem } from 'helpers/localStorage'; import { isEmailValid, isPasswordValid } from 'helpers/validators'; import { PASSWORD_MIN_LENGTH } from '../../constants'; @@ -32,15 +32,17 @@ function LoginScreen() { const performLogin = async () => { try { - const response = await AuthAdapter.loginWithEmailPassword({ email: email, password: password }); + const loginResponse = await AuthAdapter.loginWithEmailPassword({ email: email, password: password }); - const { - attributes: { access_token: accessToken, refresh_token: refreshToken }, - } = await response.data; + const { attributes: authInfo } = await loginResponse.data; - /* eslint-disable camelcase */ - setToken({ access_token: accessToken, refresh_token: refreshToken }); - /* eslint-enable camelcase */ + setItem('UserProfile', { auth: authInfo }); + + const getUserResponse = await AuthAdapter.getUser(); + + const { attributes: userInfo } = await getUserResponse.data; + + setItem('UserProfile', { auth: authInfo, user: userInfo }); navigate('/'); } catch (error) {