Магазин на JavaScript, часть 7 из 19. Регистрация и авторизация, права пользователей, тесты
29.11.2021
Теги: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • БазаДанных • ИнтернетМагазин • КаталогТоваров • Корзина • Фреймворк
Регистрация и авторизация
Для работы с JWT-токенами нам потребуется пакет jsonwebtoken
, чтобы создавать и проверять токены. Чтобы хэшировать пароли пользователей, а не хранить их в базе данных в открытом виде, нужно установить пакет bcrypt
.
> npm install jsonwebtoken bcrypt
Реализуем методы контроллера, которые отвечают за регистрацию и авторизацию пользователя:
import UserModel from '../models/User.js' import bcrypt from 'bcrypt' import jwt from 'jsonwebtoken' import AppError from '../errors/AppError.js' const makeJwt = (id, email, role) => { return jwt.sign( {id, email, role}, process.env.SECRET_KEY, {expiresIn: '24h'} ) } class User { async signup(req, res, next) { const {email, password, role = 'USER'} = req.body try { if (!email || !password) { throw new Error('Пустой email или пароль') } if (role !== 'USER') { throw new Error('Возможна только роль USER') } const hash = await bcrypt.hash(password, 5) const user = await UserModel.create({email, password: hash, role}) const token = makeJwt(user.id, user.email, user.role) return res.json({token}) } catch(e) { next(AppError.badRequest(e.message)) } } async login(req, res, next) { try { const {email, password} = req.body const user = await UserModel.getByEmail(email) let compare = bcrypt.compareSync(password, user.password) if (!compare) { throw new Error('Указан неверный пароль') } const token = makeJwt(user.id, user.email, user.role) return res.json({token}) } catch(e) { next(AppError.badRequest(e.message)) } } async getAll(req, res, next) { try { const users = await UserModel.getAll() res.json(users) } catch(e) { next(AppError.badRequest(e.message)) } } async getOne(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id пользователя') } const user = await UserModel.getOne(req.params.id) res.json(user) } catch(e) { next(AppError.badRequest(e.message)) } } async create(req, res, next) { const {email, password, role = 'USER'} = req.body try { if (!email || !password) { throw new Error('Пустой email или пароль') } if ( ! ['USER', 'ADMIN'].includes(role)) { throw new Error('Недопустимое значение роли') } const hash = await bcrypt.hash(password, 5) const user = await UserModel.create({email, password: hash, role}) return res.json(user) } catch(e) { next(AppError.badRequest(e.message)) } } async update(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id пользователя') } if (Object.keys(req.body).length === 0) { throw new Error('Нет данных для обновления') } let {email, password, role} = req.body if (role && !['USER', 'ADMIN'].includes(role)) { throw new Error('Недопустимое значение роли') } if (password) { password = await bcrypt.hash(password, 5) } const user = await UserModel.update(req.params.id, {email, password, role}) res.json(user) } catch(e) { next(AppError.badRequest(e.message)) } } async delete(req, res, next) { try { if (!req.params.id) { throw new Error('Не указан id пользователя') } const user = await UserModel.delete(req.params.id) res.json(user) } catch(e) { next(AppError.badRequest(e.message)) } } } export default new User()
USER
или ADMIN
. И эта роль дает (или не дает) ему права на выполнение того или иного действия на сервере.
Права пользователей
Обычный пользователь не должен иметь возможности создавать, редактировать и удалять товары, категории, бренды и пользователей — это может делать только пользователь с ролью ADMIN
. Давайте создадим два middleware — authMiddleware
и adminMiddleware
— и защитим с их помощью часть маршрутов.
import jwt from 'jsonwebtoken' import AppError from '../errors/AppError.js' const auth = (req, res, next) => { try { const token = req.headers.authorization?.split(' ')[1] // Bearer token if (!token) { throw new Error('Требуется авторизация') } const decoded = jwt.verify(token, process.env.SECRET_KEY) req.auth = decoded next() } catch (e) { next(AppError.forbidden(e.message)) } } export default auth
import AppError from '../errors/AppError.js' const admin = (req, res, next) => { try { if (req.auth.role !== 'ADMIN') { throw new Error('Только для администратора') } next() } catch (e) { next(AppError.forbidden(e.message)) } } export default admin
Первый middleware требует, чтобы пользователь подтвердил свою личность с помощью JWT-токена. Этот токен он получает либо после регистрации, либо после входа в личный кабинет. Второй middleware проверяет, что роль пользователя имеет значение ADMIN
— и дает ему дополнительные права по сравнению с обычным пользователем.
import express from 'express' import ProductController from '../controllers/Product.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() /* .......... */ // создать товар каталога — нужны права администратора router.post('/create', authMiddleware, adminMiddleware, ProductController.create) // обновить товар каталога — нужны права администратора router.put('/update/:id([0-9]+)', authMiddleware, adminMiddleware, ProductController.update) // удалить товар каталога — нужны права администратора router.delete('/delete/:id([0-9]+)', authMiddleware, adminMiddleware, ProductController.delete) export default router
import express from 'express' import CategoryController from '../controllers/Category.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() router.get('/getall', CategoryController.getAll) router.get('/getone/:id([0-9]+)', CategoryController.getOne) router.post('/create', authMiddleware, adminMiddleware, CategoryController.create) router.put('/update/:id([0-9]+)', authMiddleware, adminMiddleware, CategoryController.update) router.delete('/delete/:id([0-9]+)', authMiddleware, adminMiddleware, CategoryController.delete) export default router
import express from 'express' import BrandController from '../controllers/Brand.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() router.get('/getall', BrandController.getAll) router.get('/getone/:id([0-9]+)', BrandController.getOne) router.post('/create', authMiddleware, adminMiddleware, BrandController.create) router.put('/update/:id([0-9]+)', authMiddleware, adminMiddleware, BrandController.update) router.delete('/delete/:id([0-9]+)', authMiddleware, adminMiddleware, BrandController.delete) export default router
import express from 'express' import UserController from '../controllers/User.js' import authMiddleware from '../middleware/authMiddleware.js' import adminMiddleware from '../middleware/adminMiddleware.js' const router = new express.Router() router.post('/signup', UserController.signup) router.post('/login', UserController.login) router.get('/getall', authMiddleware, adminMiddleware, UserController.getAll) router.get('/getone/:id([0-9]+)', authMiddleware, adminMiddleware, UserController.getOne) router.post('/create', authMiddleware, adminMiddleware, UserController.create) router.put('/update/:id([0-9]+)', authMiddleware, adminMiddleware, UserController.update) router.delete('/delete/:id([0-9]+)', authMiddleware, adminMiddleware, UserController.delete) export default router
Клиент для PostgreSQL
Штатный клиент pgAdmin на редкость неудобный — пользоваться им совершенно невозможно. Нашел на замену HeidiSQL — не могу сказать, что идеальный — но намного удобнее pgAdmin. К тому же есть portable-версия — можно даже не устанавливать.
Тестирование запросов
Надо проверить, что все запросы отрабатывают корректно, записи в базе данных добавляются, обновляются и удаляются. Работать с одним файлом test.http
неудобно, так что создадим директорию server/test
и разместим в ней файлы product.http
, category.http
, brand.http
и user.http
. И поместим еще в эту директорию изображения picture.jpg
(при создании товара) и picture-new.jpg
(при обновлении товара) — чтобы проверить, что изображения загружаются, заменяются и удаляются.
### Список всех товаров GET /api/product/getall HTTP/1.1 Host: localhost:7000 ### Получить один товар GET /api/product/getone/4 HTTP/1.1 Host: localhost:7000 ### Создать новый товар POST /api/product/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-Type: multipart/form-data; boundary=MultiPartFormDataBoundary --MultiPartFormDataBoundary Content-Disposition: form-data; name="name" Content-Type: text/plain; charset=utf-8 Товар № 4 --MultiPartFormDataBoundary Content-Disposition: form-data; name="price" Content-Type: text/plain; charset=utf-8 88888 --MultiPartFormDataBoundary Content-Disposition: form-data; name="props" Content-type: text/plain; charset=utf-8 [{"name": "Свойство 3", "value": "Значение 3"},{"name": "Свойство 4", "value": "Значение 4"}] --MultiPartFormDataBoundary Content-Disposition: form-data; name="image"; filename="picture.jpg" Content-Type: image/jpeg < ./picture.jpg --MultiPartFormDataBoundary-- ### Обновить товар PUT /api/product/update/4 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-Type: multipart/form-data; boundary=MultiPartFormDataBoundary --MultiPartFormDataBoundary Content-Disposition: form-data; name="name" Content-Type: text/plain; charset=utf-8 Товар № 4 (new) --MultiPartFormDataBoundary Content-Disposition: form-data; name="price" Content-Type: text/plain; charset=utf-8 99999 --MultiPartFormDataBoundary Content-Disposition: form-data; name="props" Content-type: text/plain; charset=utf-8 [{"name": "Свойство 3 (new)", "value": "Значение 3 (new)"},{"name": "Свойство 4 (new)", "value": "Значение 4 (new)"}] --MultiPartFormDataBoundary Content-Disposition: form-data; name="image"; filename="picture.jpg" Content-Type: image/jpeg < ./picture-new.jpg --MultiPartFormDataBoundary-- ### Удалить товар DELETE /api/product/delete/4 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA
### Список всех категорий GET /api/category/getall HTTP/1.1 Host: localhost:7000 ### Получить одну категорию GET /api/category/getone/1 HTTP/1.1 Host: localhost:7000 ### Создать новую категорию POST /api/category/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-type: application/json; charset=utf-8 { "name": "Первая категория" } ### Обновить категорию PUT /api/category/update/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-type: application/json; charset=utf-8 { "name": "Первая категория (new)" } ### Удалить категорию DELETE /api/category/delete/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA
### Список всех брендов GET /api/brand/getall HTTP/1.1 Host: localhost:7000 ### Получить один бренд GET /api/brand/getone/1 HTTP/1.1 Host: localhost:7000 ### Создать новый бренд POST /api/brand/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-type: application/json; charset=utf-8 { "name": "Первый бренд" } ### Обновить бренд PUT /api/brand/update/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA Content-type: application/json; charset=utf-8 { "name": "Первый бренд (new)" } ### Удалить бренд DELETE /api/brand/delete/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NDR9.eDlzf...87TTA
### Регистрация нового пользователя POST /api/user/signup HTTP/1.1 Host: localhost:7000 Content-type: application/json; charset=utf-8 { "email": "user@mail.ru", "password": "qwerty" } ### Аутентификация (вход) пользователя POST /api/user/login HTTP/1.1 Host: localhost:7000 Content-type: application/json; charset=utf-8 { "email": "user@mail.ru", "password": "qwerty" } ### Список всех пользователей GET /api/user/getall HTTP/1.1 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NTZ9.D8CmI...VMU8Q Host: localhost:7000 ### Получить одного пользователя GET /api/user/getone/1 HTTP/1.1 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NTZ9.D8CmI...VMU8Q Host: localhost:7000 ### Создать нового пользователя POST /api/user/create HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NTZ9.D8CmI...VMU8Q Content-type: application/json; charset=utf-8 { "email": "admin@mail.ru", "password": "qwerty", "role": "ADMIN" } ### Обновить пользователя PUT /api/user/update/1 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NTZ9.D8CmI...VMU8Q Content-type: application/json; charset=utf-8 { "email": "updated@mail.ru", "password": "qwerty(updated)" } ### Удалить пользователя DELETE /api/user/delete/2 HTTP/1.1 Host: localhost:7000 Authorization: Bearer eyJhb...XVCJ9.eyJpZ...2NTZ9.D8CmI...VMU8Q
- Магазин на JavaScript, часть 19 из 19. Редактирование характеристик и рефакторинг приложения
- Магазин на JavaScript, часть 18 из 19. Панель управления: редактирование категорий и брендов
- Магазин на JavaScript, часть 17 из 19. Панель управления: список заказов, категорий и брендов
- Магазин на JavaScript, часть 15 из 19. Работа с заказами на сервере, оформление заказа
- Магазин на JavaScript, часть 14 из 19. Кнопка «Назад», страница товара, корзина покупателя
- Магазин на JavaScript, часть 13 из 19. Хранилище каталога, компонент витрины, кнопка «Назад»
- Магазин на JavaScript, часть12 из 19. Запросы на сервер, состояние приложения, Signup и Login
Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк