From 8ef0d4f8f2ca652dc51d1d1cc61788b169ede307 Mon Sep 17 00:00:00 2001 From: Sergey Yarkov Date: Sun, 22 Aug 2021 23:53:42 +0300 Subject: [PATCH] feat: setup auth --- src/App.tsx | 34 ++++++++-- src/components/Auth/AuthForm.tsx | 51 ++++++-------- src/components/Header/AppName.tsx | 21 ++++++ src/components/Header/Header.tsx | 37 ++++++++++ src/components/Header/User/UserInfo.tsx | 67 +++++++++++++++++++ .../Header/User/UserNotifications.tsx | 31 +++++++++ src/components/Layout/Layout.tsx | 32 +++++---- src/index.tsx | 8 ++- src/pages/404.tsx | 26 +++++++ src/pages/auth.tsx | 26 +++---- src/pages/main.tsx | 13 +++- src/providers/index.tsx | 9 +-- src/services/ApiService.ts | 30 ++++++--- src/services/AuthService.ts | 30 +++++++-- src/services/UserService.ts | 15 +++++ src/stores/AuthStore.ts | 37 ++++++++-- src/stores/RootStore.ts | 15 ++++- src/stores/UserStore.ts | 20 ++++++ 18 files changed, 412 insertions(+), 90 deletions(-) create mode 100644 src/components/Header/AppName.tsx create mode 100644 src/components/Header/Header.tsx create mode 100644 src/components/Header/User/UserInfo.tsx create mode 100644 src/components/Header/User/UserNotifications.tsx create mode 100644 src/pages/404.tsx create mode 100644 src/services/UserService.ts create mode 100644 src/stores/UserStore.ts diff --git a/src/App.tsx b/src/App.tsx index f3bee20..e6c102f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,21 +1,43 @@ -import * as React from 'react'; +import React from 'react'; import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; import PrivateRoute from './components/PrivateRoute/PrivateRoute'; -import { ChakraProvider, theme } from '@chakra-ui/react'; +import { useRootStore } from './hooks/useRootStore'; +import { observer } from 'mobx-react'; /** * Pages */ import MainPage from './pages/main'; import AuthPage from './pages/auth'; +import NotFoundPage from './pages/404'; +import { useToast } from '@chakra-ui/react'; +import { autorun } from 'mobx'; -export const App = () => ( - +const App = () => { + const rootStore = useRootStore(); + const toast = useToast(); + + React.useEffect(() => { + autorun(() => { + if (rootStore.networkApiError !== undefined) { + toast({ + title: `Network error while connecting to the server.`, + description: 'Try your request later.', + status: 'error', + }); + } + }); + }); + + return ( + - -); + ); +}; + +export default observer(App); diff --git a/src/components/Auth/AuthForm.tsx b/src/components/Auth/AuthForm.tsx index 629928f..df965ce 100644 --- a/src/components/Auth/AuthForm.tsx +++ b/src/components/Auth/AuthForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React from 'react'; import { MdAccountCircle, MdVpnKey } from 'react-icons/md'; import { FormControl, @@ -12,44 +12,39 @@ import { import { useRootStore } from '../../hooks/useRootStore'; import { useHistory } from 'react-router-dom'; import { observer } from 'mobx-react'; +import { SubmitHandler, useForm } from 'react-hook-form'; -type AuthFormState = { +type AuthInputs = { login: string; password: string; }; const AuthForm: React.FC = () => { - const [authFormState, setAuthFormState] = useState({ - login: '', - password: '', - }); + const { register, reset, handleSubmit } = useForm(); const { authStore } = useRootStore(); const history = useHistory(); - const handleLoginForm = async (e: React.FormEvent) => { - e.preventDefault(); - const { login, password } = authFormState; - await authStore.login(login, password); - history.push('/'); + const onSubmit: SubmitHandler = async (data) => { + try { + await authStore.login(data.login, data.password); + history.push('/'); + } catch (error) { + console.log('error while log in '); + } finally { + reset(); + } }; return ( -
+ Login } /> - setAuthFormState({ - ...authFormState, - login: e.target.value, - }) - } + {...register('login', { required: true })} /> @@ -59,20 +54,14 @@ const AuthForm: React.FC = () => { } /> - setAuthFormState({ - ...authFormState, - password: e.target.value, - }) - } + {...register('password', { required: true })} /> - ) -} + ); +}; -export default observer(AuthForm) \ No newline at end of file +export default observer(AuthForm); diff --git a/src/components/Header/AppName.tsx b/src/components/Header/AppName.tsx new file mode 100644 index 0000000..1389dbd --- /dev/null +++ b/src/components/Header/AppName.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { Flex, Box, Text, Heading } from '@chakra-ui/react'; +import { MdSchool } from 'react-icons/md'; + +const AppName: React.FC = () => { + return ( + + + + + + + App name + + App description + + + ); +}; + +export default AppName; diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx new file mode 100644 index 0000000..bb6e3fe --- /dev/null +++ b/src/components/Header/Header.tsx @@ -0,0 +1,37 @@ +import React from 'react'; +import { Flex, Box } from '@chakra-ui/react'; +import AppName from './AppName'; +import UserInfo from './User/UserInfo'; +import UserNotifications from './User/UserNotifications'; + +const Header: React.FC = () => { + return ( + + + + + + + + + + ); +}; + +export default Header; diff --git a/src/components/Header/User/UserInfo.tsx b/src/components/Header/User/UserInfo.tsx new file mode 100644 index 0000000..c99f886 --- /dev/null +++ b/src/components/Header/User/UserInfo.tsx @@ -0,0 +1,67 @@ +import React from 'react'; +import { + Menu, + MenuGroup, + MenuList, + MenuDivider, + MenuButton, + MenuItem, + Button, + Flex, + Box, + Avatar, + Text, + Link, +} from '@chakra-ui/react'; +import { MdSettings, MdExitToApp } from 'react-icons/md'; +import { observer } from 'mobx-react'; +import { useRootStore } from '../../../hooks/useRootStore'; +import { useHistory } from 'react-router-dom'; + +const UserInfo: React.FC = () => { + const { authStore } = useRootStore(); + const history = useHistory(); + + const onLogoutHandler = async ( + e: React.MouseEvent + ) => { + try { + await authStore.logout(); + history.push('/auth'); + } catch (error) { + console.error(error); + } + }; + + return ( + + + + + + + Username + + + + + + + + + + My profile + + + + + Logout + + + + + + ); +}; + +export default observer(UserInfo); diff --git a/src/components/Header/User/UserNotifications.tsx b/src/components/Header/User/UserNotifications.tsx new file mode 100644 index 0000000..a547239 --- /dev/null +++ b/src/components/Header/User/UserNotifications.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import { Box } from '@chakra-ui/layout'; +import { Button } from '@chakra-ui/button'; +import { MdNotifications } from 'react-icons/md'; + +const UserNotifications: React.FC = () => { + return ( + + ); +}; + +export default UserNotifications; diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx index 320dd80..d4040d0 100644 --- a/src/components/Layout/Layout.tsx +++ b/src/components/Layout/Layout.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { Flex, Box } from '@chakra-ui/react'; +import Header from '../Header/Header'; /** * @@ -8,21 +9,24 @@ import { Flex, Box } from '@chakra-ui/react'; */ const Layout: React.FC = ({ children }) => { return ( - - - - {children} + <> +
+ + + + {children} + - - + + ); }; diff --git a/src/index.tsx b/src/index.tsx index c795ec6..41d959e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,7 +1,7 @@ -import { ColorModeScript } from '@chakra-ui/react'; +import { ChakraProvider, ColorModeScript, theme } from '@chakra-ui/react'; import * as React from 'react'; import ReactDOM from 'react-dom'; -import { App } from './App'; +import App from './App'; import { RootStoreProvider } from './providers'; import reportWebVitals from './reportWebVitals'; @@ -9,7 +9,9 @@ ReactDOM.render( <> - + + + , document.getElementById('root') diff --git a/src/pages/404.tsx b/src/pages/404.tsx new file mode 100644 index 0000000..a7f359f --- /dev/null +++ b/src/pages/404.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import { Link } from 'react-router-dom'; +import { Box, Heading, Button } from '@chakra-ui/react'; + +const NotFoundPage: React.FC = () => { + return ( + <> + + Error 404 + + + + Error 404 + + + + + + + ); +}; + +export default NotFoundPage; diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx index 9871bd1..26f6a25 100644 --- a/src/pages/auth.tsx +++ b/src/pages/auth.tsx @@ -1,11 +1,8 @@ import { observer } from 'mobx-react'; -import React from 'react'; -import { - Flex, - Box, -} from '@chakra-ui/react'; -import { MdSchool } from "react-icons/md"; import Helmet from 'react-helmet'; +import React, { useEffect } 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'; @@ -19,19 +16,24 @@ const AuthPage: React.FC = () => { const { authStore } = useRootStore(); const history = useHistory(); - if (authStore.isLoggedIn) { - history.push('/'); - } + useEffect(() => { + if (authStore.isLoggedIn) { + history.push('/'); + } + }); return ( <> Auth - + = () => { - + diff --git a/src/pages/main.tsx b/src/pages/main.tsx index 272b8ae..4a0bbce 100644 --- a/src/pages/main.tsx +++ b/src/pages/main.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect } from 'react'; import { Box, Heading, @@ -9,11 +9,22 @@ import { } from '@chakra-ui/react'; import { IPageProps } from '../interfaces'; +import { useRootStore } from '../hooks/useRootStore'; +import { useHistory } from 'react-router-dom'; /** * Main page */ const MainPage: React.FC = ({ title }) => { + const { authStore } = useRootStore(); + const history = useHistory(); + + useEffect(() => { + if (!authStore.isLoggedIn) { + history.push('/auth'); + } + }); + return ( <> diff --git a/src/providers/index.tsx b/src/providers/index.tsx index 1bbf1ed..fa5ef56 100644 --- a/src/providers/index.tsx +++ b/src/providers/index.tsx @@ -2,12 +2,13 @@ import React, { ReactNode } from 'react'; import { StoreContext } from '../contexts'; import RootStore from '../stores/RootStore'; -let store: RootStore; - /** * Root store provider */ export const RootStoreProvider = ({ children }: { children: ReactNode }) => { - const root = store ?? new RootStore(); - return {children}; + const store = new RootStore(); + + return ( + {children} + ); }; diff --git a/src/services/ApiService.ts b/src/services/ApiService.ts index aee8973..001de3f 100644 --- a/src/services/ApiService.ts +++ b/src/services/ApiService.ts @@ -20,14 +20,28 @@ export default class ApiService { * Response */ this.api.interceptors.response.use(undefined, (error) => { - /** - * Unauthorized - */ - if ( - error.response.status === 401 || - error.response.data.message === '401 Unauthorized' - ) { - this.root.authStore.setIsLoggedIn(false); + if (error.response) { + /** + * Internal Server Error 500 + */ + if (error.response.status === 500) { + } + + /** + * Unauthorized Error 401 + */ + if (error.response.status === 401) { + if (this.root.authStore.isLoggedIn) { + this.root.authStore.setIsLoggedIn(false); + } + } + + /* Clear error state from root */ + if (this.root.networkApiError !== undefined) { + this.root.setNetworkApiError(undefined); + } + } else { + this.root.setNetworkApiError(error); } return Promise.reject(error); }); diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts index add932c..6fb6aa5 100644 --- a/src/services/AuthService.ts +++ b/src/services/AuthService.ts @@ -1,7 +1,7 @@ -import ApiService from './ApiService'; import { IDataResult } from '../interfaces'; +import { AxiosInstance } from 'axios'; -interface ILoginResult extends IDataResult { +export interface ILoginResult extends IDataResult { data: { token: string; type: string; @@ -10,21 +10,39 @@ interface ILoginResult extends IDataResult { } export default class AuthService { - public apiService: ApiService; + public api: AxiosInstance; - constructor(apiService: ApiService) { - this.apiService = apiService; + constructor(api: AxiosInstance) { + this.api = api; } + /** + * Create a new token + * + * @param login User login + * @param password User password + * @returns Login result + */ public async requestLogin( login: string, password: string ): Promise { - const result = await this.apiService.api.post('v1/auth/login', { + const result = await this.api.post('v1/auth/login', { login, password, }); return result.data; } + + /** + * Revoke token + * + * @returns Logout result + */ + public async requestLogout(): Promise { + const result = await this.api.post('v1/auth/logout'); + + return result.data; + } } diff --git a/src/services/UserService.ts b/src/services/UserService.ts new file mode 100644 index 0000000..e49fef7 --- /dev/null +++ b/src/services/UserService.ts @@ -0,0 +1,15 @@ +import { AxiosInstance } from 'axios'; + +export default class UserService { + public api: AxiosInstance; + + constructor(api: AxiosInstance) { + this.api = api; + } + + 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 0ffb94b..a111521 100644 --- a/src/stores/AuthStore.ts +++ b/src/stores/AuthStore.ts @@ -9,6 +9,8 @@ export default class AuthStore { public isLoggedIn: boolean = !!Cookies.get('logged_in'); + public loading: boolean = false; + constructor(root: RootStore) { this.root = root; makeAutoObservable(this); @@ -19,17 +21,44 @@ export default class AuthStore { this.isLoggedIn = value; } + public setLoading(value: boolean) { + this.loading = value; + } + public setToken(token: string) { Cookies.set('logged_in', 'true'); this.token = token; } public async login(login: string, password: string) { - const result = await this.root.authService.requestLogin(login, password); + try { + this.setLoading(true); + const result = await this.root.authService.requestLogin(login, password); - this.setIsLoggedIn(true); - this.setToken(result.data.token); + this.setIsLoggedIn(true); + this.setToken(result.data.token); + + return result; + } catch (error) { + console.error(error); + throw error; + } finally { + this.setLoading(false); + } } - public logout() {} + public async logout() { + try { + this.setLoading(true); + const result = await this.root.authService.requestLogout(); + + this.setIsLoggedIn(false); + 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 3b21813..d0b0d73 100644 --- a/src/stores/RootStore.ts +++ b/src/stores/RootStore.ts @@ -3,10 +3,16 @@ import AuthStore from './AuthStore'; import ApiService from '../services/ApiService'; import AuthService from '../services/AuthService'; +import UserStore from './UserStore'; +import { makeAutoObservable } from 'mobx'; export default class RootStore { + public networkApiError: any; + public authStore: AuthStore; + public userStore: UserStore; + public apiService: ApiService; public authService: AuthService; @@ -16,6 +22,7 @@ export default class RootStore { * Stores */ this.authStore = new AuthStore(this); + this.userStore = new UserStore(this); /** * Services @@ -23,7 +30,13 @@ export default class RootStore { this.apiService = new ApiService(this, { baseURL: API_BASE_URL, responseType: 'json', + withCredentials: true, }); - this.authService = new AuthService(this.apiService); + this.authService = new AuthService(this.apiService.api); + makeAutoObservable(this); + } + + public setNetworkApiError(error: any) { + this.networkApiError = error; } } diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts new file mode 100644 index 0000000..12ff672 --- /dev/null +++ b/src/stores/UserStore.ts @@ -0,0 +1,20 @@ +import { makeAutoObservable } from 'mobx'; +import RootStore from './RootStore'; + +export interface IUser { + id: string; + first_name: string; + last_name: string; + email: string; +} + +export default class UserStore { + public root: RootStore; + + public user: IUser | null = null; + + constructor(root: RootStore) { + this.root = root; + makeAutoObservable(this); + } +}