refactor: error handling while login

This commit is contained in:
Sergey Yarkov 2021-08-24 01:55:36 +03:00
parent 2245c4aa85
commit e7e98dd7ac
11 changed files with 84 additions and 63 deletions

View File

@ -33,10 +33,10 @@ const AuthForm: React.FC = () => {
if (error.response) { if (error.response) {
switch (error.response.status) { switch (error.response.status) {
case 404: 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; break;
case 401: case 401:
toast({ title: 'Invalid password.', status: 'error' }); toast({ title: 'Invalid password.', status: 'error', duration: 2000 });
break; break;
default: default:
handleError(error); handleError(error);

View File

@ -13,6 +13,7 @@ import {
Text, Text,
Link, Link,
useToast, useToast,
Skeleton,
} from '@chakra-ui/react'; } from '@chakra-ui/react';
import { MdSettings, MdExitToApp } from 'react-icons/md'; import { MdSettings, MdExitToApp } from 'react-icons/md';
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
@ -20,7 +21,7 @@ import { useRootStore } from '../../../hooks/useRootStore';
import { useHistory } from 'react-router-dom'; import { useHistory } from 'react-router-dom';
const UserInfo: React.FC = () => { const UserInfo: React.FC = () => {
const { authStore } = useRootStore(); const { authStore, userStore } = useRootStore();
const history = useHistory(); const history = useHistory();
const toast = useToast(); const toast = useToast();
@ -34,6 +35,10 @@ const UserInfo: React.FC = () => {
} }
}; };
if (userStore.me === null) {
return <Skeleton width='160px' borderRadius='md' />;
}
return ( return (
<Flex alignItems='center'> <Flex alignItems='center'>
<Menu> <Menu>
@ -41,12 +46,12 @@ const UserInfo: React.FC = () => {
<Flex alignItems='center'> <Flex alignItems='center'>
<Avatar size='sm' name='user name' marginRight={3} /> <Avatar size='sm' name='user name' marginRight={3} />
<Text as='span' mr={2}> <Text as='span' mr={2}>
Username {`${userStore.me.first_name} ${userStore.me.last_name}`}
</Text> </Text>
</Flex> </Flex>
</MenuButton> </MenuButton>
<MenuList mr='1rem'> <MenuList mr='1rem'>
<MenuGroup title='User fullname'> <MenuGroup title={`${userStore.me.first_name} ${userStore.me.last_name}`}>
<MenuDivider /> <MenuDivider />
<Link to='/profile'> <Link to='/profile'>
<MenuItem> <MenuItem>

View File

@ -2,6 +2,9 @@ import React from 'react';
import { Flex, Box } from '@chakra-ui/react'; import { Flex, Box } from '@chakra-ui/react';
import Header from '../Header/Header'; import Header from '../Header/Header';
import Nav from '../Nav/Nav'; 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. * Сomponent for rendering the current page.
*/ */
const Layout: React.FC = ({ children }) => { 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 ( return (
<> <>
<Header /> <Header />
@ -24,4 +40,4 @@ const Layout: React.FC = ({ children }) => {
); );
}; };
export default Layout; export default observer(Layout);

View File

@ -12,13 +12,12 @@ import { observer } from 'mobx-react';
*/ */
const PrivateRoute: React.FC<IPrivateRouteProps> = ({ children, component: Component, title, ...options }) => { const PrivateRoute: React.FC<IPrivateRouteProps> = ({ children, component: Component, title, ...options }) => {
const { authStore } = useRootStore(); const { authStore } = useRootStore();
const isLoggedIn = authStore.isLoggedIn;
return ( return (
<Route <Route
{...options} {...options}
render={props => render={props =>
isLoggedIn ? ( authStore.isLoggedIn ? (
<Layout> <Layout>
<Helmet> <Helmet>
<title>{title} • App </title> <title>{title} • App </title>

View File

@ -1,26 +1,23 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import Helmet from 'react-helmet'; import Helmet from 'react-helmet';
import React, { useEffect } from 'react'; import React from 'react';
import { Flex, Box } from '@chakra-ui/react'; import { Flex, Box } from '@chakra-ui/react';
import { MdSchool } from 'react-icons/md'; import { MdSchool } from 'react-icons/md';
import { useHistory } from 'react-router-dom';
import { useRootStore } from '../hooks/useRootStore';
import { IPageProps } from '../interfaces'; import { IPageProps } from '../interfaces';
import AuthForm from '../components/Auth/AuthForm'; import AuthForm from '../components/Auth/AuthForm';
import { useRootStore } from '../hooks/useRootStore';
import { Redirect } from 'react-router-dom';
/** /**
* Auth page * Auth page
*/ */
const AuthPage: React.FC<IPageProps> = () => { const AuthPage: React.FC<IPageProps> = () => {
const { authStore } = useRootStore(); const { authStore } = useRootStore();
const history = useHistory();
useEffect(() => {
if (authStore.isLoggedIn) { if (authStore.isLoggedIn) {
history.push('/'); return <Redirect to='/' />;
} }
});
return ( return (
<> <>

View File

@ -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 { Box, Heading, Breadcrumb, BreadcrumbItem, BreadcrumbLink, Grid } from '@chakra-ui/react';
import { IPageProps } from '../interfaces'; import { IPageProps } from '../interfaces';
import { useRootStore } from '../hooks/useRootStore'; import { observer } from 'mobx-react';
import { useHistory } from 'react-router-dom';
/** /**
* Main page * Main page
*/ */
const MainPage: React.FC<IPageProps> = ({ title }) => { const MainPage: React.FC<IPageProps> = ({ title }) => {
const { authStore } = useRootStore();
const history = useHistory();
useEffect(() => {
if (!authStore.isLoggedIn) {
history.push('/auth');
}
});
return ( return (
<> <>
<Breadcrumb fontWeight='medium' fontSize='sm'> <Breadcrumb fontWeight='medium' fontSize='sm'>
<BreadcrumbItem isCurrentPage> <BreadcrumbItem isCurrentPage>
<BreadcrumbLink href='/'>{title}</BreadcrumbLink> <BreadcrumbLink href='/'>Main page</BreadcrumbLink>
</BreadcrumbItem> </BreadcrumbItem>
</Breadcrumb> </Breadcrumb>
<Box mt='10'> <Box mt='10'>
@ -37,4 +27,4 @@ const MainPage: React.FC<IPageProps> = ({ title }) => {
); );
}; };
export default MainPage; export default observer(MainPage);

View File

@ -1,4 +1,5 @@
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import Cookies from 'js-cookie';
import RootStore from '../stores/RootStore'; import RootStore from '../stores/RootStore';
export default class ApiService { export default class ApiService {
@ -25,10 +26,9 @@ export default class ApiService {
* Unauthorized Error 401 * Unauthorized Error 401
*/ */
if (error.response.status === 401) { if (error.response.status === 401) {
if (this.root.authStore.isLoggedIn) { this.root.userStore.me = null;
this.root.userStore.user = null;
this.root.authStore.setIsLoggedIn(false); this.root.authStore.setIsLoggedIn(false);
} Cookies.remove('logged_in');
} }
} }
return Promise.reject(error); return Promise.reject(error);

View File

@ -7,9 +7,13 @@ export default class UserService {
this.api = api; this.api = api;
} }
/**
* Fetch authorized user data
*
* @returns User data
*/
public async fetchMe(): Promise<any> { public async fetchMe(): Promise<any> {
const result = await this.api.get('v1/me'); const result = await this.api.get('v1/me');
return result.data; return result.data;
} }
} }

View File

@ -1,23 +1,25 @@
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import RootStore from './RootStore'; import RootStore from './RootStore';
import AuthService from '../services/AuthService';
import { AxiosInstance } from 'axios';
export default class AuthStore { export default class AuthStore {
public root: RootStore; public root: RootStore;
public token: string | null = null; public authService: AuthService;
public isLoggedIn: boolean = !!Cookies.get('logged_in'); public isLoggedIn: boolean = !!Cookies.get('logged_in');
public loading: boolean = false; public loading: boolean = false;
constructor(root: RootStore) { constructor(root: RootStore, api: AxiosInstance) {
this.root = root; this.root = root;
this.authService = new AuthService(api);
makeAutoObservable(this); makeAutoObservable(this);
} }
public setIsLoggedIn(value: boolean) { public setIsLoggedIn(value: boolean) {
Cookies.remove('logged_in');
this.isLoggedIn = value; this.isLoggedIn = value;
} }
@ -25,22 +27,16 @@ export default class AuthStore {
this.loading = value; this.loading = value;
} }
public setToken(token: string) {
Cookies.set('logged_in', 'true');
this.token = token;
}
public async login(login: string, password: string) { public async login(login: string, password: string) {
try { try {
this.setLoading(true); this.setLoading(true);
const result = await this.root.authService.requestLogin(login, password); const result = await this.authService.requestLogin(login, password);
this.setIsLoggedIn(true); this.setIsLoggedIn(true);
this.setToken(result.data.token); Cookies.set('logged_in', 'true');
return result; return result;
} catch (error) { } catch (error) {
console.error(error);
throw error; throw error;
} finally { } finally {
this.setLoading(false); this.setLoading(false);
@ -50,12 +46,12 @@ export default class AuthStore {
public async logout() { public async logout() {
try { try {
this.setLoading(true); this.setLoading(true);
const result = await this.root.authService.requestLogout(); const result = await this.authService.requestLogout();
this.setIsLoggedIn(false); this.setIsLoggedIn(false);
Cookies.remove('logged_in');
return result; return result;
} catch (error) { } catch (error) {
console.error(error);
throw error; throw error;
} finally { } finally {
this.setLoading(false); this.setLoading(false);

View File

@ -2,7 +2,6 @@ import { API_BASE_URL } from '../constants';
import AuthStore from './AuthStore'; import AuthStore from './AuthStore';
import ApiService from '../services/ApiService'; import ApiService from '../services/ApiService';
import AuthService from '../services/AuthService';
import UserStore from './UserStore'; import UserStore from './UserStore';
import { makeAutoObservable } from 'mobx'; import { makeAutoObservable } from 'mobx';
@ -13,24 +12,22 @@ export default class RootStore {
public apiService: ApiService; public apiService: ApiService;
public authService: AuthService;
constructor() { constructor() {
/** /**
* Stores * Api service
*/
this.authStore = new AuthStore(this);
this.userStore = new UserStore(this);
/**
* Services
*/ */
this.apiService = new ApiService(this, { this.apiService = new ApiService(this, {
baseURL: API_BASE_URL, baseURL: API_BASE_URL,
responseType: 'json', responseType: 'json',
withCredentials: true, 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); makeAutoObservable(this);
} }
} }

View File

@ -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'; import RootStore from './RootStore';
export interface IUser { export interface IUser {
@ -11,10 +13,25 @@ export interface IUser {
export default class UserStore { export default class UserStore {
public root: RootStore; 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.root = root;
this.userService = new UserService(api);
makeAutoObservable(this); makeAutoObservable(this);
} }
public async loadCurrentUserData(): Promise<any> {
const result = await this.userService.fetchMe();
autorun(() => {
this.me = result.data;
});
return result;
}
} }