diff --git a/homework-01/.eslintignore b/homework-01/.eslintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/homework-01/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/homework-01/.eslintrc.js b/homework-01/.eslintrc.js new file mode 100644 index 0000000..d799332 --- /dev/null +++ b/homework-01/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + }, + extends: ['standard', 'prettier'], + parserOptions: { + ecmaVersion: 12, + }, + rules: {}, +} diff --git a/homework-01/.gitignore b/homework-01/.gitignore new file mode 100644 index 0000000..8877fc5 --- /dev/null +++ b/homework-01/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.idea +.vscode \ No newline at end of file diff --git a/homework-01/README.en.md b/homework-01/README.en.md deleted file mode 100644 index 7d96525..0000000 --- a/homework-01/README.en.md +++ /dev/null @@ -1,164 +0,0 @@ -**Read in other languages: [Russian](README.md), [Ukrainian](README.ua.md).** - -# Homework 1 - -## Step 1 - -- Initialize npm in the project -- In the root of the project, create a file `index.js` -- Install package [nodemon](https://www.npmjs.com/package/nodemon) as development dependency (devDependencies) -- In `package.json` file add "scripts" to run `index.js` -- `start` script that starts `index.js` with `node` -- `start:dev` script that starts `index.js` with `nodemon` - -## Step 2 - -Create a folder `db` in the root of the project. To store contacts, download and use the [contacts.json](./contacts.json) file, putting it in the `db` folder. - -At the root of the project, create a `contacts.js` file. - -- Make imports of modules `fs` and `path` to work with the file system -- Create a `contactsPath` variable and put the path to the `contacts.json` file in it. To compose a path, use the methods of the `path` module -- Add functions to work with a collection of contacts. In functions, use the `fs` module and its `readFile()` and `writeFile()` methods -- Make export of created functions via `module.exports` - -```js -// contacts.js - -/* - * Uncomment and write down the value - * const contactsPath = ; - */ - -// TODO: document each function -function listContacts() { - // ...your code -} - -function getContactById(contactId) { - // ...your code -} - -function removeContact(contactId) { - // ...your code -} - -function addContact(name, email, phone) { - // ...your code -} -``` - -## Step 3 - -Make an import of the `contacts.js` module in the `index.js` file and check the functionality of the functions for working with contacts. - -## Step 4 - -The `index.js` file imports the `yargs` package for convenient parsing of command line arguments. Use the ready-made function `invokeAction()` which receives the type of action to be performed and the required arguments. The function calls the appropriate method from the `contacts.js` file, passing it the necessary arguments. - -```js -// index.js -const argv = require('yargs').argv; - -// TODO: refactor -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -Alternatively, you can use the [commander](https://www.npmjs.com/package/commander) module to parse command line arguments. This is a more popular alternative to the `yargs` module. - -```js -const { Command } = require('commander'); -const program = new Command(); -program - .option('-a, --action ', 'choose action') - .option('-i, --id ', 'user id') - .option('-n, --name ', 'user name') - .option('-e, --email ', 'user email') - .option('-p, --phone ', 'user phone'); - -program.parse(process.argv); - -const argv = program.opts(); - -// TODO: refactor -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -## Step 5 - -Run the commands in the terminal and take a separate screenshot of the result of each command. - -```shell -# Get and display the entire list of contacts in the form of a table (console.table) -node index.js --action list - -# Get contact by id -node index.js --action get --id 05olLMgyVQdWRwgKfg5J6 - -# Add the contact -node index.js --action add --name Mango --email mango@gmail.com --phone 322-22-22 - -# Delete the contact -node index.js --action remove --id qdggE76Jtbfd9eWJHrssH -``` - -## Step 6 - Homework Submission - -Command execution screenshots can be uploaded to any free cloud image storage service (Example: [monosnap](https://monosnap.com/), [imgbb.com](https://imgbb.com/)) and the corresponding links are necessary add to the README.md file. Create this file at the root of the project. Then attach a link to the homework repository at [Schoology](https://app.schoology.com/login) for mentor to check. - -## Admission Criteria - -- You created a repository with homework — CLI application -- The assignment has been sent to the mentor at [Schoology](https://app.schoology.com/login) for review (repository link) -- The code corresponds to the terms of reference of the project -- No unhandled errors when executing code -- The names of variables, properties and methods start with a lowercase letter and are written in CamelCase notation. English nouns are used -- The name of the function or method contains a verb -- There are no commented sections of code in the code -- The project works correctly in the current LTS version of Node diff --git a/homework-01/README.es.md b/homework-01/README.es.md deleted file mode 100644 index a3f6957..0000000 --- a/homework-01/README.es.md +++ /dev/null @@ -1,164 +0,0 @@ -**Leer en otros idiomas: [Русский](README.md), [Українська](README.ua.md).** - -# Tarea 1 - -## Paso 1 - -- Inicia npm en el proyecto -- En la raíz del proyecto, cree un archivo `index.js` -- Pon el paquete [nodemon](https://www.npmjs.com/package/nodemon) como dependencia de desarrollo (devDependencies) -- En el archivo `package.json` añade "scripts" para iniciar `index.js` -- El script `start` que lanza `index.js` mediante `node` -- El script `start:dev` que lanza `index.js` mediante `nodemon` - -## Paso 2 - -En la raíz del proyecto cree una carpeta `db`. Descargue y utilice el archivo [contacts.json](./contacts.json) para almacenar los contactos, y colóquelo en la carpeta `db`. - -Cree un archivo `contacts.js` en la raíz del proyecto. - -- Importa los módulos `fs` y `path` para trabajar con el sistema de archivos. -- Cree una variable `contactsPath` y escribe en ella la ruta al archivo `contacts.json`. Utiliza los métodos del módulo `path` para elaborar la ruta. -- Añade funciones para trabajar con la colección de contactos. En las funciones, utiliza el módulo `fs` y sus métodos `readFile()` y `writeFile()`. -- Exporta las funciones creadas mediante `module.exports`. - -```js -// contacts.js - -/* - * Comenta y anota el valor - * const contactsPath = ; - */ - -// TODO: documenta cada función -function listContacts() { - // ...tu código -} - -function getContactById(contactId) { - // ...tu código -} - -function removeContact(contactId) { - // ...tu código -} - -function addContact(name, email, phone) { - // ...tu código -} -``` - -## Paso 3 - -Importa el módulo `contacts.js` en el archivo `index.js` y comprueba que las funciones para manipular los contactos funcionan. - -## Paso 4 - -En el archivo `index.js` se importa el paquete `yargs` para facilitar el análisis de los argumentos de la línea de comandos. Utilice la función ya preparada `invokeAction()` que obtiene el tipo de acción a realizar y los argumentos necesarios. La función llama al método apropiado del archivo `contacts.js` pasándole los argumentos necesarios. - -```js -// index.js -const argv = require('yargs').argv; - -// TODO: refactorizar -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -También puede utilizar el módulo [commander](https://www.npmjs.com/package/commander) para hacer parsing a los argumentos de la línea de comandos. Esta es una alternativa más popular al módulo `yargs'. - -```js -const { Command } = require('commander'); -const program = new Command(); -program - .option('-a, --action ', 'choose action') - .option('-i, --id ', 'user id') - .option('-n, --name ', 'user name') - .option('-e, --email ', 'user email') - .option('-p, --phone ', 'user phone'); - -program.parse(process.argv); - -const argv = program.opts(); - -// TODO: refactorizar -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -## Paso 5 - -Ejecuta los comandos en el terminal y haz una captura de pantalla del resultado de cada comando. - -```shell -# Obtenemos y mostramos la lista completa de contactos en forma de tabla (console.table). -node index.js --action list - -# Obtenemos un contacto según su id -node index.js --action get --id 05olLMgyVQdWRwgKfg5J6 - -# Añadimos un contacto -node index.js --action add --name Mango --email mango@gmail.com --phone 322-22-22 - -# Eliminamos un contacto -node index.js --action remove --id qdggE76Jtbfd9eWJHrssH -``` - -## Paso 6 - Entrega de la tarea. - -Las capturas de pantalla de la ejecución de los comandos, se pueden subir a cualquier servicio gratuito de almacenamiento de imágenes en la nube (Ejemplo: [monosnap](https://monosnap.com/), [imgbb.com](https://imgbb.com/)) y los enlaces pertinentes deben añadirse al archivo README.md. Cree este archivo en la raíz del proyecto. Después, adjunte un enlace al repositorio con la tarea a [schoology](https://app.schoology.com/login) para que el mentor la revise. - -## Requisitos para que sea admitida - -- Repositorio creado con la tarea — aplicación CLI -- Tarea enviada al mentor en [schoology](https://app.schoology.com/login) para su revisión (enlace al repositorio) -- El código se ajusta a la tarea técnica del proyecto -- No se producen errores sin procesar durante la ejecución del código -- Los nombres de variables, propiedades y métodos comienzan con una letra minúscula y se escriben en notación CamelCase. Se utilizan sustantivos en inglés -- El nombre de las funciones o métodos contiene un verbo -- No hay secciones de código comentadas en el código -- El proyecto funciona correctamente en la versión LTS actual de Node diff --git a/homework-01/README.md b/homework-01/README.md index c69e5ed..b67d049 100644 --- a/homework-01/README.md +++ b/homework-01/README.md @@ -1,165 +1,6 @@ -**Читать на других языках: [Русский](README.md), [Українська](README.ua.md).** +### Команди: -# Домашнее задание 1 - -## Шаг 1 - -- Инициализируй npm в проекте -- В корне проекта создай файл `index.js` -- Поставь пакет [nodemon](https://www.npmjs.com/package/nodemon) как зависимость разработки (devDependencies) -- В файле `package.json` добавь "скрипты" для запуска `index.js` -- Скрипт `start` который запускает `index.js` с помощью `node` -- Скрипт `start:dev` который запускает `index.js` с помощью `nodemon` - -## Шаг 2 - -В корне проекта создай папку `db`. Для хранения контактов скачай и используй файл [contacts.json](./contacts.json), положив его в папку `db`. - -В корне проекта создай файл `contacts.js`. - -- Сделай импорт модулей `fs` (в версии, которая работает с промисами - `fs/promises`) и `path` для работы с файловой системой -- Создай переменную `contactsPath` и запиши в нее путь к файле `contacts.json`. Для составления пути используй методы модуля `path`. -- Добавь функции для работы с коллекцией контактов. В функциях используй модуль `fs` и его методы `readFile()` и `writeFile()` -- Сделай экспорт созданных функций через `module.exports` - -```js -// contacts.js - -/* - * Раскомментируй и запиши значение - * const contactsPath = ; - */ - -// TODO: задокументировать каждую функцию -function listContacts() { - // ...твой код. Возвращает массив контактов. -} - -function getContactById(contactId) { - // ...твой код. Возвращает объект контакта с таким id. Возвращает null если объект с таким id не найден. -} - -function removeContact(contactId) { - // ...твой код. Возвращает объект удаленного контакта. Возвращает null если объект с таким id не найден. -} - -function addContact(name, email, phone) { - // ...твой код. Возвращает объект добавленного контакта. Возвращает null если объект с таким id не найден. -} -``` - -## Шаг 3 - -Сделай импорт модуля `contacts.js` в файле `index.js` и проверь работоспособность функций для работы с контактами. - -## Шаг 4 - -В файле `index.js` импортируется пакет `yargs` для удобного парса аргументов командной строки. Используй готовую функцию `invokeAction()` которая получает тип выполняемого действия и необходимые аргументы. Функция вызывает соответствующий метод из файла `contacts.js` передавая ему необходимые аргументы. - -```js -// index.js -const argv = require('yargs').argv; - -// TODO: рефакторить -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -Так же, вы можете использовать модуль [commander](https://www.npmjs.com/package/commander) для парсинга аргументов командной строки. Это более популярная альтернатива модуля `yargs` - -```js -const { Command } = require('commander'); -const program = new Command(); -program - .option('-a, --action ', 'choose action') - .option('-i, --id ', 'user id') - .option('-n, --name ', 'user name') - .option('-e, --email ', 'user email') - .option('-p, --phone ', 'user phone'); - -program.parse(process.argv); - -const argv = program.opts(); - -// TODO: рефакторить -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -## Шаг 5 - -Запусти команды в терминале и сделай отдельный скриншот результата выполнения каждой команды. - -```shell -# Получаем и выводим весь список контактов в виде таблицы (console.table) -node index.js --action list - -# Получаем контакт по id - выводим в консоль объект контакта или null если контакта с таким id не существует -node index.js --action get --id 05olLMgyVQdWRwgKfg5J6 - -# Добавляем контакт и выводим в консоль созданный контакт -node index.js --action add --name Mango --email mango@gmail.com --phone 322-22-22 - -# Удаляем контакт и выводим в консоль удаленный контакт или null если контакта с таким id не существует -node index.js --action remove --id qdggE76Jtbfd9eWJHrssH -``` - -## Шаг 6 - Сдача домашнего задания. - -Скриншоты выполнения команд, можно залить на любой бесплатный облачный сервис хранения картинок (Пример: [monosnap](https://monosnap.com/), [imgbb.com](https://imgbb.com/)) и соответствующие ссылки необходимо добавить в файл README.md. Создайте этот файл в корне проекта. После прикрепите ссылку на репозиторий с домашним заданием в [schoology](https://app.schoology.com/login) для проверки ментором. -Также в файл README.md нужно добавить ссылку на репозиторий со сделанной работой. - -## Критерии приема - -- Создан репозиторий с домашним заданием — CLI приложение -- Задание отправлено ментору в [schoology](https://app.schoology.com/login) на проверку (ссылка на репозиторий) -- Код соответствует техническому заданию проекта -- При выполнении кода не возникает необработанных ошибок -- Название переменных, свойств и методов начинается со строчной буквы и записываются в нотации CamelCase. Используются английские существительные -- Название функции или метода содержит глагол -- В коде нет закомментированных участков кода -- Проект корректно работает в актуальной LTS-версии Node +- `npm start` — старт сервера в режимі production +- `npm run start:dev` — старт сервера в режимі розробки (development) +- `npm run lint` — запустити виконання перевірки коду з eslint, необхідно виконувати перед кожним PR та виправляти всі помилки лінтера +- `npm lint:fix` — та ж перевірка лінтера, але з автоматичними виправленнями простих помилок diff --git a/homework-01/README.pl.md b/homework-01/README.pl.md deleted file mode 100644 index 55e2c79..0000000 --- a/homework-01/README.pl.md +++ /dev/null @@ -1,164 +0,0 @@ -**Czytaj w innych językach: [rosyjski](README.md), [ukraiński](README.ua.md).** - -# Zadanie domowe 1 - -## Krok 1 - -- Zainicjalizuj npm w projekcie. -- W root projektu utwórz plik `index.js`. -- Ustaw pakiet [nodemon](https://www.npmjs.com/package/nodemon) jako zależność opracowywania (devDependencies). -- Do pliku `package.json` dodaj "skrytpy" dla włączenia `index.js`. -- Skrypt `start`, który uruchamia `index.js` przy pomocy `node`. -- Skrypt `start:dev`, który uruchamia `index.js` przy pomocy `nodemon`. - -## Krok 2 - -W root projektu utwórz plik `db`. Dla zapisywania kontaktów ściągnij i wykorzystaj plik [contacts.json](./contacts.json), umieszczając go w folderze `db`. - -W root projektu utwórz plik `contacts.js`. - -- Zaimportuj moduły `fs` i `path` do pracy z systemem plików. -- Utwórz zmienną `contactsPath` i zapisz w niej ścieżkę do pliku `contacts.json`. Do utworzenia ścieżki wykorzystaj metody modułu `path`. -- Dodaj funkcję do pracy ze zbiorem kontaktów. W funcjach wykorzystaj moduł `fs` oraz jego metody `readFile()` i `writeFile()`. -- Zrób eksport utworzonych funkcji przez `module.exports`. - -```js -// contacts.js - -/* - * Skomentuj i zapisz wartość - * const contactsPath = ; - */ - -// TODO: udokumentuj każdą funkcję -function listContacts() { - // ...twój kod -} - -function getContactById(contactId) { - // ...twój kod -} - -function removeContact(contactId) { - // ...twój kod -} - -function addContact(name, email, phone) { - // ...twój kod -} -``` - -## Krok 3 - -Utwórz import modułu `contacts.js` w pliku `index.js` i sprawdź wydajność funkcji dla pracy z kontaktami. - -## Krok 4 - -W pliku `index.js` importuje się pakiet `yargs` dla wygodnego parserowania argumentów wiersza poleceń. Wykorzystaj gotową funkcję `invokeAction()`, która otrzymuje typ wykonywanego działania i niezbędne argumenty. Funkcja wywołuje odpowiednią metodę z pliku `contacts.js`, przekazując mu niezbędne argumenty. - -```js -// index.js -const argv = require('yargs').argv; - -// TODO: refaktor -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -Możesz wykorzystać moduł [commander](https://www.npmjs.com/package/commander) do parserowania argumentów wiersza poleceń. To popularniejsza alternatywa modułu `yargs`. - -```js -const { Command } = require('commander'); -const program = new Command(); -program - .option('-a, --action ', 'choose action') - .option('-i, --id ', 'user id') - .option('-n, --name ', 'user name') - .option('-e, --email ', 'user email') - .option('-p, --phone ', 'user phone'); - -program.parse(process.argv); - -const argv = program.opts(); - -// TODO: refaktor -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -## Krok 5 - -Uruchom polecenia w terminalu i zrób oddzielne screenshoty wyników wykonania każdego polecenia. - -```shell -# Otrzymujemy i wyprowadzamy całą listę kontaktów w postaci tabeli (console.table) -node index.js --action list - -# Otrzymujemy kontakt po id -node index.js --action get --id 05olLMgyVQdWRwgKfg5J6 - -# Dodajemy kontakt -node index.js --action add --name Mango --email mango@gmail.com --phone 322-22-22 - -# Usuwamy kontakt -node index.js --action remove --id qdggE76Jtbfd9eWJHrssH -``` - -## Krok 6 - Oddanie pracy domowej - -Screenshoty wykonania poleceń można wysłać na dowolną, bezpłatną chmurę zapisywania obrazów (Przykład: [monosnap](https://monosnap.com/), [imgbb.com](https://imgbb.com/)) i odpowiednie odnośniki należy dodać do pliku README.md. Utwórz ten plik w root projektu. Następnie dodaj odnośnik do repozytorium z pracą domową do [schoology](https://app.schoology.com/login) dla sprawdzenia przez mentora. - -## Kryteria zaliczenia - -- Utworzone repozytorium z pracą domową — CLI aplikacja. -- Zadanie wysłane do mentora na [schoology](https://app.schoology.com/login) w celu sprawdzenia (odnośnik do repozytorium). -- Kod odpowiada technicznemu zadaniu projektu. -- W trakcie wykonywania kodu nie pojawiają się nieopracowane błędy. -- Nazwanie zmiennych, właściwości i metod zaczyna się z małej litery i zapisuje w notacji CamelCase. Wykorzystywane są angielskie rzeczowniki. -- Nazwa funkcji albo metoda zawiera czasownik. -- W kodzie nie ma skomentowanych fragmentów kodu. -- Projekt działa poprawnie w aktualnej wersji LTS Node. diff --git a/homework-01/README.ua.md b/homework-01/README.ua.md deleted file mode 100755 index 71d91b1..0000000 --- a/homework-01/README.ua.md +++ /dev/null @@ -1,165 +0,0 @@ -**Читати на інших мовах: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнє завдання 1 - -## Крок 1 - -- Ініціалізується npm в проекті -- В корені проекту створи файл `index.js` -- Постав пакет [nodemon](https://www.npmjs.com/package/nodemon) як залежність [nodemon](https://www.npmjs.com/package/nodemon) як залежність розробки (devDependencies) -- В файлі `package.json` додай "скрипти" для запуску `index.js` - - Скрипт `start` який запускає `index.js` за допомогою `node` - - Скрипт `dev` який запускає `index.js` за допомогою `nodemon` - -## Крок 2 - -У корені проекту створи папку `db`. Для зберігання контактів завантаж і використовуй файл [contacts.json](./contacts.json), поклавши його в папку `db`. - -У корені проекту створи файл `contacts.js`. - -- Зроби імпорт модулів `fs` (у версії, яка працює з промісами - `fs/promises`) і `path` для роботи з файловою системою -- Створи змінну `contactsPath` і запиши в неї шлях до файлі `contacts.json`. Для складання шляху використовуй методи модуля `path`. -- Додай функції для роботи з колекцією контактів. У функціях використовуй модуль `fs` та його методи `readFile()` і `writeFile()` -- Зроби експорт створених функцій через `module.exports` - -```js -// contacts.js - -/* - * Розкоментуйте і запишить значення - * const contactsPath = ; - */ - -// TODO: задокументувати кожну функцію -function listContacts() { - // ...твій код. Повертає масив контактів. -} - -function getContactById(contactId) { - // ...твій код. Повертає об'єкт контакту з таким id. Повертає null, якщо контакт з таким id не знайдений. -} - -function removeContact(contactId) { - // ...твій код. Повертає об'єкт видаленого контакту. Повертає null, якщо контакт з таким id не знайдений. -} - -function addContact(name, email, phone) { - // ...твій код. Повертає об'єкт доданого контакту. Повертає null, якщо контакт з таким id не знайдений. -} -``` - -## Крок 3 - -Зроби імпорт модуля `contacts.js` в файлі `index.js` та перевір працездатність функцій для роботи з контактами. - -## Крок 4 - -У файлі `index.js` імпортується пакет `yargs` для зручного парсу аргументів командного рядка. Використовуй готову функцію `invokeAction()` яка отримує тип виконуваної дії і необхідні аргументи. Функція викликає відповідний метод з файлу `contacts.js` передаючи йому необхідні аргументи. - -```js -// index.js -const argv = require('yargs').argv; - -// TODO: рефакторить -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -Так само, ви можете використовувати модуль [commander] (https://www.npmjs.com/package/commander) для парсинга аргументів командного рядка. Це більш популярна альтернатива модуля `yargs` - -```js -const { Command } = require('commander'); -const program = new Command(); -program - .option('-a, --action ', 'choose action') - .option('-i, --id ', 'user id') - .option('-n, --name ', 'user name') - .option('-e, --email ', 'user email') - .option('-p, --phone ', 'user phone'); - -program.parse(process.argv); - -const argv = program.opts(); - -// TODO: рефакторить -function invokeAction({ action, id, name, email, phone }) { - switch (action) { - case 'list': - // ... - break; - - case 'get': - // ... id - break; - - case 'add': - // ... name email phone - break; - - case 'remove': - // ... id - break; - - default: - console.warn('\x1B[31m Unknown action type!'); - } -} - -invokeAction(argv); -``` - -## Крок 5 - -Запусти команди в терміналі і зроби окремий скріншот результату виконання кожної команди. - -```shell -# Отримуємо і виводимо весь список контактів у вигляді таблиці (console.table) -node index.js --action="list" - -# Отримуємо контакт по id і виводимо у консоль об'єкт контакта або null якщо контакту з таким id не існує -node index.js --action="get" --id 05olLMgyVQdWRwgKfg5J6 - -# Додаємо контакт та виводимо в консоль об'єкт новоствореного контакту -node index.js --action="add" --name Mango --email mango@gmail.com --phone 322-22-22 - -# Видаляємо контакт та виводимо в консоль об'єкт видаленого контакту або null якщо контакту з таким id не існує -node index.js --action="remove" --id qdggE76Jtbfd9eWJHrssH -``` - -## Крок 6 - Здача домашнього завдання. - -Скріншоти виконання команд, можна залити на будь-який безкоштовний хмарний сервіс зберігання картинок (Приклад: [monosnap](https://monosnap.com/), [imgbb.com](https://imgbb.com/)) і відповідні посилання необхідно додати в файл README.md. Створіть цей файл в корені проекту. Після прикріпіть посилання на сховище з домашнім завданням в [schoology](https://app.schoology.com/login) для перевірки ментором. -Також у файл README.md треба додати посилання на репозиторій зі зробленою роботою. - -## Критерії прийому - -- Створено репозиторій з домашнім завданням — CLI додаток -- Завдання відправлено менторові в [schoology](https://app.schoology.com/login) на перевірку (посилання на репозиторій) -- Код відповідає технічному завданню проекту -- При виконанні коду не виникає необроблених помилок -- Назва змінних, властивостей і методів починається з малої літери і записуються в нотації CamelCase. Використовуються англійські іменники -- Назва функції або методу містить дієслово -- У коді немає закоментованих ділянок коду -- Проект коректно працює з актуальною LTS-версією Node diff --git a/homework-01/controllers/contacts.js b/homework-01/controllers/contacts.js new file mode 100644 index 0000000..72dbfeb --- /dev/null +++ b/homework-01/controllers/contacts.js @@ -0,0 +1,56 @@ +const { CtrlWrapper, HttpErrors } = require('../helpers/'); // Підключення модулів CtrlWrapper і HttpErrors з папки helpers +const contactsMethods = require('../models/contacts'); // Підключення модуля contactsMethods з папки models/contacts + +const getAll = async (req, res, next) => { + const list = await contactsMethods.listContacts(); // Виклик методу listContacts модуля contactsMethods для отримання списку контактів + res.json(list); // Відправка списку контактів у відповідь у форматі JSON +}; + +const getById = async (req, res, next) => { + const { contactId } = req.params; // Отримання значення параметра contactId з запиту + + const contact = await contactsMethods.getContactById(contactId); // Виклик методу getContactById модуля contactsMethods для отримання контакту за його ідентифікатором + + if (!contact) { + throw HttpErrors(404, 'Not found, man'); // Генерація об'єкта помилки HttpErrors зі статусом 404 та повідомленням "Not found, man" + } + + res.json(contact); // Відправка контакту у відповідь у форматі JSON +}; + +const addContact = async (req, res, next) => { + const newContact = await contactsMethods.addContact(req.body); // Виклик методу addContact модуля contactsMethods для додавання нового контакту з отриманими даними з тіла запиту + res.status(201).json(newContact); // Встановлення статусу відповіді 201 (Створено) та відправка створеного контакту у відповідь у форматі JSON +}; + +const deleteContact = async (req, res, next) => { + const { contactId } = req.params; // Отримання значення параметра contactId з запиту + + const delContact = await contactsMethods.removeContact(contactId); // Виклик методу removeContact модуля contactsMethods для видалення контакту за його ідентифікатором + + if (!delContact) { + throw HttpErrors(404, 'Not found'); // Генерація об'єкта помилки HttpErrors зі статусом 404 та повідомленням "Not found" + } + + res.json(delContact); // Відправка видаленого контакту у відповідь у форматі JSON +}; + +const updateContact = async (req, res, next) => { + const { contactId } = req.params; // Отримання значення параметра contactId з запиту + + const updContact = await contactsMethods.updateContact(contactId, req.body); // Виклик методу updateContact модуля contactsMethods для оновлення контакту за його ідентифікатором з отриманими даними з тіла запиту + + if (!updContact) { + throw HttpErrors(404, 'Not found'); // Генерація об'єкта помилки HttpErrors зі статусом 404 та повідомленням "Not found" + } + + res.json(updContact); // Відправка оновленого контакту у відповідь у форматі JSON +}; + +module.exports = { + getAll: CtrlWrapper(getAll), // Експорт методу getAll, обгорнутого в CtrlWrapper + getById: CtrlWrapper(getById), // Експорт методу getById, обгорнутого в CtrlWrapper + addContact: CtrlWrapper(addContact), // Експорт методу addContact, обгорнутого в CtrlWrapper + deleteContact: CtrlWrapper(deleteContact), // Експорт методу deleteContact, обгорнутого в CtrlWrapper + updateContact: CtrlWrapper(updateContact), // Експорт методу updateContact, обгорнутого в CtrlWrapper +}; diff --git a/homework-01/helpers/CtrlWrapper.js b/homework-01/helpers/CtrlWrapper.js new file mode 100644 index 0000000..8653b84 --- /dev/null +++ b/homework-01/helpers/CtrlWrapper.js @@ -0,0 +1,13 @@ +const CtrlWrapper = ctrl => { + const func = async (req, res, next) => { + try { + await ctrl(req, res, next); // Виклик контролера з переданими параметрами запиту, відповіді та наступної middleware + } catch (error) { + next(error); // Передача отриманої помилки до наступної middleware + } + }; + + return func; // Повернення обгортки контролера +}; + +module.exports = CtrlWrapper; // Експорт функції CtrlWrapper для використання в інших файлів diff --git a/homework-01/helpers/HttpErrors.js b/homework-01/helpers/HttpErrors.js new file mode 100644 index 0000000..22e5089 --- /dev/null +++ b/homework-01/helpers/HttpErrors.js @@ -0,0 +1,7 @@ +const HttpErrors = (status, message) => { + const error = new Error(message); // Створення нового об'єкта помилки з переданим повідомленням + error.status = status; // Присвоєння статусу помилки до властивості "status" об'єкта помилки + return error; // Повернення об'єкта помилки +}; + +module.exports = HttpErrors; // Експорт функції HttpErrors для використання в інших файлів diff --git a/homework-01/helpers/index.js b/homework-01/helpers/index.js new file mode 100644 index 0000000..0e791c1 --- /dev/null +++ b/homework-01/helpers/index.js @@ -0,0 +1,7 @@ +const HttpErrors = require('./HttpErrors.js'); // Підключення модуля HttpErrors з файлу HttpErrors.js +const CtrlWrapper = require('./CtrlWrapper.js'); // Підключення модуля CtrlWrapper з файлу CtrlWrapper.js + +module.exports = { + HttpErrors, // Експорт модуля HttpErrors для використання в інших файлах + CtrlWrapper, // Експорт модуля CtrlWrapper для використання в інших файлах +}; diff --git a/homework-01/middlewares/index.js b/homework-01/middlewares/index.js new file mode 100644 index 0000000..4771615 --- /dev/null +++ b/homework-01/middlewares/index.js @@ -0,0 +1,5 @@ +const validateBody = require('./validateBody'); // Підключення модуля validateBody для валідації запиту + +module.exports = { + validateBody, // Експорт функції validateBody для використання у інших файлів +}; diff --git a/homework-01/middlewares/validateBody.js b/homework-01/middlewares/validateBody.js new file mode 100644 index 0000000..4623201 --- /dev/null +++ b/homework-01/middlewares/validateBody.js @@ -0,0 +1,17 @@ +const { HttpErrors } = require('../helpers'); // Підключення модуля HttpErrors з папки helpers + +const validateBody = schema => { + const func = (req, res, next) => { + const { error } = schema.validate(req.body); // Валідація тіла запиту за допомогою заданої схеми + + if (error) { + next(HttpErrors(400, error.message)); // Якщо виникає помилка валідації, створюється об'єкт помилки HttpErrors зі статусом 400 та повідомленням про помилку + } + + next(); // Передача керування до наступної middleware + }; + + return func; // Повернення функції валідації +}; + +module.exports = validateBody; // Експорт функції validateBody для використання в інших файлів diff --git a/homework-01/models/contacts.js b/homework-01/models/contacts.js new file mode 100644 index 0000000..d420176 --- /dev/null +++ b/homework-01/models/contacts.js @@ -0,0 +1,82 @@ +const fs = require('fs/promises'); // Підключення модуля fs/promises для роботи з файловою системою (асинхронний підхід) +const path = require('path'); // Підключення модуля path для роботи зі шляхами файлів та каталогів +const { nanoid } = require('nanoid'); // Підключення функції nanoid з модуля nanoid для генерації унікальних ідентифікаторів + +const contactsPath = path.join(__dirname, 'contacts.json'); // Встановлення шляху до файлу contacts.json з використанням модуля path + +const listContacts = async () => { + // Функція для отримання списку контактів + const list = await fs.readFile(contactsPath, 'utf-8'); // Асинхронне читання файлу contacts.json + return JSON.parse(list); // Парсинг отриманого списку з формату JSON в об'єкт JavaScript +}; + +const getContactById = async contactId => { + // Функція для отримання контакту за його ідентифікатором + const list = await listContacts(); // Отримання списку контактів + + const contact = list.find(item => item.id === contactId); // Пошук контакту за його ідентифікатором + + return contact || null; // Повернення знайденого контакту або значення null, якщо контакт не знайдено +}; + +const addContact = async body => { + // Функція для додавання контакту + const list = await listContacts(); // Отримання списку контактів + + const newContact = { + // Створення нового контакту з унікальним ідентифікатором та даними з тіла запиту + id: nanoid(), + ...body, + }; + + list.push(newContact); // Додавання нового контакту до списку + + await fs.writeFile(contactsPath, JSON.stringify(list, null, 2)); // Асинхронне записування списку контактів у файл contacts.json + + return newContact; // Повернення доданого контакту +}; + +const removeContact = async contactId => { + // Функція для видалення контакту + const list = await listContacts(); // Отримання списку контактів + + const idx = list.findIndex(item => item.id === contactId); // Пошук індексу контакту за його ідентифікатором + + if (idx === -1) { + // Якщо контакт не знайдено + return null; // Повернення значення null + } + + const [contact] = list.splice(idx, 1); // Видалення контакту зі списку + + await fs.writeFile(contactsPath, JSON.stringify(list, null, 2)); // Асинхронне записування оновленого списку контактів у файл contacts.json + + return contact; // Повернення видаленого контакту +}; + +const updateContact = async (contactId, body) => { + // Функція для оновлення контакту + const list = await listContacts(); // Отримання списку контактів + + const idx = list.findIndex(item => item.id === contactId); // Пошук індексу контакту за його ідентифікатором + + if (idx === -1) { + // Якщо контакт не знайдено + return null; // Повернення значення null + } + + list[idx] = { id: contactId, ...body }; // Оновлення контакту з новими даними + + await fs.writeFile(contactsPath, JSON.stringify(list, null, 2)); // Асинхронне записування оновленого списку контактів у файл contacts.json + + return list[idx]; // Повернення оновленого контакту +}; + +module.exports = { + // Експорт функцій для використання у інших файлах + listContacts, + getContactById, + addContact, + removeContact, + updateContact, +}; diff --git a/homework-01/contacts.json b/homework-01/models/contacts.json similarity index 64% rename from homework-01/contacts.json rename to homework-01/models/contacts.json index a216791..14baea0 100644 --- a/homework-01/contacts.json +++ b/homework-01/models/contacts.json @@ -1,10 +1,4 @@ [ - { - "id": "AeHIrLTr6JkxGE6SN-0Rw", - "name": "Allen Raymond", - "email": "nulla.ante@vestibul.co.uk", - "phone": "(992) 914-3792" - }, { "id": "qdggE76Jtbfd9eWJHrssH", "name": "Chaim Lewis", @@ -58,5 +52,35 @@ "name": "Alec Howard", "email": "Donec.elementum@scelerisquescelerisquedui.net", "phone": "(748) 206-2688" + }, + { + "id": "Cs_BiasmIfd4SqctpF5UD", + "name": "You are", + "email": "the best", + "phone": "i proud of u" + }, + { + "id": "EOfu7wTP81qoer2WFgOBI", + "name": "You are, you are really nice", + "email": "the best developer", + "phone": "i proud of u, love" + }, + { + "id": "zQIUjBISdpgYXFLUjqAid", + "name": "Today is the best", + "email": "nice", + "phone": "day, i am happy" + }, + { + "id": "Yu8Qhjxdqs2H3Yl1ByBTD", + "name": "Today is the the the best best", + "email": "nice nice nice lovely", + "phone": "day, i am happy happy happy" + }, + { + "id": "qOX3O5kyiEAWansLQTJFM", + "name": "Today is the the the best best", + "email": "nice", + "phone": "day, i am happy happy happy" } -] +] \ No newline at end of file diff --git a/homework-01/nodemon.json b/homework-01/nodemon.json new file mode 100644 index 0000000..54d6947 --- /dev/null +++ b/homework-01/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["node_modules", "models/contacts.json"] +} diff --git a/homework-01/routes/api/contacts.js b/homework-01/routes/api/contacts.js new file mode 100644 index 0000000..22a04df --- /dev/null +++ b/homework-01/routes/api/contacts.js @@ -0,0 +1,21 @@ +const express = require('express'); // Підключення модуля Express для створення роутера +const Joi = require('joi'); // Підключення модуля Joi для валідації даних +const ctrl = require('../../controllers/contacts'); // Підключення модуля контролера contacts +const { validateBody } = require('../../middlewares'); // Підключення мідлвари для валідації запиту + +const router = express.Router(); // Створення роутера за допомогою Express + +const addSchema = Joi.object({ + // Визначення схеми валідації для додавання контакту + name: Joi.string().required(), // Вимагається рядок з ім'ям + email: Joi.string().required(), // Вимагається рядок з електронною поштою + phone: Joi.string().required(), // Вимагається рядок з номером телефону +}); + +router.get('/', ctrl.getAll); // Роут для отримання всіх контактів +router.get('/:contactId', ctrl.getById); // Роут для отримання контакту за його ідентифікатором +router.post('/', validateBody(addSchema), ctrl.addContact); // Роут для додавання контакту з використанням мідлвари валідації +router.delete('/:contactId', validateBody(addSchema), ctrl.deleteContact); // Роут для видалення контакту з використанням мідлвари валідації +router.put('/:contactId', validateBody(addSchema), ctrl.updateContact); // Роут для оновлення контакту з використанням мідлвари валідації + +module.exports = router; // Експорт роутера для використання у інших файлів diff --git a/homework-01/server.js b/homework-01/server.js new file mode 100644 index 0000000..79e55cb --- /dev/null +++ b/homework-01/server.js @@ -0,0 +1,6 @@ +const app = require('./app'); // Підключення модуля `app` + +app.listen(3000, () => { + // Запуск сервера на порті 3000 + console.log('Server running. Use our API on port: 3000'); // Вивід повідомлення у консолі +}); diff --git a/homework-03/.eslintignore b/homework-03/.eslintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/homework-03/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/homework-03/.eslintrc.js b/homework-03/.eslintrc.js new file mode 100644 index 0000000..d799332 --- /dev/null +++ b/homework-03/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + }, + extends: ['standard', 'prettier'], + parserOptions: { + ecmaVersion: 12, + }, + rules: {}, +} diff --git a/homework-03/.gitignore b/homework-03/.gitignore new file mode 100644 index 0000000..5c53556 --- /dev/null +++ b/homework-03/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +.env +.env.local +.idea +.vscode \ No newline at end of file diff --git a/homework-03/README.en.md b/homework-03/README.en.md deleted file mode 100644 index 9963878..0000000 --- a/homework-03/README.en.md +++ /dev/null @@ -1,71 +0,0 @@ -**Read in other languages: [Russian](README.md), [Ukrainian](README.ua.md).** - -# Homework 3 - -Create branch `hw03-mongodb` from `master` branch. - -Continue building a REST API to work with the contact collection. - -## Step 1 - -Create an account on [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). Then create a new project in your account and set up a **free cluster**. When setting up the cluster, select the provider and region as in the screenshot below. If you select a region that is too remote, the server response time will be slower. - -![atlas cluster setup](./atlas-cluster.jpg) - -## Step 2 - -Install the graphical editor [MongoDB Compass](https://www.mongodb.com/products/compass) for convenient work with the database for MongoDB. Set up your cloud database connection to Compass. In MongoDB Atlas, don't forget to create an admin user. - -## Step 3 - -Through Compass, create the `db-contacts` database and the `contacts` collection in it. Take [link to json](./contacts.json) and use Compass to populate the `contacts` collection (make an import) with its contents. - -![data](./json-data.png) - -If you did everything right, the data should appear in your database in the `contacts` collection - -![data](./mongo-data.png) -## Step 4 - -Use the source code of [homework #2](../homework-02/README.md) and replace the contact storage from the json file with the database you created. - -- Write code to create a connection to MongoDB using [Mongoose](https://mongoosejs.com/). -- If the connection is successful, print the message `"Database connection successful"` to the console. -- Be sure to handle the connection error. Print an error message to the console and terminate the process using `process.exit(1)`. -- In the request processing functions, replace the code for CRUD operations on contacts from a file with Mongoose methods for working with a collection of contacts in the database. - -Model schema for the `contacts` collection: - -```js - { - name: { - type: String, - required: [true, 'Set name for contact'], - }, - email: { - type: String, - }, - phone: { - type: String, - }, - favorite: { - type: Boolean, - default: false, - }, - } -``` - -## Step 5 - -We have an additional status field `favorite` in contacts, which takes the boolean value `true` or `false`. It is responsible for the fact that the specified contact is in the favorites or not. Implement a new route to update contact status. - -### @ PATCH /api/contacts/:id/favorite - -- Gets the `contactId` parameter -- Gets `body` in JSON format with the update of the `favorite` field -- If there is no `body`, returns JSON with key `{"message": "missing field favorite"}` and status `400` -- If everything is fine with `body`, call the `updateStatusContact(contactId, body)` function (write it) to update the contact in the database -- Based on the result of the function, it returns an updated contact object with a status of `200`. Otherwise, returns JSON with `"message": "Not found"` key and `404` status - - -For the `POST /api/contacts` route, make changes: If the `favorite` field is not specified in `body`, then when saving a new contact to the database, make the `favorite` field equal to the default `false`. Don't forget about data validation! diff --git a/homework-03/README.es.md b/homework-03/README.es.md deleted file mode 100644 index 88c672f..0000000 --- a/homework-03/README.es.md +++ /dev/null @@ -1,71 +0,0 @@ -**Leer en otros idiomas: [Русский](README.md), [Українська](README.ua.md).** - -# Tarea 3 - -Crea una rama `hw03-mongodb` de la rama `master`. - -Continúa con la creación de la API REST para trabajar con una colección de contactos. - -## Paso 1 - -Crea una cuenta en [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). A continuación, cree un nuevo proyecto en su cuenta y configure un **clúster gratuito**. Cuando configure el clúster, seleccione su ISP y su región como se muestra en la captura de pantalla siguiente. Si elige una región demasiado alejada, la velocidad de respuesta del servidor será más lenta. - -![atlas cluster setup](./atlas-cluster.jpg) - -## Paso 2 - -Instala el redactor gráfico [MongoDB Compass](https://www.mongodb.com/products/compass) para un funcionamiento sencillo de la base de datos para MongoDB. Configure la conexión de su base de datos en la nube con Compass. En MongoDB Atlas, recuerda crear un usuario con privilegios de administrador. - -## Paso 3 - -Utilice Compass para crear una base de datos `db-contacts` y cree una colección `contacts` en ella. Usando el [link a json](./contacts.json) y utilizando Compass, llena la colección `contacts` (importándolo) con su contenido. - -![data](./json-data.png) - -Si lo has hecho correctamente, los datos deberían aparecer en tu base de datos en la colección `contacts`. - -![data](./mongo-data.png) -## Paso 4 - -Utiliza el código fuente [tarea #2](../homework-02/README.md) y sustituye el almacenamiento de contactos del archivo json, por la base de datos que has creado. - -- Escribe el código para crear una conexión a MongoDB usando [Mongoose](https://mongoosejs.com/). -- Si la conexión tiene éxito, muestra en la consola el mensaje `"Database connection successful"`. -- Asegúrese de atender el error de conexión. Muestra un mensaje de error en la consola y termina el proceso usando `process.exit(1)`. -- En las funciones de consulta, sustituya el código de las operaciones CRUD sobre contactos del archivo, por métodos de Mongoose para trabajar con una colección de contactos en la base de datos. - -Esquema del modelo de la colección `contacts`: - -```js - { - name: { - type: String, - required: [true, 'Set name for contact'], - }, - email: { - type: String, - }, - phone: { - type: String, - }, - favorite: { - type: Boolean, - default: false, - }, - } -``` - -## Paso 5 - -Tenemos un campo de estado adicional `favorite` en los contactos, que toma un valor booleano de `true` o `false`. Es responsable de que el contacto especificado esté o no en favoritos. Implementa una nueva ruta para actualizar el estado del contacto - -### @ PATCH /api/contacts/:id/favorite - -- Recibe el parámetro `contactId` -- Recibe `body` en formato json con el campo `favorito` actualizado -- Si no se encuentra `body`, devuelve un json con la llave `{"message": "missing field favorite"}` y el estado `400`. -- Si `body` está bien, llama a la función `updateStatusContact(contactId, body)` (escríbela) para actualizar el contacto en la base de datos -- La función devuelve un objeto de contacto actualizado con el estado `200`. En caso contrario, devuelve el json con la llave `"message": "Not found"` y el estado `404`. - - -Para el route `POST /api/contacts`, haz un cambio: si el campo `favorite` no se especifica en `body`, haz que el campo `favorite` sea por defecto `false` al guardar un nuevo contacto en la base de datos. No te olvides de la validación de los datos. diff --git a/homework-03/README.md b/homework-03/README.md index a4e86b2..0e4233d 100644 --- a/homework-03/README.md +++ b/homework-03/README.md @@ -1,71 +1,9 @@ -**Читать на других языках: [Русский](README.md), [Українська](README.ua.md).** +### Команди: -# Домашнее задание 3 +- `npm start` — старт сервера в режимі production +- `npm run start:dev` — старт сервера в режимі розробки (development) +- `npm run lint` — запустити виконання перевірки коду з eslint, необхідно виконувати перед кожним PR та виправляти всі помилки лінтера +- `npm lint:fix` — та ж перевірка лінтера, але з автоматичними виправленнями простих помилок +- `npm lint:fix` — та ж перевірка лінтера, але з автоматичними виправленнями простих помилок -Создай ветку `hw03-mongodb` из ветки `master`. -Продолжи создание REST API для работы с коллекцией контактов. - -## Шаг 1 - -Создай аккаунт на [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). После чего в аккаунте создай новый проект и настрой **бесплатный кластер**. Во время настройки кластера выбери провайдера и регион как на скриншоте ниже. Если выбрать слишком удаленный регион, скорость ответа сервера будет дольше. - -![atlas cluster setup](./atlas-cluster.jpg) - -## Шаг 2 - -Установи графический редактор [MongoDB Compass](https://www.mongodb.com/products/compass) для удобной работы с базой данных для MongoDB. Настрой подключение своей облачной базы данных к Compass. В MongoDB Atlas не забудь создать пользователя с правами администратора. - -## Шаг 3 - -Через Compass создай базу данных `db-contacts` и в ней коллекцию `contacts`. Возьми [ссылка на json](./contacts.json) и при помощи Compass наполни коллекцию `contacts` (сделай импорт) его содержимым. - -![data](./json-data.png) - -Если вы все сделали правильно, данные должны появиться в вашей базе в коллекции `contacts` - -![data](./mongo-data.png) -## Шаг 4 - -Используйте исходный код [домашней работы #2](../homework-02/README.md) и замените хранение контактов из json-файла на созданную вами базу данных. - -- Напишите код для создания подключения к MongoDB при помощи [Mongoose](https://mongoosejs.com/). -- При успешном подключении выведите в консоль сообщение `"Database connection successful"`. -- Обязательно обработайте ошибку подключения. Выведите в консоль сообщение ошибки и завершите процесс используя `process.exit(1)`. -- В функциях обработки запросов замените код CRUD-операций над контактами из файла, на Mongoose-методы для работы с коллекцией контактов в базе данных. - -Схема модели для коллекции `contacts`: - -```js - { - name: { - type: String, - required: [true, 'Set name for contact'], - }, - email: { - type: String, - }, - phone: { - type: String, - }, - favorite: { - type: Boolean, - default: false, - }, - } -``` - -## Шаг 5 - -У нас появилось в контактах дополнительное поле статуса `favorite`, которое принимает логическое значение `true` или `false`. Оно отвечает за то, что в избранном или нет находится указанный контакт. Реализуй для обновления статуса контакта новый маршрут - -### @ PATCH /api/contacts/:id/favorite - -- Получает параметр `contactId` -- Получает `body` в json-формате c обновлением поля `favorite` -- Если `body` нет, возвращает json с ключом `{"message": "missing field favorite"}` и статусом `400` -- Если с `body` все хорошо, вызывает функцию `updateStatusContact(contactId, body)` (напиши ее) для обновления контакта в базе -- По результату работы функции возвращает обновленный объект контакта и статусом `200`. В противном случае, возвращает json с ключом `"message": "Not found"` и статусом `404` - - -Для роута `POST /api/contacts` внесите изменения: если поле `favorite` не указали в `body`, то при сохранении в базу нового контакта, сделайте поле `favorite` равным по умолчанию `false`. Не забываем про валидацию данных! \ No newline at end of file diff --git a/homework-03/README.pl.md b/homework-03/README.pl.md deleted file mode 100644 index ed79dd9..0000000 --- a/homework-03/README.pl.md +++ /dev/null @@ -1,72 +0,0 @@ -**Czytaj w innych językach: [rosyjski](README.md), [ukraiński](README.ua.md).** - -# Zadanie domowe 3 - -Utwórz gałąź `hw03-mongodb` z gałęzi `master`. - -Kontynuuj tworzenie REST API do pracy ze zbiorem kontaktów. - -## Krok 1 - -Stwórz konto na [MongoDB Atlas](https://www.mongodb.com/cloud/atlas), a następnie na koncie utwórz nowy projekt i skonfiguruj **bezpłatny klaster**. W czasie konfigurowania klastera wybierz provider i region, jak na screenshocie poniżej. Jeżeli wybierzesz zbyt oddalony region, serwer odpowie wolniej. - -![atlas cluster setup](./atlas-cluster.jpg) - -## Krok 2 - -Skonfiguruj edytor graficzny [MongoDB Compass](https://www.mongodb.com/products/compass) do wygodnej pracy z bazą danych dla MongoDB. Skonfiguruj podłączenie swojej chmury do Compass. W MongoDB Atlas nie zapomnij utworzyć użytkownika z prawami administratora. - -## Krok 3 - -Przez Compass utwórz bazę danych `db-contacts`, a w niej zbiór `contacts`. Weź [odnośnik do json](./contacts.json) i przy pomocy Compass wypełnij zbiór `contacts` (zaimportuj) jego zawartością. - -![data](./json-data.png) - -Jeżeli wszystko zrobiłeś prawidłowo, dane powinny się pojawić w twojej bazie w zbiorze `contacts` - -![data](./mongo-data.png). - -## Krok 4 - -Wykorzystaj kod źródłowy [zadania domowego #2](../homework-02/README.md) i zamień zapisywanie kontaktów z pliku json na utworzoną przez siebie bazę danych. - -- Napisz kod do utworzenia podłączenia do MongoDB przy pomocy [Mongoose](https://mongoosejs.com/). -- Przy sukcesie podłączenia wyprowadź na konsolę wiadomość `"Database connection successful"`. -- Obowiązkowo opracuj błąd podłączenia. Wyprowadź na konsolę wiadomość o błędzie i zakończ proces, wykorzystując `process.exit(1)`. -- W funkcjach opracowywania zapytań zamień kod operacji CRUD na kontaktach z pliku, na metody Mongoose do pracy ze zbiorem kontaktów w bazie danych. - -Schemat modeli dla zbioru `contacts`: - -```js - { - name: { - type: String, - required: [true, 'Set name for contact'], - }, - email: { - type: String, - }, - phone: { - type: String, - }, - favorite: { - type: Boolean, - default: false, - }, - } -``` - -## Krok 5 - -W naszych kontaktach pojawiło się dodatkowe pole statusu `favorite`, które przyjmuje logiczną wartość `true` lub `false`. Odpowiada ono za to, że wskazany kontakt znajduje się lub nie w ulubionych. Zrealizuj dla aktualizacji statusu kontaktu nową trasę. - -### @ PATCH /api/contacts/:id/favorite - -- Otrzymuje parametr `contactId`. -- Otrzymuje `body` w formacie json z aktualizacją pola `favorite`. -- Jeżeli `body` nie ma, zwraca json z kluczem `{"message": "missing field favorite"}` i statusem `400`. -- Jeżeli w `body` wszystko się zgadza to wywołaj funkcję `updateStatusContact(contactId, body)` (napisz ją), aby zaktualizować kontakt w bazie danych -- W wyniku pracy funkcji zwraca zaktualizowany obiekt kontaktu ze statusem `200`. W przeciwnym razie zwraca json z kluczem `"message": "Not found"` i statusem `404`. - - -Dla routa `POST /api/contacts` wprowadź zmiany: jeśli pole `favorite` nie zostało wskazane w `body`, to przy zapisaniu w bazie nowego kontaktu ustaw pole `favorite` domyślnie w `false`. Nie zapominajmy o walidacji danych! diff --git a/homework-03/README.ua.md b/homework-03/README.ua.md deleted file mode 100755 index 0791cc5..0000000 --- a/homework-03/README.ua.md +++ /dev/null @@ -1,77 +0,0 @@ -**Читати на інших мовах: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнє завдання 3 - -Створи гілку `03-mongodb` з гілки `master`. - -Продовж створення REST API для роботи з колекцією контактів. - -## Крок 1 - -Створи аккаунт на [MongoDB Atlas](https://www.mongodb.com/cloud/atlas). Після чого в акаунті створи новий проект і налаштуй **безкоштовний кластер**. Під час налаштування кластера вибери провавйдера і регіон як на скріншоті нижче. Якщо вибрати занадто віддалений регіон, швидкість відповіді сервера буде довше. - -![atlas cluster setup](./atlas-cluster.jpg) - -## Крок 2 - -Установи графічний редактор -[MongoDB Compass](https://www.mongodb.com/products/compass) для зручної -роботи з базою даних для MongoDB. Налаштуй підключення своєї хмарної бази даних -до Compass. У MongoDB Atlas не забудь створити користувача з правами -адміністратора. - -## Крок 3 - -Через Compass створи базу даних `db-contacts` і в ній колекцію `contacts`. Візьми [посилання на json](./contacts) і за допомогою Compass наповни колекцію `contacts` (зроби імпорт) його вмістом. - -![data](./json-data.png) - -Якщо ви все зробили правильно, дані повинні з'явитися у вашій базі в колекції `contacts` - -![data](./mongo-data.png) - -## Крок 4 - -Використовуй вихідний код [домашньої роботи #2](../homework-02/README.md) і -заміни зберігання контактів з json-файлу на створену тобою базу даних. - -- Напиши код для створення підключення до MongoDB за допомогою [Mongoose](https://mongoosejs.com/). -- При успішному підключенні виведи в консоль повідомлення `"Database connection successful"`. -- Обов'язково обробив помилку підключення. Виведи в консоль повідомлення помилки і заверши процес використовуючи `process.exit(1)`. -- У функціях обробки запитів заміни код CRUD-операцій над контактами з файлу, на Mongoose-методи для роботи з колекцією контактів в базі даних. - -Схема моделі для колекції `contacts`: - -```js - { - name: { - type: String, - required: [true, 'Set name for contact'], - }, - email: { - type: String, - }, - phone: { - type: String, - }, - favorite: { - type: Boolean, - default: false, - }, - } -``` - -## Крок 5 - -У нас з'явилося в контактах додаткове поле статусу `favorite`, яке приймає логічне значення` true` або `false`. Воно відповідає за те, що в обраному чи ні знаходиться зазначений контакт. Потрібно реалізувати для оновлення статусу контакту новий роутер - -### @ PATCH /api/contacts/:id/favorite - -- Отримує параметр `contactId` -- Отримує `body` в json-форматі з оновленням поля` favorite` -- Якщо `body` немає, повертає json з ключем` { "message": "missing field favorite"} `і статусом` 400` -- Якщо з `body` все добре, викликає функцію` updateStatusContact (contactId, body)` (напиши її) для поновлення контакту в базі) -- За результатом роботи функції повертає оновлений об'єкт контакту і статусом `200`. В іншому випадку, повертає json з ключем `" message ":" Not found "` і статусом `404` - - -Для роута `POST /api/contacts` внеси зміни: якщо поле `favorite` не вказали в `body`, то при збереженні в базу нового контакту, зроби поле `favorite` рівним за замовчуванням `false` \ No newline at end of file diff --git a/homework-03/atlas-cluster.jpg b/homework-03/atlas-cluster.jpg deleted file mode 100644 index 1002e72..0000000 Binary files a/homework-03/atlas-cluster.jpg and /dev/null differ diff --git a/homework-03/contacts.json b/homework-03/contacts.json deleted file mode 100644 index f2fa2e4..0000000 --- a/homework-03/contacts.json +++ /dev/null @@ -1,62 +0,0 @@ -[ - { - "name": "Allen Raymond", - "email": "nulla.ante@vestibul.co.uk", - "phone": "(992) 914-3792", - "favorite": false - }, - { - "name": "Chaim Lewis", - "email": "dui.in@egetlacus.ca", - "phone": "(294) 840-6685", - "favorite": true - }, - { - "name": "Kennedy Lane", - "email": "mattis.Cras@nonenimMauris.net", - "phone": "(542) 451-7038", - "favorite": false - }, - { - "name": "Wylie Pope", - "email": "est@utquamvel.net", - "phone": "(692) 802-2949", - "favorite": true - }, - { - "name": "Cyrus Jackson", - "email": "nibh@semsempererat.com", - "phone": "(501) 472-5218", - "favorite": true - }, - { - "name": "Abbot Franks", - "email": "scelerisque@magnis.org", - "phone": "(186) 568-3720", - "favorite": true - }, - { - "name": "Reuben Henry", - "email": "pharetra.ut@dictum.co.uk", - "phone": "(715) 598-5792", - "favorite": true - }, - { - "name": "Simon Morton", - "email": "dui.Fusce.diam@Donec.com", - "phone": "(233) 738-2360", - "favorite": true - }, - { - "name": "Thomas Lucas", - "email": "nec@Nulla.com", - "phone": "(704) 398-7993", - "favorite": false - }, - { - "name": "Alec Howard", - "email": "Donec.elementum@scelerisquescelerisquedui.net", - "phone": "(748) 206-2688", - "favorite": true - } -] diff --git a/homework-03/controllers/contacts.js b/homework-03/controllers/contacts.js new file mode 100644 index 0000000..edb8535 --- /dev/null +++ b/homework-03/controllers/contacts.js @@ -0,0 +1,76 @@ +const { Contact, addSchema, updateFavoriteSchema } = require('../models/contact'); + +const { HttpError } = require('../helpers'); +const ctrlWrapper = require('../helpers/ctrlWrapper'); + +const listContacts = async (req, res, next) => { + const result = await Contact.find(); + res.json(result); + console.log(result); +}; + +const getById = async (req, res, next) => { + const { id } = req.params; + const result = await Contact.findById(id); + if (!result) { + throw HttpError(404, 'Not found'); + } + res.json(result); +}; + +const addContact = async (req, res, next) => { + const { error } = addSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing required name field'); + } + const result = await Contact.create(req.body); + res.status(201).json(result); +}; + +const updateById = async (req, res, next) => { + const { error } = addSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing fields'); + } + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + if (!result) { + throw HttpError(404, 'Not found'); + } + res.json(result); +}; + +const updateStatusContact = async (req, res) => { + const { error } = updateFavoriteSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing field favorite'); + } + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + if (!result) { + throw HttpError(404, 'Not found'); + } + res.json(result); +}; + +const removeContact = async (req, res, next) => { + console.log(req.params); + const { id } = req.params; + const result = await Contact.findByIdAndDelete(id); + + if (!result) { + throw HttpError(404, 'Not found'); + } + res.json({ + message: 'contact deleted', + }); +}; + +module.exports = { + listContacts: ctrlWrapper(listContacts), + getById: ctrlWrapper(getById), + addContact: ctrlWrapper(addContact), + updateById: ctrlWrapper(updateById), + updateStatusContact: ctrlWrapper(updateStatusContact), + removeContact: ctrlWrapper(removeContact), +}; diff --git a/homework-03/helpers/HttpError.js b/homework-03/helpers/HttpError.js new file mode 100644 index 0000000..3f38b50 --- /dev/null +++ b/homework-03/helpers/HttpError.js @@ -0,0 +1,7 @@ +const HttpError = (status, message) => { + const error = new Error(message); + error.status = status; + return error; +}; + +module.exports = HttpError; diff --git a/homework-03/helpers/ctrlWrapper.js b/homework-03/helpers/ctrlWrapper.js new file mode 100644 index 0000000..4e36751 --- /dev/null +++ b/homework-03/helpers/ctrlWrapper.js @@ -0,0 +1,13 @@ +const ctrlWrapper = ctrl => { + const func = async (req, res, next) => { + try { + await ctrl(req, res, next); + } catch (error) { + next(error); + } + }; + + return func; +}; + +module.exports = ctrlWrapper; diff --git a/homework-03/helpers/handleMongooseError.js b/homework-03/helpers/handleMongooseError.js new file mode 100644 index 0000000..a3540bb --- /dev/null +++ b/homework-03/helpers/handleMongooseError.js @@ -0,0 +1,6 @@ +const handleMongooseError = (error, data, next) => { + error.status = 400; + next(); +}; + +module.exports = handleMongooseError; diff --git a/homework-03/helpers/index.js b/homework-03/helpers/index.js new file mode 100644 index 0000000..67fd13d --- /dev/null +++ b/homework-03/helpers/index.js @@ -0,0 +1,9 @@ +const HttpError = require('./HttpError'); +const ctrl = require('./ctrlWrapper'); +const handleMongooseError = require('./handleMongooseError'); + +module.exports = { + HttpError, + ctrl, + handleMongooseError, +}; diff --git a/homework-03/json-data.png b/homework-03/json-data.png deleted file mode 100644 index 904c47e..0000000 Binary files a/homework-03/json-data.png and /dev/null differ diff --git a/homework-03/middlewares/index.js b/homework-03/middlewares/index.js new file mode 100644 index 0000000..328e4eb --- /dev/null +++ b/homework-03/middlewares/index.js @@ -0,0 +1,2 @@ +const isValidId = require('./isValidId'); +module.exports = isValidId; diff --git a/homework-03/middlewares/isValidId.js b/homework-03/middlewares/isValidId.js new file mode 100644 index 0000000..41cd311 --- /dev/null +++ b/homework-03/middlewares/isValidId.js @@ -0,0 +1,14 @@ +const { isValidObjectId } = require('mongoose'); + +const { HttpError } = require('../helpers'); + +const isValidId = (req, res, next) => { + const { id } = req.params; + if (!isValidObjectId(id)) { + next(HttpError(400, `${id} is not valid id`)); + } + + next(); +}; + +module.exports = isValidId; diff --git a/homework-03/models/contact.js b/homework-03/models/contact.js new file mode 100644 index 0000000..a55c1c7 --- /dev/null +++ b/homework-03/models/contact.js @@ -0,0 +1,39 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const contactSchema = new Schema( + { + name: { + type: String, + required: [true, 'Set name for contact'], + }, + email: { + type: String, + }, + phone: { + type: String, + }, + favorite: { + type: Boolean, + default: false, + }, + }, + { versionKey: false }, +); + +contactSchema.post('save', handleMongooseError); + +const Contact = model('contact', contactSchema); + +const addSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), +}); + +const updateFavoriteSchema = Joi.object({ + favorite: Joi.boolean().required(), +}); + +module.exports = { Contact, addSchema, updateFavoriteSchema }; diff --git a/homework-03/mongo-data.png b/homework-03/mongo-data.png deleted file mode 100644 index c46c427..0000000 Binary files a/homework-03/mongo-data.png and /dev/null differ diff --git a/homework-03/nodemon.json b/homework-03/nodemon.json new file mode 100644 index 0000000..54d6947 --- /dev/null +++ b/homework-03/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["node_modules", "models/contacts.json"] +} diff --git a/homework-03/routes/api/contacts.js b/homework-03/routes/api/contacts.js new file mode 100644 index 0000000..455a1cc --- /dev/null +++ b/homework-03/routes/api/contacts.js @@ -0,0 +1,21 @@ +const express = require('express'); + +const router = express.Router(); + +const ctrl = require('../../controllers/contacts'); + +const isValidId = require('../../middlewares'); + +router.get('/', ctrl.listContacts); + +router.get('/:id', ctrl.getById); + +router.post('/', ctrl.addContact); + +router.put('/:id', ctrl.updateById); + +router.patch('/:id/favorite', isValidId, ctrl.updateStatusContact); + +router.delete('/:id', ctrl.removeContact); + +module.exports = router; diff --git a/homework-03/server.js b/homework-03/server.js new file mode 100644 index 0000000..18f8256 --- /dev/null +++ b/homework-03/server.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); + +const app = require('./app'); + +const { DB_HOST, PORT = 3000 } = process.env; +// console.log(DB_HOST); + +mongoose + .connect(DB_HOST) + .then(() => { + app.listen(PORT); + }) + .catch(error => { + console.log(error.message); + process.exit(1); + }); diff --git a/homework-04/.eslintignore b/homework-04/.eslintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/homework-04/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/homework-04/.eslintrc.js b/homework-04/.eslintrc.js new file mode 100644 index 0000000..d799332 --- /dev/null +++ b/homework-04/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + }, + extends: ['standard', 'prettier'], + parserOptions: { + ecmaVersion: 12, + }, + rules: {}, +} diff --git a/homework-04/.gitignore b/homework-04/.gitignore new file mode 100644 index 0000000..8877fc5 --- /dev/null +++ b/homework-04/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.idea +.vscode \ No newline at end of file diff --git a/homework-04/README.en.md b/homework-04/README.en.md deleted file mode 100644 index bf1a16a..0000000 --- a/homework-04/README.en.md +++ /dev/null @@ -1,255 +0,0 @@ -**Read in other languages: [Russian](README.md), [Ukrainian](README.ua.md).** - -# Homework 4 - -Create a `hw04-auth` branch from the `master` branch. - -Continue building a REST API to work with the contact collection. Add user authentication/authorization logic using [JWT](https://jwt.io/). - -## Step 1 - -In code, create a user schema and model for the `users` collection. - -```js -{ - password: { - type: String, - required: [true, 'Password is required'], - }, - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - }, - subscription: { - type: String, - enum: ["starter", "pro", "business"], - default: "starter" - }, - token: { - type: String, - default: null, - }, -} -``` - -For each user to work and see only their contacts in the contact scheme, add the `owner` property. - -```js - owner: { - type: Schema.Types.ObjectId, - ref: 'user', - } -``` -Note: `'user'` is the name of the collection (singular) in which users are stored. - -## Step 2 - -### Registration - -Create an endpoint [`/users/signup`](#registration-request). - -Validate all required fields (`email` and `password`). Return on validation error -[Validation error](#registration-validation-error). - -In case of successful validation in the `User` model, create a user according to the data that has passed validation. For password salting use [bcrypt](https://www.npmjs.com/package/bcrypt) or [bcryptjs](https://www.npmjs.com/package/bcryptjs). - -- If the mail is already in use by someone else, return [Error Conflict](#registration-conflict-error). -- Otherwise return [Successful response](#registration-success-response). - -#### Registration Request - -```shell -POST /users/signup -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Registration Validation Error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Registration Conflict Error - -```shell -Status: 409 Conflict -Content-Type: application/json -ResponseBody: { - "message": "Email in use" -} -``` - -#### Registration Success Response - -```shell -Status: 201 Created -Content-Type: application/json -ResponseBody: { - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -### Login - -Create an endpoint [`/users/login`](#login-request) - -In the `User` model, find the user by `email`. - -Validate all required fields (`email` and `password`). If validation fails, return [Validation Error](#validation-error-login). - -- Otherwise, compare password for found user, if passwords match create token, store in current user and return a [Successful response](#login-success-response). - -- If password or email is incorrect, return [Error Unauthorized](#login-auth-error). - -#### Login request - -```shell -POST /users/login -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Login validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Login success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "token": "exampletoken", - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -#### Login auth error - -```shell -Status: 401 Unauthorized -ResponseBody: { - "message": "Email or password is wrong" -} -``` - -## Step 3 - -### Checking the token - -Create a middleware to validate the token and add it to all routes that need to be secured. - -- Middleware takes the token from the `Authorization` headers, checks the token for validity -- Return [Unauthorized Error](#middleware-unauthorized-error) on error -- If the validation was successful, get the user's `id` from the token. Find a user in the database by this id -- If the user exists and the token matches what is in the database, write his data to `req.user` and call the `next()` method -- If no user with that `id` exists or tokens don't match, return [Unauthorized Error](#middleware-unauthorized-error) - -#### Middleware unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -## Step 4 - -### Logout - -Create an endpoint [`/users/logout`](#logout-request) - -Add token verification middleware to the route. - -- In the `User` model, find the user by `_id` -- If the user does not exist, return [Error Unauthorized](#logout-unauthorized-error) -- Otherwise, delete the token in the current user and return [Successful response](#logout-success-response) - -#### Logout Request - -```shell -GET /users/logout -Authorization: "Bearer {{token}}" -``` - -#### Logout Unauthorized Error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Logout Success Response - -```shell -Status: 204 No Content -``` - -## Step 5 -### Current User - Get User Data by Token - -Create an endpoint [`/users/current`](#current-user-request) - -Add token verification middleware to the route. - -- If user does not have return [Error Unauthorized](#current-user-unauthorized-error) -- Otherwise, return [Successful response](#current-user-success-response) - -#### Current User Request - -```shell -GET /users/current -Authorization: "Bearer {{token}}" -``` - -#### Current User Unauthorized Error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Current User Success Response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "email": "example@example.com", - "subscription": "starter" -} -``` - -## Additional Task - Optional - -- Make pagination for the collection of contacts (GET /contacts?page=1&limit=20) -- Filter contacts by favorite field (GET /contacts?favorite=true) -- Updating a user's `subscription` via the `PATCH` `/users` endpoint. The subscription must have one of the following values `['starter', 'pro', 'business']` diff --git a/homework-04/README.es.md b/homework-04/README.es.md deleted file mode 100644 index 74ae915..0000000 --- a/homework-04/README.es.md +++ /dev/null @@ -1,254 +0,0 @@ -**Leer en otros idiomas: [Русский](README.md), [Українська](README.ua.md).** - -# Tarea 4 - -Crea una rama `hw04-auth` de la rama `master`. - -Continúe con la creación de una API REST para manejar la colección de contactos. Añadir la lógica de autenticación/autorización de usuarios mediante [JWT](https://jwt.io/). - -## Paso 1 - -En el código, crea un esquema y un modelo de usuario para la colección `users`. - -```js -{ - password: { - type: String, - required: [true, 'Password is required'], - }, - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - }, - subscription: { - type: String, - enum: ["starter", "pro", "business"], - default: "starter" - }, - token: { - type: String, - default: null, - }, -} -``` - -Para que cada usuario funcione y vea sólo sus propios contactos en el esquema de contactos, añada la propiedad `owner`. - -```js - owner: { - type: Schema.Types.ObjectId, - ref: 'user', - } -``` -Nota: `'user'` es el nombre de la colección (única) en la que se almacenan los usuarios. - -## Paso 2 - -### Registro - -Crea el endpoint [`/users/signup`](#registration-request) - -Valida todos los campos obligatorios (`email` y `password`). Si se produce un error de validación, devuelve -[error de validación](#registration-validation-error). - -Si la validación es exitosa en el modelo `User`, crea un usuario basado en los datos validados. Para ponerle sal a las contraseñas, utilice [bcrypt](https://www.npmjs.com/package/bcrypt) o [bcryptjs](https://www.npmjs.com/package/bcryptjs) - -- Si el correo ya está en uso por otra persona, devuelve [Error Conflict](#registration-conflict-error). -- En caso contrario, devuelve [Respuesta exitosa](#registration-success-response). - -#### Registration request - -```shell -POST /users/signup -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Registration validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Registration conflict error - -```shell -Status: 409 Conflict -Content-Type: application/json -ResponseBody: { - "message": "Email in use" -} -``` - -#### Registration success response - -```shell -Status: 201 Created -Content-Type: application/json -ResponseBody: { - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -### Login - -Crea el endpoint [`/users/login`](#login-request) - -En el modelo `User`, busca el usuario por `email`. - -Valide todos los campos obligatorios (`email` y `password`). Si la validación falla, devuelve [Error de validación](#validation-error-login). - -- En caso contrario, compara la contraseña del usuario encontrado, si las contraseñas coinciden crea un token, guárdalo en el usuario actual y devuelve [Respuesta exitosa](#login-success-response). -- Si la contraseña o el correo electrónico son incorrectos, devuelve [Error Unauthorized](#login-auth-error). - -#### Login request - -```shell -POST /users/login -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Login validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Login success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "token": "exampletoken", - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -#### Login auth error - -```shell -Status: 401 Unauthorized -ResponseBody: { - "message": "Email or password is wrong" -} -``` - -## Paso 3 - -### Comprobación del token - -Crea un middleware para comprobar el token y añádelo a todas las rutas que deban ser protegidas. - -- El middleware toma el token de los encabezados de `Authorization`, comprueba la validez del token. -- En caso de error, devuelve [Error Unauthorized](#middleware-unauthorized-error). -- Si la validación es exitosa, recupera el `id` del usuario del token. Encuentra el usuario en la base de datos a partir de este ID. -- Si el usuario existe y el token coincide con el de la base de datos, escribe sus datos en `req.user` y llama al método `next()`. -- Si el usuario con este `id` no existe o los tokens no coinciden, devuelve [Error Unauthorized](#middleware-unauthorized-error) - -#### Middleware unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -## Paso 4 - -### Logout - -Crea el endpoint [`/users/logout`](#logout-request) - -Añade a la ruta el middleware para la comprobacióon de tokens. - -- En el modelo `User`, busca el usuario por `_id`. -- Si el usuario no existe, devuelve [Error Unauthorized](#logout-unauthorized-error). -- En caso contrario, elimina el token en el usuario actual y devuelve [Respuesta exitosa](#logout-success-response). - -#### Logout request - -```shell -GET /users/logout -Authorization: "Bearer {{token}}" -``` - -#### Logout unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Logout success response - -```shell -Status: 204 No Content -``` - -## Paso 5 -### Usuario actual. Recuperar los datos del usuario según el token - -Crea el endpoint [`/users/current`](#current-user-request) - -Añade a la ruta el middleware para la comprobacióon de tokens. - -- Si el usuario no existe, devuelve [Error Unauthorized](#current-user-unauthorized-error) -- EN caso contrario devuelve [Respuesta exitosa](#current-user-success-response) - -#### Current user request - -```shell -GET /users/current -Authorization: "Bearer {{token}}" -``` - -#### Current user unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Current user success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "email": "example@example.com", - "subscription": "starter" -} -``` - -## Tarea adicional (opcional) - -- Hacer una paginación de la colección de contactos (GET /contacts?page=1&limit=20). -- Filtra los contactos por favoritos (GET /contacts?favorite=true) -- Renovación de la suscripción (`subscription`) del usuario a travez del endpoint `PATCH` `/users`. La suscripción debe tener uno de los siguientes valores `['starter', 'pro', 'business']` diff --git a/homework-04/README.md b/homework-04/README.md index 3d908cf..b67d049 100644 --- a/homework-04/README.md +++ b/homework-04/README.md @@ -1,254 +1,6 @@ -**Читать на других языках: [Русский](README.md), [Українська](README.ua.md).** +### Команди: -# Домашнее задание 4 - -Создайте ветку `hw04-auth` из ветки `master`. - -Продолжите создание REST API для работы с коллекцией контактов. Добавьте логику аутентификации/авторизации пользователя с помощью [JWT](https://jwt.io/). - -## Шаг 1 - -В коде создайте схему и модель пользователя для коллекции `users`. - -```js -{ - password: { - type: String, - required: [true, 'Password is required'], - }, - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - }, - subscription: { - type: String, - enum: ["starter", "pro", "business"], - default: "starter" - }, - token: { - type: String, - default: null, - }, -} -``` - -Чтобы каждый пользователь работал и видел только свои контакты в схеме контактов добавьте свойство `owner` - -```js - owner: { - type: Schema.Types.ObjectId, - ref: 'user', - } -``` -Примечание: `'user'` - название коллекции (в единственном числе), в которой хранятся пользователи. - -## Шаг 2 - -### Регистрация - -Создайте эндпоинт [`/users/signup`](#registration-request) - -Сделать валидацию всех обязательных полей (`email` и `password`). При ошибке валидации вернуть -[Ошибку валидации](#registration-validation-error). - -В случае успешной валидации в модели `User` создать пользователя по данным которые прошли валидацию. Для засолки паролей используй [bcrypt](https://www.npmjs.com/package/bcrypt) или [bcryptjs](https://www.npmjs.com/package/bcryptjs) - -- Если почта уже используется кем-то другим, вернуть [Ошибку Conflict](#registration-conflict-error). -- В противном случае вернуть [Успешный ответ](#registration-success-response). - -#### Registration request - -```shell -POST /users/signup -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Registration validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Ошибка от Joi или другой библиотеки валидации> -``` - -#### Registration conflict error - -```shell -Status: 409 Conflict -Content-Type: application/json -ResponseBody: { - "message": "Email in use" -} -``` - -#### Registration success response - -```shell -Status: 201 Created -Content-Type: application/json -ResponseBody: { - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -### Логин - -Создайте эндпоинт [`/users/login`](#login-request) - -В модели `User` найти пользователя по `email`. - -Сделать валидацию всех обязательных полей (`email` и `password`). При ошибке валидации вернуть [Ошибку валидации](#validation-error-login). - -- В противном случае, сравнить пароль для найденного юзера, если пароли совпадают создать токен, сохранить в текущем юзере и вернуть [Успешный ответ](#login-success-response). -- Если пароль или email неверный, вернуть [Ошибку Unauthorized](#login-auth-error). - -#### Login request - -```shell -POST /users/login -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Login validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Ошибка от Joi или другой библиотеки валидации> -``` - -#### Login success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "token": "exampletoken", - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -#### Login auth error - -```shell -Status: 401 Unauthorized -ResponseBody: { - "message": "Email or password is wrong" -} -``` - -## Шаг 3 - -### Проверка токена - -Создайте мидлвар для проверки токена и добавь его ко всем маршрутам, которые должны быть защищены. - -- Мидлвар берет токен из заголовков `Authorization`, проверяет токен на валидность. -- В случае ошибки вернуть [Ошибку Unauthorized](#middleware-unauthorized-error). -- Если валидация прошла успешно, получить из токена `id` пользователя. Найти пользователя в базе данных по этому id. -- Если пользователь существует и токен совпадает с тем, что находится в базе, записать его данные в `req.user` и вызвать метод`next()`. -- Если пользователя с таким `id` не существует или токены не совпадают, вернуть [Ошибку Unauthorized](#middleware-unauthorized-error) - -#### Middleware unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -## Шаг 4 - -### Логаут - -Создайте ендпоинт [`/users/logout`](#logout-request) - -Добавьте в маршрут мидлвар проверки токена. - -- В модели `User` найти пользователя по `_id`. -- Если пользователя не существует вернуть [Ошибку Unauthorized](#logout-unauthorized-error). -- В противном случае, удалить токен в текущем юзере и вернуть [Успешный ответ](#logout-success-response). - -#### Logout request - -```shell -GET /users/logout -Authorization: "Bearer {{token}}" -``` - -#### Logout unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Logout success response - -```shell -Status: 204 No Content -``` - -## Шаг 5 -### Текущий пользователь - получить данные юзера по токену - -Создайте эндпоинт [`/users/current`](#current-user-request) - -Добавьте в маршрут мидлвар проверки токена. - -- Если пользователя не существует вернуть [Ошибку Unauthorized](#current-user-unauthorized-error) -- В противном случае вернуть [Успешный ответ](#current-user-success-response) - -#### Current user request - -```shell -GET /users/current -Authorization: "Bearer {{token}}" -``` - -#### Current user unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Current user success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "email": "example@example.com", - "subscription": "starter" -} -``` - -## Дополнительное задание - необязательное - -- Сделать пагинацию для коллекции контактов (GET /contacts?page=1&limit=20). -- Сделать фильтрацию контактов по полю избранного (GET /contacts?favorite=true) -- Обновление подписки (`subscription`) пользователя через эндпоинт `PATCH` `/users`. Подписка должна иметь одно из следующих значений `['starter', 'pro', 'business']` +- `npm start` — старт сервера в режимі production +- `npm run start:dev` — старт сервера в режимі розробки (development) +- `npm run lint` — запустити виконання перевірки коду з eslint, необхідно виконувати перед кожним PR та виправляти всі помилки лінтера +- `npm lint:fix` — та ж перевірка лінтера, але з автоматичними виправленнями простих помилок diff --git a/homework-04/README.pl.md b/homework-04/README.pl.md deleted file mode 100644 index 2008167..0000000 --- a/homework-04/README.pl.md +++ /dev/null @@ -1,253 +0,0 @@ -**Czytaj w innych językach: [rosyjski](README.md), [ukraiński](README.ua.md).** - -# Zadanie domowe 4 - -Utwórz gałąź `hw04-auth` z gałęzi `master`. - -Kontynuuj tworzenie REST API do pracy ze zbiorem kontaktów. Dodaj logikę uwierzytelnienia/autoryzacji użytkownika przy pomocy [JWT](https://jwt.io/). - -## Krok 1 - -Utwórz w kodzie schemat i model użytkownika dla zbioru `users`. - -```js -{ - password: { - type: String, - required: [true, 'Password is required'], - }, - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - }, - subscription: { - type: String, - enum: ["starter", "pro", "business"], - default: "starter" - }, - token: { - type: String, - default: null, - }, -} -``` - -Aby każdy użytkownik działał i widział tylko swoje kontakty w schemacie kontaktów, dodaj właściwość `owner`. - -```js - owner: { - type: Schema.Types.ObjectId, - ref: 'user', - } -``` -Uwaga: `'user'` - nazwa zbioru (w liczbie pojedynczej), w którym zapisują się użytkownicy. - - -## Krok 2 - -### Rejestracja - -Utwórz endpoint [`/users/signup`](#registration-request). - -Zrób walidację wszystkich obowiązkowych pól (`email` i `password`). W przypadku błędu walidacji zwróć [Błąd walidacji](#registration-validation-error). - -W przypadku pomyślnej walidacji w modelu `User` utwórz użytkownika z danymi, które przeszły walidację. Dla wprowadzenia soli do haseł wykorzystaj [bcrypt](https://www.npmjs.com/package/bcrypt) lub [bcryptjs](https://www.npmjs.com/package/bcryptjs). - -- Jeśli poczta jest już wykorzystywana przez kogoś innego, zwróć [Błąd Conflict](#registration-conflict-error). -- W przeciwnym razie zwróć [Sukces odpowiedzi](#registration-success-response). - -#### Registration request - -```shell -POST /users/signup -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Registration validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Registration conflict error - -```shell -Status: 409 Conflict -Content-Type: application/json -ResponseBody: { - "message": "Email in use" -} -``` - -#### Registration success response - -```shell -Status: 201 Created -Content-Type: application/json -ResponseBody: { - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -### Login - -Utwórz endpoint [`/users/login`](#login-request). - -W modelu `User` znajdź użytkownika po `email`. - -Utwórz walidację wszystkich pól obowiązkowych (`email` i `password`). W przypadku błędu walidacji zwróć [Błąd walidacji](#validation-error-login). - -- W przeciwnym razie porównaj hasło dla znalezionego usera. Jeżeli hasła pokrywają się, utwórz token, zapisz w obecnym userze i zwróć [Sukces odpowiedzi](#login-success-response). -- Jeżeli hasło lub email nie są dokładne, zwróć [Błąd Unauthorized](#login-auth-error). - -#### Login request - -```shell -POST /users/login -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Login validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Login success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "token": "exampletoken", - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -#### Login auth error - -```shell -Status: 401 Unauthorized -ResponseBody: { - "message": "Email or password is wrong" -} -``` - -## Krok 3 - -### Sprawdzenie tokena - -Utwórz oprogramowanie pośredniczące tokena i dodaj je do wszystkich tras, które powinny być chronione. - -- Oprogramowanie pośredniczące bierze token z nagłówków `Authorization`, sprawdza token pod względem ważności. -- W przypadku błędu zwróć [Błąd Unauthorized](#middleware-unauthorized-error). -- Jeżeli walidacja przeszła pomyślnie, otrzymaj z tokena `id` użytkownika. Znajdź użytkownika w bazie danych po tym id. -- Jeśli użytkownik istnieje i token pokrywa się z tym, co znajduje się w bazie, zapisz jego dane w `req.user` i wywołaj metodę `next()`. -- Jeżeli użytkownika z takim `id` nie ma lub tokeny nie pokrywają się, zwróć [Błąd Unauthorized](#middleware-unauthorized-error). - -#### Middleware unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` -Krok 4 - -### Logout - -Utwórz endpoint [`/users/logout`](#logout-request). - -Dodaj do trasy program pośredniczący sprawdzania tokena. - -- W modelu `User` znajdź użytkownika po `_id`. -- Jeżeli nie można zwrócić użytkownika [Błąd Unauthorized](#logout-unauthorized-error). -- W przeciwnym razie usuń token w obecnym userze i zwróć [Sukces odpowiedzi](#logout-success-response). - -#### Logout request - -```shell -GET /users/logout -Authorization: "Bearer {{token}}" -``` - -#### Logout unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Logout success response - -```shell -Status: 204 No Content -``` - -## Krok 5 -### Obecny użytkownik – otrzymaj dane usera zgodnie z tokenem - -Utwórz endpoint [`/users/current`](#current-user-request). - -Dodaj do trasy program pośredniczący sprawdzania tokena. - -- Jeżeli użytkownik nie istnieje, zwróć [Błąd Unauthorized](#current-user-unauthorized-error). -- W przeciwnym razie zwróć [Sukces odpowiedzi](#current-user-success-response). - -#### Current user request - -```shell -GET /users/current -Authorization: "Bearer {{token}}" -``` - -#### Current user unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Current user success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "email": "example@example.com", - "subscription": "starter" -} -``` - -## Zadanie dodatkowe – nieobowiązkowe - -- Stwórz paginację dla zbioru kontaktów (GET /contacts?page=1&limit=20). -- Utwórz filtrowanie kontaktów zgodnie z polem wybranego (GET /contacts?favorite=true). -- Aktualizacja subskrypcji (`subscription`) użytkownika przez endpoint `PATCH` `/users`. Subskrypcja powinna mieć jedną z następujących wartości `['starter', 'pro', 'business']`. diff --git a/homework-04/README.ua.md b/homework-04/README.ua.md deleted file mode 100755 index 164fd71..0000000 --- a/homework-04/README.ua.md +++ /dev/null @@ -1,251 +0,0 @@ -**Читати на інших мовах: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнє завдання 4 - -Створи гілку `04-auth` з гілки `master`. - -Продовж створення REST API для роботи з колекцією контактів. Додай логіку аутентифікації/авторизації користувача через [JWT](https://jwt.io/). - -## Крок 1 - -У коді створи схему і модель користувача для колекції `users`. - -```js -{ - password: { - type: String, - required: [true, 'Set password for user'], - }, - email: { - type: String, - required: [true, 'Email is required'], - unique: true, - }, - subscription: { - type: String, - enum: ["starter", "pro", "business"], - default: "starter" - }, - token: String -} -``` - -Змініть схему контактів, щоб кожен користувач бачив тільки свої контакти. Для цього в схемі контактів додайте властивість - -```js - owner: { - type: Schema.Types.ObjectId, - ref: 'user', - } -``` -Примітка: `'user'` - назва колекції, у якій зберігаються користувачі - -## Крок 2 - -### Реєстрація - -Створити ендпоінт [`/users/register`](#registration-request) - -Зробити валідацію всіх обов'язкових полів (email і password). При помилці валідації повернути [Помилку валідації](#registration-validation-error). - -У разі успішної валідації в моделі `User` створити користувача за даними, які пройшли валідацію. Для засолювання паролів використовуй [bcrypt](https://www.npmjs.com/package/bcrypt) або [bcryptjs](https://www.npmjs.com/package/bcryptjs) - -- Якщо пошта вже використовується кимось іншим, повернути [Помилку Conflict](#registration-conflict-error). -- В іншому випадку повернути [Успішна відповідь](#registration-success-response). - -#### Registration request - -```shell -POST /users/register -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Registration validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Помилка від Joi або іншої бібліотеки валідації> -``` - -#### Registration conflict error - -```shell -Status: 409 Conflict -Content-Type: application/json -ResponseBody: { - "message": "Email in use" -} -``` - -#### Registration success response - -```shell -Status: 201 Created -Content-Type: application/json -ResponseBody: { - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -### Логін - -Створити ендпоінт [`/users/login`](#login-request) - -В моделі `User` знайти користувача за `email`. - -Зробити валідацію всіх обов'язкових полів (email і password). При помилці валідації повернути [Помилку валідації](#validation-error-login). - -- В іншому випадку, порівняти пароль для знайденого користувача, якщо паролі збігаються створити токен, зберегти в поточному юзера і повернути [Успішна відповідь](#login-success-response). -- Якщо пароль або імейл невірний, повернути [Помилку Unauthorized](#login-auth-error). - -#### Login request - -```shell -GET /users/login -Content-Type: application/json -RequestBody: { - "email": "example@example.com", - "password": "examplepassword" -} -``` - -#### Login validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Помилка від Joi або іншої бібліотеки валідації> -``` - -#### Login success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "token": "exampletoken", - "user": { - "email": "example@example.com", - "subscription": "starter" - } -} -``` - -#### Login auth error - -```shell -Status: 401 Unauthorized -ResponseBody: { - "message": "Email or password is wrong" -} -``` - -## Крок 3 - -### Перевірка токена - -Створи мідлвар для перевірки токена і додай його до всіх раутів, які повинні бути захищені. - -- Мідлвар бере токен з заголовків `Authorization`, перевіряє токен на валідність. -- У випадку помилки повернути [Помилку Unauthorized](#middleware-unauthorized-error). -- Якщо валідація пройшла успішно, отримати з токена `id` користувача. Знайти користувача в базі даних з цим `id`. -- Якщо користувач існує і токен збігається з тим, що знаходиться в базі, записати його дані в `req.user` і викликати `next()`. -- Якщо користувача з таким `id` НЕ існує або токени не збігаються, повернути [Помилку Unauthorized](#middleware-unauthorized-error) - -#### Middleware unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -## Крок 4 - -### Логаут - -Створити ендпоінт [`/users/logout`](#logout-request) - -Додай в маршрут мідлвар перевірки токена. - -- У моделі `User` знайти користувача за `_id`. -- Якщо користувача не існує повернути [Помилку Unauthorized](#logout-unauthorized-error). -- В іншому випадку, видалити токен у поточного юзера і повернути [Успішна відповідь](#logout-success-response). - -#### Logout request - -```shell -POST /users/logout -Authorization: "Bearer {{token}}" -``` - -#### Logout unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Logout success response - -```shell -Status: 204 No Content -``` - -## Крок 5 - -### Поточний користувач - отримати дані юзера по токену - -Створити ендпоінт [`/users/current`](#current-user-request) - -Додай в раут мідлвар перевірки токена. - -- Якщо користувача не існує повернути [Помилку Unauthorized](#current-user-unauthorized-error) -- В іншому випадку повернути [Успішну відповідь](#current-user-success-response) - -#### Current user request - -```shell -GET /users/current -Authorization: "Bearer {{token}}" -``` - -#### Current user unauthorized error - -```shell -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -#### Current user success response - -```shell -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "email": "example@example.com", - "subscription": "starter" -} -``` - -## Додаткове завдання - необов'язкове - -- Зробити пагінацію для колекції контактів (GET /contacts?page=1&limit=20). -- Зробити фільтрацію контактів по полю обраного (GET /contacts?favorite=true) -- Оновлення підписки (`subscription`) користувача через ендпоінт` PATCH` `/users`. Підписка повинна мати одне з наступних значень `['starter', 'pro', 'business']` diff --git a/homework-04/controllers/auth/changeSubscription.js b/homework-04/controllers/auth/changeSubscription.js new file mode 100644 index 0000000..1c11ed9 --- /dev/null +++ b/homework-04/controllers/auth/changeSubscription.js @@ -0,0 +1,13 @@ +const { User } = require('../../models/user'); + +const changeSubscription = async (req, res) => { + const { _id } = req.user; + const { subscription } = req.body; + await User.findByIdAndUpdate(_id, { subscription }); + + res.json({ + message: `Your subscription has been changed to ${subscription}`, + }); +}; + +module.exports = changeSubscription; diff --git a/homework-04/controllers/auth/getCurrent.js b/homework-04/controllers/auth/getCurrent.js new file mode 100644 index 0000000..8612179 --- /dev/null +++ b/homework-04/controllers/auth/getCurrent.js @@ -0,0 +1,11 @@ +const getCurrent = async (req, res) => { + const { name, email, subscription } = req.user; + + res.json({ + name, + email, + subscription, + }); +}; + +module.exports = getCurrent; diff --git a/homework-04/controllers/auth/index.js b/homework-04/controllers/auth/index.js new file mode 100644 index 0000000..20f103d --- /dev/null +++ b/homework-04/controllers/auth/index.js @@ -0,0 +1,13 @@ +const register = require('./register'); +const login = require('./login'); +const logout = require('./logout'); +const getCurrent = require('./getCurrent'); +const changeSubscription = require('./changeSubscription'); + +module.exports = { + register, + login, + logout, + getCurrent, + changeSubscription, +}; diff --git a/homework-04/controllers/auth/login.js b/homework-04/controllers/auth/login.js new file mode 100644 index 0000000..6d2a602 --- /dev/null +++ b/homework-04/controllers/auth/login.js @@ -0,0 +1,41 @@ +const bcryptjs = require('bcryptjs'); + +const jwt = require('jsonwebtoken'); + +const { User } = require('../../models/user'); + +const { HttpError } = require('../../helpers'); + +const { SECRET_KEY } = process.env; + +const login = async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); + + if (!user) { + throw HttpError(401, 'Email or password is wrong'); + } + + const passwordCompare = await bcryptjs.compare(password, user.password); + + if (!passwordCompare) { + throw HttpError(401, 'Email or password is wrong'); + } + + const payload = { + id: user._id, + }; + + const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '7d' }); + await User.findByIdAndUpdate(user._id, { token }); + + res.json({ + token, + user: { + email: user.email, + subscription: user.subscription, + }, + }); +}; + +module.exports = login; diff --git a/homework-04/controllers/auth/logout.js b/homework-04/controllers/auth/logout.js new file mode 100644 index 0000000..c1505ae --- /dev/null +++ b/homework-04/controllers/auth/logout.js @@ -0,0 +1,10 @@ +const { User } = require('../../models/user'); + +const logout = async (req, res) => { + const { _id } = req.user; + await User.findByIdAndUpdate(_id, { token: '' }); + + res.status(204).json(); +}; + +module.exports = logout; diff --git a/homework-04/controllers/auth/register.js b/homework-04/controllers/auth/register.js new file mode 100644 index 0000000..5e46bdb --- /dev/null +++ b/homework-04/controllers/auth/register.js @@ -0,0 +1,22 @@ +const bcryptjs = require('bcryptjs'); +const { User } = require('../../models/user'); +const { HttpError } = require('../../helpers'); + +const register = async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); + + if (user) { + throw HttpError(409, 'Email in use'); + } + + const hashPassword = await bcryptjs.hash(password, 10); + const newUser = await User.create({ ...req.body, password: hashPassword }); + + res.status(201).json({ + name: newUser.name, + email: newUser.email, + }); +}; + +module.exports = register; diff --git a/homework-04/controllers/contacts/addContact.js b/homework-04/controllers/contacts/addContact.js new file mode 100644 index 0000000..2734a0b --- /dev/null +++ b/homework-04/controllers/contacts/addContact.js @@ -0,0 +1,16 @@ +const { Contact, addSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const addContact = async (req, res) => { + const { error } = addSchema.validate(req.body); + + if (error) { + throw HttpError(400, 'missing required name field'); + } + + const { _id: owner } = req.user; + const result = await Contact.create({ ...req.body, owner }); + res.status(201).json(result); +}; + +module.exports = addContact; diff --git a/homework-04/controllers/contacts/getById.js b/homework-04/controllers/contacts/getById.js new file mode 100644 index 0000000..c44471b --- /dev/null +++ b/homework-04/controllers/contacts/getById.js @@ -0,0 +1,15 @@ +const { Contact } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const getById = async (req, res) => { + const { id } = req.params; + const result = await Contact.findById(id); + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = getById; diff --git a/homework-04/controllers/contacts/index.js b/homework-04/controllers/contacts/index.js new file mode 100644 index 0000000..b4a4ed2 --- /dev/null +++ b/homework-04/controllers/contacts/index.js @@ -0,0 +1,15 @@ +const listContacts = require('./listContacts'); +const getById = require('./getById'); +const addContact = require('./addContact'); +const updateById = require('./updateById'); +const updateStatusContact = require('./updateStatusContact'); +const removeContact = require('./removeContact'); + +module.exports = { + listContacts, + getById, + addContact, + updateById, + updateStatusContact, + removeContact, +}; diff --git a/homework-04/controllers/contacts/listContacts.js b/homework-04/controllers/contacts/listContacts.js new file mode 100644 index 0000000..5f0b5e1 --- /dev/null +++ b/homework-04/controllers/contacts/listContacts.js @@ -0,0 +1,22 @@ +const { Contact } = require('../../models/contact'); + +const listContacts = async (req, res) => { + const { _id: owner } = req.user; + const { page = 1, limit = 10 } = req.query; + const skip = (page - 1) * limit; + + if (req.query.favorite) { + const favorite = req.query.favorite === 'true'; + const result = await Contact.find({ owner, favorite }, '', { + skip, + limit, + }).populate('owner', 'name email'); + + return res.json(result); + } + + const result = await Contact.find({ owner }, '', { skip, limit }).populate('owner', 'name email'); + res.json(result); +}; + +module.exports = listContacts; diff --git a/homework-04/controllers/contacts/removeContact.js b/homework-04/controllers/contacts/removeContact.js new file mode 100644 index 0000000..e0b2351 --- /dev/null +++ b/homework-04/controllers/contacts/removeContact.js @@ -0,0 +1,17 @@ +const { Contact } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const removeContact = async (req, res) => { + const { id } = req.params; + const result = await Contact.findByIdAndDelete(id); + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json({ + message: 'contact deleted', + }); +}; + +module.exports = removeContact; diff --git a/homework-04/controllers/contacts/updateById.js b/homework-04/controllers/contacts/updateById.js new file mode 100644 index 0000000..429b8cc --- /dev/null +++ b/homework-04/controllers/contacts/updateById.js @@ -0,0 +1,21 @@ +const { Contact, addSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const updateById = async (req, res) => { + const { error } = addSchema.validate(req.body); + + if (error) { + throw HttpError(400, 'missing fields'); + } + + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateById; diff --git a/homework-04/controllers/contacts/updateStatusContact.js b/homework-04/controllers/contacts/updateStatusContact.js new file mode 100644 index 0000000..ac5b428 --- /dev/null +++ b/homework-04/controllers/contacts/updateStatusContact.js @@ -0,0 +1,21 @@ +const { Contact, updateFavoriteSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const updateStatusContact = async (req, res) => { + const { error } = updateFavoriteSchema.validate(req.body); + + if (error) { + throw HttpError(400, 'missing field favorite'); + } + + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateStatusContact; diff --git a/homework-04/helpers/HttpError.js b/homework-04/helpers/HttpError.js new file mode 100644 index 0000000..3f38b50 --- /dev/null +++ b/homework-04/helpers/HttpError.js @@ -0,0 +1,7 @@ +const HttpError = (status, message) => { + const error = new Error(message); + error.status = status; + return error; +}; + +module.exports = HttpError; diff --git a/homework-04/helpers/ctrlWrapper.js b/homework-04/helpers/ctrlWrapper.js new file mode 100644 index 0000000..4e36751 --- /dev/null +++ b/homework-04/helpers/ctrlWrapper.js @@ -0,0 +1,13 @@ +const ctrlWrapper = ctrl => { + const func = async (req, res, next) => { + try { + await ctrl(req, res, next); + } catch (error) { + next(error); + } + }; + + return func; +}; + +module.exports = ctrlWrapper; diff --git a/homework-04/helpers/handleMongooseError.js b/homework-04/helpers/handleMongooseError.js new file mode 100644 index 0000000..f98813b --- /dev/null +++ b/homework-04/helpers/handleMongooseError.js @@ -0,0 +1,8 @@ +const handleMongooseError = (error, data, next) => { + const { name, code } = error; + const status = name === 'MongoServerError' && code === 11000 ? 409 : 400; + error.status = status; + next(); +}; + +module.exports = handleMongooseError; diff --git a/homework-04/helpers/index.js b/homework-04/helpers/index.js new file mode 100644 index 0000000..06f8c83 --- /dev/null +++ b/homework-04/helpers/index.js @@ -0,0 +1,9 @@ +const HttpError = require('./HttpError'); +const ctrlWrapper = require('./ctrlWrapper'); +const handleMongooseError = require('./handleMongooseError'); + +module.exports = { + HttpError, + ctrlWrapper, + handleMongooseError, +}; diff --git a/homework-04/middlewares/authenticate.js b/homework-04/middlewares/authenticate.js new file mode 100644 index 0000000..e79a9a9 --- /dev/null +++ b/homework-04/middlewares/authenticate.js @@ -0,0 +1,33 @@ +const jwt = require('jsonwebtoken'); +const { User } = require('../models/user'); + +require('dotenv').config(); +const { SECRET_KEY } = process.env; + +const { HttpError } = require('../helpers'); + +const authenticate = async (req, res, next) => { + const { authorization = '' } = req.headers; + const [bearer, token] = authorization.split(' '); + + if (bearer !== 'Bearer') { + next(HttpError(401, 'Not authorized')); + } + + try { + const { id } = jwt.verify(token, SECRET_KEY); + const user = await User.findById(id); + + if (!user || !user.token || user.token !== token) { + next(HttpError(401, 'Not authorized')); + } + + req.user = user; + + next(); + } catch { + next(HttpError(401, 'Not authorized')); + } +}; + +module.exports = authenticate; diff --git a/homework-04/middlewares/index.js b/homework-04/middlewares/index.js new file mode 100644 index 0000000..4b9832e --- /dev/null +++ b/homework-04/middlewares/index.js @@ -0,0 +1,5 @@ +const isValidId = require('./isValidId'); +const authenticate = require('./authenticate'); +const validateBody = require('./validateBody'); + +module.exports = { isValidId, authenticate, validateBody }; diff --git a/homework-04/middlewares/isValidId.js b/homework-04/middlewares/isValidId.js new file mode 100644 index 0000000..3654e9a --- /dev/null +++ b/homework-04/middlewares/isValidId.js @@ -0,0 +1,15 @@ +const { isValidObjectId } = require('mongoose'); + +const { HttpError } = require('../helpers'); + +const isValidId = (req, res, next) => { + const { id } = req.params; + + if (!isValidObjectId(id)) { + next(HttpError(400, `${id} is not valid id`)); + } + + next(); +}; + +module.exports = isValidId; diff --git a/homework-04/middlewares/validateBody.js b/homework-04/middlewares/validateBody.js new file mode 100644 index 0000000..67ea2d5 --- /dev/null +++ b/homework-04/middlewares/validateBody.js @@ -0,0 +1,17 @@ +const { HttpError } = require('../helpers'); + +const validateBody = schema => { + const func = (req, res, next) => { + const { error } = schema.validate(req.body); + + if (error) { + next(HttpError(400, error.message)); + } + + next(); + }; + + return func; +}; + +module.exports = validateBody; diff --git a/homework-04/models/contact.js b/homework-04/models/contact.js new file mode 100644 index 0000000..c7a2729 --- /dev/null +++ b/homework-04/models/contact.js @@ -0,0 +1,45 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const contactSchema = new Schema( + { + name: { + type: String, + required: [true, 'Set name for contact'], + }, + email: { + type: String, + }, + phone: { + type: String, + }, + favorite: { + type: Boolean, + default: false, + }, + owner: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true, + }, + }, + + { versionKey: false }, +); + +contactSchema.post('save', handleMongooseError); + +const Contact = model('contact', contactSchema); + +const addSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), +}); + +const updateFavoriteSchema = Joi.object({ + favorite: Joi.boolean().required(), +}); + +module.exports = { Contact, addSchema, updateFavoriteSchema }; diff --git a/homework-04/models/user.js b/homework-04/models/user.js new file mode 100644 index 0000000..719a2f3 --- /dev/null +++ b/homework-04/models/user.js @@ -0,0 +1,69 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const emailRegexp = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; +const subscriptionList = ['starter', 'pro', 'business']; + +const userSchema = new Schema( + { + name: { + type: String, + required: true, + }, + email: { + type: String, + match: emailRegexp, + required: [true, 'Email is required'], + unique: true, + }, + password: { + type: String, + minlength: 6, + required: [true, 'Set password for user'], + }, + + subscription: { + type: String, + enum: subscriptionList, + default: 'starter', + }, + token: { + type: String, + default: '', + }, + }, + { versionKey: false }, +); + +userSchema.post('save', handleMongooseError); + +const registerSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().pattern(emailRegexp).required(), + password: Joi.string().min(6).required(), +}); + +const loginSchema = Joi.object({ + email: Joi.string().required(), + password: Joi.string().min(6).required(), +}); + +const changeSubscriptionSchema = Joi.object({ + subscription: Joi.string() + .valid(...subscriptionList) + .required(), +}); + +const User = model('user', userSchema); + +const schemas = { + registerSchema, + loginSchema, + changeSubscriptionSchema, +}; + +module.exports = { + User, + schemas, +}; diff --git a/homework-04/nodemon.json b/homework-04/nodemon.json new file mode 100644 index 0000000..54d6947 --- /dev/null +++ b/homework-04/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["node_modules", "models/contacts.json"] +} diff --git a/homework-04/routes/api/auth.js b/homework-04/routes/api/auth.js new file mode 100644 index 0000000..cac67b8 --- /dev/null +++ b/homework-04/routes/api/auth.js @@ -0,0 +1,22 @@ +const express = require('express'); + +const { validateBody, authenticate } = require('../../middlewares'); +const { schemas } = require('../../models/user'); + +const router = express.Router(); + +const ctrl = require('../../controllers/auth'); + +const { ctrlWrapper } = require('../../helpers'); + +router.post('/register', validateBody(schemas.registerSchema), ctrlWrapper(ctrl.register)); + +router.post('/login', validateBody(schemas.loginSchema), ctrlWrapper(ctrl.login)); + +router.get('/current', authenticate, ctrlWrapper(ctrl.getCurrent)); + +router.post('/logout', authenticate, ctrlWrapper(ctrl.logout)); + +router.patch('/', authenticate, validateBody(schemas.changeSubscriptionSchema), ctrlWrapper(ctrl.changeSubscription)); + +module.exports = router; diff --git a/homework-04/routes/api/contacts.js b/homework-04/routes/api/contacts.js new file mode 100644 index 0000000..eeec574 --- /dev/null +++ b/homework-04/routes/api/contacts.js @@ -0,0 +1,23 @@ +const express = require('express'); + +const router = express.Router(); + +const ctrl = require('../../controllers/contacts'); + +const { ctrlWrapper } = require('../../helpers'); + +const { isValidId, authenticate } = require('../../middlewares'); + +router.get('/', authenticate, ctrlWrapper(ctrl.listContacts)); + +router.get('/:id', authenticate, isValidId, ctrlWrapper(ctrl.getById)); + +router.post('/', authenticate, ctrlWrapper(ctrl.addContact)); + +router.put('/:id', authenticate, isValidId, ctrlWrapper(ctrl.updateById)); + +router.patch('/:id/favorite', authenticate, isValidId, ctrlWrapper(ctrl.updateStatusContact)); + +router.delete('/:id', authenticate, isValidId, ctrlWrapper(ctrl.removeContact)); + +module.exports = router; diff --git a/homework-04/server.js b/homework-04/server.js new file mode 100644 index 0000000..2d1e335 --- /dev/null +++ b/homework-04/server.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); + +const app = require('./app'); +const { DB_HOST } = process.env; + +mongoose + .connect(DB_HOST) + .then(() => { + console.log('Database connection successful'); + app.listen(3000, () => { + console.log('Server running. Use our API on port: 3000'); + }); + }) + .catch(error => { + console.log(error.message); + process.exit(1); + }); diff --git a/homework-05/.eslintignore b/homework-05/.eslintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/homework-05/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/homework-05/.eslintrc.js b/homework-05/.eslintrc.js new file mode 100644 index 0000000..d799332 --- /dev/null +++ b/homework-05/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + }, + extends: ['standard', 'prettier'], + parserOptions: { + ecmaVersion: 12, + }, + rules: {}, +} diff --git a/homework-05/.gitignore b/homework-05/.gitignore new file mode 100644 index 0000000..8877fc5 --- /dev/null +++ b/homework-05/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.idea +.vscode \ No newline at end of file diff --git a/homework-05/README.en.md b/homework-05/README.en.md deleted file mode 100644 index 40df6c6..0000000 --- a/homework-05/README.en.md +++ /dev/null @@ -1,84 +0,0 @@ -**Read in other languages: [Russian](README.md), [Ukrainian](README.ua.md).** - -# Homework 5 - -Create the `hw05-avatars` branch from the `master` branch. - -Continue building a REST API to work with the contact collection. Add the ability to upload the user's avatar via [Multer](https://github.com/expressjs/multer). - -## Step 1 - -Create a folder `public` for distribution of statics. In this folder make a folder `avatars`. Set up Express to serve static files from the `public` folder. - -Put any image in the `public/avatars` folder and check that the static distribution works. When you navigate to such a URL, the browser will display the image. - -```shell -http://localhost:/avatars/ -``` - -## Step 2 - -Add a new `avatarURL` property to the user schema to hold the image. - -```shell -{ - ... - avatarURL: String, - ... -} -``` - -- Use the package [gravatar](https://www.npmjs.com/package/gravatar) to immediately generate an avatar for him by his `email` when registering a new user. - -## Step 3 - -When registering a user: - -- Create a link to the user's avatar with [gravatar](https://www.npmjs.com/package/gravatar) -- Save the resulting URL in the `avatarURL` field during user creation - -## Step 4 - -Add the ability to update the avatar by creating an endpoint `/users/avatars` and using the `PATCH` method. - -![avatar upload from postman](./avatar-upload.png) - -```shell -# Request -PATCH /users/avatars -Content-Type: multipart/form-data -Authorization: "Bearer {{token}}" -RequestBody: uploaded file - -# Successful response -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "avatarURL": "image link goes here" -} - -# Unsuccessful response -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -- Create a tmp folder in the root of the project and save the uploaded avatar to it -- Process the avatar with the [jimp] package (https://www.npmjs.com/package/jimp) and set its dimensions to 250 by 250 -- Move the user's avatar from the tmp folder to the `public/avatars` folder and give it a unique name for the specific user -- The resulting `URL` is `/avatars/`, save it in the user's `avatarURL` field - - -## Additional Task - Optional - -### 1. Write Unit Tests for the Login Controler (login/signin) - -Using [Jest](https://jestjs.io/ru/docs/getting-started) - -- Response must have status code 200 -- The token must be returned in the response -- The response should return a `user` object with 2 fields `email` and `subscription`, having the data type `String` - - diff --git a/homework-05/README.es.md b/homework-05/README.es.md deleted file mode 100644 index 0525dfc..0000000 --- a/homework-05/README.es.md +++ /dev/null @@ -1,82 +0,0 @@ -**Leer en otros idiomas: [Русский](README.md), [Українська](README.ua.md).** - -# Tarea 5 - -Crea una rama `hw05-avatars` de la rama `master`. - -Continúe construyendo la API REST para su colección de contactos. Añade la posibilidad de subir el avatar de usuario a través de [Multer](https://github.com/expressjs/multer). - -## Paso 1 - -Crea una carpeta `public` para la distribución de los archivos estáticos. Crea una carpeta llamada `avatars` en esta carpeta. Configura Express para que distribuya los archivos estáticos de la carpeta `public`. - -Coloca cualquier imagen en la carpeta `public/avatars` y asegúrate de que la distribución de archivos estáticos funciona. Al ir a esta URL, el navegador mostrará la imagen. - -```shell -http://localhost:/avatars/ -``` - -## Paso 2 - -Añade una nueva propiedad `avatarURL` al esquema de usuario para almacenar la imagen. - -```shell -{ - ... - avatarURL: String, - ... -} -``` - -- Utilice el paquete [gravatar](https://www.npmjs.com/package/gravatar) para generar inmediatamente un avatar cuando se registra un nuevo usuario, basandose en el `email` del usuario. - -## Paso 3 - -Al registrarse un usuario: - -- Crea un enlace al avatar del usuario utilizando [gravatar](https://www.npmjs.com/package/gravatar) -- Durante la creación del usuario, guarda la URL resultante en el campo `avatarURL` durante la creación del usuario - -## Paso 4 - -Añade la posibilidad de actualizar tu avatar creando un endpoint `/users/avatars` y utilizando el método `PATCH`. - -![avatar upload from postman](./avatar-upload.png) - -```shell -# Petición -PATCH /users/avatars -Content-Type: multipart/form-data -Authorization: "Bearer {{token}}" -RequestBody: archivo cargado - -# Respuesta exitosa -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "avatarURL": "aquí habrá un enlace a la imagen" -} - -# Respuesta fallida -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -- Crea una carpeta tmp en la raíz del proyecto y guarda en ella el avatar subido. -- Procesa el avatar con el paquete [jimp](https://www.npmjs.com/package/jimp) y asignales una dimensión de 250 por 250 -- Mueve el avatar del usuario de la carpeta tmp a la carpeta `public/avatars` y dale un nombre único para el usuario específico. -- Guardar la `URL` recuperada `/avatars/` en el campo `avatarURL` del usuario - -## Tarea adicional (opcional) - -### 1. Escribir pruebas unitarias para el controlador de entrada (login/signin) - -Usando [Jest](https://jestjs.io/ru/docs/getting-started) - -- la respuesta debe tener un código de estado de 200 -- la respuesta debe devolver un token -- La respuesta debe devolver un objeto `user` con 2 campos `email` y `subscription` de tipo de datos `String` - diff --git a/homework-05/README.md b/homework-05/README.md index daa90e3..b67d049 100644 --- a/homework-05/README.md +++ b/homework-05/README.md @@ -1,82 +1,6 @@ -**Читать на других языках: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнее задание 5 - -Создай ветку `hw05-avatars` из ветки `master`. - -Продолжи создание REST API для работы с коллекцией контактов. Добавь возможность загрузки аватарки пользователя через [Multer](https://github.com/expressjs/multer). - -## Шаг 1 - -Создай папку `public` для раздачи статики. В этой папке сделай папку `avatars`. Настрой Express на раздачу статических файлов из папки `public`. - -Положи любое изображение в папку `public/avatars` и проверь что раздача статики работает. При переходе по такому URL браузер отобразит изображение. - -```shell -http://localhost:<порт>/avatars/<имя файла с расширением> -``` - -## Шаг 2 - -В схему пользователя добавь новое свойство `avatarURL` для хранения изображения. - -```shell -{ - ... - avatarURL: String, - ... -} -``` - -- Используй пакет [gravatar](https://www.npmjs.com/package/gravatar) для того чтобы при регистрации нового пользователя сразу сгенерить ему аватар по его `email`. - -## Шаг 3 - -При регистрации пользователя: - -- Создавай ссылку на аватарку пользователя с помощью [gravatar](https://www.npmjs.com/package/gravatar) -- Полученный URL сохрани в поле `avatarURL` во время создания пользователя - -## Шаг 4 - -Добавь возможность обновления аватарки, создав эндпоинт `/users/avatars` и используя метод `PATCH`. - -![avatar upload from postman](./avatar-upload.png) - -```shell -# Запрос -PATCH /users/avatars -Content-Type: multipart/form-data -Authorization: "Bearer {{token}}" -RequestBody: загруженный файл - -# Успешный ответ -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "avatarURL": "тут будет ссылка на изображение" -} - -# Неуспешный ответ -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -- Создай папку tmp в корне проекта и сохраняй в неё загруженную аватарку. -- Обработай аватарку пакетом [jimp](https://www.npmjs.com/package/jimp) и задай для нее размеры 250 на 250 -- Перенеси аватарку пользователя из папки tmp в папку `public/avatars` и дай ей уникальное имя для конкретного пользователя. -- Полученный `URL` `/avatars/<имя файла с расширением>` сохрани в поле `avatarURL` пользователя - -## Дополнительное задание - необязательное - -### 1. Написать unit-тесты для контроллера входа (login/signin) - -При помощи [Jest](https://jestjs.io/ru/docs/getting-started) - -- ответ должен иметь статус-код 200 -- в ответе должен возвращаться токен -- в ответе должен возвращаться объект `user` с 2 полями `email` и `subscription`, имеющие тип данных `String` +### Команди: +- `npm start` — старт сервера в режимі production +- `npm run start:dev` — старт сервера в режимі розробки (development) +- `npm run lint` — запустити виконання перевірки коду з eslint, необхідно виконувати перед кожним PR та виправляти всі помилки лінтера +- `npm lint:fix` — та ж перевірка лінтера, але з автоматичними виправленнями простих помилок diff --git a/homework-05/README.pl.md b/homework-05/README.pl.md deleted file mode 100644 index 3ec1b6b..0000000 --- a/homework-05/README.pl.md +++ /dev/null @@ -1,82 +0,0 @@ -**Czytaj w innych językach: [rosyjski](README.md), [ukraiński](README.ua.md).** - -# Zadanie domowe 5 - -Utwórz gałąź `hw05-avatars` z gałęzi `master`. - -Kontynuuj tworzenie REST API do pracy ze zbiorem kontaktów. Dodaj opcję ładowania awataru użytkownika przez [Multer](https://github.com/expressjs/multer). - -## Krok 1 - -Stwórz folder `public` do rozdawania statyki. W tym folderze utwórz folder `avatars`. Narzędzie Express do rozdawania plików statycznych z folderu `public`. - -Umieść dowolny obraz w folderze `public/avatars` i sprawdź, czy rozdawanie statyki działa. Po przejściu po takim URL przeglądarka wyświetli obraz. - -```shell -http://localhost:<порт>/avatars/ -``` - -## Krok 2 - -Do schematu użytkownika dodaj nową właściwość `avatarURL` dla przechowywania obrazu. - -```shell -{ - ... - avatarURL: String, - ... -} -``` - -Wykorzystaj pakiet [gravatar](https://www.npmjs.com/package/gravatar), aby przy rejestracji nowego użytkownika od razu wygenerować mu awatar po jego `email`. - -## Krok 3 - -Przy rejestracji użytkownika: - -- Utwórz odnośnik do awatara użytkownika przy pomocy [gravatar](https://www.npmjs.com/package/gravatar). -- Otrzymany URL zapisz w polu `avatarURL` w czasie tworzenia użytkownika. - -## Krok 4 - -Dodaj możliwość aktualizacji awatara, tworząc endpoint `/users/avatars` i wykorzystując metodę `PATCH`. - -![avatar upload from postman](./avatar-upload.png) - -```shell -# Zapytanie -PATCH /users/avatars -Content-Type: multipart/form-data -Authorization: "Bearer {{token}}" -RequestBody: załadowany plik - -# Poprawna odpowiedź -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "avatarURL": "tu będzie odnośnik do obrazu" -} - -# Błędna odpowiedź -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -- Utwórz folder tmp w root projektu i zapisuj w nim załadowany awatar. -- Opracuj awatar przy pomocy pakietu [jimp](https://www.npmjs.com/package/jimp) i wprowadź dla niego wymiary 250 na 250. -- Przenieś awatar użytkownika z folderu tmp do folderu `public/avatars` i nadaj mu unikalną nazwę dla konkretnego użytkownika. -- Otrzymany `URL` `/avatars/` zapisz w polu `avatarURL` użytkownika. - -## Zadanie dodatkowe – nieobowiązkowe - -### 1. Napisać unit-testy dla kontrolera wejścia (login/signin) - -Przy pomocy [Jest](https://jestjs.io/ru/docs/getting-started) - -- odpowiedź powinna mieć status kod 200; -- w odpowiedzi powinien być zwracany token; -- w odpowiedzi powinien być zwracany obiekt `user` z 2 polami `email` i `subscription`, mającymi typ danych `String`. - diff --git a/homework-05/README.ua.md b/homework-05/README.ua.md deleted file mode 100755 index e5c117a..0000000 --- a/homework-05/README.ua.md +++ /dev/null @@ -1,80 +0,0 @@ -**Читати на інших мовах: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнє завдання 5 - -Створи гілку `hw05-avatars` з гілки ` master`. - -Продовж створення REST API для роботи з колекцією контактів. Додай можливість завантаження аватарки користувача через [Multer] (https://github.com/expressjs/multer). - -## Крок 1 - -Створи папку `public` для роздачі статики. У цій папці зроби папку `avatars`. Налаштуй Express на роздачу статичних файлів з папки `public`. - -Поклади будь-яке зображення в папку `public/avatars` і перевір, що роздача статики працює. При переході по такому URL браузер відобразить зображення. - -`` `Shell http://locahost:<порт>/avatars/<ім'я файлу з розширенням> `` ` - -## Крок 2 - -У схему користувача додай нову властивість `avatarURL` для зберігання зображення. - -```shell -{ - ... - avatarURL: String, - ... -} -``` - -- Використовуй пакет [gravatar](https://www.npmjs.com/package/gravatar) для того, щоб при реєстрації нового користувача відразу згенерувати йому аватар по його `email`. - -## Крок 3 - -При реєстрації користувача: - -- Створюй посилання на аватарку користувача за допомогою [gravatar](https://www.npmjs.com/package/gravatar) -- Отриманий URL збережи в поле `avatarURL` під час створення користувача - -## Крок 4 - -Додай можливість поновлення аватарки, створивши ендпоінт `/users/avatars` і використовуючи метод` PATCH`. - -![avatar upload from postman](./avatar-upload.png) - -```shell -# Запит -PATCH /users/avatars -Content-Type: multipart/form-data -Authorization: "Bearer {{token}}" -RequestBody: завантажений файл - -# Успішна відповідь -Status: 200 OK -Content-Type: application/json -ResponseBody: { - "avatarURL": "тут буде посилання на зображення" -} - -# Неуспішна відповідь -Status: 401 Unauthorized -Content-Type: application/json -ResponseBody: { - "message": "Not authorized" -} -``` - -- Створи папку `tmp` в корені проекту і зберігай в неї завантажену аватарку. -- Оброби аватарку пакетом [jimp](https://www.npmjs.com/package/jimp) і постав для неї розміри 250 на 250 -- Перенеси аватарку користувача з папки `tmp` в папку `public/avatars` і дай їй унікальне ім'я для конкретного користувача. -- Отриманий `URL` `/avatars/<ім'я файлу з розширенням>` та збережи в поле `avatarURL` користувача - -## Додаткове завдання - необов'язкове - -### 1. Написати unit-тести для контролера входу (login/signin) - -За допомогою [Jest](https://jestjs.io/ru/docs/getting-started) - -- відповідь повина мати статус-код 200 -- у відповіді повинен повертатися токен -- у відповіді повинен повертатися об'єкт `user` з 2 полями `email` та `subscription` з типом даних `String` - diff --git a/homework-05/avatar-upload.png b/homework-05/avatar-upload.png deleted file mode 100644 index 3851da6..0000000 Binary files a/homework-05/avatar-upload.png and /dev/null differ diff --git a/homework-05/controllers/auth/changeSubscription.js b/homework-05/controllers/auth/changeSubscription.js new file mode 100644 index 0000000..ba2afde --- /dev/null +++ b/homework-05/controllers/auth/changeSubscription.js @@ -0,0 +1,13 @@ +const changeSubscription = async (req, res) => { + const { _id } = req.user; + const { subscription } = req.body; + + await User.findByIdAndUpdate(_id, { subscription }); + + // Відправка відповіді клієнту зі статусом 200 (OK) та об'єктом, що містить повідомлення про зміну підписки. + res.json({ + message: `Your subscription has been changed to ${subscription}`, + }); +}; + +module.exports = changeSubscription; diff --git a/homework-05/controllers/auth/getCurrent.js b/homework-05/controllers/auth/getCurrent.js new file mode 100644 index 0000000..8612179 --- /dev/null +++ b/homework-05/controllers/auth/getCurrent.js @@ -0,0 +1,11 @@ +const getCurrent = async (req, res) => { + const { name, email, subscription } = req.user; + + res.json({ + name, + email, + subscription, + }); +}; + +module.exports = getCurrent; diff --git a/homework-05/controllers/auth/index.js b/homework-05/controllers/auth/index.js new file mode 100644 index 0000000..f6ced5c --- /dev/null +++ b/homework-05/controllers/auth/index.js @@ -0,0 +1,15 @@ +const register = require('./register'); +const login = require('./login'); +const logout = require('./logout'); +const getCurrent = require('./getCurrent'); +const changeSubscription = require('./changeSubscription'); +const updateAvatar = require('./updateAvatar'); + +module.exports = { + register, + login, + logout, + getCurrent, + changeSubscription, + updateAvatar, +}; diff --git a/homework-05/controllers/auth/login.js b/homework-05/controllers/auth/login.js new file mode 100644 index 0000000..e12124a --- /dev/null +++ b/homework-05/controllers/auth/login.js @@ -0,0 +1,44 @@ +const bcryptjs = require('bcryptjs'); // Підключення бібліотеки для шифрування паролей +const jwt = require('jsonwebtoken'); +const { User } = require('../../models/user'); +const { HttpError } = require('../../helpers'); +const { SECRET_KEY } = process.env; + +const login = async (req, res) => { + const { email, password } = req.body; + + // Пошук користувача в базі даних за його електронною поштою + const user = await User.findOne({ email }); + + if (!user) { + throw HttpError(401, 'Email or password is wrong'); + } + + // Порівняння введеного користувачем пароля з захешованим паролем з бази даних + const passwordCompare = await bcryptjs.compare(password, user.password); + + if (!passwordCompare) { + throw HttpError(401, 'Email or password is wrong'); + } + + // Створення пейлоада для JWT-токена, який містить ID користувача + const payload = { + id: user._id, + }; + + // Генерація JWT-токена з використанням пейлоада, секретного ключа та терміну дії токена + const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '7d' }); + + // Оновлення користувача в базі даних, зберігаючи згенерований токен + await User.findByIdAndUpdate(user._id, { token }); + + res.json({ + token, + user: { + email: user.email, + subscription: user.subscription, + }, + }); +}; + +module.exports = login; diff --git a/homework-05/controllers/auth/logout.js b/homework-05/controllers/auth/logout.js new file mode 100644 index 0000000..5ebf5a9 --- /dev/null +++ b/homework-05/controllers/auth/logout.js @@ -0,0 +1,11 @@ +const { User } = require('../../models/user'); + +const logout = async (req, res) => { + const { _id } = req.user; + // Оновлення користувача в базі даних, встановивши поле `token` пустим рядком, що призведе до деактивації токена + await User.findByIdAndUpdate(_id, { token: '' }); + + res.status(204).json(); +}; + +module.exports = logout; diff --git a/homework-05/controllers/auth/register.js b/homework-05/controllers/auth/register.js new file mode 100644 index 0000000..c1efe96 --- /dev/null +++ b/homework-05/controllers/auth/register.js @@ -0,0 +1,24 @@ +const bcryptjs = require('bcryptjs'); // Підключення бібліотеки для хешування паролів +const { User } = require('../../models/user'); +const { HttpError } = require('../../helpers'); + +const register = async (req, res) => { + const { email, password } = req.body; + + // Перевірка, чи в базі даних вже існує користувач з таким email + const user = await User.findOne({ email }); + if (user) { + throw HttpError(409, 'Email in use'); + } + + const hashPassword = await bcryptjs.hash(password, 10); + + const newUser = await User.create({ ...req.body, password: hashPassword }); + + res.status(201).json({ + name: newUser.name, + email: newUser.email, + }); +}; + +module.exports = register; diff --git a/homework-05/controllers/auth/updateAvatar.js b/homework-05/controllers/auth/updateAvatar.js new file mode 100644 index 0000000..c2721a2 --- /dev/null +++ b/homework-05/controllers/auth/updateAvatar.js @@ -0,0 +1,56 @@ +const { User } = require('../../models/user'); +const path = require('path'); +const fs = require('fs/promises'); +const { HttpError, resizeImage } = require('../../helpers'); + +const avatarsDir = path.join(__dirname, '../../public/avatars'); + +// Тимчасова директорія для завантаження аватарів перед зміною розміру +const tempDir = path.join(__dirname, '../../temp'); + +const avatarExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff']; + +const updateAvatar = async (req, res) => { + const { _id } = req.user; + + // Отримання даних про завантажений аватар з об'єкту файлу, що був завантажений з допомогою multer + const { path: tempUpload, originalname } = req.file; + + const avatarName = `${_id}_${originalname}`; + + const fileExtension = originalname.substring(originalname.lastIndexOf('.') + 1); + + // Перевірка, чи відповідне розширення файлу знаходиться серед дозволених + if (!avatarExtensions.includes(fileExtension.toLowerCase())) { + throw new HttpError( + 400, + `${originalname} includes an invalid file extension! Must be: ${avatarExtensions.join(', or ')}`, + ); + } + + // Формуємо шлях до тимчасового файлу з аватаром + const tempImagePath = path.join(tempDir, avatarName); + + // Формуємо шлях до файлу з зміненим розміром (аватаром) у директорії public/avatars + const resizedImagePath = path.join(avatarsDir, avatarName); + + try { + // Перевіряємо, чи файл з аватаром вже існує у тимчасовій директорії + await fs.stat(tempImagePath); + } catch (error) { + // Якщо файл відсутній, це означає, що це перший раз завантажується аватар, + // тому ми копіюємо його без зміни розміру в тимчасову директорію + await fs.copyFile(tempUpload, tempImagePath); + } + + await resizeImage(tempImagePath, resizedImagePath); + + // Оновлюємо поле avatarURL в об'єкті користувача в базі даних за допомогою методу findByIdAndUpdate + await User.findByIdAndUpdate(_id, { avatarURL: path.join('avatars', avatarName) }); + + res.json({ + avatarURL: path.join('avatars', avatarName), + }); +}; + +module.exports = updateAvatar; diff --git a/homework-05/controllers/contacts/addContact.js b/homework-05/controllers/contacts/addContact.js new file mode 100644 index 0000000..8d53d38 --- /dev/null +++ b/homework-05/controllers/contacts/addContact.js @@ -0,0 +1,15 @@ +const addContact = async (req, res) => { + // Валідація даних контакту згідно схеми addSchema + const { error } = addSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing required name field'); + } + + const { _id: owner } = req.user; + + const result = await Contact.create({ ...req.body, owner }); + + res.status(201).json(result); +}; + +module.exports = addContact; diff --git a/homework-05/controllers/contacts/getById.js b/homework-05/controllers/contacts/getById.js new file mode 100644 index 0000000..609dfd6 --- /dev/null +++ b/homework-05/controllers/contacts/getById.js @@ -0,0 +1,14 @@ +const getById = async (req, res) => { + const { id } = req.params; + + const result = await Contact.findById(id); + + // Перевірка, чи був знайдений контакт за вказаним ідентифікатором + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = getById; diff --git a/homework-05/controllers/contacts/index.js b/homework-05/controllers/contacts/index.js new file mode 100644 index 0000000..b4a4ed2 --- /dev/null +++ b/homework-05/controllers/contacts/index.js @@ -0,0 +1,15 @@ +const listContacts = require('./listContacts'); +const getById = require('./getById'); +const addContact = require('./addContact'); +const updateById = require('./updateById'); +const updateStatusContact = require('./updateStatusContact'); +const removeContact = require('./removeContact'); + +module.exports = { + listContacts, + getById, + addContact, + updateById, + updateStatusContact, + removeContact, +}; diff --git a/homework-05/controllers/contacts/listContacts.js b/homework-05/controllers/contacts/listContacts.js new file mode 100644 index 0000000..cf7ff96 --- /dev/null +++ b/homework-05/controllers/contacts/listContacts.js @@ -0,0 +1,26 @@ +const listContacts = async (req, res) => { + const { _id: owner } = req.user; + const { page = 1, limit = 10 } = req.query; + + // Обчислення кількості пропущених контактів (skip) відповідно до номера сторінки та кількості елементів на сторінці + const skip = (page - 1) * limit; + + if (req.query.favorite) { + // Визначення, чи користувач хоче побачити улюблені контакти (значення 'true' або 'false') + const favorite = req.query.favorite === 'true'; + + // Пошук контактів у базі даних, які належать користувачу (owner) та є або не є улюбленими (відповідно до параметра 'favorite') + const result = await Contact.find({ owner, favorite }, '', { + skip, + limit, + }).populate('owner', 'name email'); + return res.json(result); + } + + // Якщо в запиті не вказано, що показувати улюблені контакти, виконується пошук всіх контактів користувача + const result = await Contact.find({ owner }, '', { skip, limit }).populate('owner', 'name email'); + + res.json(result); +}; + +module.exports = listContacts; diff --git a/homework-05/controllers/contacts/removeContact.js b/homework-05/controllers/contacts/removeContact.js new file mode 100644 index 0000000..51aa5a6 --- /dev/null +++ b/homework-05/controllers/contacts/removeContact.js @@ -0,0 +1,16 @@ +const removeContact = async (req, res) => { + const { id } = req.params; + + const result = await Contact.findByIdAndDelete(id); + + // Перевірка, чи було успішне видалення контакту + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json({ + message: 'contact deleted', + }); +}; + +module.exports = removeContact; diff --git a/homework-05/controllers/contacts/updateById.js b/homework-05/controllers/contacts/updateById.js new file mode 100644 index 0000000..fd78fde --- /dev/null +++ b/homework-05/controllers/contacts/updateById.js @@ -0,0 +1,20 @@ +const updateById = async (req, res) => { + // Перевірка валідності даних контакту з тілом запиту згідно схеми addSchema + const { error } = addSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing fields'); + } + + const { id } = req.params; + + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + + // Перевірка, чи було успішне оновлення контакту + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateById; diff --git a/homework-05/controllers/contacts/updateStatusContact.js b/homework-05/controllers/contacts/updateStatusContact.js new file mode 100644 index 0000000..221e563 --- /dev/null +++ b/homework-05/controllers/contacts/updateStatusContact.js @@ -0,0 +1,20 @@ +const updateStatusContact = async (req, res) => { + // Перевірка валідності поля favorite у тілі запиту згідно схеми updateFavoriteSchema + const { error } = updateFavoriteSchema.validate(req.body); + if (error) { + throw HttpError(400, 'missing field favorite'); + } + + const { id } = req.params; + + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); + + // Перевірка, чи було успішне оновлення контакту + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateStatusContact; diff --git a/homework-05/helpers/HttpError.js b/homework-05/helpers/HttpError.js new file mode 100644 index 0000000..67d68a4 --- /dev/null +++ b/homework-05/helpers/HttpError.js @@ -0,0 +1,7 @@ +const HttpError = (status, message) => { + const error = new Error(message); + error.status = status; // Додавання властивості "status" з переданим статусом до об'єкту помилки + return error; +}; + +module.exports = HttpError; // Експорт функції HttpError для використання її в інших модулях проекту diff --git a/homework-05/helpers/ctrlWrapper.js b/homework-05/helpers/ctrlWrapper.js new file mode 100644 index 0000000..8d3e6fb --- /dev/null +++ b/homework-05/helpers/ctrlWrapper.js @@ -0,0 +1,14 @@ +const ctrlWrapper = ctrl => { + const func = async (req, res, next) => { + try { + // Виклик контролера з передачею йому об'єкта запиту (req), об'єкта відповіді (res) та функції next + await ctrl(req, res, next); + } catch (error) { + next(error); + } + }; + + return func; +}; + +module.exports = ctrlWrapper; diff --git a/homework-05/helpers/handleMongooseError.js b/homework-05/helpers/handleMongooseError.js new file mode 100644 index 0000000..d30bcf2 --- /dev/null +++ b/homework-05/helpers/handleMongooseError.js @@ -0,0 +1,11 @@ +const handleMongooseError = (error, data, next) => { + const { name, code } = error; + // Якщо помилка є MongoDB Duplicate Key Error (код 11000), статус 409 - Conflict + // В інших випадках, статус 400 - Bad Request + const status = name === 'MongoServerError' && code === 11000 ? 409 : 400; + // Додавання статусу до об'єкту помилки + error.status = status; + next(); +}; + +module.exports = handleMongooseError; diff --git a/homework-05/helpers/index.js b/homework-05/helpers/index.js new file mode 100644 index 0000000..a2d0670 --- /dev/null +++ b/homework-05/helpers/index.js @@ -0,0 +1,11 @@ +const HttpError = require('./HttpError'); +const ctrlWrapper = require('./ctrlWrapper'); +const handleMongooseError = require('./handleMongooseError'); +const resizeImage = require('./resizeImage'); + +module.exports = { + HttpError, + ctrlWrapper, + handleMongooseError, + resizeImage, +}; diff --git a/homework-05/helpers/resizeImage.js b/homework-05/helpers/resizeImage.js new file mode 100644 index 0000000..5f10f65 --- /dev/null +++ b/homework-05/helpers/resizeImage.js @@ -0,0 +1,10 @@ +const Jimp = require('jimp'); // Підключення бібліотеки Jimp для роботи з зображеннями + +const resizeImage = async (sourcePath, destinationPath) => { + // Асинхронна функція для зміни розміру зображення + const image = await Jimp.read(sourcePath); // Зчитування вихідного зображення з вказаного шляху + image.resize(250, 250); + await image.writeAsync(destinationPath); // Збереження зменшеного зображення за вказаним шляхом +}; + +module.exports = resizeImage; diff --git a/homework-05/middlewares/authenticate.js b/homework-05/middlewares/authenticate.js new file mode 100644 index 0000000..b324a91 --- /dev/null +++ b/homework-05/middlewares/authenticate.js @@ -0,0 +1,34 @@ +const jwt = require('jsonwebtoken'); +const { User } = require('../models/user'); + +require('dotenv').config(); +const { SECRET_KEY } = process.env; + +const { HttpError } = require('../helpers'); + +const authenticate = async (req, res, next) => { + const { authorization = '' } = req.headers; // Отримання заголовку Authorization з запиту, якщо такий існує + const [bearer, token] = authorization.split(' '); // Розділення токена на частини по пробілу + + if (bearer !== 'Bearer') { + // Перевірка на наявність токена та перевірка його типу + next(HttpError(401, 'Not authorized')); // Якщо токен не валідний, викликаємо функцію HttpError зі статусом 401 та повідомленням про помилку + } + + try { + const { id } = jwt.verify(token, SECRET_KEY); + const user = await User.findById(id); + if (!user || !user.token || user.token !== token) { + // Перевірка чи користувач з таким id існує та чи токен співпадає з токеном з бази даних + next(HttpError(401, 'Not authorized')); + } + + req.user = user; + + next(); // Переходимо до наступного middleware + } catch { + next(HttpError(401, 'Not authorized')); + } +}; + +module.exports = authenticate; diff --git a/homework-05/middlewares/index.js b/homework-05/middlewares/index.js new file mode 100644 index 0000000..f0fc7f7 --- /dev/null +++ b/homework-05/middlewares/index.js @@ -0,0 +1,5 @@ +const isValidId = require('./isValidId'); +const authenticate = require('./authenticate'); +const validateBody = require('./validateBody'); +const upload = require('./upload'); +module.exports = { isValidId, authenticate, validateBody, upload }; diff --git a/homework-05/middlewares/isValidId.js b/homework-05/middlewares/isValidId.js new file mode 100644 index 0000000..719e4c6 --- /dev/null +++ b/homework-05/middlewares/isValidId.js @@ -0,0 +1,14 @@ +const { isValidObjectId } = require('mongoose'); +const { HttpError } = require('../helpers'); + +const isValidId = (req, res, next) => { + const { id } = req.params; + + if (!isValidObjectId(id)) { + next(HttpError(400, `${id} is not valid id`)); + } + + next(); // Якщо id валідний, переходимо до наступного middleware +}; + +module.exports = isValidId; diff --git a/homework-05/middlewares/upload.js b/homework-05/middlewares/upload.js new file mode 100644 index 0000000..495de58 --- /dev/null +++ b/homework-05/middlewares/upload.js @@ -0,0 +1,19 @@ +const multer = require('multer'); // Імпорт бібліотеки multer для обробки завантаження файлів +const path = require('path'); + +const tempDir = path.join(__dirname, '../', 'temp'); // Шлях до тимчасової директорії для збереження завантажених файлів + +const multerConfig = multer.diskStorage({ + destination: tempDir, // Встановлення шляху до директорії, де будуть зберігатись тимчасово завантажені файли + filename: (req, file, cb) => { + // Функція для визначення імені завантаженого файлу + cb(null, file.originalname); // Встановлення оригінального імені файлу для збереження + }, +}); + +const upload = multer({ + // Створення middleware для завантаження файлів з використанням налаштувань multerConfig + storage: multerConfig, +}); + +module.exports = upload; diff --git a/homework-05/middlewares/validateBody.js b/homework-05/middlewares/validateBody.js new file mode 100644 index 0000000..861daee --- /dev/null +++ b/homework-05/middlewares/validateBody.js @@ -0,0 +1,17 @@ +const { HttpError } = require('../helpers'); + +const validateBody = schema => { + const func = (req, res, next) => { + const { error } = schema.validate(req.body); + + if (error) { + next(HttpError(400, error.message)); + } + + next(); // Якщо валідація успішна, переходимо до наступного middleware + }; + + return func; +}; + +module.exports = validateBody; diff --git a/homework-05/models/contact.js b/homework-05/models/contact.js new file mode 100644 index 0000000..3dcb287 --- /dev/null +++ b/homework-05/models/contact.js @@ -0,0 +1,44 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const contactSchema = new Schema( + { + name: { + type: String, + required: [true, 'Set name for contact'], + }, + email: { + type: String, + }, + phone: { + type: String, + }, + favorite: { + type: Boolean, + default: false, + }, + owner: { + type: Schema.Types.ObjectId, + ref: 'user', // Посилання на колекцію "user" + required: true, + }, + }, + { versionKey: false }, // Прибирає ключ __v з документів у базі даних +); + +contactSchema.post('save', handleMongooseError); + +const Contact = model('contact', contactSchema); + +const addSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), +}); + +const updateFavoriteSchema = Joi.object({ + favorite: Joi.boolean().required(), +}); + +module.exports = { Contact, addSchema, updateFavoriteSchema }; diff --git a/homework-05/models/user.js b/homework-05/models/user.js new file mode 100644 index 0000000..bee944f --- /dev/null +++ b/homework-05/models/user.js @@ -0,0 +1,73 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const emailRegexp = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; +const subscriptionList = ['starter', 'pro', 'business']; + +const userSchema = new Schema( + { + name: { + type: String, + required: true, + }, + email: { + type: String, + match: emailRegexp, + required: [true, 'Email is required'], + unique: true, + }, + password: { + type: String, + minlength: 6, + required: [true, 'Set password for user'], + }, + + subscription: { + type: String, + enum: subscriptionList, + default: 'starter', + }, + token: { + type: String, + default: '', // Поле "token" за замовчуванням є пустим рядком + }, + avatarURL: { + type: String, + required: true, + }, + }, + { versionKey: false }, // Прибирає ключ __v з документів у базі даних +); + +userSchema.post('save', handleMongooseError); + +const registerSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().pattern(emailRegexp).required(), + password: Joi.string().min(6).required(), +}); + +const loginSchema = Joi.object({ + email: Joi.string().required(), + password: Joi.string().min(6).required(), +}); + +const changeSubscriptionSchema = Joi.object({ + subscription: Joi.string() + .valid(...subscriptionList) + .required(), +}); + +const User = model('user', userSchema); + +const schemas = { + registerSchema, + loginSchema, + changeSubscriptionSchema, +}; + +module.exports = { + User, + schemas, +}; diff --git a/homework-05/nodemon.json b/homework-05/nodemon.json new file mode 100644 index 0000000..54d6947 --- /dev/null +++ b/homework-05/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["node_modules", "models/contacts.json"] +} diff --git a/homework-05/public/avatars/.gitkeep b/homework-05/public/avatars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/homework-05/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg.jpg b/homework-05/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg.jpg new file mode 100644 index 0000000..86e804f Binary files /dev/null and b/homework-05/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg.jpg differ diff --git a/homework-05/routes/api/auth.js b/homework-05/routes/api/auth.js new file mode 100644 index 0000000..db18d06 --- /dev/null +++ b/homework-05/routes/api/auth.js @@ -0,0 +1,31 @@ +const express = require('express'); +const { validateBody, authenticate, upload } = require('../../middlewares'); +const { schemas } = require('../../models/user'); + +const router = express.Router(); + +const ctrl = require('../../controllers/auth'); +const { ctrlWrapper } = require('../../helpers'); + +router.post( + '/register', + validateBody(schemas.registerSchema), + ctrlWrapper(ctrl.register), // Обробка запиту контролером register +); + +router.post('/login', validateBody(schemas.loginSchema), ctrlWrapper(ctrl.login)); + +router.get('/current', authenticate, ctrlWrapper(ctrl.getCurrent)); // Встановлення маршруту GET з middleware автентифікації до обробки контролером getCurrent + +router.post('/logout', authenticate, ctrlWrapper(ctrl.logout)); + +router.patch( + '/users/avatars', + authenticate, + upload.single('avatar'), // Використання middleware upload для завантаження одного файла з ім'ям "avatar" + ctrlWrapper(ctrl.updateAvatar), // Обробка запиту контролером updateAvatar +); + +router.patch('/', authenticate, validateBody(schemas.changeSubscriptionSchema), ctrlWrapper(ctrl.changeSubscription)); + +module.exports = router; diff --git a/homework-05/routes/api/contacts.js b/homework-05/routes/api/contacts.js new file mode 100644 index 0000000..fb235e7 --- /dev/null +++ b/homework-05/routes/api/contacts.js @@ -0,0 +1,16 @@ +const express = require('express'); +const router = express.Router(); + +const ctrl = require('../../controllers/contacts'); +const { ctrlWrapper } = require('../../helpers'); + +const { isValidId, authenticate } = require('../../middlewares'); + +router.get('/', authenticate, ctrlWrapper(ctrl.listContacts)); +router.get('/:id', authenticate, isValidId, ctrlWrapper(ctrl.getById)); +router.post('/', authenticate, ctrlWrapper(ctrl.addContact)); +router.put('/:id', authenticate, isValidId, ctrlWrapper(ctrl.updateById)); +router.patch('/:id/favorite', authenticate, isValidId, ctrlWrapper(ctrl.updateStatusContact)); +router.delete('/:id', authenticate, isValidId, ctrlWrapper(ctrl.removeContact)); + +module.exports = router; diff --git a/homework-05/server.js b/homework-05/server.js new file mode 100644 index 0000000..1ee1d63 --- /dev/null +++ b/homework-05/server.js @@ -0,0 +1,16 @@ +const mongoose = require('mongoose'); +const app = require('./app'); +const { DB_HOST } = process.env; + +mongoose + .connect(DB_HOST) + .then(() => { + console.log('Database connection successful'); + app.listen(3000, () => { + console.log('Server running. Use our API on port: 3000'); + }); + }) + .catch(error => { + console.log(error.message); + process.exit(1); // Завершити процес з кодом 1, щоб позначити, що виникла помилка + }); diff --git a/homework-05/temp/.gitkeep b/homework-05/temp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/homework-05/temp/64a581f17ab9d2f81652c536_avatar.jpg.jpg b/homework-05/temp/64a581f17ab9d2f81652c536_avatar.jpg.jpg new file mode 100644 index 0000000..ef6fac6 Binary files /dev/null and b/homework-05/temp/64a581f17ab9d2f81652c536_avatar.jpg.jpg differ diff --git a/homework-05/temp/avatar.jpg b/homework-05/temp/avatar.jpg new file mode 100644 index 0000000..ef6fac6 Binary files /dev/null and b/homework-05/temp/avatar.jpg differ diff --git a/homework-06/.eslintignore b/homework-06/.eslintignore new file mode 100644 index 0000000..b512c09 --- /dev/null +++ b/homework-06/.eslintignore @@ -0,0 +1 @@ +node_modules \ No newline at end of file diff --git a/homework-06/.eslintrc.js b/homework-06/.eslintrc.js new file mode 100644 index 0000000..d799332 --- /dev/null +++ b/homework-06/.eslintrc.js @@ -0,0 +1,12 @@ +module.exports = { + env: { + commonjs: true, + es2021: true, + node: true, + }, + extends: ['standard', 'prettier'], + parserOptions: { + ecmaVersion: 12, + }, + rules: {}, +} diff --git a/homework-06/.gitignore b/homework-06/.gitignore new file mode 100644 index 0000000..8877fc5 --- /dev/null +++ b/homework-06/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +.env +.idea +.vscode \ No newline at end of file diff --git a/homework-06/README.en.md b/homework-06/README.en.md deleted file mode 100644 index 305ec42..0000000 --- a/homework-06/README.en.md +++ /dev/null @@ -1,154 +0,0 @@ -**Read in other languages: [Russian](README.md), [Ukrainian](README.ua.md).** - -# Homework 6 - -Create branch `hw06-email` from branch `master`. - -We continue to create a REST API for working with a collection of contacts. Add user email verification after registration using the [SendGrid](https://sendgrid.com/) service. - -## How the Verification Process Should Work - -1. After registration, the user should receive a letter to the email address specified during registration with a link to verify his email -2. After clicking the link in the received email for the first time, the user should receive a [Response with status 200](#verification-success-response), which will imply successful email verification -3. After clicking on the link again, the user should get [Error with status 404](#verification-user-not-found) - -## Step 1 - -### Preparing Integration with SendGrid API - -- Register at [SendGrid](https://sendgrid.com/) -- Create an email sender. To do this, in the SendGrid administrative panel, go to the Marketing menu in the senders submenu and click the "Create New Sender" button in the upper right corner. Fill in the required fields in the proposed form. Save. You should get the following result as in the picture, only with your email instead. - -![sender](sender-not-verify.png) - -A verification email should be sent to the specified email address (check spam if you don't see the email). Click on the link in it and complete the process. The result should change to: - -![sender](sender-verify.png) - -- Now you need to create an API access token. Select the "Email API" menu, and the "Integration Guide" submenu. Here we select "Web API" - -![api-key](web-api.png) - -Next, you need to select the Node.js technology - -![api-key](node.png) - -In the third step, we give a name to our token. For example systemcats, we press the generate button and we get the result as in the screenshot below. You need to copy this token (this is important because you won't be able to see it anymore). After completing the token creation process: - -![api-key](api-key.png) - -- The resulting API token must be added to the `.env` file in our project - -## Step 2 - -### Create an Endpoint for Email Verification - -- Add two fields `verificationToken` and `verify` to the `User` model. A value of the `verify` field equal to `false` will mean that his email has not been verified yet. - -```js -{ - verify: { - type: Boolean, - default: false, - }, - verificationToken: { - type: String, - required: [true, 'Verify token is required'], - }, -} -``` - -- Create a GET [`/users/verify/:verificationToken`](#verification-request) endpoint, where we will search for a user in the `User` model by the `verificationToken` parameter. -- If user with such token is not found, return [Error 'Not Found'](#verification-user-not-found) -- If the user is found - set `verificationToken` to `null`, and set the `verify` field to `true` in the user document and return [Successful response](#verification-success-response) - -### Verification Request - -```shell -GET /users/verify/:verificationToken -``` - -### Verification User Not Found - -```shell -Status: 404 Not Found -ResponseBody: { - message: 'User not found' -} -``` - -### Verification Success Response - -```shell -Status: 200 OK -ResponseBody: { - message: 'Verification successful', -} -``` - -## Step 3 - -### Adding an Email to the User with a Verificaton Link - -When creating a user during registration: - -- Create a `verificationToken` for the user and write it to the database (to generate a token, use the package [uuid](https://www.npmjs.com/package/uuid) or [nanoid](https://www.npmjs.com /package/nanoid)) -- Send an email to the user's mail and specify a link to verify the email (`/users/verify/:verificationToken`) in the message -- It is also necessary to take into account that now the user login is not allowed with an unverified email - -## Step 4 - -### Adding a Resend Email to the User with a Verification Link - -It is necessary to provide for the option that the user can accidentally delete the letter. It may not reach the addressee for some reason. Our service for sending letters during registration gave an error, etc. - -#### @ POST /users/verify/ - -- Gets `body` in `{ email }` format -- If there is no required field `email` in `body`, returns JSON with key `{"message": "missing required field email"}` and status `400` -- If everything is fine with `body`, resend the letter with `verificationToken` to the specified email, but only if the user is not verified -- If the user has already passed verification, send json with the key `{ message: "Verification has already been passed"}` with status `400 Bad Request` - -#### Resending an Email Request - -```shell -POST /users/verify -Content-Type: application/json -RequestBody: { - "email": "example@example.com" -} -``` - -#### Resending an Email Validation Error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Resending an Email Success Response - -```shell -Status: 200 Ok -Content-Type: application/json -ResponseBody: { - "message": "Verification email sent" -} -``` - -#### Resend Email for Verified User - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: { - message: "Verification has already been passed" -} -``` - -> Note: As an alternative to SendGrid, you can use the [nodemailer] package(https://www.npmjs.com/package/nodemailer) - -## Additional Task - Optional - -### 1. Write a dockerfile for your application diff --git a/homework-06/README.es.md b/homework-06/README.es.md deleted file mode 100644 index c0bd115..0000000 --- a/homework-06/README.es.md +++ /dev/null @@ -1,155 +0,0 @@ -**Leer en otros idiomas: [Русский](README.md), [Українська](README.ua.md).** - -# Tarea 6 - -Crea una rama `hw06-email` de la rama `master`. - -Continuamos construyendo la API REST para la manipulación de colecciones de contactos. Añade la verificación del email del usuario después de registrarse, mediante el servicio [SendGrid](https://sendgrid.com/). - -## Cómo debe funcionar el proceso de verificación - -1. Después de registrarse, el usuario debería recibir un mensaje de correo electrónico en la dirección facilitada durante el registro con un enlace para verificar su email -2. Al seguir el enlace en el correo recibido la primera vez, el usuario debería recibir una [Respuesta con el estado 200](#verification-success-response), lo que significaría la verificación exitosa del correo electrónico -3. Al seguir de nuevo el enlace, el usuario debería recibir [Error con el estado 404](#verification-user-not-found) - -## Paso 1 - -### Preparando la integración con SendGrid API - -- Regístrate en [SendGrid](https://sendgrid.com/). -- Crea un remitente de correo electrónico. Para ello, en el panel de administración de SendGrid, vaya al menú Marketing en el submenú senders y en la esquina superior derecha haz clic en "Create New Sender". Rellena los campos obligatorios del formulario propuesto. Guárdalo. Deberías obtener el siguiente resultado como en la imagen, sólo que con tu email: - -![sender](sender-not-verify.png) - -Debería enviarse un correo de verificación al email especificado (comprueba el correo no deseado si no lo ves). Haz clic en el enlace que aparece en él y completa el proceso. El resultado debería cambiar a: - -![sender](sender-verify.png) - -- Ahora necesitas crear un token de acceso a la API. Selecciona el menú "Email API" y el submenú "Integration Guide". Aquí seleccione "Web API" - -![api-key](web-api.png) - -A continuación, seleccione la tecnología Node.js - -![api-key](node.png) - -En el tercer paso, le damos un nombre a nuestro token. Por ejemplo, systemcats, pulse el botón de generar y obtenga el resultado como se muestra en la captura de pantalla siguiente. Tienes que copiar este token (esto es importante porque no podrás volver a verlo). Después de completar el proceso de creación del token - -![api-key](api-key.png) - -- El token de la API obtenido debe añadirse al archivo `.env` de nuestro proyecto - -## Paso 2 - -### Creación de un endpoint para la verificación del email - -- añade dos campos `verificationToken` y `verify` al modelo `User`. Si el valor del campo `verify` igual a `false` significa que su correo electrónico aún no ha sido verificado. - -```js -{ - verify: { - type: Boolean, - default: false, - }, - verificationToken: { - type: String, - required: [true, 'Verify token is required'], - }, -} -``` - -- crea el endpoint GET [`/users/verify/:verificationToken`](#verification-request), donde para buscar un usuario en el modelo `User` se utilizaremos el parámetro `verificationToken` -- si no se encuentra un usuario con este token, debe devolver [Error 'Not Found'](#verification-user-not-found) -- si el usuario es encontrado, se establece `verificationToken` a `null` y se establece el campo `verify` a `true` en el documento del usuario, y se devuelve [Respuesta exitosa](#verification-success-response) - -### Verification request - -```shell -GET /users/verify/:verificationToken -``` - -### Verification user Not Found - -```shell -Status: 404 Not Found -ResponseBody: { - message: 'User not found' -} -``` - -### Verification success response - -```shell -Status: 200 OK -ResponseBody: { - message: 'Verification successful', -} -``` - -## Paso 3 - -### Añadir el correo electrónico al usuario con un enlace de verificación - -Al crear un usuario durante el registro: - -- se crea `verificationToken` para el usuario y se escribe en la base de datos (para generar el token, utilice el paquete [uuid](https://www.npmjs.com/package/uuid) o [nanoid](https://www.npmjs.com/package/nanoid)) -- Se envía un correo electrónico al usuario y se proporciona un enlace para verificar el email (`/users/verify/:verificationToken`) en el mensaje -- También hay que tener en cuenta que ahora no se permite el inicio de sesión del usuario si el correo electrónico no está verificado - -## Paso 4 - -### Realizar un reenvío del correo al usuario con el enlace de verificación - -Es posible que el usuario pueda borrar accidentalmente el correo electrónico. Puede que no llegue al destinatario por alguna razón, o que nuestro servicio de envío de correos electrónicos haya tenido un error durante el registro, etc. - -#### @ POST /users/verify/ - -- Recibe `body` en formato `{ email }` -- Si en `body` no existe el campo obligatorio `email`, devuelve un json con la llave `{"message": "missing required field email"}` y el estado `400` -- Si en `body` todo está bien, reenviamos el correo electrónico con un `verificationToken` al email especificado, pero sólo si el usuario no está verificado -- Si el usuario ya ha sido verificado, envía un json con la llave `{ message: "Verification has already been passed"}` con el estado `400 Bad Request` - -#### Resending a email request - -```shell -POST /users/verify -Content-Type: application/json -RequestBody: { - "email": "example@example.com" -} -``` - -#### Resending a email validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Resending a email success response - -```shell -Status: 200 Ok -Content-Type: application/json -ResponseBody: { - "message": "Verification email sent" -} -``` - -#### Resend email for verified user - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: { - message: "Verification has already been passed" -} -``` - -> Nota: Como alternativa a SendGrid, puede utilizar el paquete [nodemailer](https://www.npmjs.com/package/nodemailer) - -## Tarea adicional (opcional) - -### 1. Escriba un dockerfile para su aplicación - diff --git a/homework-06/README.md b/homework-06/README.md index 240110c..79af6d6 100644 --- a/homework-06/README.md +++ b/homework-06/README.md @@ -1,155 +1 @@ -**Читать на других языках: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнее задание 6 - -Создай ветку `hw06-email` из ветки `master`. - -Продолжаем создание REST API для работы с коллекцией контактов. Добавьте верификацию email пользователя после регистрации при помощи сервиса [SendGrid](https://sendgrid.com/). - -## Как процесс верификации должен работать - -1. После регистрации, пользователь должен получить письмо на указанную при регистрации почту с ссылкой для верификации своего email -2. Пройдя ссылке в полученном письме, в первый раз, пользователь должен получить [Ответ со статусом 200](#verification-success-response), что будет подразумевать успешную верификацию email -3. Пройдя по ссылке повторно пользователь должен получить [Ошибку со статусом 404](#verification-user-not-found) - -## Шаг 1 - -### Подготовка интеграции с SendGrid API - -- Зарегистрируйся на [SendGrid](https://sendgrid.com/). -- Создай email-отправителя. Для это в административной панели SendGrid зайдите в меню Marketing в подменю senders и в правом верхнем углу нажмите кнопку "Create New Sender". Заполните необходимые поля в предложенной форме. Сохраните. Должен получится следующий как на картинке результат, только с вашим email: - -![sender](sender-not-verify.png) - -На указанный email должно прийти письмо верификации (проверьте спам если не видите письма). Кликните на ссылку в нем и завершите процесс. Результат должен изменится на: - -![sender](sender-verify.png) - -- Теперь необходимо создать API токен доступа. Выбираем меню "Email API", и подменю "Integration Guide". Здесь выбираем "Web API" - -![api-key](web-api.png) - -Дальше необходимо выбрать технологию Node.js - -![api-key](node.png) - -На третьем шаге даем имя нашему токену. Например systemcats, нажимаем кнопку сгенерировать и получаем результат как на скриншоте ниже. Необходимо скопировать этот токен (это важно, так как больше вы не сможете его посмотреть). После завершить процесс создания токена - -![api-key](api-key.png) - -- Полученный API-токен надо добавить в `.env` файл в нашем проекте - -## Шаг 2 - -### Создание ендпоинта для верификации email'а - -- добавить в модель `User` два поля `verificationToken` и `verify`. Значение поля `verify` равное `false` будет означать, что его email еще не прошел верификацию - -```js -{ - verify: { - type: Boolean, - default: false, - }, - verificationToken: { - type: String, - required: [true, 'Verify token is required'], - }, -} -``` - -- создать эндпоинт GET [`/users/verify/:verificationToken`](#verification-request), где по параметру `verificationToken` мы будем искать пользователя в модели `User` -- если пользователь с таким токеном не найден, необходимо вернуть [Ошибку 'Not Found'](#verification-user-not-found) -- если пользователь найден - устанавливаем `verificationToken` в `null`, а поле `verify` ставим равным `true` в документе пользователя и возвращаем [Успешный ответ](#verification-success-response) - -### Verification request - -```shell -GET /users/verify/:verificationToken -``` - -### Verification user Not Found - -```shell -Status: 404 Not Found -ResponseBody: { - message: 'User not found' -} -``` - -### Verification success response - -```shell -Status: 200 OK -ResponseBody: { - message: 'Verification successful', -} -``` - -## Шаг 3 - -### Добавление отправки email пользователю с ссылкой для верификации - -При создания пользователя при регистрации: - -- создать `verificationToken` для пользователя и записать его в БД (для генерации токена используйте пакет [uuid](https://www.npmjs.com/package/uuid) или [nanoid](https://www.npmjs.com/package/nanoid)) -- отправить email на почту пользователя и указать ссылку для верификации email'а (`/users/verify/:verificationToken`) в сообщении -- Так же необходимо учитывать, что теперь логин пользователя не разрешен при не верифицированном email - -## Шаг 4 - -### Добавление повторной отправки email пользователю с ссылкой для верификации - -Необходимо предусмотреть, вариант, что пользователь может случайно удалить письмо. Оно может не дойти по какой-то причине к адресату. Наш сервис отправки писем во время регистрации выдал ошибку и т.д. - -#### @ POST /users/verify/ - -- Получает `body` в формате `{ email }` -- Если в `body` нет обязательного поля `email`, возвращает json с ключом `{"message": "missing required field email"}` и статусом `400` -- Если с `body` все хорошо, выполняем повторную отправку письма с `verificationToken` на указанный email, но только если пользователь не верифицирован -- Если пользователь уже прошел верификацию отправить json с ключом `{ message: "Verification has already been passed"}` со статусом `400 Bad Request` - -#### Resending a email request - -```shell -POST /users/verify -Content-Type: application/json -RequestBody: { - "email": "example@example.com" -} -``` - -#### Resending a email validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Ошибка от Joi или другой библиотеки валидации> -``` - -#### Resending a email success response - -```shell -Status: 200 Ok -Content-Type: application/json -ResponseBody: { - "message": "Verification email sent" -} -``` - -#### Resend email for verified user - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: { - message: "Verification has already been passed" -} -``` - -> Примечание: Как альтернативу SendGrid можно использовать пакет [nodemailer](https://www.npmjs.com/package/nodemailer) - -## Дополнительное задание - необязательное - -### 1. Напишите dockerfile для вашего приложения - +## GoIT Node.js Course Template Homework diff --git a/homework-06/README.pl.md b/homework-06/README.pl.md deleted file mode 100644 index 77000ca..0000000 --- a/homework-06/README.pl.md +++ /dev/null @@ -1,155 +0,0 @@ -**Czytaj w innych językach: [rosyjski](README.md), [ukraiński](README.ua.md).** - -# Zadanie domowe 6 - -Utwórz gałąź `hw06-email` z gałęzi `master`. - -Kontynuujemy tworzenie REST API pracy ze zbiorem kontaktów. Dodaj weryfikację emaila użytkownika po rejestracji przy pomocy serwisu [SendGrid](https://sendgrid.com/). - -## Jak powinien działać proces weryfikacji - -1. Po rejestracji użytkownik powinien otrzymać wiadomość na wskazaną przy rejestracji pocztę z odnośnikiem do weryfikacji swojego emaila. -2. Przechodząc do odnośnika w otrzymanej wiadomości po raz pierwszy, użytkownik powinien otrzymać [Odpowiedź ze statusem 200](#verification-success-response), co będzie oznaczać pomyślną weryfikację emaila. -3. Przechodząc po odnośniku powtórnie użytkownik powinien otrzymać [Błąd ze statusem 404](#verification-user-not-found). - -## Krok 1 - -### Przygotowanie integracji z SendGrid API - -- Zarejestruj się na [SendGrid](https://sendgrid.com/). -- Utwórz email nadawcy. W tym celu w panelu administratora SendGrid przejdź do menu Marketing w podmenu senders i w prawym górnym rogu wciśnij przycisk "Create New Sender". Uzupełnij wymagane pola w dołączonym formularzu. Zapisz. Rezultat powinien wyglądać jak na obrazku, tylko z twoim adresem email: - -![sender](sender-not-verify.png) - -Na wskazany email powinna przyjść wiadomość weryfikacyjna (sprawdź spam, jeśli nie widzisz wiadomości). Kliknij na odnośnik w niej i zakończ proces. Wynik powinien zmienić się na: - -![sender](sender-verify.png) - -- Teraz należy utworzyć API token dostępu. Wybieramy menu "Email API" i podmenu "Integration Guide". Tutaj wybieramy "Web API". - -![api-key](web-api.png) - -Dalej należy wybrać technologię Node.js. - -![api-key](node.png) - -W trzecim kroku nazywamy nasz token, na przykład systemcats. Klikamy na przycisk "wygeneruj" i otrzymujemy wynik jak na zrzucie ekranu niżej. Należy skopiować ten token (to ważne, ponieważ więcej nie możesz go zobaczyć). Następnie zakończ proces tworzenia tokena. - -![api-key](api-key.png) - -- Otrzymany token API należy dodać do pliku `.env` w naszym projekcie. - -## Krok 2 - -### Utworzenie endpointu dla weryfikacji emaila - -- Dodaj do modelu `User` dwa pola `verificationToken` i `verify`. Wartość pola `verify` równa `false` będzie oznaczać, że email jeszcze nie przeszedł weryfikacji. - -```js -{ - verify: { - type: Boolean, - default: false, - }, - verificationToken: { - type: String, - required: [true, 'Verify token is required'], - }, -} -``` - -- Utwórz endpoint GET [`/users/verify/:verificationToken`](#verification-request), gdzie w parametrze `verificationToken` będziemy szukać użytkownika w modelu `User`; -- jeśli użytkownik z takim tokenem nie zostanie znaleziony, należy zwrócić [Błąd 'Not Found'](#verification-user-not-found); -- jeśli użytkownik został odnaleziony – ustawiamy `verificationToken` na `null`, a pole `verify` ustawiamy jako równe `true` w dokumencie użytkownika zwracamy [Sukces odpowiedzi](#verification-success-response). - -### Verification request - -```shell -GET /users/verify/:verificationToken -``` - -### Verification user Not Found - -```shell -Status: 404 Not Found -ResponseBody: { - message: 'User not found' -} -``` - -### Verification success response - -```shell -Status: 200 OK -ResponseBody: { - message: 'Verification successful', -} -``` - -## Krok 3 - -### Dodanie wysłania emaila do użytkownika z odnośnikiem dla weryfikacji - -Podczas tworzenia użytkownika przy rejestracji: - -- utworzyć `verificationToken` dla użytkownika i zapisać go w bazie danych (do wygenerowania tokena wykorzystaj pakiet [uuid](https://www.npmjs.com/package/uuid) lub [nanoid](https://www.npmjs.com/package/nanoid)); -- wysłać email na pocztę użytkownika i wskazać odnośnik do weryfikacji emaila (`/users/verify/:verificationToken`) w wiadomości; -- należy wziąć pod uwagę, że teraz login użytkownika nie jest dozwolony przy nieweryfikowanym emailu. - -## Krok 4 - -### Dodanie powtórnego wysłania emaila do użytkownika z odnośnikiem dla weryfikacji - -Należy przewidzieć wariant, że użytkownik może po prostu usunąć wiadomość, z jakiejś przyczyna może ona nie dojść do adresata albo nasz serwis wysyłania wiadomości w czasie rejestracji wyświetlił błąd i tak dalej. - -#### @ POST /users/verify/ - -- Otrzymuje `body` w formacie `{ email }`. -- Jeśli w `body` nie ma obowiązkowego pola `email`, zwraca json z kluczem `{"message": "missing required field email"}` i statusem `400`. -- Jeśli z `body` wszystko w porządku, wykonujemy ponownie wysłanie wiadomości z `verificationToken` na wskazany email, ale tylko jeśli użytkownik nie został zweryfikowany. -- Jeżeli użytkownik przeszedł już weryfikację, wysłać json z kluczem `{ message: "Verification has already been passed"}` ze statusem `400 Bad Request`. - -#### Resending a email request - -```shell -POST /users/verify -Content-Type: application/json -RequestBody: { - "email": "example@example.com" -} -``` - -#### Resending a email validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: -``` - -#### Resending a email success response - -```shell -Status: 200 Ok -Content-Type: application/json -ResponseBody: { - "message": "Verification email sent" -} -``` - -#### Resend email for verified user - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: { - message: "Verification has already been passed" -} -``` - -> Uwaga: Jako alternatywę SendGrid można wykorzystać pakiet [nodemailer](https://www.npmjs.com/package/nodemailer). - -## Zadanie dodatkowe – nieobowiązkowe - -### 1. Napisz dockerfile dla twojej aplikacji - diff --git a/homework-06/README.ua.md b/homework-06/README.ua.md deleted file mode 100755 index f659031..0000000 --- a/homework-06/README.ua.md +++ /dev/null @@ -1,154 +0,0 @@ -**Читати на інших мовах: [Русский](README.md), [Українська](README.ua.md).** - -# Домашнє завдання 6 - -Створи гілку `hw06-email` з гілки` master`. - -Продовжуємо створення REST API для роботи з колекцією контактів. Додайте верифікацію email користувача після реєстрації за допомогою сервісу [SendGrid](https://sendgrid.com/). - -## Як процес верифікації повинен працювати - -1. Після реєстрації, користувач повинен отримати лист на вказану при реєстрації пошту з посиланням для верифікації свого email -2. Пройшовши по посиланню в отриманому листі, в перший раз, користувач повинен отримати [Відповідь зі статусом 200](#verification-success-response), що буде мати на увазі успішну верифікацію email -3. Пройшовши по посиланню повторно користувач повинен отримати [Помилку зі статусом 404](#verification-user-not-found) - -## Крок 1 - -### Підготовка інтеграції з SendGrid API - -- Зареєструйся на [SendGrid](https://sendgrid.com/). -- Створи email-відправника. Для цього в адміністративній панелі SendGrid зайдіть в меню Marketing в підменю senders і в правому верхньому куті натисніть кнопку "Create New Sender". Заповніть поля в запропонованій формі. Збережіть. Повинен вийти наступний, як на картинці, результат, тільки з вашим email: - -![Sender](sender-not-verify.png) - -На вказаний email має прийти лист верифікації (перевірте спам якщо не бачите листи). Натисніть на посилання в ньому і завершіть процес. Результат повинен зміниться на: - -![Sender](sender-verify.png) - -- Тепер необхідно створити API токен доступу. Вибираємо меню "Email API", і підменю "Integration Guide". Тут вибираємо "Web API" - -![Api-key](web-api.png) - -Далі необхідно вибрати технологію Node.js - -![Api-key](node.png) - -На третьому кроці даємо ім'я нашого токену. Наприклад, systemcats, натискаємо кнопку згенерувати і отримуємо результат як на скріншоті нижче. Необхідно скопіювати цей токен (це важливо, тому що більше ви не зможете його подивитися). Після цього завершити процес створення токена - -![Api-key](api-key.png) - -- Отриманий API-токен треба додати в `.env` файл в нашому проекті - -## Крок 2 - -### Створення ендпоінта для верифікації email - -- додати в модель `User` два поля `verificationToken` і `verify`. Значення поля `verify` рівне `false` означатиме, що його email ще не пройшов верифікацію - -```js -{ - verify: { - type: Boolean, - default: false, - }, - verificationToken: { - type: String, - required: [true, 'Verify token is required'], - }, -} -``` - -- створити ендпоінт GET [`/users/verify/:verificationToken`](# verification-request), де по параметру `verificationToken` ми будемо шукати користувача в моделі `User` -- якщо користувач з таким токеном не знайдений, необхідно повернути [Помилку 'Not Found'](#verification-user-not-found) -- якщо користувач знайдений - встановлюємо `verificationToken` в `null`, а поле `verify` ставимо рівним `true` в документі користувача і повертаємо [Успішну відповідь](#verification-success-response) - -### Verification request - -```shell -GET /users/verify/:verificationToken -``` - -### Verification user Not Found - -```shell -Status: 404 Not Found -ResponseBody: { - message: 'User not found' -} -``` - -### Verification success response - -```shell -Status: 200 OK -ResponseBody: { - message: 'Verification successful', -} -``` - -## Крок 3 - -### Додавання відправки email користувачу з посиланням для верифікації - -При створенні користувача при реєстрації: - -- створити `verificationToken` для користувача і записати його у БД (для генерації токена використовуйте пакет [uuid](https://www.npmjs.com/package/uuid) або [nanoid](https://www.npmjs.com/package/nanoid)) -- відправити email на пошту користувача і вказати посилання для верифікації email'а ( `/users/verify/:verificationToken`) в повідомленні -- Так само необхідно враховувати, що тепер логін користувача не дозволений, якщо не верифікувано email - -## Крок 4 - -### Додавання повторної відправки email користувачу з посиланням для верифікації - -Необхідно передбачити, варіант, що користувач може випадково видалити лист. Воно може не дійти з якоїсь причини до адресата. Наш сервіс відправки листів під час реєстрації видав помилку і т.д. - -#### @ POST /users/verify - -- Отримує `body` у форматі `{email}` -- Якщо в `body` немає обов'язкового поля `email`, повертає json з ключем `{"message":"missing required field email"}` і статусом `400` -- Якщо з `body` все добре, виконуємо повторну відправку листа з `verificationToken` на вказаний email, але тільки якщо користувач не верифікований -- Якщо користувач вже пройшов верифікацію відправити json з ключем `{"message":"Verification has already been passed"}` зі статусом `400 Bad Request` - -#### Resending a email request - -````shell -POST /users/verify -Content-Type: application/json -RequestBody: { - "email": "example@example.com" -} -`` ` - -#### Resending a email validation error - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: <Помилка від Joi або іншої бібліотеки валідації> -```` - -#### Resending a email success response - -```shell -Status: 200 Ok -Content-Type: application/json -ResponseBody: { - "message": "Verification email sent" -} -``` - -#### Resend email for verified user - -```shell -Status: 400 Bad Request -Content-Type: application/json -ResponseBody: { - message: "Verification has already been passed" -} -``` - -> Примітка: Як альтернативу SendGrid можна використовувати пакет [nodemailer](https://www.npmjs.com/package/nodemailer) - -## Додаткове завдання - необов'язкове - -### 1. Напишіть dockerfile для вашої програми diff --git a/homework-06/api-key.png b/homework-06/api-key.png deleted file mode 100644 index 76a9978..0000000 Binary files a/homework-06/api-key.png and /dev/null differ diff --git a/homework-06/controllers/auth/changeSubscription.js b/homework-06/controllers/auth/changeSubscription.js new file mode 100644 index 0000000..7fa746c --- /dev/null +++ b/homework-06/controllers/auth/changeSubscription.js @@ -0,0 +1,13 @@ +const { User } = require("../../models/user"); + +const changeSubscription = async (req, res) => { + const { _id } = req.user; + const { subscription } = req.body; + await User.findByIdAndUpdate(_id, { subscription }); + + res.json({ + message: `Your subscription has been changed to ${subscription}`, + }); +}; + +module.exports = changeSubscription; diff --git a/homework-06/controllers/auth/getCurrent.js b/homework-06/controllers/auth/getCurrent.js new file mode 100644 index 0000000..8612179 --- /dev/null +++ b/homework-06/controllers/auth/getCurrent.js @@ -0,0 +1,11 @@ +const getCurrent = async (req, res) => { + const { name, email, subscription } = req.user; + + res.json({ + name, + email, + subscription, + }); +}; + +module.exports = getCurrent; diff --git a/homework-06/controllers/auth/index.js b/homework-06/controllers/auth/index.js new file mode 100644 index 0000000..61b3b6d --- /dev/null +++ b/homework-06/controllers/auth/index.js @@ -0,0 +1,19 @@ +const register = require("./register"); +const login = require("./login"); +const logout = require("./logout"); +const getCurrent = require("./getCurrent"); +const changeSubscription = require("./changeSubscription"); +const updateAvatar = require("./updateAvatar"); +const resendVerifyEmail = require("./resendVerifyEmail"); +const verifyEmail = require("./verifyEmail"); + +module.exports = { + register, + login, + logout, + getCurrent, + changeSubscription, + updateAvatar, + resendVerifyEmail, + verifyEmail, +}; diff --git a/homework-06/controllers/auth/login.js b/homework-06/controllers/auth/login.js new file mode 100644 index 0000000..24b7db0 --- /dev/null +++ b/homework-06/controllers/auth/login.js @@ -0,0 +1,40 @@ +const bcryptjs = require('bcryptjs'); // Підключення бібліотеки для хешування паролів +const jwt = require('jsonwebtoken'); +const { User } = require('../../models/user'); +const { HttpError } = require('../../helpers'); +const { SECRET_KEY } = process.env; + +const login = async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); + + if (!user) { + throw HttpError(401, 'Email or password is wrong'); + } + + if (!user.verify) { + throw HttpError(401, 'Email not verified'); + } + + const passwordCompare = await bcryptjs.compare(password, user.password); // Перевірка, чи введений пароль збігається з хешованим паролем користувача + if (!passwordCompare) { + throw HttpError(401, 'Email or password is wrong'); + } + + const payload = { + id: user._id, + }; // Створення об'єкту payload для підпису в JWT токені + + const token = jwt.sign(payload, SECRET_KEY, { expiresIn: '7d' }); // Створення JWT токена з підписом на основі payload та секретного ключа + await User.findByIdAndUpdate(user._id, { token }); // Збереження JWT токена в базі даних + + res.json({ + token, + user: { + email: user.email, + subscription: user.subscription, + }, + }); // Відправлення відповіді з JWT токеном та даними користувача (email та підписка) +}; + +module.exports = login; diff --git a/homework-06/controllers/auth/logout.js b/homework-06/controllers/auth/logout.js new file mode 100644 index 0000000..9b33d95 --- /dev/null +++ b/homework-06/controllers/auth/logout.js @@ -0,0 +1,10 @@ +const { User } = require("../../models/user"); + +const logout = async (req, res) => { + const { _id } = req.user; + await User.findByIdAndUpdate(_id, { token: "" }); + + res.status(204).json(); +}; + +module.exports = logout; diff --git a/homework-06/controllers/auth/register.js b/homework-06/controllers/auth/register.js new file mode 100644 index 0000000..275efd2 --- /dev/null +++ b/homework-06/controllers/auth/register.js @@ -0,0 +1,44 @@ +const bcryptjs = require('bcryptjs'); // Підключення бібліотеки для хешування паролів +const gravatar = require('gravatar'); // Підключення бібліотеки для отримання аватарки з сервісу Gravatar +const { User } = require('../../models/user'); +const { HttpError, sendEmail } = require('../../helpers'); +const { nanoid } = require('nanoid'); +require('dotenv').config(); + +const { BASE_URL } = process.env; + +const register = async (req, res) => { + const { email, password } = req.body; + const user = await User.findOne({ email }); // Знаходження користувача за email в базі даних (звернення до бази даних потребує await, щоб дочекатись результату пошуку) + + if (user) { + throw HttpError(409, 'Email in use'); + } + + const hashPassword = await bcryptjs.hash(password, 10); // Хешування пароля з використанням bcryptjs + const avatarUrl = gravatar.url(email); // Отримання посилання на аватарку з сервісу Gravatar + const verificationToken = nanoid(); + + const newUser = await User.create({ + ...req.body, + password: hashPassword, + avatarUrl, + verificationToken, + }); + + const verifyEmail = { + to: email, + subject: 'Verify email', + html: `Click to verify email`, + }; // Створення об'єкту email для відправки листа з посиланням для верифікації email + + await sendEmail(verifyEmail); // Відправлення листа з посиланням на верифікацію email + + res.status(201).json({ + name: newUser.name, + email: newUser.email, + avatar: newUser.avatarUrl, + }); // Відправлення відповіді з даними про створеного користувача +}; + +module.exports = register; diff --git a/homework-06/controllers/auth/resendVerifyEmail.js b/homework-06/controllers/auth/resendVerifyEmail.js new file mode 100644 index 0000000..2228baa --- /dev/null +++ b/homework-06/controllers/auth/resendVerifyEmail.js @@ -0,0 +1,31 @@ +const { User } = require('../../models/user'); +const { HttpError, sendEmail } = require('../../helpers'); +require('dotenv').config(); +const { BASE_URL } = process.env; + +const resendVerifyEmail = async (req, res) => { + const { email } = req.body; + const user = await User.findOne({ email }); + + if (!user) { + throw HttpError(401, 'User not found'); + } + + if (user.verify) { + throw HttpError(400, 'Verification has already been passed'); + } + + const verifyEmail = { + to: email, + subject: 'Verify email', + html: `Click to verify email`, + }; // Створення об'єкту email для відправки листа з посиланням для верифікації email + + await sendEmail(verifyEmail); // Відправлення листа з посиланням на верифікацію email + + res.json({ + message: 'Verification email sent', + }); // Відправлення відповіді з підтвердженням відправки листа +}; + +module.exports = resendVerifyEmail; diff --git a/homework-06/controllers/auth/updateAvatar.js b/homework-06/controllers/auth/updateAvatar.js new file mode 100644 index 0000000..c545726 --- /dev/null +++ b/homework-06/controllers/auth/updateAvatar.js @@ -0,0 +1,43 @@ +const { User } = require('../../models/user'); +const path = require('path'); +const fs = require('fs/promises'); +const { HttpError, resizeImage } = require('../../helpers'); +const avatarsDir = path.join(__dirname, '../../public/avatars'); +const tempDir = path.join(__dirname, '../../temp'); +const avatarExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'tiff']; + +const updateAvatar = async (req, res) => { + const { _id } = req.user; + const { path: tempUpload, originalname } = req.file; + const avatarName = `${_id}_${originalname}`; + + const fileExtension = originalname.substring(originalname.lastIndexOf('.') + 1); + + if (!avatarExtensions.includes(fileExtension.toLowerCase())) { + // Перевірка, чи допустиме розширення файлу + throw new HttpError( + 400, + `${originalname} includes an invalid file extension! Must be: ${avatarExtensions.join(', or ')}`, + ); + } + + const tempImagePath = path.join(tempDir, avatarName); + const resizedImagePath = path.join(avatarsDir, avatarName); + + try { + await fs.stat(tempImagePath); // Перевірка, чи існує тимчасовий файл з аватаркою + } catch (error) { + await fs.copyFile(tempUpload, tempImagePath); // Копіювання завантаженого тимчасового файлу в папку temp, якщо файлу ще немає + } + + await resizeImage(tempImagePath, resizedImagePath); + + const avatarURL = path.join('avatars', avatarName); // Формування шляху до аватарки для збереження в базу даних + await User.findByIdAndUpdate(_id, { avatarURL }); // Оновлення запису користувача з новим посиланням на аватарку + + res.json({ + avatarURL, + }); +}; + +module.exports = updateAvatar; diff --git a/homework-06/controllers/auth/verifyEmail.js b/homework-06/controllers/auth/verifyEmail.js new file mode 100644 index 0000000..9c72488 --- /dev/null +++ b/homework-06/controllers/auth/verifyEmail.js @@ -0,0 +1,22 @@ +const { User } = require("../../models/user"); +const { HttpError } = require("../../helpers"); + +const verifyEmail = async (req, res) => { + const { verificationToken } = req.params; + const user = await User.findOne({ verificationToken }); + + if (!user) { + throw HttpError(401, "User Not found"); + } + + await User.findByIdAndUpdate(user._id, { + verify: true, + verificationToken: "", + }); + + res.json({ + message: "Verification successful", + }); +}; + +module.exports = verifyEmail; diff --git a/homework-06/controllers/contacts/addContact.js b/homework-06/controllers/contacts/addContact.js new file mode 100644 index 0000000..6b38b95 --- /dev/null +++ b/homework-06/controllers/contacts/addContact.js @@ -0,0 +1,17 @@ +const { Contact, addSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const addContact = async (req, res) => { + const { error } = addSchema.validate(req.body); + + if (error) { + throw HttpError(400, 'missing required name field'); + } + + const { _id: owner } = req.user; + const result = await Contact.create({ ...req.body, owner }); + + res.status(201).json(result); +}; + +module.exports = addContact; diff --git a/homework-06/controllers/contacts/getById.js b/homework-06/controllers/contacts/getById.js new file mode 100644 index 0000000..3c46b85 --- /dev/null +++ b/homework-06/controllers/contacts/getById.js @@ -0,0 +1,13 @@ +const { Contact } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const getById = async (req, res) => { + const { id } = req.params; + const result = await Contact.findById(id); + if (!result) { + throw HttpError(404, 'Not found'); + } + res.json(result); +}; + +module.exports = getById; diff --git a/homework-06/controllers/contacts/index.js b/homework-06/controllers/contacts/index.js new file mode 100644 index 0000000..126f94b --- /dev/null +++ b/homework-06/controllers/contacts/index.js @@ -0,0 +1,15 @@ +const listContacts = require("./listContacts"); +const getById = require("./getById"); +const addContact = require("./addContact"); +const updateById = require("./updateById"); +const updateStatusContact = require("./updateStatusContact"); +const removeContact = require("./removeContact"); + +module.exports = { + listContacts, + getById, + addContact, + updateById, + updateStatusContact, + removeContact, +}; diff --git a/homework-06/controllers/contacts/listContacts.js b/homework-06/controllers/contacts/listContacts.js new file mode 100644 index 0000000..ac9272f --- /dev/null +++ b/homework-06/controllers/contacts/listContacts.js @@ -0,0 +1,22 @@ +const { Contact } = require('../../models/contact'); + +const listContacts = async (req, res) => { + const { _id: owner } = req.user; + const { page = 1, limit = 10 } = req.query; // Отримуємо номер сторінки (page) та кількість контактів на сторінці (limit) з параметрів запиту + const skip = (page - 1) * limit; // Обчислюємо кількість контактів, які необхідно пропустити перед отриманням даних з бази даних + + if (req.query.favorite) { + const favorite = req.query.favorite === 'true'; // Конвертуємо параметр favorite в булеве значення + const result = await Contact.find({ owner, favorite }, '', { + // Знаходимо контакти в базі даних, де owner дорівнює id користувача і favorite дорівнює переданому значенню + skip, + limit, + }).populate('owner', 'name email'); + return res.json(result); + } + + const result = await Contact.find({ owner }, '', { skip, limit }).populate('owner', 'name email'); // Знаходимо всі контакти, де owner дорівнює id користувача, з врахуванням пагінації + res.json(result); +}; + +module.exports = listContacts; diff --git a/homework-06/controllers/contacts/removeContact.js b/homework-06/controllers/contacts/removeContact.js new file mode 100644 index 0000000..ec1b303 --- /dev/null +++ b/homework-06/controllers/contacts/removeContact.js @@ -0,0 +1,17 @@ +const { Contact } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const removeContact = async (req, res) => { + const { id } = req.params; + const result = await Contact.findByIdAndDelete(id); + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json({ + message: 'contact deleted', // Відправляємо відповідь у форматі JSON з повідомленням про успішне видалення контакта + }); +}; + +module.exports = removeContact; \ No newline at end of file diff --git a/homework-06/controllers/contacts/updateById.js b/homework-06/controllers/contacts/updateById.js new file mode 100644 index 0000000..c7a63ea --- /dev/null +++ b/homework-06/controllers/contacts/updateById.js @@ -0,0 +1,21 @@ +const { Contact, addSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const updateById = async (req, res) => { + const { error } = addSchema.validate(req.body); // Перевіряємо, чи дані відповідають схемі addSchema + + if (error) { + throw HttpError(400, 'missing fields'); + } + + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); // Знаходимо контакт за id та оновлюємо його дані на ті, що були передані у запиті + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateById; diff --git a/homework-06/controllers/contacts/updateStatusContact.js b/homework-06/controllers/contacts/updateStatusContact.js new file mode 100644 index 0000000..c4534ef --- /dev/null +++ b/homework-06/controllers/contacts/updateStatusContact.js @@ -0,0 +1,21 @@ +const { Contact, updateFavoriteSchema } = require('../../models/contact'); +const { HttpError } = require('../../helpers'); + +const updateStatusContact = async (req, res) => { + const { error } = updateFavoriteSchema.validate(req.body); // Перевіряємо, чи дані з введеним полем favorite відповідають схемі updateFavoriteSchema + + if (error) { + throw HttpError(400, 'missing field favorite'); + } + + const { id } = req.params; + const result = await Contact.findByIdAndUpdate(id, req.body, { new: true }); // Знаходимо контакт за id та оновлюємо його дані на ті, що були передані у запиті + + if (!result) { + throw HttpError(404, 'Not found'); + } + + res.json(result); +}; + +module.exports = updateStatusContact; diff --git a/homework-06/helpers/HttpError.js b/homework-06/helpers/HttpError.js new file mode 100644 index 0000000..3f38b50 --- /dev/null +++ b/homework-06/helpers/HttpError.js @@ -0,0 +1,7 @@ +const HttpError = (status, message) => { + const error = new Error(message); + error.status = status; + return error; +}; + +module.exports = HttpError; diff --git a/homework-06/helpers/ctrlWrapper.js b/homework-06/helpers/ctrlWrapper.js new file mode 100644 index 0000000..4e36751 --- /dev/null +++ b/homework-06/helpers/ctrlWrapper.js @@ -0,0 +1,13 @@ +const ctrlWrapper = ctrl => { + const func = async (req, res, next) => { + try { + await ctrl(req, res, next); + } catch (error) { + next(error); + } + }; + + return func; +}; + +module.exports = ctrlWrapper; diff --git a/homework-06/helpers/handleMongooseError.js b/homework-06/helpers/handleMongooseError.js new file mode 100644 index 0000000..f98813b --- /dev/null +++ b/homework-06/helpers/handleMongooseError.js @@ -0,0 +1,8 @@ +const handleMongooseError = (error, data, next) => { + const { name, code } = error; + const status = name === 'MongoServerError' && code === 11000 ? 409 : 400; + error.status = status; + next(); +}; + +module.exports = handleMongooseError; diff --git a/homework-06/helpers/index.js b/homework-06/helpers/index.js new file mode 100644 index 0000000..1a0dd31 --- /dev/null +++ b/homework-06/helpers/index.js @@ -0,0 +1,13 @@ +const HttpError = require("./HttpError"); +const ctrlWrapper = require("./ctrlWrapper"); +const handleMongooseError = require("./handleMongooseError"); +const resizeImage = require("./resizeImage"); +const sendEmail = require("./sendEmail"); + +module.exports = { + HttpError, + ctrlWrapper, + handleMongooseError, + resizeImage, + sendEmail, +}; diff --git a/homework-06/helpers/resizeImage.js b/homework-06/helpers/resizeImage.js new file mode 100644 index 0000000..099db71 --- /dev/null +++ b/homework-06/helpers/resizeImage.js @@ -0,0 +1,9 @@ +const Jimp = require('jimp'); // Підключаємо бібліотеку Jimp для роботи з зображеннями + +const resizeImage = async (sourcePath, destinationPath) => { + const image = await Jimp.read(sourcePath); + image.resize(250, 250); + await image.writeAsync(destinationPath); // Зберігаємо зображення у вказаному шляху (destinationPath) за допомогою методу writeAsync() +}; + +module.exports = resizeImage; diff --git a/homework-06/helpers/sendEmail.js b/homework-06/helpers/sendEmail.js new file mode 100644 index 0000000..5eafcf1 --- /dev/null +++ b/homework-06/helpers/sendEmail.js @@ -0,0 +1,14 @@ +const sgMail = require('@sendgrid/mail'); // Підключаємо бібліотеку sendgrid/mail для надсилання електронних листів +require('dotenv').config(); // Підключаємо .env для отримання SENDGRID_API_KEY + +const { SENDGRID_API_KEY } = process.env; // Отримуємо SENDGRID_API_KEY з оточення + +sgMail.setApiKey(SENDGRID_API_KEY); + +const sendEmail = async data => { + const email = { ...data, from: 'supermetamail@meta.ua' }; + await sgMail.send(email); + return true; // Після успішного надсилання повертаємо значення true +}; + +module.exports = sendEmail; diff --git a/homework-06/middlewares/authenticate.js b/homework-06/middlewares/authenticate.js new file mode 100644 index 0000000..92acdd6 --- /dev/null +++ b/homework-06/middlewares/authenticate.js @@ -0,0 +1,33 @@ +const jwt = require('jsonwebtoken'); +const { User } = require('../models/user'); + +require('dotenv').config(); // Підключаємо .env для отримання SECRET_KEY +const { SECRET_KEY } = process.env; + +const { HttpError } = require('../helpers'); + +const authenticate = async (req, res, next) => { + const { authorization = '' } = req.headers; // Отримуємо заголовок Authorization з запиту, який містить токен доступу + const [bearer, token] = authorization.split(' '); // Розбиваємо заголовок Authorization на дві частини: тип токена та сам токен + + if (bearer !== 'Bearer') { + next(HttpError(401, 'Not authorized')); + } + + try { + const { id } = jwt.verify(token, SECRET_KEY); + const user = await User.findById(id); + + if (!user || !user.token || user.token !== token) { + next(HttpError(401, 'Not authorized')); + } + + req.user = user; + + next(); // Переходимо до наступної мідлвари або обробника запиту + } catch { + next(HttpError(401, 'Not authorized')); + } +}; + +module.exports = authenticate; diff --git a/homework-06/middlewares/index.js b/homework-06/middlewares/index.js new file mode 100644 index 0000000..52b70c5 --- /dev/null +++ b/homework-06/middlewares/index.js @@ -0,0 +1,5 @@ +const isValidId = require("./isValidId"); +const authenticate = require("./authenticate"); +const validateBody = require("./validateBody"); +const upload = require("./upload"); +module.exports = { isValidId, authenticate, validateBody, upload }; diff --git a/homework-06/middlewares/isValidId.js b/homework-06/middlewares/isValidId.js new file mode 100644 index 0000000..b2bf179 --- /dev/null +++ b/homework-06/middlewares/isValidId.js @@ -0,0 +1,14 @@ +const { isValidObjectId } = require('mongoose'); +const { HttpError } = require('../helpers'); + +const isValidId = (req, res, next) => { + const { id } = req.params; + if (!isValidObjectId(id)) { + // Перевіряємо чи переданий ідентифікатор є дійсним ObjectId + next(HttpError(400, `${id} is not valid id`)); + } + + next(); // Якщо ідентифікатор є дійсним ObjectId, переходимо до наступної мідлвари або обробника запиту +}; + +module.exports = isValidId; diff --git a/homework-06/middlewares/upload.js b/homework-06/middlewares/upload.js new file mode 100644 index 0000000..bdd49ce --- /dev/null +++ b/homework-06/middlewares/upload.js @@ -0,0 +1,17 @@ +const multer = require('multer'); // Підключаємо бібліотеку multer для обробки завантаження файлів +const path = require('path'); + +const tempDir = path.join(__dirname, '../', 'temp'); + +const multerConfig = multer.diskStorage({ + destination: tempDir, // Вказуємо директорію, куди будуть збережені тимчасові завантажені файли + filename: (req, file, cb) => { + cb(null, file.originalname); // Генеруємо ім'я файлу на основі його оригінального імені + }, +}); + +const upload = multer({ + storage: multerConfig, // Передаємо конфігурацію multer для завантаження файлів +}); + +module.exports = upload; diff --git a/homework-06/middlewares/validateBody.js b/homework-06/middlewares/validateBody.js new file mode 100644 index 0000000..70bcdd2 --- /dev/null +++ b/homework-06/middlewares/validateBody.js @@ -0,0 +1,15 @@ +const { HttpError } = require('../helpers'); + +// Мідлвар для перевірки валідності даних в запиті на відповідну схему "schema" +const validateBody = schema => { + const func = (req, res, next) => { + const { error } = schema.validate(req.body); + if (error) { + next(HttpError(400, error.message)); + } + next(); // Якщо дані валідні, передаємо управління наступному мідлвару або обробнику запиту + }; + return func; +}; + +module.exports = validateBody; diff --git a/homework-06/models/contact.js b/homework-06/models/contact.js new file mode 100644 index 0000000..ea91407 --- /dev/null +++ b/homework-06/models/contact.js @@ -0,0 +1,48 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const contactSchema = new Schema( + { + name: { + type: String, + required: [true, 'Set name for contact'], + }, + email: { + type: String, + }, + phone: { + type: String, + }, + favorite: { + type: Boolean, + default: false, + }, + owner: { + type: Schema.Types.ObjectId, + ref: 'user', + required: true, + }, + }, + { versionKey: false }, // Вимкнення версій документів (поле "__v") +); + +// Після збереження документа виникає помилка, обробка помилки зі спеціальною функцією +contactSchema.post('save', handleMongooseError); + +// Модель контакту зі схемою імені "contact" +const Contact = model('contact', contactSchema); + +// Схема валідації даних для додавання контакту +const addSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().required(), + phone: Joi.string().required(), +}); + +// Схема валідації даних для оновлення поля "favorite" контакту +const updateFavoriteSchema = Joi.object({ + favorite: Joi.boolean().required(), +}); + +module.exports = { Contact, addSchema, updateFavoriteSchema }; diff --git a/homework-06/models/user.js b/homework-06/models/user.js new file mode 100644 index 0000000..89249f2 --- /dev/null +++ b/homework-06/models/user.js @@ -0,0 +1,89 @@ +const { Schema, model } = require('mongoose'); +const Joi = require('joi'); +const { handleMongooseError } = require('../helpers'); + +const emailRegexp = /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/; +const subscriptionList = ['starter', 'pro', 'business']; + +// Створення схеми користувача з використанням mongoose +const userSchema = new Schema( + { + name: { + type: String, + required: true, + }, + email: { + type: String, + match: emailRegexp, + required: [true, 'Email is required'], + unique: true, + }, + password: { + type: String, + minlength: 6, + required: [true, 'Set password for user'], + }, + subscription: { + type: String, + enum: subscriptionList, + default: 'starter', + }, + token: { + type: String, + default: '', // Значення за замовчуванням для токена + }, + avatarURL: { + type: String, + required: true, // Обов'язкове поле для посилання на аватар + }, + verify: { + type: Boolean, + default: false, // Значення за замовчуванням для підтвердження електронної пошти + }, + verificationToken: { + type: String, + required: [true, 'Verify token is required'], + }, + }, + { versionKey: false }, // Вимкнення версій документів (поле "__v") +); + +// Після збереження документа виникає помилка, обробка помилки зі спеціальною функцією +userSchema.post('save', handleMongooseError); + +// Об'єкт Joi схем для валідації даних +const registerSchema = Joi.object({ + name: Joi.string().required(), + email: Joi.string().pattern(emailRegexp).required(), + password: Joi.string().min(6).required(), +}); + +const emailSchema = Joi.object({ + email: Joi.string().pattern(emailRegexp).required(), +}); + +const loginSchema = Joi.object({ + email: Joi.string().required(), + password: Joi.string().min(6).required(), +}); + +const changeSubscriptionSchema = Joi.object({ + subscription: Joi.string() + .valid(...subscriptionList) + .required(), +}); + +// Модель користувача зі схемою імені "user" та заданими схемами валідації +const User = model('user', userSchema); + +const schemas = { + registerSchema, + loginSchema, + changeSubscriptionSchema, + emailSchema, +}; + +module.exports = { + User, + schemas, +}; diff --git a/homework-06/node.png b/homework-06/node.png deleted file mode 100644 index 4383463..0000000 Binary files a/homework-06/node.png and /dev/null differ diff --git a/homework-06/nodemon.json b/homework-06/nodemon.json new file mode 100644 index 0000000..54d6947 --- /dev/null +++ b/homework-06/nodemon.json @@ -0,0 +1,3 @@ +{ + "ignore": ["node_modules", "models/contacts.json"] +} diff --git a/homework-06/public/avatars/.gitkeep b/homework-06/public/avatars/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/homework-06/public/avatars/649c7f75733d3ba6bb281c92_avatar.jpg b/homework-06/public/avatars/649c7f75733d3ba6bb281c92_avatar.jpg new file mode 100644 index 0000000..86e804f Binary files /dev/null and b/homework-06/public/avatars/649c7f75733d3ba6bb281c92_avatar.jpg differ diff --git a/homework-06/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg b/homework-06/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg new file mode 100644 index 0000000..86e804f Binary files /dev/null and b/homework-06/public/avatars/64a581f17ab9d2f81652c536_avatar.jpg differ diff --git a/homework-06/routes/api/auth.js b/homework-06/routes/api/auth.js new file mode 100644 index 0000000..b780700 --- /dev/null +++ b/homework-06/routes/api/auth.js @@ -0,0 +1,28 @@ +const express = require('express'); + +const { validateBody, authenticate, upload } = require('../../middlewares'); +const { schemas } = require('../../models/user'); + +const router = express.Router(); + +const ctrl = require('../../controllers/auth'); + +const { ctrlWrapper } = require('../../helpers'); + +router.post('/register', validateBody(schemas.registerSchema), ctrlWrapper(ctrl.register)); + +router.get('/verify/:verificationToken', ctrlWrapper(ctrl.verifyEmail)); + +router.post('/verify', validateBody(schemas.emailSchema), ctrlWrapper(ctrl.resendVerifyEmail)); + +router.post('/login', validateBody(schemas.loginSchema), ctrlWrapper(ctrl.login)); + +router.get('/current', authenticate, ctrlWrapper(ctrl.getCurrent)); + +router.post('/logout', authenticate, ctrlWrapper(ctrl.logout)); + +router.patch('/users/avatars', authenticate, upload.single('avatar'), ctrlWrapper(ctrl.updateAvatar)); + +router.patch('/', authenticate, validateBody(schemas.changeSubscriptionSchema), ctrlWrapper(ctrl.changeSubscription)); + +module.exports = router; diff --git a/homework-06/routes/api/contacts.js b/homework-06/routes/api/contacts.js new file mode 100644 index 0000000..935d6f2 --- /dev/null +++ b/homework-06/routes/api/contacts.js @@ -0,0 +1,23 @@ +const express = require('express'); + +const router = express.Router(); + +const ctrl = require('../../controllers/contacts'); // Підключення контролерів для обробки запитів + +const { ctrlWrapper } = require('../../helpers'); + +const { isValidId, authenticate } = require('../../middlewares'); // Підключення middleware для перевірки валідності id та аутентифікації + +router.get('/', authenticate, ctrlWrapper(ctrl.listContacts)); + +router.get('/:id', authenticate, isValidId, ctrlWrapper(ctrl.getById)); + +router.post('/', authenticate, ctrlWrapper(ctrl.addContact)); + +router.put('/:id', authenticate, isValidId, ctrlWrapper(ctrl.updateById)); + +router.patch('/:id/favorite', authenticate, isValidId, ctrlWrapper(ctrl.updateStatusContact)); + +router.delete('/:id', authenticate, isValidId, ctrlWrapper(ctrl.removeContact)); + +module.exports = router; diff --git a/homework-06/sender-not-verify.png b/homework-06/sender-not-verify.png deleted file mode 100644 index 98aea79..0000000 Binary files a/homework-06/sender-not-verify.png and /dev/null differ diff --git a/homework-06/sender-verify.png b/homework-06/sender-verify.png deleted file mode 100644 index 11a236a..0000000 Binary files a/homework-06/sender-verify.png and /dev/null differ diff --git a/homework-06/server.js b/homework-06/server.js new file mode 100644 index 0000000..1d43d4c --- /dev/null +++ b/homework-06/server.js @@ -0,0 +1,17 @@ +const mongoose = require('mongoose'); +const app = require('./app'); // Підключення Express-додатку з файлу `app.js` +const { DB_HOST } = process.env; + +// Встановлення з'єднання з базою даних MongoDB +mongoose + .connect(DB_HOST) + .then(() => { + console.log('Database connection successful'); + app.listen(3000, () => { + console.log('Server running. Use our API on port: 3000'); + }); + }) + .catch(error => { + console.log(error.message); + process.exit(1); // Вихід з процесу з кодом помилки 1, щоб позначити, що виникла помилка при підключенні + }); diff --git a/homework-06/temp/.gitkeep b/homework-06/temp/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/homework-06/temp/649c7f75733d3ba6bb281c92_avatar.jpg b/homework-06/temp/649c7f75733d3ba6bb281c92_avatar.jpg new file mode 100644 index 0000000..ef6fac6 Binary files /dev/null and b/homework-06/temp/649c7f75733d3ba6bb281c92_avatar.jpg differ diff --git a/homework-06/temp/64a581f17ab9d2f81652c536_avatar.jpg b/homework-06/temp/64a581f17ab9d2f81652c536_avatar.jpg new file mode 100644 index 0000000..ef6fac6 Binary files /dev/null and b/homework-06/temp/64a581f17ab9d2f81652c536_avatar.jpg differ diff --git a/homework-06/temp/avatar.jpg b/homework-06/temp/avatar.jpg new file mode 100644 index 0000000..ef6fac6 Binary files /dev/null and b/homework-06/temp/avatar.jpg differ diff --git a/homework-06/web-api.png b/homework-06/web-api.png deleted file mode 100644 index 6c4fe39..0000000 Binary files a/homework-06/web-api.png and /dev/null differ