Skip to content

Commit

Permalink
[#25] Include user profile details in localstorage with token informa…
Browse files Browse the repository at this point in the history
…tion
  • Loading branch information
liamstevens111 committed Mar 8, 2023
1 parent a1e44aa commit 06139d8
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 96 deletions.
21 changes: 7 additions & 14 deletions src/components/PrivateRoutes/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 <h3>Loading...</h3>;
}

return user ? <Outlet context={user} /> : <Navigate to={LOGIN_URL} />;
Expand Down
43 changes: 43 additions & 0 deletions src/helpers/localStorage.test.ts
Original file line number Diff line number Diff line change
@@ -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 */
13 changes: 13 additions & 0 deletions src/helpers/localStorage.ts
Original file line number Diff line number Diff line change
@@ -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);
};
43 changes: 0 additions & 43 deletions src/helpers/userToken.test.ts

This file was deleted.

11 changes: 0 additions & 11 deletions src/helpers/userToken.ts

This file was deleted.

24 changes: 11 additions & 13 deletions src/lib/requestManager.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
},
Expand All @@ -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;
Expand Down
3 changes: 2 additions & 1 deletion src/screens/Home/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ const HomeScreen = (): JSX.Element => {
<div className="my-8 text-white opacity-50" data-test-id="app-main-heading">
Home Screen
</div>
<div className="my-8 text-white opacity-50">{user && user.email}</div>
{/* TODO: Remove when header implemented in #19 */}
<div className="my-8 text-white opacity-50">{`${user?.name} - ${user?.email} - ${user?.avatarUrl}`}</div>
</>
);
};
Expand Down
38 changes: 32 additions & 6 deletions src/screens/Login/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,39 @@ 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: '[email protected]',
name: 'TestName',
avatar_url: 'https://secure.gravatar.com/avatar/6733d09432e89459dba795de8312ac2d',
};

const commonLoginResponse = {
data: {
id: '18339',
type: 'token',
attributes: {
token_type: 'Bearer',
expires_in: 7200,
created_at: 1677045997,
...mockTokenData,
},
},
};

const commonUserProfileResponse = {
data: {
id: '1',
type: 'user',
attributes: {
email: '[email protected]',
name: 'TestName',
avatar_url: 'https://secure.gravatar.com/avatar/6733d09432e89459dba795de8312ac2d',
},
},
};

const testCredentials = {
email: '[email protected]',
password: 'password123',
Expand Down Expand Up @@ -80,14 +98,19 @@ describe('LoginScreen', () => {
.defaultReplyHeaders({
'access-control-allow-origin': '*',
'access-control-allow-credentials': 'true',
'access-control-allow-headers': 'Authorization',
})
.post('/oauth/token', {
...commonLoginParams,
...testCredentials,
})
.reply(200, {
...commonLoginResponse,
});
})
.options('/me')
.reply(204)
.get('/me')
.reply(200, { ...commonUserProfileResponse });

render(<LoginScreen />, { wrapper: BrowserRouter });

Expand All @@ -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(() => {
Expand Down Expand Up @@ -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(() => {
Expand Down
18 changes: 10 additions & 8 deletions src/screens/Login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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) {
Expand Down

0 comments on commit 06139d8

Please sign in to comment.