diff --git a/package-lock.json b/package-lock.json index db8ec2a..6bebd30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2819,6 +2819,12 @@ } } }, + "@types/history": { + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.9.tgz", + "integrity": "sha512-MUc6zSmU3tEVnkQ78q0peeEjKWPUADMlC/t++2bI8WnAG2tvYRPIgHG8lWkXwqc8MsUF6Z2MOf+Mh5sazOmhiQ==", + "dev": true + }, "@types/html-minifier-terser": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-5.1.2.tgz", @@ -2845,6 +2851,12 @@ "@types/istanbul-lib-report": "*" } }, + "@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "dev": true + }, "@types/json-schema": { "version": "7.0.9", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", @@ -2924,6 +2936,36 @@ } } }, + "@types/react-helmet": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.2.tgz", + "integrity": "sha512-dcfAZNlWb5JYFbO9CGfrPWLJAyFcT6UeR3u35eBbv8liY2Rg4K7fM1G5+HnwVgot+C+kVwXAZ8pLEn2jsMfTDg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, + "@types/react-router": { + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz", + "integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*" + } + }, + "@types/react-router-dom": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz", + "integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==", + "dev": true, + "requires": { + "@types/history": "*", + "@types/react": "*", + "@types/react-router": "*" + } + }, "@types/resolve": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-0.0.8.tgz", @@ -3763,6 +3805,14 @@ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.2.tgz", "integrity": "sha512-5LMaDRWm8ZFPAEdzTYmgjjEdj1YnQcpfrVajO/sn/LhbpGp0Y0H64c2hLZI1gRMxfA+w1S71Uc/nHaOXgcCvGg==" }, + "axios": { + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", + "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "requires": { + "follow-redirects": "^1.10.0" + } + }, "axobject-query": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", @@ -9493,6 +9543,11 @@ } } }, + "js-cookie": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.0.tgz", + "integrity": "sha512-oUbbplKuH07/XX2YD2+Q+GMiPpnVXaRz8npE7suhBH9QEkJe2W7mQ6rwuMXHue3fpfcftQwzgyvGzIHyfCSngQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -10121,6 +10176,24 @@ "minimist": "^1.2.5" } }, + "mobx": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.3.2.tgz", + "integrity": "sha512-xGPM9dIE1qkK9Nrhevp0gzpsmELKU4MFUJRORW/jqxVFIHHWIoQrjDjL8vkwoJYY3C2CeVJqgvl38hgKTalTWg==" + }, + "mobx-react": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/mobx-react/-/mobx-react-7.2.0.tgz", + "integrity": "sha512-KHUjZ3HBmZlNnPd1M82jcdVsQRDlfym38zJhZEs33VxyVQTvL77hODCArq6+C1P1k/6erEeo2R7rpE7ZeOL7dg==", + "requires": { + "mobx-react-lite": "^3.2.0" + } + }, + "mobx-react-lite": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-3.2.0.tgz", + "integrity": "sha512-q5+UHIqYCOpBoFm/PElDuOhbcatvTllgRp3M1s+Hp5j0Z6XNgDbgqxawJ0ZAUEyKM8X1zs70PCuhAIzX1f4Q/g==" + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", diff --git a/package.json b/package.json index 7ee6078..f09465f 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,11 @@ "@types/jest": "^25.0.0", "@types/node": "^12.0.0", "@types/react-dom": "^16.9.0", + "axios": "^0.21.1", "framer-motion": "^4.0.0", + "js-cookie": "^3.0.0", + "mobx": "^6.3.2", + "mobx-react": "^7.2.0", "react": "^17.0.2", "react-dom": "^17.0.2", "react-helmet": "^6.1.0", @@ -47,6 +51,9 @@ ] }, "devDependencies": { - "@types/react": "^16.14.13" + "@types/js-cookie": "^2.2.7", + "@types/react": "^16.14.13", + "@types/react-helmet": "^6.1.2", + "@types/react-router-dom": "^5.1.8" } } diff --git a/src/App.tsx b/src/App.tsx index a896318..f3bee20 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,14 +1,21 @@ -import * as React from "react"; -import { ChakraProvider, Box, Text, Grid, theme } from "@chakra-ui/react"; -import { ColorModeSwitcher } from "./ColorModeSwitcher"; +import * as 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'; + +/** + * Pages + */ +import MainPage from './pages/main'; +import AuthPage from './pages/auth'; export const App = () => ( - - - - Appliction - - + + + + + + ); diff --git a/src/axios/index.ts b/src/axios/index.ts new file mode 100644 index 0000000..e792de1 --- /dev/null +++ b/src/axios/index.ts @@ -0,0 +1,10 @@ +import axios from 'axios'; +import { API_BASE_URL } from '../constants'; + +/** + * Create axios config + */ +export default axios.create({ + baseURL: API_BASE_URL, + responseType: 'json', +}); diff --git a/src/components/Auth/Auth.tsx b/src/components/Auth/Auth.tsx new file mode 100644 index 0000000..924bcf2 --- /dev/null +++ b/src/components/Auth/Auth.tsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { MdAccountCircle, MdVpnKey } from 'react-icons/md'; +import { + Flex, + Box, + FormControl, + FormLabel, + InputGroup, + Input, + InputLeftElement, + Button, + Icon, +} from '@chakra-ui/react'; +import { useRootStore } from '../../hooks/useRootStore'; +import { useHistory } from 'react-router-dom'; +import { observer } from 'mobx-react'; + +type LoginDataInput = { + login: string; + password: string; +}; + +/** + * Authorize user through a form. + */ +const Auth: React.FC = () => { + const [loginData, setLoginData] = useState({ + login: '', + password: '', + }); + const { authStore } = useRootStore(); + const history = useHistory(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const { login, password } = loginData; + await authStore.login(login, password); + history.push('/'); + }; + + return ( + + + + + {/* */} + + + +
+ + Login + + } /> + + setLoginData({ ...loginData, login: e.target.value }) + } + /> + + + + Password + + } /> + + setLoginData({ ...loginData, password: e.target.value }) + } + /> + + + +
+
+
+
+ ); +}; + +export default observer(Auth); diff --git a/src/ColorModeSwitcher.tsx b/src/components/ColorModeSwitcher/ColorModeSwitcher.tsx similarity index 100% rename from src/ColorModeSwitcher.tsx rename to src/components/ColorModeSwitcher/ColorModeSwitcher.tsx diff --git a/src/components/Layout/Layout.tsx b/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..320dd80 --- /dev/null +++ b/src/components/Layout/Layout.tsx @@ -0,0 +1,29 @@ +import React from 'react'; +import { Flex, Box } from '@chakra-ui/react'; + +/** + * + * Layout component + * Сomponent for rendering the current page. + */ +const Layout: React.FC = ({ children }) => { + return ( + + + + {children} + + + + ); +}; + +export default Layout; diff --git a/src/components/PrivateRoute/PrivateRoute.tsx b/src/components/PrivateRoute/PrivateRoute.tsx new file mode 100644 index 0000000..d590aa0 --- /dev/null +++ b/src/components/PrivateRoute/PrivateRoute.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import Helmet from 'react-helmet'; +import Layout from '../Layout/Layout'; +import { Route, Redirect } from 'react-router-dom'; +import { IPrivateRouteProps } from '../../interfaces'; +import { useRootStore } from '../../hooks/useRootStore'; +import { observer } from 'mobx-react'; + +/** + * Checks if the user is loggedIn by reactive variable and if not, + * then a redirect to "/auth" route. + */ +const PrivateRoute: React.FC = ({ + children, + component: Component, + title, + ...options +}) => { + const { authStore } = useRootStore(); + const isLoggedIn = authStore.isLoggedIn; + + return ( + + isLoggedIn ? ( + + + {title} • App + + + + ) : ( + + ) + } + /> + ); +}; + +export default observer(PrivateRoute); diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..120b1eb --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const API_BASE_URL: string = 'http://localhost:3333/api'; diff --git a/src/contexts/index.ts b/src/contexts/index.ts new file mode 100644 index 0000000..403067e --- /dev/null +++ b/src/contexts/index.ts @@ -0,0 +1,6 @@ +import React from 'react'; +import RootStore from '../stores/RootStore'; + +export const StoreContext = React.createContext( + undefined +); diff --git a/src/hooks/useRootStore.ts b/src/hooks/useRootStore.ts new file mode 100644 index 0000000..dc8750c --- /dev/null +++ b/src/hooks/useRootStore.ts @@ -0,0 +1,11 @@ +import React from 'react'; +import { StoreContext } from '../contexts'; + +export const useRootStore = () => { + const context = React.useContext(StoreContext); + if (context === undefined) { + throw new Error('useRootStore must be used within RootStoreProvider'); + } + + return context; +}; diff --git a/src/index.tsx b/src/index.tsx index 586fc62..c795ec6 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,15 +1,18 @@ -import { ColorModeScript } from "@chakra-ui/react"; -import * as React from "react"; -import ReactDOM from "react-dom"; -import { App } from "./App"; -import reportWebVitals from "./reportWebVitals"; +import { ColorModeScript } from '@chakra-ui/react'; +import * as React from 'react'; +import ReactDOM from 'react-dom'; +import { App } from './App'; +import { RootStoreProvider } from './providers'; +import reportWebVitals from './reportWebVitals'; ReactDOM.render( - + <> - - , - document.getElementById("root") + + + + , + document.getElementById('root') ); // If you want to start measuring performance in your app, pass a function diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts new file mode 100644 index 0000000..651050e --- /dev/null +++ b/src/interfaces/index.ts @@ -0,0 +1,17 @@ +import type { RouteProps } from 'react-router-dom'; +import type { RouteComponentProps } from 'react-router-dom'; + +export interface IDataResult { + data: any; + message: string; + status: number; +} + +export interface IPageProps extends RouteComponentProps { + title?: string; +} + +export interface IPrivateRouteProps extends RouteProps { + component: React.FC; + title?: string; +} diff --git a/src/pages/auth.tsx b/src/pages/auth.tsx new file mode 100644 index 0000000..9755732 --- /dev/null +++ b/src/pages/auth.tsx @@ -0,0 +1,30 @@ +import { observer } from 'mobx-react'; +import React from 'react'; +import Helmet from 'react-helmet'; +import { useHistory } from 'react-router-dom'; +import Auth from '../components/Auth/Auth'; +import { useRootStore } from '../hooks/useRootStore'; +import { IPageProps } from '../interfaces'; + +/** + * Auth page + */ +const AuthPage: React.FC = () => { + const { authStore } = useRootStore(); + const history = useHistory(); + + if (authStore.isLoggedIn) { + history.push('/'); + } + + return ( + <> + + Auth + + + + ); +}; + +export default observer(AuthPage); diff --git a/src/pages/main.tsx b/src/pages/main.tsx new file mode 100644 index 0000000..217cb08 --- /dev/null +++ b/src/pages/main.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { + Box, + Heading, + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + Grid, +} from '@chakra-ui/react'; +import { IPageProps } from '../interfaces'; + +/** + * Main page + */ +const MainPage: React.FC = ({ title, history }) => { + return ( + <> + + + {title} + + + + + Main page + + ... + + + + + ); +}; + +export default MainPage; diff --git a/src/providers/index.tsx b/src/providers/index.tsx new file mode 100644 index 0000000..1bbf1ed --- /dev/null +++ b/src/providers/index.tsx @@ -0,0 +1,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}; +}; diff --git a/src/services/AuthService.ts b/src/services/AuthService.ts new file mode 100644 index 0000000..13d01b7 --- /dev/null +++ b/src/services/AuthService.ts @@ -0,0 +1,24 @@ +import api from '../axios/index'; +import { IDataResult } from '../interfaces'; + +interface ILoginResult extends IDataResult { + data: { + token: string; + type: string; + expires_at: string; + }; +} + +export default class AuthService { + public async requestLogin( + login: string, + password: string + ): Promise { + const result = await api.post('v1/auth/login', { + login, + password, + }); + + return result.data; + } +} diff --git a/src/stores/AuthStore.ts b/src/stores/AuthStore.ts new file mode 100644 index 0000000..5a95621 --- /dev/null +++ b/src/stores/AuthStore.ts @@ -0,0 +1,42 @@ +import { makeAutoObservable } from 'mobx'; +import Cookies from 'js-cookie'; +import AuthService from '../services/AuthService'; +import RootStore from './RootStore'; + +export default class AuthStore { + public root: RootStore; + + public token: string | null = null; + + public isLoggedIn: boolean = !!Cookies.get('logged_in'); + + public authService: AuthService; + + constructor(root: RootStore) { + this.root = root; + this.authService = new AuthService(); + makeAutoObservable(this); + } + + get getToken(): string | null { + return this.token; + } + + public setIsLoggedIn(value: boolean) { + this.isLoggedIn = value; + } + + public setToken(token: string) { + Cookies.set('logged_in', 'true'); + this.token = token; + } + + public async login(login: string, password: string) { + const result = await this.authService.requestLogin(login, password); + + this.setIsLoggedIn(true); + this.setToken(result.data.token); + } + + public logout() {} +} diff --git a/src/stores/RootStore.ts b/src/stores/RootStore.ts new file mode 100644 index 0000000..b58e4b4 --- /dev/null +++ b/src/stores/RootStore.ts @@ -0,0 +1,9 @@ +import AuthStore from './AuthStore'; + +export default class RootStore { + public authStore: AuthStore; + + constructor() { + this.authStore = new AuthStore(this); + } +}