Магазин на JavaScript, часть 6 из 19. Пагинация, свойства товара, JWT (JSON Web Token)

26.11.2021

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

Товаров в каталоге может быть много и показывать их все на одной странице — плохая идея. Так что давайте заложим возможность запрашивать только часть товаров. Клиент может добавить GET-параметры limit (количество товаров на странице) и page (товары какой страницы возвращать). Sequelize предоставляет в наше распоряжение метод findAndCountAll, которая принимает опции limit и offset.

Вносим изменения в контроллер и модель товара:

import ProductModel from '../models/Product.js'
import AppError from '../errors/AppError.js'

class Product {
    async getAll(req, res, next) {
        try {
            const {categoryId = null, brandId = null} = req.params
            let {limit, page} = req.query
            limit = limit && /[0-9]+/.test(limit) && parseInt(limit) ? parseInt(limit) : 3
            page = page && /[0-9]+/.test(page) && parseInt(page) ? parseInt(page) : 1
            const options = {categoryId, brandId, limit, page}
            const products = await ProductModel.getAll(options)
            res.json(products)
        } catch(e) {
            next(AppError.badRequest(e.message))
        }
    }
    /* .......... */
}
import { Product as ProductMapping } from './mapping.js'
import FileService from '../services/File.js'
import AppError from '../errors/AppError.js'

class Product {
    async getAll(options) {
        const {categoryId, brandId, limit, page} = options
        const offset = (page - 1) * limit
        const where = {}
        if (categoryId) where.categoryId = categoryId
        if (brandId) where.brandId = brandId
        const products = await ProductMapping.findAndCountAll({where, limit, offset})
        return products
    }
    /* .......... */
}

Свойства товара

У товара может быть несколько свойств, например «Напряжение питания» или «Размер диагонали» — для этого у нас есть отдельная таблица в базе данных, связанная с таблицей товаров через промежуточную таблицу. Если в POST-запросе на создание нового товара есть свойства — мы должны их добавить в эту таблицу и связать с новым товаром.

import { Product as ProductMapping } from './mapping.js'
import { ProductProp as ProductPropMapping } from './mapping.js'
import FileService from '../services/File.js'
import AppError from '../errors/AppError.js'

class Product {
    /* .......... */
    async create(data, img) {
        // поскольку image не допускает null, задаем пустую строку
        const image = FileService.save(img) ?? ''
        const {name, price, categoryId = null, brandId = null} = data
        const product = await ProductMapping.create({name, price, image, categoryId, brandId})
        if (data.props) { // свойства товара
            const props = JSON.parse(data.props)
            for (let prop of props) {
                await ProductPropMapping.create({
                    name: prop.name,
                    value: prop.value,
                    productId: product.id
                })
            }
        }
        return product
    }
    /* .......... */
}

export default new Product()

Давайте проверим, что свойства добавляются — редактируем test.http и отправляем POST-запрос:

### создание товара, у которого есть свойства
POST /api/product/create HTTP/1.1
Host: localhost:7000
Content-Type: multipart/form-data; boundary=MultiPartFormDataBoundary

--MultiPartFormDataBoundary
Content-Disposition: form-data; name="name"
Content-Type: text/plain; charset=utf-8

Название товара 18
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="price"
Content-Type: text/plain; charset=utf-8

12345
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="props"
Content-type: text/plain; charset=utf-8

[{"name": "Свойство 1", "value": "Значение 1"},{"name": "Свойство 2", "value": "Значение 2"}]
--MultiPartFormDataBoundary
Content-Disposition: form-data; name="image"; filename="picture.jpg"
Content-Type: image/jpeg

< ./picture.jpg
--MultiPartFormDataBoundary--

Кроме того, метод getOne() модели должен вместе с товаром возвращать и его свойства:

import { Product as ProductMapping } from './mapping.js'
import { ProductProp as ProductPropMapping } from './mapping.js'
import FileService from '../services/File.js'
import AppError from '../errors/AppError.js'

class Product {
    /* .......... */
    async getOne(id) {
        const product = await ProductMapping.findOne({
            where: {id: id},
            include: [{model: ProductPropMapping}]
        })
        if (!product) {
            throw new Error('Товар не найден в БД')
        }
        return product
    }
    /* .......... */
}

