Евгений Колесников 17 jam lalu
induk
melakukan
921d32f9f9
2 mengubah file dengan 587 tambahan dan 505 penghapusan
  1. 88 505
      articles/expressjs/express02.md
  2. 499 0
      articles/expressjs/express03.md

+ 88 - 505
articles/expressjs/express02.md

@@ -11,12 +11,11 @@
 * [Добавление сущностей БД, реализация REST.](#добавление-сущностей-бд-реализация-rest)
 * [JWT-авторизация](#jwt-авторизация)
 
-
 Для подключения к БД можно использовать пакет `mysql`, но для более-менее сложных проектов, подразумевающих дальнейшее развитие, лучше использовать библиотеки, поддерживающие миграции (инициализация и изменение структуры базы данных). Для **JavaScript** наиболее популярна __ORM__ библиотека [sequelize](https://sequelize.org/). На [хабре](https://habr.com/ru/articles/565062/) есть цикл статей, посвящённый этой библиотеке.
 
 Всеми возможностями мы пользоваться не будем, нам достаточно миграций.
 
-### Установим зависимости
+## Установим зависимости
 
 >Выполняем в каталоге с проектом
 
@@ -26,11 +25,11 @@ npm i mysql2 sequelize sequelize-cli
 
 * Пакет `mysql2` нужен для работы с БД MySQL (явно мы его не используем, но он нужен для **sequelize**) 
 * Пакет `sequelize` нужен для использования в нашем коде 
-* Консольная утилита `sequelize-cli` нужно для создания и управление миграциями
+* Консольная утилита `sequelize-cli` нужна для создания и управления миграциями
 
-### Инициализация и настройка sequelize (только перед первым запуском)
+## Инициализация и настройка sequelize (только перед первым запуском)
 
-#### Инициализация:
+### Инициализация:
 
 ```
 npx sequelize-cli init
@@ -43,9 +42,9 @@ npx sequelize-cli init
 * `migrations` — файлы с миграциями
 * `seeders` — файлы для заполнения БД начальными данными
 
-#### Настройка
+### Настройка
 
-Далее нам нужно сообщить консольной утилите (`sequelize-cli`), как подключиться к БД. Для этого откроем файл `config/config.json`. Он выглядит примерно так:
+Далее нам нужно сообщить консольной утилите (`sequelize-cli`), как подключаться к БД. Для этого откроем файл `config/config.json`. Он выглядит примерно так:
 
 ```json
 {
@@ -98,10 +97,22 @@ npx sequelize-cli init
 }
 ```
 
+>В каталоге `models` есть файл `index.js` в котором происходит инициализация подключения, нам в нём ничего менять не нужно, но я приведу кусок кода из него, чтобы было понятно где используется параметр `use_env_variable`
+>
+>```js
+>if (config.use_env_variable) {
+>  sequelize = new Sequelize(process.env[config.use_env_variable], config);
+>} else {
+>  sequelize = new Sequelize(config.database, config.username, config.password, config);
+>}
+>```
+>
+>То есть, если в конфиге есть параметр `use_env_variable`, то в конструктор передается строка подключения из переменной окружения, которая задана в этом параметре (в нашем случае `DATABASE_URL`). Иначе используется конструктор с параметрами `database`, `username` и `password`.
+
 **Sequelize** поддерживает загрузку строки подключения из переменных окружения. Нам нужно создать переменную окружения в таком формате:
 
 ```
-DATABASE_URL=mysql://[user]:[pass]@[sqldomain]/[db name]
+DATABASE_URL=mysql://user:password@sqldomain/db_name
 ```
 
 В **VSCode** можно задать переменные в настройках:
@@ -114,15 +125,20 @@ DATABASE_URL=mysql://[user]:[pass]@[sqldomain]/[db name]
 
 ![](./env.png)
 
-### Создание базы данных
+## Создание базы данных
+
+>Базу можно создать и в менеджере баз данных, но сделаем в консоли, чтобы проверить все настройки
+
+Для создания БД используется команда:
 
 ```
 npx sequelize-cli db:create [--url=<строка подключения>]
 ```
 
-`--url <строка подключения к БД>` можно не указывать, если:
+Параметр `--url <строка подключения к БД>` можно не указывать, если:
+
+* оставили стандартный вариант инициализации БД или в переменных окружения есть `DATABASE_URL` (учитывайте, что на командную строку не распространяются настройки VSCode)
 
-* оставили стандартный вариант инициализации БД или в переменных окружения есть `DATABASE_URL` (учитывайте, что на командную строку не распространяются настойки VSCode)
 * в корне проекта создан файл `.sequelizerc`, в котором задана переменная `url`:
 
     ```js
@@ -131,25 +147,27 @@ npx sequelize-cli db:create [--url=<строка подключения>]
     }
     ```
 
-    Такой вариант предпочтительнее, чтобы не писать `url` при каждой миграции. Только не забудьте и этот файл прописать в `.gitignore`
+    Такой вариант предпочтительнее, чтобы не писать `url` при каждой миграции (мы рассматриваем вариант, когда в конфиге не хранятся логин и пароли, а используется переменная окружения). Только не забудьте и этот файл прописать в `.gitignore`
 
-Если всё настроили правильно, то при запуске выдаст примерно такое:
+Если всё настроили правильно, то при запуске команды `npx sequelize-cli db:create` выдаст примерно такое (и на сервере MySQL будет создана БД, которая указана в строке подключения):
 
 ```
-> npx sequelize-cli db:create
-
 Sequelize CLI [Node: 23.9.0, CLI: 6.6.3, ORM: 6.37.7]
 
 Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
 Database restaurant created.
 ```
 
-### Создадим миграцию для начальной инициализации базы данных
+## Создадим миграцию для начальной инициализации базы данных
+
+Миграции создаются командой:
 
 ```
 npx sequelize-cli migration:generate --name first
 ```
 
+>Вместо `first` лучше писать что-то человеко-понятное, например `create_MenuItem`
+
 В каталоге `migrations` будет создан файл `YYYYMMDDhhmmss-first.js`:
 
 ```js
@@ -177,7 +195,7 @@ module.exports = {
 };
 ```
 
-В методе `up` мы должны прописать команды для создания таблиц и связей, а в методе `down` для удаления. В первой минграции мы создадим таблицу `MenuItem` для хранения блюд (тут вроде все более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):
+В методе `up` мы должны прописать команды для создания таблиц и связей, а в методе `down` для удаления. В первой миграции мы создадим таблицу `MenuItem` для хранения блюд (тут вроде всё более менее понятно, подробно расписывать не буду - если что-то не понятно, то можно загуглить):
 
 ```js
 'use strict';
@@ -244,10 +262,31 @@ Parsed url mysql://ekolesnikov:*****@kolei.ru/restaurant
 npx sequelize-cli db:migrate:undo
 ```
 
-Приемры создания внешних ключей в миграции:
+Дополнительные примеры создания миграций:
 
 ```js
 async up (queryInterface, Sequelize) {
+  // создаём словарь
+  await queryInterface.createTable('NewsType', {
+    id: {
+      allowNull: false,
+      autoIncrement: true,
+      primaryKey: true,
+      type: Sequelize.DataTypes.INTEGER
+    },
+    title: {
+      type: Sequelize.DataTypes.STRING(16),
+      allowNull: false
+    },
+  })
+
+  // заполняем словарь данными
+  await queryInterface.bulkInsert('NewsType',[   
+    {id: 1, title: 'Срочное'},
+    {id: 2, title: 'Важное'}
+  ])
+
+  // создаём таблицу, в которой будет использоваться словарь
   await queryInterface.createTable('News', {
     id: {
       allowNull: false,
@@ -255,513 +294,57 @@ async up (queryInterface, Sequelize) {
       primaryKey: true,
       type: Sequelize.DataTypes.INTEGER
     },
+    // поле для внешнего ключа
     newsTypeId: {
-      type: Sequelize.DataTypes.INTEGER,
-      allowNull: true
-    },
-    newsStatusId: {
       type: Sequelize.DataTypes.INTEGER,
       allowNull: false
     }
+    // тут еще много всяких полей
   })
 
+  // создаём внешний ключ
   await queryInterface.addConstraint('News', {
     fields: ['newsTypeId'],
     type: 'foreign key',
+    // названия ключей должны быть уникальными
     name: 'FK_News_NewsType',
     references: {
       table: 'NewsType',
       field: 'id'
     }
-  })    
-}
-```
-
-## Использование sequelize для получения данных из БД
-
-Возвращаемся к нашему `app.js`
-
-Добавляем в начале файла импорт библиотеки
-
-```js
-const { sequelize } = require('./models')
-const { QueryTypes } = require('sequelize')
-```
-
-И поменяем _endpoint_ (я добавил префикс `/api`, он нам понадобится при настройке **nginx**, когда будем заворачивать апи в контейнер):
-
-```js
-app.get('/api/menu-item', async (req, res) => {
-  try {
-    res.json(await sequelize.query(`
-      SELECT *
-      FROM MenuItem
-    `, {
-      logging: false,
-      type: QueryTypes.SELECT
-    }))
-  } catch (error) {
-    console.error(error)
-  } finally {
-    res.end()
-  }
-})
-```
-
-1. Запросы к БД асинхронные, поэтому заворачиваем код в async/await
-1. Список блюд, возвращаемый запросом `sequelize.query` заворачиваем в `res.json()`, это добавит в заголовок ответа `Content-Type: application/json`.
-
-Пока у нас таблица блюд пустая, поэтому в ответе тоже будет пустой массив
-
-## Создание скрипта для наполнения БД начальными данными
-
-**Sequelize** позволяет добавить записи в таблицы (можно использовать для первоначальной инициализации словарей или при тестировании)
-
-```
-npx sequelize-cli seed:generate --name menu-items
-```
-
-В каталоге `seeders` будет создан файл аналогичный файлам миграции (собственно и отличий между ними нет, просто логически выделены отдельно)
-
-С помощью метода `bulkInsert` формируем список блюд для добавления:
-
-```js
-'use strict';
-
-/** @type {import('sequelize-cli').Migration} */
-module.exports = {
-  async up (queryInterface, Sequelize) {
-    await queryInterface.bulkInsert('MenuItem', [   
-      {id: 1, title: 'Салат', image: 'Салат.jpg', price: 100},
-      {id: 2, title: 'Суп', image: 'Суп.jpg', price: 100},
-      {id: 3, title: 'Компот', image: 'Компот.jpg', price: 100}
-    ])
-  },
-
-  async down (queryInterface, Sequelize) {
-    await queryInterface.bulkDelete(
-      'MenuItem', 
-      null /* тут можно прописать условие WHERE */
-    )
-  }
-}
-```
-
-И запускаем добавление данных командой
-
-```
-npx sequelize-cli db:seed:all [--url=...]
-```
-
-Можно откатить отдельный импорт, но накатить можно только всё разом. Поэтому для заполнения словарей лучше использовать _bulkInsert_ в обычной миграции, а seeders использовать для тестов.
-
-## Добавление сущностей БД, реализация REST.
-
-Добавим в БД таблицу _Cart_ (корзина) и связи между _Cart_ и _MenuItem_
-
-```
-npx sequelize-cli migration:generate --name cart
-```
-
-И пропишем в созданном файле команды для создания таблицы с внешним ключем и команды для удаления:
-
->В реальном проекте ещё нужно добавить таблицу пользователей и привязать корзину к пользователю
-
-```js
-'use strict';
-
-/** @type {import('sequelize-cli').Migration} */
-module.exports = {
-  async up (queryInterface, Sequelize) {
-    await queryInterface.createTable('Cart', {
-      id: {
-        allowNull: false,
-        autoIncrement: true,
-        primaryKey: true,
-        type: Sequelize.DataTypes.INTEGER
-      },
-      menuItemId: {
-        type: Sequelize.DataTypes.INTEGER,
-        allowNull: false,
-        comment: 'для внешнего ключа'
-      },
-      quantity: {
-        type: Sequelize.DataTypes.INTEGER,
-        allowNull: false,
-        comment: 'количество'
-      }
-    })
-
-    /**
-     * Внешний ключ корзина_блюдо
-     */
-    await queryInterface.addConstraint('Cart', {
-      fields: ['menuItemId'],
-      type: 'foreign key',
-      name: 'FK_cart_menu-item',
-      references: {
-          table: 'MenuItem',
-          field: 'id'
-      },
-      onDelete: 'no action',
-      onUpdate: 'no action'
-    })
-
-  },
-
-  async down (queryInterface, Sequelize) {
-    await queryInterface.removeConstraint('Cart', 'FK_cart_menu-item')
-    await queryInterface.dropTable('Cart')
-  }
-}
-```
-
-Накатываем миграцию командой `npx sequelize-cli db:migrate` и у нас в схеме бд появится таблица Cart:
-
-![](./img/cart.png)
-
-### CRUD для корзины
-
-Для того, чтобы тело запроса автоматически преобразовывалось в объект _body_ нужно после создания экземпляра приложения подключить _middleware_ `express.json()` (_middleware_ это функции, которые выполняются до обработки _endpoints_, используются для служебных целей, как в нашем случае, и в целях авторизации. Позже добавим jwt-авторизацию и напишем для неё _middleware_):
-
-```js
-...
-const app = express()
-app.use(express.json())
-...
-```
-
-#### Create. Для добавления записей в таблицу используется метод POST
-
-```js
-app.post('/api/cart', async function(req, res) {
-  try {
-    await sequelize.query(`
-      INSERT INTO Cart (menuItemId, quantity)
-      -- значения :menuItemId и :quantity задаются в replacements
-      VALUES (:menuItemId, :quantity)
-    `,{
-      logging: false,
-      type: QueryTypes.INSERT,
-      // объект replacements должен содержать значения для замены
-      replacements: {
-        menuItemId: req.body.menuItemId,
-        quantity: req.body.quantity
-      }
-    })
-    // ничего не возвращаем, но код ответа меняем на 201 (Created)
-    // можно и не менять, по-умолчанию возвращает 200 (OK)
-    res.status(201)
-  } catch (error) {
-    console.warn('ошибка при добавлении блюда в корзину:', error.message)
-    // при ошибке возвращаем код ошибки 500 и текст ошибки
-    res.status(500).send(error.message)
-  } finally {
-    res.end()
-  }
-})
-```
-
-#### Read. Чтение
-
-Используется метод get. Пример приводить не буду, он аналогичен запросу списка блюд
-
-#### Update. Редактирование корзины
-
-Для редактирования в REST используются методы PUT или PATCH. PUT - полная перезапись (замещение объекта), PATCH - частичная перезапись (изменение части объекта)
-
-Мы будем только менять количество, поэтому используем PATCH
-
-```js
-// в пути можно описать параметр (поставив знак ":")
-app.patch('/api/cart/:id', async (req, res) => {
-  try {
-    await sequelize.query(`
-      UPDATE Cart 
-      SET quantity=:quantity
-      WHERE id=:id
-    `,{
-      logging: false,
-      replacements: {
-        // используем параметр из пути
-        id: req.params.id,
-        quantity: req.body.quantity
-      }
-    })
-  } catch (error) {
-    console.warn('ошибка при редактировании корзины:', error.message)
-    res.status(500).send(error.message)
-  } finally {
-    res.end()
-  }
-})
-```
-
-#### Delete. Удаление блюда из корзины
-
-Тут ничего нового
-
-```js
-app.delete('/api/cart/:id', async (req, res) => {
-  try {
-    await sequelize.query(`
-      DELETE 
-      FROM Cart
-      WHERE id=:id
-    `,{
-      logging: false,
-      replacements: {
-        id: req.params.id
-      }
-    })
-  } catch (error) {
-    console.warn('ошибка при удалении блюда из корзины:', error.message)
-    res.status(500).send(error.message)
-  } finally {
-    res.end()
-  }
-}) 
-```
-
----
-
-## JWT-авторизация
-
->JSON Web Token (JWT) — это JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д. и подписи (signature).
-
-### Структура JWT
-
-JWT состоит из трех частей: заголовок `header`, полезные данные `payload` и подпись `signature`. Давайте пройдемся по каждой из них.
-
-
-#### Шаг 1. Создаем HEADER
-
-Хедер JWT содержит информацию о том, как должна вычисляться JWT подпись. Хедер — это тоже JSON объект, который выглядит следующим образом:
-
-```js
-header = { "alg": "HS256", "typ": "JWT"}
-```
-
-Поле `typ` не говорит нам ничего нового, только то, что это _JSON Web Token_. Интереснее здесь будет поле `alg`, которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256, для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3). Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности.
-
-
-#### Шаг 2. Создаем PAYLOAD
-
-`Payload` — это полезные данные, которые хранятся внутри **JWT**. Эти данные также называют JWT-claims (заявки). В примере, который рассматриваем мы, сервер аутентификации создает **JWT** с информацией об `id` пользователя — `userId`.
-
-```js
-payload = { "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" }
-```
-
-Мы положили только одну заявку (claim) в `payload`. Вы можете положить столько заявок, сколько захотите. Существует список стандартных заявок для JWT payload — вот некоторые из них:
-
-* iss (issuer) — определяет приложение, из которого отправляется токен.
-* sub (subject) — определяет тему токена.
-* exp (expiration time) — время жизни токена.
-
-Эти поля могут быть полезными при создании **JWT**, но они не являются обязательными. Если хотите знать весь список доступных полей для **JWT**, можете заглянуть в **Wiki**. Но стоит помнить, что чем больше передается информации, тем больший получится в итоге сам **JWT**. Обычно с этим не бывает проблем, но все-таки это может негативно сказаться на производительности и вызвать задержки во взаимодействии с сервером.
-
-#### Шаг 3. Создаем SIGNATURE
-
-Подпись вычисляется с использованием следующего псевдо-кода:
-
-```
-const SECRET_KEY = 'cAtwa1kkEy'
-const unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload)
-const signature = HMAC-SHA256(unsignedToken, SECRET_KEY)
-```
-
-Алгоритм _base64url_ кодирует `хедер` и `payload`, созданные на 1 и 2 шаге. Алгоритм соединяет закодированные строки через точку. Затем полученная строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа.
-
-```
-// header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
-// payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
-// signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
-```
-
-#### Шаг 4. Теперь объединим все три JWT компонента вместе
-
-Теперь, когда у нас есть все три составляющих, мы можем создать наш **JWT**. Это довольно просто, мы соединяем все полученные элементы в строку через точку.
-
-```js
-const token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature)
-// JWT Token
-// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
-```
-
-Вы можете попробовать создать свой собственный JWT на сайте jwt.io.
-
-Вернемся к нашему примеру. Теперь сервер аутентификации может слать пользователю JWT.
-
-### Как JWT защищает наши данные?
-
-Очень важно понимать, что использование **JWT** НЕ скрывает и не маскирует данные. Причина, почему **JWT** используются — это проверка, что отправленные данные были действительно отправлены авторизованным источником. Как было продемонстрировано выше, данные внутри **JWT** закодированы и подписаны, обратите внимание, это не одно и тоже, что зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных. Таким образом закодирование и подпись данных не защищает их. С другой стороны, главная цель шифрования — это защита данных от неавторизованного доступа.
-
-### Простыми словами
-
-1. При авторизации по логину/паролю сервер убеждается, что такой пользователь есть, формирует **JWT**-токен, содержащий полезную информацию и подпись, и возвращает этот токен клиенту (сайт, приложение)
-
-1. Клиент при последующих запрсах добавляет **JWT**-токен в заголовок запроса:
-
-    ```
-    Authorization: Bearer <JWT-токен>
-    ```
-
-1. Сервер, получая **JWT**-токен уже не лезет в таблицу пользователей, а просто проверяет подпись токена, убеждаясь что она не подделана (ключ подписи есть только у севрера). А затем может использовать полезную информацию из токена в запросах к БД. Таким образом мы экономим на каждом входящем запросе одно обращение к БД (а работа с БД самое медленное действие, вычисление подписи намного дешевле)
-
-### Подключаем JWT-авторизацию к нашему проекту
-
-#### Устанавливаем необходимые пакеты:
-
-```
-npm i jsonwebtoken md5
-```
-
-* **jsonwebtoken** - нужен для формирования и проверки токена
-* **md5** - для хеширования пароля
-
-#### Подключаем зависимости
-
-```js
-const md5 = require('md5')
-const jwt = require('jsonwebtoken')
-```
-
-#### Задаём подпись
-
-Этот параметр тоже нельзя светить, поэтому считываем из переменных окружения (добавьте в `jaunch.json`)
-
-```js
-const JWT_SECRET = process.env.JWT_SECRET
-```
-
-#### Реализуем метод авторизации
-
-```js
-/**
- * В теле запроса должен быть объект с логином и паролем:
- * {
- *  "login": "ваш логин",
- *  "password": "пароль"
- * }
- */
-app.post('/api/user/login', async (req, res) => {
-  try {
-    // const user = await sequelize.query(`
-    //   SELECT *
-    //   FROM User
-    //   WHERE login=:login
-    // `, {
-    //   // параметр plain нужен, чтобы запрос вернул не массив записей, а конкретную запись
-    //   // если записи с таким логином нет, то вернет null
-    //   plain: true,
-    //   logging: false,
-    //   type: QueryTypes.SELECT,
-    //   replacements: {
-    //     login: req.body.login
-    //   }
-    // })
-
-    // у меня нет таблицы User, поэтому я сделал заглушку
-    // вам нужно брать данные для авторизации из БД
-    const user = {
-      password: md5('123456'),
-      id: 1,
-      roleId: 1
-    }
-
-    if (user) {
-      // хешируем пароль
-      const passwordMD5 = md5(req.body.password)
-
-      if (user.password == passwordMD5) {
-
-        // формируем токен
-        const jwtToken = jwt.sign({
-            id: user.id, 
-            firstName: user.firstName,
-            roleId: user.roleId 
-          }, 
-          JWT_SECRET
-        )
-
-        res.json(jwtToken)
-      } else {
-        res.status(401).send('не верный пароль')
-      }
-    } else {
-      res.status(404).send('пользователь не найден')
+  })   
+  
+  // Пример создания таблицы связей (многие-ко-многим)
+  // особенность в том, что первичный ключ в таких таблицах составной
+  // и создаётся отдельно
+  await queryInterface.createTable('NewsRole', {
+    newsId: {
+      type: Sequelize.DataTypes.INTEGER,
+      allowNull: false
+    },
+    roleId: {
+      type: Sequelize.DataTypes.INTEGER,
+      allowNull: false
     }
-  } catch (error) {
-    console.warn('ошибка при авторизации:', error.message)
-    res.status(500).send(error.message)
-  } finally {
-    res.end()
-  }
-})
-```
+  })
 
-#### Реализуем аутентификацию пользователя в запросе корзины, используя middleware
+  // добавление первичного ключа 
+  await queryInterface.addConstraint('NewsRole', {
+    fields: ['newsId', 'roleId'],
+    type: 'primary key',
+    name: 'PK_NewsRole'
+  })    
 
-**Middleware** функция принимает на входе параметры `req`, `res`, `next`, где `req`, `res` соответственно запрос и ответ, а `next` нужно вызвать, если всё нормально и можно вызывать следующую middleware функцию (их может быть несколько)
+  // далее создаются внешние ключи для полей newsId и roleId
 
-```js
-/**
- * Middleware авторизации
- */
-const authenticateJWT = (req, res, next) => {
-  const authHeader = req.headers.authorization
-
-  if (authHeader) {
-    const token = authHeader.split(' ')
-
-    if (token[0].toLowerCase() != 'bearer')
-      return res.status(400).send('не поддерживаемый тип авторизации')
-
-    jwt.verify(token[1], JWT_SECRET, (err, data) => {
-      // если в результате есть ошибка (err)
-      // то возвращаем статус forbidden
-      if (err) return res.status(403).send(err)
-
-      // иначе сохраняем полезные данные в объект user запроса
-      req.user = data
-      next()
-    })
-  } else {
-    res.status(401).send('нет заголовка авторизации')
-  }
 }
 ```
 
-Теперь добавляем эту функцию в запрос получения корзины (обратите внимание, у меня в бд нет пользователей, поэтому я закомментировал часть кода, в вашем проекте нужно раскомментировать):
-
-```js
-/**
- * Вторым параметром запроса можно добавить массив middleware
- */
-app.get('/api/cart', [authenticateJWT], async (req, res) => {
-  try {
-    res.json(await sequelize.query(`
-      SELECT *
-      FROM Cart
-      -- в моей БД нет пользователей
-      -- WHERE userId=:userId
-    `, {
-      logging: false,
-      type: QueryTypes.SELECT
-      // replacements: {
-      //   userId: req.user.id
-      //               ^^^^ обратите внимание, тут используется объект user, заполненный в middleware аутентификации
-      // }
-    }))
-  } catch (error) {
-    console.error(error)
-  } finally {
-    res.end()
-  }
-})
-```
-
 ## Задание
 
-Реализовать АПИ для своего курсового проекта по образцу. В следующем году постараюсь написать лекции по DevOps (завернём АПИ, базу и приложение в контейнеры).
+- Добавить в проект пакеты для работы с базой данных
+- настройте `sequelize`
+- создайте базу данных
+- создайте миграцию с командами создания таблиц и связей между ними (заполните словари, если они есть)
+
+    Этот пункт делать только после утверждения ERD

+ 499 - 0
articles/expressjs/express03.md

@@ -0,0 +1,499 @@
+# Использование sequelize для получения данных из БД
+
+Продолжаем разработку АПИ для проекта "ресторан" используя **express.js**.
+
+**Содержание:**
+
+* [Создание проекта](./express01.md#создание-проекта)
+* [Подключение к БД](./express02.md#подключение-к-бд)
+* [Использование sequelize для получения данных из БД](./express03.md#использование-sequelize-для-получения-данных-из-бд)
+* [Создание скрипта для наполнения БД начальными данными](#создание-скрипта-для-наполнения-бд-начальными-данными)
+* [Добавление сущностей БД, реализация REST.](#добавление-сущностей-бд-реализация-rest)
+* [JWT-авторизация](#jwt-авторизация)
+
+Возвращаемся к нашему `app.js`
+
+Добавляем в начале файла импорт библиотеки
+
+```js
+const { sequelize } = require('./models')
+const { QueryTypes } = require('sequelize')
+```
+
+И добавим _endpoint_ для получения списка блюд (я добавил префикс `/api`, он нам понадобится при настройке **nginx**, когда будем заворачивать апи в контейнер):
+
+```js
+app.get('/api/menu-item', async (req, res) => {
+  try {
+    res.json(await sequelize.query(`
+      SELECT *
+      FROM MenuItem
+    `, {
+      logging: false,
+      type: QueryTypes.SELECT
+    }))
+  } catch (error) {
+    console.error(error)
+  } finally {
+    res.end()
+  }
+})
+```
+
+1. Запросы к БД асинхронные, поэтому заворачиваем код в async/await
+1. Список блюд, возвращаемый запросом `sequelize.query` заворачиваем в `res.json()`, это добавит в заголовок ответа `Content-Type: application/json`.
+
+Пока у нас таблица блюд пустая, поэтому в ответе тоже будет пустой массив
+
+## Создание скрипта для наполнения БД начальными данными
+
+**Sequelize** позволяет добавить записи в таблицы (можно использовать для первоначальной инициализации словарей или при тестировании)
+
+```
+npx sequelize-cli seed:generate --name menu-items
+```
+
+В каталоге `seeders` будет создан файл аналогичный файлам миграции (собственно и отличий между ними нет, просто логически выделены отдельно)
+
+С помощью метода `bulkInsert` формируем список блюд для добавления:
+
+```js
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+  async up (queryInterface, Sequelize) {
+    await queryInterface.bulkInsert('MenuItem', [   
+      {id: 1, title: 'Салат', image: 'Салат.jpg', price: 100},
+      {id: 2, title: 'Суп', image: 'Суп.jpg', price: 100},
+      {id: 3, title: 'Компот', image: 'Компот.jpg', price: 100}
+    ])
+  },
+
+  async down (queryInterface, Sequelize) {
+    await queryInterface.bulkDelete(
+      'MenuItem', 
+      null /* тут можно прописать условие WHERE */
+    )
+  }
+}
+```
+
+И запускаем добавление данных командой
+
+```
+npx sequelize-cli db:seed:all [--url=...]
+```
+
+Можно откатить отдельный импорт, но накатить можно только всё разом. Поэтому для заполнения словарей лучше использовать _bulkInsert_ в обычной миграции, а seeders использовать для тестов.
+
+## Добавление сущностей БД, реализация REST.
+
+Добавим в БД таблицу _Cart_ (корзина) и связи между _Cart_ и _MenuItem_
+
+```
+npx sequelize-cli migration:generate --name cart
+```
+
+И пропишем в созданном файле команды для создания таблицы с внешним ключем и команды для удаления:
+
+>В реальном проекте ещё нужно добавить таблицу пользователей и привязать корзину к пользователю
+
+```js
+'use strict';
+
+/** @type {import('sequelize-cli').Migration} */
+module.exports = {
+  async up (queryInterface, Sequelize) {
+    await queryInterface.createTable('Cart', {
+      id: {
+        allowNull: false,
+        autoIncrement: true,
+        primaryKey: true,
+        type: Sequelize.DataTypes.INTEGER
+      },
+      menuItemId: {
+        type: Sequelize.DataTypes.INTEGER,
+        allowNull: false,
+        comment: 'для внешнего ключа'
+      },
+      quantity: {
+        type: Sequelize.DataTypes.INTEGER,
+        allowNull: false,
+        comment: 'количество'
+      }
+    })
+
+    /**
+     * Внешний ключ корзина_блюдо
+     */
+    await queryInterface.addConstraint('Cart', {
+      fields: ['menuItemId'],
+      type: 'foreign key',
+      name: 'FK_cart_menu-item',
+      references: {
+          table: 'MenuItem',
+          field: 'id'
+      },
+      onDelete: 'no action',
+      onUpdate: 'no action'
+    })
+
+  },
+
+  async down (queryInterface, Sequelize) {
+    await queryInterface.removeConstraint('Cart', 'FK_cart_menu-item')
+    await queryInterface.dropTable('Cart')
+  }
+}
+```
+
+Накатываем миграцию командой `npx sequelize-cli db:migrate` и у нас в схеме бд появится таблица Cart:
+
+![](./img/cart.png)
+
+### CRUD для корзины
+
+Для того, чтобы тело запроса автоматически преобразовывалось в объект _body_ нужно после создания экземпляра приложения подключить _middleware_ `express.json()` (_middleware_ это функции, которые выполняются до обработки _endpoints_, используются для служебных целей, как в нашем случае, и в целях авторизации. Позже добавим jwt-авторизацию и напишем для неё _middleware_):
+
+```js
+...
+const app = express()
+app.use(express.json())
+...
+```
+
+#### Create. Для добавления записей в таблицу используется метод POST
+
+```js
+app.post('/api/cart', async function(req, res) {
+  try {
+    await sequelize.query(`
+      INSERT INTO Cart (menuItemId, quantity)
+      -- значения :menuItemId и :quantity задаются в replacements
+      VALUES (:menuItemId, :quantity)
+    `,{
+      logging: false,
+      type: QueryTypes.INSERT,
+      // объект replacements должен содержать значения для замены
+      replacements: {
+        menuItemId: req.body.menuItemId,
+        quantity: req.body.quantity
+      }
+    })
+    // ничего не возвращаем, но код ответа меняем на 201 (Created)
+    // можно и не менять, по-умолчанию возвращает 200 (OK)
+    res.status(201)
+  } catch (error) {
+    console.warn('ошибка при добавлении блюда в корзину:', error.message)
+    // при ошибке возвращаем код ошибки 500 и текст ошибки
+    res.status(500).send(error.message)
+  } finally {
+    res.end()
+  }
+})
+```
+
+#### Read. Чтение
+
+Используется метод get. Пример приводить не буду, он аналогичен запросу списка блюд
+
+#### Update. Редактирование корзины
+
+Для редактирования в REST используются методы PUT или PATCH. PUT - полная перезапись (замещение объекта), PATCH - частичная перезапись (изменение части объекта)
+
+Мы будем только менять количество, поэтому используем PATCH
+
+```js
+// в пути можно описать параметр (поставив знак ":")
+app.patch('/api/cart/:id', async (req, res) => {
+  try {
+    await sequelize.query(`
+      UPDATE Cart 
+      SET quantity=:quantity
+      WHERE id=:id
+    `,{
+      logging: false,
+      replacements: {
+        // используем параметр из пути
+        id: req.params.id,
+        quantity: req.body.quantity
+      }
+    })
+  } catch (error) {
+    console.warn('ошибка при редактировании корзины:', error.message)
+    res.status(500).send(error.message)
+  } finally {
+    res.end()
+  }
+})
+```
+
+#### Delete. Удаление блюда из корзины
+
+Тут ничего нового
+
+```js
+app.delete('/api/cart/:id', async (req, res) => {
+  try {
+    await sequelize.query(`
+      DELETE 
+      FROM Cart
+      WHERE id=:id
+    `,{
+      logging: false,
+      replacements: {
+        id: req.params.id
+      }
+    })
+  } catch (error) {
+    console.warn('ошибка при удалении блюда из корзины:', error.message)
+    res.status(500).send(error.message)
+  } finally {
+    res.end()
+  }
+}) 
+```
+
+---
+
+## JWT-авторизация
+
+>JSON Web Token (JWT) — это JSON объект, который определен в открытом стандарте RFC 7519. Он считается одним из безопасных способов передачи информации между двумя участниками. Для его создания необходимо определить заголовок (header) с общей информацией по токену, полезные данные (payload), такие как id пользователя, его роль и т.д. и подписи (signature).
+
+### Структура JWT
+
+JWT состоит из трех частей: заголовок `header`, полезные данные `payload` и подпись `signature`. Давайте пройдемся по каждой из них.
+
+
+#### Шаг 1. Создаем HEADER
+
+Хедер JWT содержит информацию о том, как должна вычисляться JWT подпись. Хедер — это тоже JSON объект, который выглядит следующим образом:
+
+```js
+header = { "alg": "HS256", "typ": "JWT"}
+```
+
+Поле `typ` не говорит нам ничего нового, только то, что это _JSON Web Token_. Интереснее здесь будет поле `alg`, которое определяет алгоритм хеширования. Он будет использоваться при создании подписи. HS256 — не что иное, как HMAC-SHA256, для его вычисления нужен лишь один секретный ключ (более подробно об этом в шаге 3). Еще может использоваться другой алгоритм RS256 — в отличие от предыдущего, он является ассиметричным и создает два ключа: публичный и приватный. С помощью приватного ключа создается подпись, а с помощью публичного только лишь проверяется подлинность подписи, поэтому нам не нужно беспокоиться о его безопасности.
+
+
+#### Шаг 2. Создаем PAYLOAD
+
+`Payload` — это полезные данные, которые хранятся внутри **JWT**. Эти данные также называют JWT-claims (заявки). В примере, который рассматриваем мы, сервер аутентификации создает **JWT** с информацией об `id` пользователя — `userId`.
+
+```js
+payload = { "userId": "b08f86af-35da-48f2-8fab-cef3904660bd" }
+```
+
+Мы положили только одну заявку (claim) в `payload`. Вы можете положить столько заявок, сколько захотите. Существует список стандартных заявок для JWT payload — вот некоторые из них:
+
+* iss (issuer) — определяет приложение, из которого отправляется токен.
+* sub (subject) — определяет тему токена.
+* exp (expiration time) — время жизни токена.
+
+Эти поля могут быть полезными при создании **JWT**, но они не являются обязательными. Если хотите знать весь список доступных полей для **JWT**, можете заглянуть в **Wiki**. Но стоит помнить, что чем больше передается информации, тем больший получится в итоге сам **JWT**. Обычно с этим не бывает проблем, но все-таки это может негативно сказаться на производительности и вызвать задержки во взаимодействии с сервером.
+
+#### Шаг 3. Создаем SIGNATURE
+
+Подпись вычисляется с использованием следующего псевдо-кода:
+
+```
+const SECRET_KEY = 'cAtwa1kkEy'
+const unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload)
+const signature = HMAC-SHA256(unsignedToken, SECRET_KEY)
+```
+
+Алгоритм _base64url_ кодирует `хедер` и `payload`, созданные на 1 и 2 шаге. Алгоритм соединяет закодированные строки через точку. Затем полученная строка хешируется алгоритмом, заданным в хедере на основе нашего секретного ключа.
+
+```
+// header eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
+// payload eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ
+// signature -xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
+```
+
+#### Шаг 4. Теперь объединим все три JWT компонента вместе
+
+Теперь, когда у нас есть все три составляющих, мы можем создать наш **JWT**. Это довольно просто, мы соединяем все полученные элементы в строку через точку.
+
+```js
+const token = encodeBase64Url(header) + '.' + encodeBase64Url(payload) + '.' + encodeBase64Url(signature)
+// JWT Token
+// eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VySWQiOiJiMDhmODZhZi0zNWRhLTQ4ZjItOGZhYi1jZWYzOTA0NjYwYmQifQ.-xN_h82PHVTCMA9vdoHrcZxH-x5mb11y1537t3rGzcM
+```
+
+Вы можете попробовать создать свой собственный JWT на сайте jwt.io.
+
+Вернемся к нашему примеру. Теперь сервер аутентификации может слать пользователю JWT.
+
+### Как JWT защищает наши данные?
+
+Очень важно понимать, что использование **JWT** НЕ скрывает и не маскирует данные. Причина, почему **JWT** используются — это проверка, что отправленные данные были действительно отправлены авторизованным источником. Как было продемонстрировано выше, данные внутри **JWT** закодированы и подписаны, обратите внимание, это не одно и тоже, что зашифрованы. Цель кодирования данных — преобразование структуры. Подписанные данные позволяют получателю данных проверить аутентификацию источника данных. Таким образом закодирование и подпись данных не защищает их. С другой стороны, главная цель шифрования — это защита данных от неавторизованного доступа.
+
+### Простыми словами
+
+1. При авторизации по логину/паролю сервер убеждается, что такой пользователь есть, формирует **JWT**-токен, содержащий полезную информацию и подпись, и возвращает этот токен клиенту (сайт, приложение)
+
+1. Клиент при последующих запрсах добавляет **JWT**-токен в заголовок запроса:
+
+    ```
+    Authorization: Bearer <JWT-токен>
+    ```
+
+1. Сервер, получая **JWT**-токен уже не лезет в таблицу пользователей, а просто проверяет подпись токена, убеждаясь что она не подделана (ключ подписи есть только у севрера). А затем может использовать полезную информацию из токена в запросах к БД. Таким образом мы экономим на каждом входящем запросе одно обращение к БД (а работа с БД самое медленное действие, вычисление подписи намного дешевле)
+
+### Подключаем JWT-авторизацию к нашему проекту
+
+#### Устанавливаем необходимые пакеты:
+
+```
+npm i jsonwebtoken md5
+```
+
+* **jsonwebtoken** - нужен для формирования и проверки токена
+* **md5** - для хеширования пароля
+
+#### Подключаем зависимости
+
+```js
+const md5 = require('md5')
+const jwt = require('jsonwebtoken')
+```
+
+#### Задаём подпись
+
+Этот параметр тоже нельзя светить, поэтому считываем из переменных окружения (добавьте в `jaunch.json`)
+
+```js
+const JWT_SECRET = process.env.JWT_SECRET
+```
+
+#### Реализуем метод авторизации
+
+```js
+/**
+ * В теле запроса должен быть объект с логином и паролем:
+ * {
+ *  "login": "ваш логин",
+ *  "password": "пароль"
+ * }
+ */
+app.post('/api/user/login', async (req, res) => {
+  try {
+    // const user = await sequelize.query(`
+    //   SELECT *
+    //   FROM User
+    //   WHERE login=:login
+    // `, {
+    //   // параметр plain нужен, чтобы запрос вернул не массив записей, а конкретную запись
+    //   // если записи с таким логином нет, то вернет null
+    //   plain: true,
+    //   logging: false,
+    //   type: QueryTypes.SELECT,
+    //   replacements: {
+    //     login: req.body.login
+    //   }
+    // })
+
+    // у меня нет таблицы User, поэтому я сделал заглушку
+    // вам нужно брать данные для авторизации из БД
+    const user = {
+      password: md5('123456'),
+      id: 1,
+      roleId: 1
+    }
+
+    if (user) {
+      // хешируем пароль
+      const passwordMD5 = md5(req.body.password)
+
+      if (user.password == passwordMD5) {
+
+        // формируем токен
+        const jwtToken = jwt.sign({
+            id: user.id, 
+            firstName: user.firstName,
+            roleId: user.roleId 
+          }, 
+          JWT_SECRET
+        )
+
+        res.json(jwtToken)
+      } else {
+        res.status(401).send('не верный пароль')
+      }
+    } else {
+      res.status(404).send('пользователь не найден')
+    }
+  } catch (error) {
+    console.warn('ошибка при авторизации:', error.message)
+    res.status(500).send(error.message)
+  } finally {
+    res.end()
+  }
+})
+```
+
+#### Реализуем аутентификацию пользователя в запросе корзины, используя middleware
+
+**Middleware** функция принимает на входе параметры `req`, `res`, `next`, где `req`, `res` соответственно запрос и ответ, а `next` нужно вызвать, если всё нормально и можно вызывать следующую middleware функцию (их может быть несколько)
+
+```js
+/**
+ * Middleware авторизации
+ */
+const authenticateJWT = (req, res, next) => {
+  const authHeader = req.headers.authorization
+
+  if (authHeader) {
+    const token = authHeader.split(' ')
+
+    if (token[0].toLowerCase() != 'bearer')
+      return res.status(400).send('не поддерживаемый тип авторизации')
+
+    jwt.verify(token[1], JWT_SECRET, (err, data) => {
+      // если в результате есть ошибка (err)
+      // то возвращаем статус forbidden
+      if (err) return res.status(403).send(err)
+
+      // иначе сохраняем полезные данные в объект user запроса
+      req.user = data
+      next()
+    })
+  } else {
+    res.status(401).send('нет заголовка авторизации')
+  }
+}
+```
+
+Теперь добавляем эту функцию в запрос получения корзины (обратите внимание, у меня в бд нет пользователей, поэтому я закомментировал часть кода, в вашем проекте нужно раскомментировать):
+
+```js
+/**
+ * Вторым параметром запроса можно добавить массив middleware
+ */
+app.get('/api/cart', [authenticateJWT], async (req, res) => {
+  try {
+    res.json(await sequelize.query(`
+      SELECT *
+      FROM Cart
+      -- в моей БД нет пользователей
+      -- WHERE userId=:userId
+    `, {
+      logging: false,
+      type: QueryTypes.SELECT
+      // replacements: {
+      //   userId: req.user.id
+      //               ^^^^ обратите внимание, тут используется объект user, заполненный в middleware аутентификации
+      // }
+    }))
+  } catch (error) {
+    console.error(error)
+  } finally {
+    res.end()
+  }
+})
+```
+
+## Задание
+
+Реализовать АПИ для своего курсового проекта по образцу. В следующем году постараюсь написать лекции по DevOps (завернём АПИ, базу и приложение в контейнеры).