чорнетка

У налаштуванні Mongo Atlas ми отримали URL підключення до бази даних. Пропишемо його у нашому файлі .env в проєкті. І при завантаженні середовища посилання на підключення до бази даних буде зберігатися в process.env.MONGO_URL.

enviroment/developement.env
MONGO_URL='mongodb+srv://khomiak:password1234@cluster0.z7jjmxa.mongodb.net/khomiak_db'

Для роботи з базою даних MongoDB в проєкті будемо використовувати додатковий пакет mongoosearrow-up-right.

terminal
npm i mongoose

Проведемо далі факторинг нашого проєкту.

Модель

Створимо папку models, а в ній файл userModel.js. По суті це ніби форма в якій ми описуємо структуру даних, які будемо зберігати.

З бібліотеки mongoose отримуємо Schema. На основі цього класу створюємо нову схему в яку передаємо обʼєкт з описом всіх параметрів.

Ми знаємо, що змінна буде рядок, або число тощо, то можемо просто прописати name: String або year: Number.

Якщо хочемо докладніше описати структуру даних, то описуємо кожен параметр в обʼєкті. Тут type - формат даних, required - що параметр обовʼязковий. unique - вказує що дані в такому полі мають бути унікальні. В даному полі (як на прикладі коду) можна вказати масив, де перший параметр true, а другий - наше кастомне повідомлення у разі помилки.

Оскільки створюємо базу користувачів, то можна розмежувати їх за правами доступу. для цього визначають ролі користувачів. У схемі в параметрі enum вказують масив значень, яких може набувати цей параметр. default відповідно вказує, яке буде значення за замовчанням. У прикладі ці значення винесені в окремий файл const. Також напроти поля password вказано select: false - це означає, що за GET-запитом із результатів буде прибиратися поле пароля, що ми побачимо далі.

const User = model('User', userSchema); - по суті створення моделі, назву якій задаємо як перший параметр (з великої літери), а другий вказується схема моделі.

models/userModel.js
const { model, Schema } = require('mongoose');

const userRolesEnum = require('../constants/userRolesEnum');

// схема з обʼєктом налаштувань користувача
const userSchema = new Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
      required: true,
      unique: [true, 'Duplicated email..'],
    },
    password: {
      type: String,
      required: true,
      select: false,
    },
    year: Number,
    role: {
      type: String,
      // enum: ['admin', 'user', 'moderator'],
      // default: 'user'
      enum: Object.values(userRolesEnum),
      default: userRolesEnum.USER,
    },
  },
);

const User = model('User', userSchema);

module.exports = User;

Сталі значення зазвичай заведено виносити в окремі файли. (це, наприклад переліки можливих варіантів вибору). Якщо подивимося, то ми їх використовуємо у схемі налаштувань користувача вище. Винесли в окремий файл, бо можемо використовувати й в інших модулях нашого проєкту.

Рефакторинг Валідації

Тепер трохи рефакторнемо нашу валідацію Joi. Зокрема під нашу схему напишемо валідацію перевірки. Для пароля будемо використовувати патерн із регулярного виразу. У ньому {8,128} означає, що пароль має бути від 8 до 128 символів. Також обовʼязково повинна бути велика, мала літера, число і якийсь спецсимвол.

Розберемо змінену валідацію прописану раніше.

Із нового тут regex - приймає значення і перевіряє на відповідність регулярному виразу вказаному в круглих дужках.

Там де перевірка ролі користувача у полі valid вказуються всі можливі значення.

Валідація у проміжному пз

Дотепер валідацію ми проводили в контролерах, але логічно винести валідацію у проміжне пз (middleware). Спочатку підключаємо нашу функцію валідатора Joi userValidators. Функція буде асинхронна тому ми її огортаємо в catchAsync.

Далі викликаємо функцію валідації Joi. Передаємо в неї всі дані які отримали (req.body). Як відповідь перевірки отримуємо error і value. Якщо є помилка, то валідація не пройдена, генеруємо помилку.