export default new Product()

Выполним GET-запрос на получение товара по идентификатору и посмотрим результат:

{
    "id": 1,
    "name": "Название товара 18",
    "price": 12345,
    "rating": 0,
    "image": "426fe772-f70c-4279-8dd0-7257a644e469.jpeg",
    "createdAt": "2021-11-26T13:41:22.592Z",
    "updatedAt": "2021-11-26T13:41:22.592Z",
    "categoryId": null,
    "brandId": null,
    "product_props": [
        {
            "id": 1,
            "name": "Свойство 1",
            "value": "Значение 1",
            "createdAt": "2021-11-26T13:41:22.786Z",
            "updatedAt": "2021-11-26T13:41:22.786Z",
            "productId": 1
        },
        {
            "id": 2,
            "name": "Свойство 2",
            "value": "Значение 2",
            "createdAt": "2021-11-26T13:41:22.787Z",
            "updatedAt": "2021-11-26T13:41:22.787Z",
            "productId": 1
        }
    ]
}

Давайте только изменим product_props на props, для этого добавим алиас в файле sequelize.js и используем его в модели.

// связь товара с его свойствами: у товара может быть несколько свойств, но
// каждое свойство связано только с одним товаром
Product.hasMany(ProductProp, {as: 'props'})
ProductProp.belongsTo(Product)
import { Product as ProductMapping } from './mapping.js'
import { ProductProp as ProductPropMapping } from './mapping.js'
import FileService from '../services/File.js'
import AppError from '../errors/AppError.js'

class Product {
    /* .......... */
    async getOne(id) {
        const product = await ProductMapping.findOne({
            where: {id: id},
            include: [{model: ProductPropMapping, as: 'props'}]
        })
        if (!product) {
            throw new Error('Товар не найден в БД')
        }
        return product
    }
    /* .......... */
}

export default new Product()

Что такое JWT?

Прежде, чем двигаться дальше, нужно разабраться, что такое JWT (JSON Web Token). Если говорить простыми словами, то это строка в специальном формате, которая содержит данные — например, идентификатор и адрес почты зарегистрированного пользователя. Эта строка передается при каждом запросе на сервер, когда необходимо идентифицировать клиента и понять, кто прислал этот запрос.

Эта строка состоит из трех частей, разделенных точкой — header, payload и signature:

eyJhbGciOi...I6IkpXVCJ9.eyJ1c2VyX2...EzNTcwMzl9.E4FNMef6tk...NzAeEd4wF0

Псевдокод, который показывает, как на сервере создать такую строку:

const header = {alg: 'HS256', typ: 'JWT'}
const payload = {user_id: 1, exp: 1581357039}
// кодируем заголовок и данные в base64url
const headerBase64url = base64urlEncode(JSON.stringify(header))
const payloadBase64url = base64urlEncode(JSON.stringify(payload))
// склеиваем точкой полученные строки
const data = headerBase64url + '.' + payloadBase64url
// кодируем алгоритмом шифрования нашим ключом шифрования
const secret = 'qwerty123456'
const signature = HMAC_SHA256(data, secret)
// и, наконец, получаем окончательный токен
const jwt = data + '.' + signature

Данные header и payload не зашифрованы (только закодированы, чтобы передавать по http-протоколу), клиент может получить эти данные. Но злоумышленник не может эти данные подделать, поскольку не знает secret — который есть только на сервере. А на сервере мы можем проверить подлинность JWT-токена с помощью signature. Берем склейку заголовок + данные, кодируем с помощью алгоритма HMAC-SHA256 и нашего приватного ключа. А далее берем сигнатуру полученного токена и сверяем с результатом кодирования.

JWT-токен клиент получает не просто так, а после того, как пройдет аутентификацию на сервере. И должен сохранить его в cookie или LocalStorage — с тем, чтобы отправлять на сервер при каждом запросе, тем самым подтверждая свои права на те или иные действия. Например, если мы говорим об интернет-магазине — на просмотр своей корзины или на доступ к истории заказов в личном кабинете.

Поиск: 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.