Магазин на JavaScript, часть 6 из 19. Пагинация, свойства товара, JWT (JSON Web Token)
26.11.2021
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Товаров в каталоге может быть много и показывать их все на одной странице — плохая идея. Так что давайте заложим возможность запрашивать только часть товаров. Клиент может добавить 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
— с тем, чтобы отправлять на сервер при каждом запросе, тем самым подтверждая свои права на те или иные действия. Например, если мы говорим об интернет-магазине — на просмотр своей корзины или на доступ к истории заказов в личном кабинете.
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Фреймворк • Корзина