Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
29.12.2021
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Запросы на сервер
Библиотека axios позволяет перехватывать все запросы и ответы и применять к ним определенную логику. Нам нужны два экземпляра axios — для запросов на сервер от любого посетителя сайта и для запросов от авторизованного пользователя. Создаем директорию src/http
и внутри нее — файл index.js
.
import axios from 'axios' const guestInstance = axios.create({ baseURL: process.env.REACT_APP_API_URL }) const authInstance = axios.create({ baseURL: process.env.REACT_APP_API_URL }) // добавляем в запрос данные для авторизации с помощью перехватчика (interceptor) const authInterceptor = (config) => { const token = localStorage.getItem('token') if (token) { config.headers.authorization = 'Bearer ' + localStorage.getItem('token') } return config } authInstance.interceptors.request.use(authInterceptor) export { guestInstance, authInstance }
Мы здесь используем переменную среды REACT_APP_API_URL
, которую будем хранить в client/.env
. Обратите внимание, что все переменные среды в react-приложении должны начинаться на REACT_APP_
.
REACT_APP_API_URL=http://localhost:7000/api/
Теперь создаем внутри src/http
файл userAPI.js
:
import { guestInstance, authInstance } from './index.js' import jwtDecode from 'jwt-decode' export const signup = async (email, password) => { try { const response = await guestInstance.post('user/signup', {email, password, role: 'USER'}) const token = response.data.token const user = jwtDecode(token) localStorage.setItem('token', token) return user } catch (e) { alert(e.response.data.message) return false } } export const login = async (email, password) => { try { const response = await guestInstance.post('user/login', {email, password}) const token = response.data.token const user = jwtDecode(token) localStorage.setItem('token', token) return user } catch (e) { alert(e.response.data.message) return false } } export const logout = () => { localStorage.removeItem('token') } export const check = async () => { let userToken, userData try { let userToken = localStorage.getItem('token') // если в хранилище нет действительного токена if (!userToken) { return false } // токен есть, надо проверить его подлинность const response = await authInstance.get('user/check') userToken = response.data.token userData = jwtDecode(userToken) localStorage.setItem('token', userToken) return userData } catch(e) { localStorage.removeItem('token') return false } }
Мы здесь используем jwt-decode
, так что сразу устанавливаем:
> npm install jwt-decode
Состояние приложения
Для управления состоянием используем MobX (см. здесь), для этого создадим файл src/store/UserStore.js
.
import { makeAutoObservable } from 'mobx' class UserStore { id = null email = null isAuth = false isAdmin = false constructor() { makeAutoObservable(this) } login({id, email, role}) { this.id = id this.email = email this.isAuth = true this.isAdmin = role === 'ADMIN' } logout() { this.id = null this.email = null this.isAuth = false this.isAdmin = false } } export default UserStore
И будем в AppContext
использовать класс UserStore
:
import React from 'react' import UserStore from '../store/UserStore.js' const AppContext = React.createContext() // контекст, который будем передавать const context = { user: new UserStore(), products: [ /* .......... */ ], categories: [ /* .......... */ ], brands: [ /* .......... */ ], basket: [ /* .......... */ ], } const AppContextProvider = (props) => { return ( <AppContext.Provider value={context}> {props.children} </AppContext.Provider> ); } export {AppContext, AppContextProvider}
Компоненты Signup и Login
Компоненты обернем в функцию observer
из mobx-react-lite
, которая будет следить за observable-значениями, которые используют эти компоненты — и в случае необходимости вызывать новый рендер.
import { AppContext } from '../components/AppContext.js' import { useContext, useEffect } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Container, Row, Card, Form, Button } from 'react-bootstrap' import { signup } from '../http/userAPI.js' import { observer } from 'mobx-react-lite' const Signup = observer(() => { const { user } = useContext(AppContext) const navigate = useNavigate() // если пользователь авторизован — ему здесь делать нечего useEffect(() => { if (user.isAdmin) navigate('/admin', {replace: true}) if (user.isAuth) navigate('/user', {replace: true}) }, []) const handleSubmit = async (event) => { event.preventDefault() const email = event.target.email.value.trim() const password = event.target.password.value.trim() const data = await signup(email, password) if (data) { user.login(data) if (user.isAdmin) navigate('/admin') if (user.isAuth) navigate('/user') } } return ( <Container className="d-flex justify-content-center"> <Card style={{width: '50%'}} className="p-2 mt-5 bg-light"> <h3 className="m-auto">Регистрация</h3> <Form className="d-flex flex-column" onSubmit={handleSubmit}> <Form.Control name="email" className="mt-3" placeholder="Введите ваш email..." /> <Form.Control name="password" className="mt-3" placeholder="Введите ваш пароль..." /> <Row className="d-flex justify-content-between mt-3 pl-3 pr-3"> <Button type="submit"> Регистрация </Button> <p> Уже есть аккаунт? <Link to="/login">Войдите!</Link> </p> </Row> </Form> </Card> </Container> ) }) export default Signup
import { AppContext } from '../components/AppContext.js' import { useContext, useEffect } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Container, Row, Card, Form, Button } from 'react-bootstrap' import { login } from '../http/userAPI.js' import { observer } from 'mobx-react-lite' const Login = observer(() => { const { user } = useContext(AppContext) const navigate = useNavigate() // если пользователь авторизован — ему здесь делать нечего useEffect(() => { if (user.isAdmin) navigate('/admin', {replace: true}) if (user.isAuth) navigate('/user', {replace: true}) }, []) const handleSubmit = async (event) => { event.preventDefault() const email = event.target.email.value.trim() const password = event.target.password.value.trim() const data = await login(email, password) if (data) { user.login(data) if (user.isAdmin) navigate('/admin') if (user.isAuth) navigate('/user') } } return ( <Container className="d-flex justify-content-center"> <Card style={{width: '50%'}} className="p-2 mt-5 bg-light"> <h3 className="m-auto">Авторизация</h3> <Form className="d-flex flex-column" onSubmit={handleSubmit}> <Form.Control name="email" className="mt-3" placeholder="Введите ваш email..." /> <Form.Control name="password" className="mt-3" placeholder="Введите ваш пароль..." /> <Row className="d-flex justify-content-between mt-3 pl-3 pr-3"> <Button type="submit"> Войти </Button> <p> Нет аккаунта? <Link to="/signup">Зарегистрирутесь!</Link> </p> </Row> </Form> </Card> </Container> ) }) export default Login
Автоматическая авторизация
Если пользователь авторизовался, то в главном меню будут ссылки «Личный кабинет» и «Панель управления» (для администратора). Если теперь перезагрузить страницу, то в главном меню будут ссылки «Войти» и «Регистрация». Потому что страница загружается с сервера и у приложения еще нет состояния — вызов new UserStore()
в AppContext
присвоит всем свойствам класса значения null
и false
.
Такое положение вещей нас не очень устраивает, поэтому создадим HOC-компонент components/CheckAuth.js
, который авторизует пользователя — при наличии в хранилище правильного токена.
import { AppContext } from './AppContext.js' import { check } from '../http/userAPI.js' import { useContext, useEffect, useState } from 'react' import { Spinner } from 'react-bootstrap' const CheckAuth = (props) => { const { user } = useContext(AppContext) const [checking, setChecking] = useState(true) useEffect(() => { check() .then(data => { if (data) { user.login(data) } }) .finally( () => setChecking(false) ) }, []) if (checking) { return <Spinner animation="border" variant="light" /> } return props.children } export default CheckAuth
В компоненте NavBar
используем этот HOC, чтобы показывать loader до того момента, как пользователь будет авторизован (или не будет, если токена нет или его время действия истекло).
import { Container, Navbar, Nav } from 'react-bootstrap' import { NavLink } from 'react-router-dom' import { LinkContainer } from 'react-router-bootstrap' import { AppContext } from './AppContext.js' import { useContext } from 'react' import { observer } from 'mobx-react-lite' import CheckAuth from './CheckAuth.js' const NavBar = observer(() => { const { user } = useContext(AppContext) return ( <Navbar bg="dark" variant="dark"> <Container> <NavLink to="/" className="navbar-brand">Магазин</NavLink> <Nav className="ml-auto"> <NavLink to="/delivery" className="nav-link">Доставка</NavLink> <NavLink to="/contacts" className="nav-link">Контакты</NavLink> <CheckAuth> {user.isAuth ? ( <NavLink to="/user" className="nav-link">Личный кабинет</NavLink> ) : ( <> <NavLink to="/login" className="nav-link">Войти</NavLink> <NavLink to="/signup" className="nav-link">Регистрация</NavLink> </> )} {user.isAdmin && ( <NavLink to="/admin" className="nav-link">Панель управления</NavLink> )} </CheckAuth> </Nav> </Container> </Navbar> ) }) export default NavBar
Компонент User
Отвечает за личный кабинет, там пока ничего нет, только добавим кнопку для выхода:
import { Container, Button } from 'react-bootstrap' import { useContext } from 'react' import { AppContext } from '../components/AppContext.js' import { useNavigate } from 'react-router-dom' import { logout } from '../http/userAPI.js' const User = () => { const { user } = useContext(AppContext) const navigate = useNavigate() const handleLogout = (event) => { logout() user.logout() navigate('/login', {replace: true}) } return ( <Container> <h1>Личный кабинет</h1> <p> Это личный кабинет постоянного покупателя магазина </p> <Button onClick={handleLogout}>Выйти</Button> </Container> ) } export default User
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть 11 из 19. Компоненты Signup и Login, компоненты Shop и Basket
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк