Магазин на JavaScript, часть 7 из 19. Регистрация и авторизация, права пользователей, тесты

29.11.2021

Теги: BackendExpress.jsFrontendJavaScriptNode.jsORMReact.jsWeb-разработкаБазаДанныхИнтернетМагазинКаталогТоваровКорзинаФреймворк

Регистрация и авторизация

Для работы с 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

Поиск: Backend • Express.js • Frontend • JavaScript • Node.js • ORM • React.js • Web-разработка • База данных • Интернет магазин • Каталог товаров • Корзина • Фреймворк

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.