Далі в цьому ж проміжному пз перевіримо чи є такий email в базі. Для цього уже будемо використовувати створену раніше модель, тому її спочатку підключаємо. і за допомогою методу exists (бібліотеки mongoose) перевіряємо чи є вже такий email користувача в базі даних. Якщо є, то генеруємо помилку. Якщо немає, то перевірка пройдена, перезаписуємо провалідовані дані та передаємо виконання коду наступній функції обробки.

Після цього функцію валідації checkCreateUserData потрібно вказати в роутах, як проміжне пз.

Підключення до MongoDB

Щоб мати доступ до бази даних потрібно її під'єднати у головному файлі index.js.

Для цього імпортуємо mongoose і підключаємо перед middleware нашу базу даних. Далі наведено один із прикладів підʼєднання бази даних через метод connect. І оскільки це асинхронна операція, то повертає проміс, тому прописуємо then і catch. Якщо помилка виникає, то виходимо з процесу.

Рефакторинг контроллерів для роботи з БД для створення користувача і отримання списку користувачів.

Тепер нам тут не потрібно генерувати id та записувати в файл, тому коментуймо, або видаляємо наступні бібліотеки.

Модель підключаємо в контролерах. Створюють користувача за допомогою метода create. А шукають користувачів за допомогою метода find.

Можна перевірити всі хибні варіанти переданих даних і подивитися які видаються помилки в консолі, а також, що повертає сервер. І врешті зробити POST-запит на сервер з коректними даними.

Після цього перевірити дані, які надійшли в базу даних:

Як бачимо, у відповідь із сервера приходить створений запис із вмістом password, чого не має бути.

Щоб на GET-запит не повертався пароль ми прописали у схемі.

Також для GET-запитів у контролерах можна вказати параметри, які ми хочемо повернути (в прикладі повернеться тільки імʼя і рік):

Або параметри, які хочемо прибрати із результату, який повертається (в прикладі повернеться все крім імені (ну і пароля, який не повертається через налаштування схеми)):

Якщо ми хочемо примусово витягнути якесь поле, яке в схемі позначено, що не провертається (пароль) то потрібно вказати +.

Також може повертатися версія обʼєкта, яку можна прибрати і не повертати

Але при створенні нового юзера у нас все одно у відповідь повертається обʼєкт з паролем. тобто цей select не відпрацьовується на новостворений обʼєкт. Для цього при створенні користувача перед поверненням присвоїмо паролю значення undefind. Тобто приховані дані з res потрібно завжди ЗАБИРАТИ!!

Опціональні конфіги схеми

У моделі можна іще додати опціонально обʼєкт з конфігами

chevron-rightКод схеми повністюhashtag

versionKey: false - означає, що нам не буде повертатися ключ версії __v і не треба вказувати, зоб його прибирати, як було розглянуто раніше

timestamps: true - додає в базу даних інформацію, коли обʼєкт створено і коли обʼєкт оновлено. (буває корисно)

Рефакторинг контроллера отримання користувача за id

