From e7e98dd7acc42ec629ff89859a487e557d2bf78c Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Tue, 24 Aug 2021 01:55:36 +0300 Subject: [PATCH] refactor: error handling while login --- src/components/Auth/AuthForm.tsx | 4 ++-- src/components/Header/User/UserInfo.tsx | 11 +++++++--- src/components/Layout/Layout.tsx | 18 ++++++++++++++- src/components/PrivateRoute/PrivateRoute.tsx | 3 +-- src/pages/auth.tsx | 15 +++++-------- src/pages/main.tsx | 18 ++++----------- src/services/ApiService.ts | 8 +++---- src/services/UserService.ts | 6 ++++- src/stores/AuthStore.ts | 22 ++++++++----------- src/stores/RootStore.ts | 19 +++++++--------- src/stores/UserStore.ts | 23 +++++++++++++++++--- 11 files changed, 84 insertions(+), 63 deletions(-) diff --git a/src/components/Auth/AuthForm.tsx b/src/components/Auth/AuthForm.tsx index 75ce643..b110abb 100644 --- a/src/components/Auth/AuthForm.tsx +++ b/src/components/Auth/AuthForm.tsx @@ -33,10 +33,10 @@ const AuthForm: React.FC = () => { if (error.response) { switch (error.response.status) { case 404: - toast({ title: 'User not found in a system.', status: 'error' }); + toast({ title: 'User not found in a system.', status: 'error', duration: 2000 }); break; case 401: - toast({ title: 'Invalid password.', status: 'error' }); + toast({ title: 'Invalid password.', status: 'error', duration: 2000 }); break; default: handleError(error); diff --git a/src/components/Header/User/UserInfo.tsx b/src/components/Header/User/UserInfo.tsx index fd14f1c..ab3da2f 100644 --- a/src/components/Header/User/UserInfo.tsx +++ b/src/components/Header/User/UserInfo.tsx @@ -13,6 +13,7 @@ import { Text, Link, useToast, + Skeleton, } from '@chakra-ui/react'; import { MdSettings, MdExitToApp } from 'react-icons/md'; import { observer } from 'mobx-react'; @@ -20,7 +21,7 @@ import { useRootStore } from '../../../hooks/useRootStore'; import { useHistory } from 'react-router-dom'; const UserInfo: React.FC = () => { - const { authStore } = useRootStore(); + const { authStore, userStore } = useRootStore(); const history = useHistory(); const toast = useToast(); @@ -34,6 +35,10 @@ const UserInfo: React.FC = () => { } }; + if (userStore.me === null) { + return ; + } + return ( @@ -41,12 +46,12 @@ const UserInfo: React.FC = () => { - Username + {`${userStore.me.first_name} ${userStore.me.last_name}`} - + diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index f19262d..58423e6 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -2,6 +2,9 @@ import React from 'react'; import { Flex, Box } from '@chakra-ui/react'; import Header from '../Header/Header'; import Nav from '../Nav/Nav'; +import { observer } from 'mobx-react'; +import { useRootStore } from '../../hooks/useRootStore'; +import { useErrorHandler } from 'react-error-boundary'; /** * @@ -9,6 +12,19 @@ import Nav from '../Nav/Nav'; * Сomponent for rendering the current page. */ const Layout: React.FC = ({ children }) => { + const { authStore } = useRootStore(); + const handleError = useErrorHandler(); + + React.useEffect(() => { + if (authStore.isLoggedIn) { + authStore.root.userStore.loadCurrentUserData().catch(error => { + if (error.response && error.response.status === 401) { + handleError(error); + } + }); + } + }); + return ( <>
@@ -24,4 +40,4 @@ const Layout: React.FC = ({ children }) => { ); }; -export default Layout; +export default observer(Layout); diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx index 69488bd..17cd8fd 100644 --- a/src/components/PrivateRoute/PrivateRoute.tsx +++ b/src/components/PrivateRoute/PrivateRoute.tsx @@ -12,13 +12,12 @@ import { observer } from 'mobx-react'; */ const PrivateRoute: React.FC = ({ children, component: Component, title, ...options }) => { const { authStore } = useRootStore(); - const isLoggedIn = authStore.isLoggedIn; return ( - isLoggedIn ? ( + authStore.isLoggedIn ? ( {title} • App diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx index 8c92fbf..0e9da67 100644 --- a/src/pages/auth.tsx +++ b/src/pages/auth.tsx @@ -1,26 +1,23 @@ import { observer } from 'mobx-react'; import Helmet from 'react-helmet'; -import React, { useEffect } from 'react'; +import React from 'react'; import { Flex, Box } from '@chakra-ui/react'; import { MdSchool } from 'react-icons/md'; -import { useHistory } from 'react-router-dom'; -import { useRootStore } from '../hooks/useRootStore'; import { IPageProps } from '../interfaces'; import AuthForm from '../components/Auth/AuthForm'; +import { useRootStore } from '../hooks/useRootStore'; +import { Redirect } from 'react-router-dom'; /** * Auth page */ const AuthPage: React.FC = () => { const { authStore } = useRootStore(); - const history = useHistory(); - useEffect(() => { - if (authStore.isLoggedIn) { - history.push('/'); - } - }); + if (authStore.isLoggedIn) { + return ; + } return ( <> diff --git a/src/pages/main.tsx b/src/pages/main.tsx index d3aa93b..006fd78 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,28 +1,18 @@ -import React, { useEffect } from 'react'; +import React from 'react'; import { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react'; import { IPageProps } from '../interfaces'; -import { useRootStore } from '../hooks/useRootStore'; -import { useHistory } from 'react-router-dom'; +import { observer } from 'mobx-react'; /** * Main page */ const MainPage: React.FC = ({ title }) => { - const { authStore } = useRootStore(); - const history = useHistory(); - - useEffect(() => { - if (!authStore.isLoggedIn) { - history.push('/auth'); - } - }); - return ( <> - {title} + Main page @@ -37,4 +27,4 @@ const MainPage: React.FC = ({ title }) => { ); }; -export default MainPage; +export default observer(MainPage); diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts index 0f591a0..f170d1e 100644 --- a/src/services/ApiService.ts +++ b/src/services/ApiService.ts @@ -1,4 +1,5 @@ import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; +import Cookies from 'js-cookie'; import RootStore from '../stores/RootStore'; export default class ApiService { @@ -25,10 +26,9 @@ export default class ApiService { * Unauthorized Error 401 */ if (error.response.status === 401) { - if (this.root.authStore.isLoggedIn) { - this.root.userStore.user = null; - this.root.authStore.setIsLoggedIn(false); - } + this.root.userStore.me = null; + this.root.authStore.setIsLoggedIn(false); + Cookies.remove('logged_in'); } } return Promise.reject(error); diff --git a/src/services/UserService.ts b/src/services/UserService.ts index e49fef7..f78f08c 100644 --- a/src/services/UserService.ts +++ b/src/services/UserService.ts @@ -7,9 +7,13 @@ export default class UserService { this.api = api; } + /** + * Fetch authorized user data + * + * @returns User data + */ public async fetchMe(): Promise { const result = await this.api.get('v1/me'); - return result.data; } } diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts index a111521..7cee833 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -1,23 +1,25 @@ import { makeAutoObservable } from 'mobx'; import Cookies from 'js-cookie'; import RootStore from './RootStore'; +import AuthService from '../services/AuthService'; +import { AxiosInstance } from 'axios'; export default class AuthStore { public root: RootStore; - public token: string | null = null; + public authService: AuthService; public isLoggedIn: boolean = !!Cookies.get('logged_in'); public loading: boolean = false; - constructor(root: RootStore) { + constructor(root: RootStore, api: AxiosInstance) { this.root = root; + this.authService = new AuthService(api); makeAutoObservable(this); } public setIsLoggedIn(value: boolean) { - Cookies.remove('logged_in'); this.isLoggedIn = value; } @@ -25,22 +27,16 @@ export default class AuthStore { this.loading = value; } - public setToken(token: string) { - Cookies.set('logged_in', 'true'); - this.token = token; - } - public async login(login: string, password: string) { try { this.setLoading(true); - const result = await this.root.authService.requestLogin(login, password); + const result = await this.authService.requestLogin(login, password); this.setIsLoggedIn(true); - this.setToken(result.data.token); + Cookies.set('logged_in', 'true'); return result; } catch (error) { - console.error(error); throw error; } finally { this.setLoading(false); @@ -50,12 +46,12 @@ export default class AuthStore { public async logout() { try { this.setLoading(true); - const result = await this.root.authService.requestLogout(); + const result = await this.authService.requestLogout(); this.setIsLoggedIn(false); + Cookies.remove('logged_in'); return result; } catch (error) { - console.error(error); throw error; } finally { this.setLoading(false); diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts index 61c60ab..26c9433 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -2,7 +2,6 @@ import { API_BASE_URL } from '../constants'; import AuthStore from './AuthStore'; import ApiService from '../services/ApiService'; -import AuthService from '../services/AuthService'; import UserStore from './UserStore'; import { makeAutoObservable } from 'mobx'; @@ -13,24 +12,22 @@ export default class RootStore { public apiService: ApiService; - public authService: AuthService; - constructor() { /** - * Stores - */ - this.authStore = new AuthStore(this); - this.userStore = new UserStore(this); - - /** - * Services + * Api service */ this.apiService = new ApiService(this, { baseURL: API_BASE_URL, responseType: 'json', withCredentials: true, }); - this.authService = new AuthService(this.apiService.api); + + /** + * Stores + */ + this.authStore = new AuthStore(this, this.apiService.api); + this.userStore = new UserStore(this, this.apiService.api); + makeAutoObservable(this); } } diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts index 12ff672..6928709 100644 --- a/src/stores/UserStore.ts +++ b/src/stores/UserStore.ts @@ -1,4 +1,6 @@ -import { makeAutoObservable } from 'mobx'; +import { AxiosInstance } from 'axios'; +import { autorun, makeAutoObservable } from 'mobx'; +import UserService from '../services/UserService'; import RootStore from './RootStore'; export interface IUser { @@ -11,10 +13,25 @@ export interface IUser { export default class UserStore { public root: RootStore; - public user: IUser | null = null; + public userService: UserService; - constructor(root: RootStore) { + public me: IUser | null = null; + + public users: IUser[] = []; + + constructor(root: RootStore, api: AxiosInstance) { this.root = root; + this.userService = new UserService(api); makeAutoObservable(this); } + + public async loadCurrentUserData(): Promise { + const result = await this.userService.fetchMe(); + + autorun(() => { + this.me = result.data; + }); + + return result; + } }