Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login

29.12.2021

Теги: BackendExpress.jsFrontendJavaScriptNode.jsORMReact.jsWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровКорзинаФреймворк

Запросы на сервер

Библиотека 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

Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.