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) {