спочатку перепишемо middleware перевірки id користувача(пам'ятаємо, що в рутах він іде як userId).

За допомогою Types ми можемо перевірити чи валідний сам по собі userId об'єкту _id. Друга перевірка - чи існує такий користувач із таким полем.

В роутах ця middleware уже прописана раніше, бо ми її тільки рефакторили. Тому там нічого правити не треба.

В контролерах пропишемо обробку вибірку користувача за id.

Рефакторинг оновлення користувача за id

Валідація оновлення даних. спрочатку пропишемо валідатор Joi в нащих утилітах. На перевірку вдповідності формату даних. PASSWD_REGEX - ми вказували раніще у файлі, коли прописували валідацію формату даних під час створення користувача. Також прибрали необхідність приходу всіх полів, оскільки з фронтенда може прийти оновлення тільки деякі параметри.

Пропишемо middleware перед оновленням користувача. спочатку перевіряємо на відповідність формату даних, які приходять. а потім перевіряємо чи існує користувач із таким email в базі даних, якщо захочемо оновити пароль. Якщо аткий пароль існуєу іншого користувача то повертаємо помилку. якщо ваділація пройдена і немає збігів, то керування передається наступній функції в роутах. Тут тікавий синтаксис { email: value.email, _id: { $ne: req.params.id } }. Тут ми перевіряємо чи є такий email в базі даних, але за умовим що цей імейл не в поточному записі. тобто. такий імейл має бути тільки в одному записі в тому, який саме і оновлюємо. $ne - означає not exist

Вкажемо цю перевірку в роутах

рефакторинг контроллера:

Як найпростіший варіант:

АЛЕ ПРАВИЛЬНО ОНОВЛЮВАТИ ДАНІ В БД ПОЕТАПНО ОСТ ТАК ЧЕРЕЗ МЕТОЛ SAVE (але тут ми двічі звертаємося до бази даних):

Рефакторинг видалення користувача за id із бази даних

Хешування паролів у БД

Для захисту даних потрібно хешувати паролі. І зберігати їх у базі даних лише у такому форматі. Для хешування паролів існує багато модулів.

Для фронтенду часто використовують bcrypt.jsarrow-up-right

Для бекенду використовують node.bcrypt.jsarrow-up-right

Захешуємо пароль на етапі створення юзера в контроллерах. Спочатку створюється "соль" в яку передаємо цифрою рівень захищенності. зазвичай в проміжку мід 8 і 12. А щоб захешувати пароль потрібно використати метод hash і в нього передати пароль який хочемо захешувати і друшим параметром "соль". Новий користувач - че розпилений користувач із перезаписаним паролем тобто хешем замість нього.

Тепер в базу даних буде записуватися не пароль а його хеш. У такому разі виникає потреба звірення правильності ведення пароля. Для цього у bcrypt існує метод compare. У нього 1-м передають параметр для порівняння А 2-м наш захешований пароль. Він повертає true або false.

Хешування паролю в самій моделі при створенні користувача

Можна провести рефакторинг попереднього коду. Зазвичай хешування як додатковий сервіс виносять окрему функцію в папку utils.

Але є й інші варіанти - з використанням хуків. Свого роду гачок, який спрацьовує, коли відбувається певна подія.

Розглянемо для цього ще одну опцію створення запису в базі даних на прикладі контролера створення користувача

Але повернемо на створення користувача такий синтаксис, на нього теж реагуватиме подальгий код. Тобто подальштй хук буде відпрацьовувати на методи create і save.

І є мождивість проведення операцій в схемі перед самим створенням запису. Реагуватиме на метод save. Для цього порефакторимо нашу модель. підключаємо bcrypt, а потім використовуємо userSchema.pre. Пергий параметр - на який буде реагувати цей алгоритм, друший параметр колбек функція. ми перевіряємо, якщо не було змін в паролі, то виходимо, а якшо були, то хешуємо пароль і перезаписуємо його. ТОбто цей хук буде хешувати наш пароль при створенні чи збереженні юзера.

????

custom mongoose metod

можна створити у схемі метод наприклад checkPassword, і це асинхронна функція яка повертає проміс. і порівнює вона кандидата і хеш

порядок написання бекенд додатку

  1. запустити сервер (express + пережбачити помилку сервера і коли немає збігу 404 )

  2. в пекедж json налаштувати nodemon і NODE_ENV В залежності в якому режимі запустили сервер

  3. налаштувати оточення env. і завантаження відповідного env в задежності від NODE_ENV

  4. прописати в оточенні урл на могно дб

  5. підключити в головному файлі програми базу монго дб

  6. пілключити всі необхідні мідлвари (логгер, корс, експрессджсон )

  7. підключити в головному файлі посилання на всі необхіні роути.

  8. у файлі роутів прописати всі використовувані методи роутів. переважно get (2 шт), post, put(patch), delete

  9. підключити до файлу роутів відповідні контроллери

  10. в контролерах у обгортці асинзронної функції, яка відловлює помилку прописати функціонал асинхронної функції (шаблон)

  11. стоврити модель даних для бази (схема + модель) експортувати для використання в контролерах і мідлварах.

Покликання:

Mongoosearrow-up-right

Last updated