RESTful backend приложение. Базовый шаблон

от автора

Постановка задачи

Необходимо собрать базовый шаблон RESTful backend приложения на NodeJS + Express, который:

  • легко документируется

  • просто наполняется функционалом

  • позволяет легко настраивать защиту маршрутов

  • имеет простую встроенную автоматическую валидацию

Гайд достаточно обширный, поэтому сначала мы разберем и реализуем различные части, а затем соберем приложение воедино. Готовый репозиторий можно посмотреть на Github.

Набор инструментов

Сердце нашего приложения – спецификация OpenApi 3.0. В нашем случае это описание API на языке разметки YAML, которое позволит автоматически генерировать и защищать маршруты и документировать API.

Для простоты возьмем MongoDB и mongoose, в целом ничего не помешает заменить эту связку на любую другую в своём шаблоне.

Passport.js – защита маршрутов, аутентификация и авторизация. Стратегия passport-jwt. Мы будем использовать jwt-access и refresh токены.

Первоначальная настройка

Инициализируем проект, запустив npm init или yarn init, я предпочитаю yarn.

Для начала стоит позаботиться об удобности разработки, стиле кода и допущениях.
За стиль кода у меня отвечают eslint и prettier.

В корне создаем конфиги для eslint и prettier. Для удобства разработки и сборки я использую nodemon, npm-run-all, rimraf, babel. Ниже мои настройки:

.eslintrc.json
{     "env": {         "node": true,         "es2021": true     },     "extends": [         "eslint:recommended",         "airbnb-base",         "prettier"     ],     "plugins": [         "prettier"     ],     "rules": {         "no-console": 0,         "prettier/prettier": ["error"],         "import/extensions": 0,         "import/prefer-default-export": "off",         "import/no-unresolved": 0,         "no-duplicate-imports": ["error", { "includeExports": true }],         "react/prop-types": 0,         "no-underscore-dangle": 0,         "no-param-reassign": ["error", { "props": false }],         "no-case-declarations": 0,         "no-plusplus": ["error", { "allowForLoopAfterthoughts": true }],         "space-infix-ops": ["error", { "int32Hint": false }],         "no-unused-vars": ["error", { "argsIgnorePattern": "next" }]     } }
.prettierrc
{     "printWidth": 100,     "singleQuote": true,     "tabWidth": 4,     "bracketSpacing": true,     "endOfLine": "lf",     "semi": true,     "trailingComma": "none" }
Добавьте в свой package.json
"dependencies": {     "@babel/node": "^7.13.13",     "body-parser": "^1.19.0",     "connect": "^3.7.0",     "cookie-parser": "^1.4.5",     "cors": "^2.8.5",     "dotenv": "^8.2.0",     "express": "^4.17.1",     "express-openapi-validator": "^4.12.6",     "jsonwebtoken": "^8.5.1",     "mongoose": "^5.12.2",     "morgan": "^1.10.0",     "passport": "^0.4.1",     "passport-jwt": "^4.0.0",     "swagger-routes-express": "^3.3.0",     "swagger-ui-express": "^4.1.6",     "uuid": "^8.3.2",     "validator": "^13.5.2",     "yamljs": "^0.3.0"   },   "devDependencies": {     "@babel/cli": "^7.13.14",     "@babel/core": "^7.13.14",     "@babel/preset-env": "^7.13.12",     "eslint": "^7.23.0",     "eslint-config-airbnb-base": "^14.2.1",     "eslint-config-prettier": "^8.1.0",     "eslint-plugin-import": "^2.22.1",     "eslint-plugin-prettier": "^3.3.1",     "nodemon": "^2.0.7",     "npm-run-all": "^4.1.5",     "prettier": "^2.2.1",     "rimraf": "^3.0.2"   },   "babel": {     "presets": [       "@babel/preset-env"     ]   },   "scripts": {     "transpile": "babel ./src --out-dir bin --copy-files",     "clean": "rimraf bin",     "build": "npm-run-all clean transpile",     "server": "node ./bin/app.js",     "dev": "npm-run-all build server",     "start": "yarn dev",     "watch": "nodemon"   }
Создайте nodemon.json в корне
{     "watch": ["src/*"],     "ext": "js, json, yaml",     "exec": "yarn run dev" }

Установите зависимости, запустив npm или yarn.

Немного про безопасность

Я подготовил несколько диаграмм, чтобы пошагово разобрать подход, который мы реализуем. На всякий случай собрал их в PDF.

Логика такая:

  1. При успешной аутентификации клиент получает JWT access токен и защищённый http-only куки с refresh токеном.

  2. При каждом запросе клиент проверяет, не истек ли срок жизни токена доступа (JWT access token), если все ок, вставляет его в заголовок «Authorization», если токен истек, то сначала запрашивает новый токен доступа. Ниже более подробно про наш бэкенд.

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

  1. На Backend передаем в открытом виде(но только по HTTPS) e-mail, пароль, какие-то дополнительные данные, которые вам нужны (nickname для примера на диаграмме)

  2. Генерируем уникальную соль и хэшируем пароль с этой солью, после чего записываем в базу

Аутентификация

Полная версия изображения

  1. Клиент передает по HTTPS email и пароль

  2. Пытаемся получить пользователя из базы

  3. Получаем либо пользователя, либо undefined

  4. Если undefined возвращаем сообщение об ошибке, и не говорим неверна почта или пароль

  5. Берем из базы соль пользователя, хэшируем с этой солью введенный пользователем пароль и сверяем с сохраненным в базе хэшем. Если пароль введен неверно, возвращаем сообщение об ошибке, аналогично пункту 4

  6. Если пароль введен верно, генерируем JWT токен доступа, с коротким сроком жизни и Refresh токен, который нужен для получения нового токена доступа, с более длительным сроком жизни. Это не показано на диаграмме, но refresh токен записывается в базу как одно из полей пользователя.

  7. Возвращаем пользователю JWT токен доступа и устанавливаем HTTP-only cookie (secure, т.к. у нас HTTPS).

Обновление JWT токена доступа

Полная версия изображения

  1. Обращаемся на маршрут обновления токена доступа

  2. Получаем из HTTP-only cookie refresh токен

  3. Если refresh токена нет – возвращаем ошибку

  4. Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.

  5. Ищем пользователя по refresh токену.

  6. Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.

  7. Если пользователь найден, то генерируем новую пару JWT токена доступа и refresh токена. Записываем refresh в базу

  8. И как в предыдущем разделе передаем токен доступа и записываем refresh токен в cookie

Выход из системы

Полная версия изображения

  1. Переход на маршрут выхода из системы. Внимание на диаграмме ошибка, маршрут что-то типа /user/logout

  2. Получаем из HTTP-only cookie refresh токен

  3. Если refresh токена нет – возвращаем ошибку

  4. Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.

  5. Ищем пользователя по refresh токену.

  6. Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.

  7. Удаляем у пользователя из базы refresh токен

  8. Сбрасываем cookie у клиента

Вспомогательные модули безопасности

Создайте следующую файловую структуру в корне проекта:

Для работы необходимо подготовить:

  • SSL сертификат и закрытый ключ к нему

  • Закрытый и публичный ключи для генерации JWT токена доступа

  • Закрытый и публичный ключи для генерации JWT refresh токена. На самом деле для реализации refresh токена достаточно генерации уникальной строки, можно использовать uuid, например, но я не ищу легких путей.

Если у вас нет SSL сертификата, можно сгенерировать свой, но использовать такой сертификат в боевом проекте не стоит, так как к self-signed сертификатам нет доверия.

Итак для генерации SSL сертификата и закрытого ключа можно воспользоваться openssl:

openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout ssl.key -out ssl.crt

Генерируем ключи для JWT:

ssh-keygen -t rsa -b 4096 -m PEM -f jwtPrivate.key openssl rsa -in jwtPrivate.key -pubout -outform PEM -out jwtPublic.pem  ssh-keygen -t rsa -b 4096 -m PEM -f refreshPrivate.key openssl rsa -in refreshPrivate.key -pubout -outform PEM -out refreshPublic.pem

Все ключи и сертификаты складываем в ./src/crypto/

Напишем несколько вспомогательных модулей:

./src/utils/cryptoHelper.js
import crypto from 'crypto';  /**  * Валидация пароля  */ export function validatePassword(password, hash, salt) {     const hashCandidate = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');     return hash === hashCandidate; }  /**  * Генерация соли и хэша пароля  */ export function genHashWithSalt(password) {     const salt = crypto.randomBytes(32).toString('hex');     const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');      return {         salt,         hash     }; }
./src/utils/jwtHelper.js
import fs from 'fs'; import path from 'path'; import jsonwebtoken from 'jsonwebtoken';  // настраиваем пути и читаем ключи const jwtPrivate = path.join(__dirname, '../crypto/', 'jwtPrivate.pem'); const refreshPrivate = path.join(__dirname, '../crypto/', 'refreshPrivate.pem'); const refreshPublic = path.join(__dirname, '../crypto/', 'refreshPublic.pem'); const JWT_PRIV_KEY = fs.readFileSync(jwtPrivate, 'utf8'); const REFRESH_PRIV_KEY = fs.readFileSync(refreshPrivate, 'utf8'); const REFRESH_PUBLIC_KEY = fs.readFileSync(refreshPublic, 'utf8');  // выпуск JWT токена доступа export function issueJWT(user) {     const { _id } = user;     const expiresIn = '10m';      const payload = {         uid: _id,         iat: Math.floor(Date.now() / 1000)     };      const signedToken = jsonwebtoken.sign(payload, JWT_PRIV_KEY, { expiresIn, algorithm: 'RS256' });      return {         token: `Bearer ${signedToken}`,         expires: expiresIn     }; }  //выпуск JWT refresh токена export function issueRefresh(user) {     const { _id } = user;     const expiresIn = '7d';      const payload = {         uid: _id,         iat: Math.floor(Date.now() / 1000)     };      const signedToken = jsonwebtoken.sign(payload, REFRESH_PRIV_KEY, {         expiresIn,         algorithm: 'RS256'     });      return {         token: signedToken,         expires: expiresIn     }; }  //валидация refresh токена  export function isValidRefresh(token) {     try {         jsonwebtoken.verify(token, REFRESH_PUBLIC_KEY, { algorithm: 'RS256' });     } catch (error) {         return false;     }     return true; } 
./src/utils/passport.js
import { Strategy, ExtractJwt } from 'passport-jwt'; import fs from 'fs'; import path from 'path'; import mongoose from 'mongoose'; import { userSchema } from '../db/models/User';  const User = mongoose.model('User', userSchema);  const pathToKey = path.join(__dirname, '../crypto/', 'jwtPublic.pem'); const PUB_KEY = fs.readFileSync(pathToKey, 'utf8');  const options = {     jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),     secretOrKey: PUB_KEY,     algorithms: ['RS256'] };  export const strategy = (pass) => {     pass.use(         new Strategy(options, (jwtPayload, done) => {             User.findOne({ _id: jwtPayload.uid }, (err, user) => {                 if (err) {                     return done(err, false);                 }                 if (user) {                     return done(null, user);                 }                 return done(null, null);             });         })     ); };

Это описание jwt стратегии, слизано из официальной документации, с небольшими изменениями. Одно из главных — использование публичного ключа, для получения информации из токена.

./src/utils/securityMiddleware.js

Это промежуточная функция будет использоваться для защиты наших маршрутов

export const securityMiddleware = (req, res, next, passport, groups) => {     passport.authenticate('jwt', { session: false }, (err, user) => {         if (err) {             return next(err);         }         if (!user) {             return res.status(401).send('Unauthorized');         }         // добавляем в req поле user с определенным набором полей, отдавать здесь хэш, соль не надо.         const { _id, email, nickname, group } = user;         req.user = {             _id,             email,             nickname,             group         };         if (groups.includes(user.group)) {             return next();         }         return res.status(403).send('Insufficient access rights');     })(req, res, next); };

Описание API

Наш минимальный API опишет процесс регистрации, аутентификации и авторизации, а также тестовые маршруты для проверки работы разных групп пользователей и открытых разделов.

Этот файл будет использоваться валидатором запросов, генератором документации и генератором маршрутов.

Обратите внимание на поля operationId – это имена функций-контроллеров, которые мы реализуем и они будут вызываться, чтобы обработать эндпоинты.

./src/api/apiV1.yaml
openapi: 3.0.3 info:     title: Passport test     description: Test of passport.js     version: 1.0.0     license:         name: MIT License         url: https://opensource.org/licenses/MIT paths: # Тестовый публичный маршрут     /test/ping:         get:             description: 'Returns pong'             tags:                 - Test             operationId: ping             responses:                 '200':                     description: OK                     $ref: '#/components/responses/standardResponse'                      # Тестовый маршрут, для зарегистрированных пользователей                         /test/private:         get:             description: 'Testing private section'             tags:                 - Test             operationId: testPrivate             security:                 - access: ['free']             responses:                 '200':                     $ref: '#/components/responses/standardResponse'  # Тестовый маршрут, для платных подписчиков     /test/subscription:         get:             description: 'Testing subscribers section'             tags:                 - Test             operationId: testSubscription             security:                 - access: ['subscriber']             responses:                 '200':                     $ref: '#/components/responses/standardResponse'  # Маршрут для регистрации пользователей # обратите внимание на поле email, валидатор будет ожидать формат email     /user/register:         post:             description: 'Register user'             tags:                 - User             operationId: userRegister             requestBody:                 required: true                 content:                     application/json:                         schema:                             type: object                             properties:                                 email:                                     type: string                                     format: email                                 nickname:                                     type: string                                 password:                                     type: string                                     format: password             responses:                 '200':                     description: OK                     $ref: '#/components/responses/standardResponse'                 '400':                     description: Bad Request                     $ref: '#/components/responses/standardResponse'  # Маршрут для аутентификации     /user/login:         post:             description: 'Login user'             tags:                 - User             operationId: userLogin             requestBody:                 required: true                 content:                     application/json:                         schema:                             type: object                             properties:                                 email:                                     type: string                                     format: email                                 password:                                     type: string                                     format: password             responses:                 '200':                     description: Returns boolean success state and jwt object                     content:                         application/json:                             schema:                                 type: object                                 properties:                                     success:                                         type: boolean                                     jwt:                                         type: object                 '400':                     description: Login failed                     $ref: '#/components/responses/standardResponse'                                              /user/refresh:         get:             description: 'Refresh token'             tags:                 - User             operationId: userRefreshToken             responses:                 '200':                     description: 'Token refreshed'                     $ref: '#/components/responses/jwtResponse'                 '403':                     description: 'Token refresh error'                     $ref: '#/components/responses/standardResponse'      /user/logout:         get:             description: 'Logout user. Remove cookie. Remove refresh token in DB'             tags:                 - User             operationId: userLogout             responses:                 '200':                     description: 'Successfully logged out'                     $ref: '#/components/responses/standardResponse'                 '403':                     description: 'You are not logged in to logout!'                     $ref: '#/components/responses/standardResponse'                         /user/profile:         get:             description: 'Returns user object'             tags:                 - User             operationId: userProfile             security:                 - access: [ 'free' ]             responses:                 '200':                     $ref: '#/components/responses/standardResponse'                 '403':                     $ref: '#/components/responses/standardResponse' components:     responses:         standardResponse:             description: Returns boolean success state and string message             content:                 application/json:                     schema:                         type: object                         properties:                             success:                                 type: boolean                             message:                                 type: string         jwtResponse:             description: Returns boolean success state and jwt object             content:                 application/json:                     schema:                         type: object                         properties:                             success:                                 type: boolean                             jwt:                                 type: object

Модель пользователя и mongoose

Процесс создания базы данных и настройку доступа пользователя к ней, я описывать не буду, процесс максимально доступно описан в официальной документации.

В корне проекта создайте файл .env и укажите в нем порт, на котором будет работать ваше приложение, а также параметры подключения к БД.

.env
PORT = 3007 DB_HOST = localhost DB_PORT = 27017 DB_NAME = passport DB_USER = passport DB_PASS = passport

Настройка подключения к БД

./src/db/db.js
import Mongoose from 'mongoose';  export const connect = async () => {     const dbHost = process.env.DB_HOST;     const dbPort = process.env.DB_PORT;     const dbName = process.env.DB_NAME;     const user = process.env.DB_USER;     const pass = process.env.DB_PASS;      const uri = `mongodb://${dbHost}:${dbPort}/${dbName}?authSource=dbWithCredentials`;      await Mongoose.connect(uri, {         authSource: dbName,         user,         pass,         useNewUrlParser: true,         useFindAndModify: true,         useUnifiedTopology: true,         useCreateIndex: true     }).catch((err) => console.error(err));      const db = Mongoose.connection;     db.on('error', () => {         throw new Error('Error connecting database');     }); };

Модель нашего пользователя

./src/db/models/User.js
import mongoose from 'mongoose';  export const userSchema = new mongoose.Schema(     {         email: {             type: String,             required: true,             unique: true         },         nickname: {             type: String,             required: true,             unique: true         },         hash: {             type: String,             required: true         },         salt: {             type: String,             required: false         },         refreshToken: {             type: Object         },         group: {             type: String         }     },     { versionKey: false } );

Обработка эндпоинтов

Контроллеры имеет смысл группировать по функционалу в один файл, если там много всего, то возможно даже по отдельным директориям. В нашем случае хватит файлов.

Контроллер user.js содержит функции, логика работы которых подробно описана в диаграммах в разделе про безопасность. Здесь без особых комментариев, код должен быть вполне понятен.

./src/api/controllers/user.js
import mongoose from 'mongoose';  import { userSchema } from '../../db/models/User'; import * as cryptoHelper from '../../utils/cryptoHelper'; import * as jwtHelper from '../../utils/jwtHelper';  const User = mongoose.model('User', userSchema);  async function sendAndSetTokens(req, res, user) {     const jwt = jwtHelper.issueJWT(user);     const refresh = jwtHelper.issueRefresh(user);      user.refreshToken = refresh;     await user.save();      res.cookie('refreshToken', refresh, {         secure: true,         httpOnly: true     });     res.status(200).json({         success: true,         jwt: {             token: jwt.token,             expiresIn: jwt.expires         }     }); }  // удаление refresh токена из базы async function resetRefresh(user) {     user.refreshToken = '';     await user.save(); }  // Сброс куки, путем установки пустого значения и короткого срока жизни function resetCookie(req, res) {     res.cookie('refreshToken', '', {         maxAge: 1000,         secure: true,         httpOnly: true     });     res.status(200).json({         success: true,         message: 'Successfully logged out'     }); }  export function userRegister(req, res, next) {     const { email, nickname, password } = req.body;     const saltHash = cryptoHelper.genHashWithSalt(password);     const { salt, hash } = saltHash;      const newUser = new User({         email,         nickname,         hash,         salt,         group: 'free'     });      newUser         .save()         .then(() => {             res.status(200).json({ success: true, message: 'User registered' });         })         .catch((err) => {             if (err.code === 11000 || err.code === 11001) {                 res.status(409).json({                     success: false,                     message: `E-mail ${email} already registered, try another or log in.`                 });             } else {                 res.status(400).json({                     success: false,                     message: err.message                 });             }         }); }  export function userLogin(req, res, next) {     User.findOne({ email: req.body.email })         .then(async (user) => {             if (!user) {                 res.status(401).json({                     success: false,                     message: 'Wrong login or password'                 });             }             const isValid = cryptoHelper.validatePassword(req.body.password, user.hash, user.salt);             if (isValid) {                 await sendAndSetTokens(req, res, user);             } else {                 res.status(401).json({                     success: false,                     message: 'Wrong login or password'                 });             }         })         .catch((err) => next(err)); }  export function userRefreshToken(req, res, next) {     const refreshCandidate = req.cookies.refreshToken;     if (refreshCandidate) {         if (jwtHelper.isValidRefresh(refreshCandidate.token)) {             User.findOne({ refreshToken: refreshCandidate })                 .then(async (user) => {                     await sendAndSetTokens(req, res, user);                 })                 .catch(() => {                     res.status(403).json({                         success: false,                         message: 'Invalid Refresh Token!'                     });                 });         } else {             res.status(403).json({                 success: false,                 message: 'Invalid Refresh Token!'             });         }     } else {         res.status(401).json({             success: false,             message: 'Refresh Token Empty!'         });     } }  export function userLogout(req, res, next) {     const refreshCandidate = req.cookies.refreshToken;     if (refreshCandidate) {         if (jwtHelper.isValidRefresh(refreshCandidate.token)) {             User.findOne({ refreshToken: refreshCandidate })                 .then(async (user) => {                     await resetRefresh(user);                     resetCookie(req, res);                 })                 .catch((err) => next(err));         } else {             res.status(401).json({                 success: false,                 message: 'You are not logged in to logout!'             });         }     } else {         res.status(401).json({             success: false,             message: 'Refresh Token Empty!!'         });     } }  export function userProfile(req, res, next) {     if (req.user) {         res.status(200).json(req.user);     } } 

Контроллер test.js – набор простейших функций, для проверки работы авторизации и работы незащищенного маршрута.

./src/api/controllers/test.js
export function ping(req, res) {     res.json({         success: true,         message: 'Pong'     }); }  export function testSubscription(req, res) {     res.status(200).json({         success: true,         message: 'You are subscriber!'     }); }  export function testPrivate(req, res) {     res.status(200).json({         success: true,         message: 'You are in Private!'     }); }

Осталось экспортировать все это разом в ./src/api/controllers/index.js

export * from './test'; export * from './user';

Собираем все воедино

Нам осталось собрать все в кучу и написать точку входа. Для этого мы напишем server.js и положим его в ./src/utils и app.js, который положим в ./src

На этих файлах остановимся подробнее. Начнем с импортов, что для чего нужно:

  • express – сам наш сервер

  • cookieParser – промежуточное ПО, которое позволит нам работать с куки

  • swaggerUI – интерфейс документации, который строится на основании описания API в yaml файле.

  • swagger-routes-express – автоматическая генерация маршрутов (линковка эндпоинтов к функциям контроллеров на основании того же yaml файла API)

  • yaml – работа с yaml файлами

  • express-openapi-validator – простой валидатор запросов (может и ответы валидировать, но я не включал. Включается элементарно изменением значения в true)

  • morgan – мощный инструмент логирования, который я использую для вывода информации в консоль, чтобы дебажить в реальном времени.

  • cors – установка заголовков CORS, чтобы не делать ручками. Немного подробнее поговорим ниже.

  • passport – та самая библиотека, которая упрощает нам работу по защите маршрутов

  • дальше подключаем контроллеры, базу, стратегию passport.

Теперь первым делом инициализируем нашу стратегию, передав ей объект passport:

strategy(passport);

Подключаемся к БД:

db.connect()     .then(() => console.log('MongoDB connected'))     .catch((error) => console.error(error));

Загружаем и выводим в консоль информацию по API:

const yamlSpecFile = './bin/api/apiV1.yaml'; const apiDefinition = YAML.load(yamlSpecFile);  const apiSummary = summarise(apiDefinition); console.info(apiSummary);

Инициализируем инстанс express:

const server = express();

Настройка сервера

// подключаем логирование с помощью morgan server.use(morgan('dev')); // позволяем себе читать параметры из url server.use(express.urlencoded({ extended: true }));  // это промежуточное по позволяет парсить входящие запросы с application/json server.use(express.json());  // позволяет работать с куки server.use(cookieParser()); // настройка CORS. В боевом проекте стоит указать адреса, для которых будет доступен наш БЭК //var corsOptions = { //  origin: 'http://example.com', //  optionsSuccessStatus: 200 // some legacy browsers (IE11, various SmartTVs) choke on 204 //} // cors(corsOptions) // Более подробно смотрите документацию пакета на npmjs.com server.use(cors()); // инициализируем passport.js server.use(passport.initialize());

Автоматическая валидация запросов

// Чтобы включить валидацию ответов, поправьте параметр validateResponses // обратите внимание, что здесь мы указываем yaml файл API  const validatorOptions = {     coerceTypes: false,     apiSpec: yamlSpecFile,     validateRequests: true,     validateResponses: false }; server.use(OpenApiValidator.middleware(validatorOptions));  // Кастомизация ошибок, если валидация не пройдена server.use((err, req, res, next) => {     res.status(err.status).json({         error: {             type: 'request_validation',             message: err.message,             errors: err.errors         }     }); });

Самый главный участок – генерация маршрутов и их защита.

Коннектору передается объект, в который мы импортировали все функции контроллеров и описание API. На основании этих данных он линкует и создает маршруты, которые в стандартной документации и гайдах выглядят как

server.use('/route/to/something', controllerFunction...

у нас этого не будет.

Также обратите внимание на объект security, объекты subscriber и free, это поля из yaml файла описания api, в разделе security acess. Промежуточному ПО здесь мы передаем стандартный набор для middleware + объект paspport + массив групп, которым разрешен доступ к маршрутам, отмеченным определенным уровнем доступа.

const connect = connector(api, apiDefinition, {     onCreateRoute: (method, descriptor) => {         console.log(             `Method ${method} of endpoint ${descriptor[0]} linked to ${descriptor[1].name}`         );     },     security: {         subscriber: (req, res, next) => {             securityMiddleware(req, res, next, passport, ['subscriber', 'admin']);         },         free: (req, res, next) => {             securityMiddleware(req, res, next, passport, ['free', 'subscriber', 'admin']);         }     } });

Осталось обернуть наш сервер коннектором и экспортировать

connect(server);  module.exports = server;
./src/utils/server.js
import express from 'express'; import cookieParser from 'cookie-parser'; import swaggerUi from 'swagger-ui-express';  import { connector, summarise } from 'swagger-routes-express'; import YAML from 'yamljs'; import * as OpenApiValidator from 'express-openapi-validator'; import morgan from 'morgan'; import cors from 'cors'; import passport from 'passport'; import * as api from '../api/controllers'; import * as db from '../db/db'; import { securityMiddleware } from './securityMiddleware';  import { strategy } from './passport';  strategy(passport);  // connect to DB db.connect()     .then(() => console.log('MongoDB connected'))     .catch((error) => console.error(error));  // load API definition const yamlSpecFile = './bin/api/apiV1.yaml'; const apiDefinition = YAML.load(yamlSpecFile);  const apiSummary = summarise(apiDefinition); console.info(apiSummary);  const server = express();   server.use(morgan('dev')); server.use(express.urlencoded({ extended: true })); server.use(express.json()); server.use(cookieParser());  server.use(cors());  server.use(passport.initialize());  // API Documentation server.use('/api-docs', swaggerUi.serve, swaggerUi.setup(apiDefinition, { explorer: false }));  // Automatic validation const validatorOptions = {     coerceTypes: false,     apiSpec: yamlSpecFile,     validateRequests: true,     validateResponses: false }; server.use(OpenApiValidator.middleware(validatorOptions));  // error customization, if request is invalid server.use((err, req, res, next) => {     res.status(err.status).json({         error: {             type: 'request_validation',             message: err.message,             errors: err.errors         }     }); });  // Automatic routing based on api definition const connect = connector(api, apiDefinition, {     onCreateRoute: (method, descriptor) => {         console.log(             `Method ${method} of endpoint ${descriptor[0]} linked to ${descriptor[1].name}`         );     },     security: {         subscriber: (req, res, next) => {             securityMiddleware(req, res, next, passport, ['subscriber', 'admin']);         },         free: (req, res, next) => {             securityMiddleware(req, res, next, passport, ['free', 'subscriber', 'admin']);         }     } });  connect(server);  module.exports = server; 

Осталась точка входа – app.js. Здесь все достаточно просто, распишу все в комментариях.

import https from 'https'; import fs from 'fs'; import * as dotenv from 'dotenv';  import server from './utils/server';  // помещаем в process.env переменные из .env файла dotenv.config(); const { PORT } = process.env;  // загружаем сертификат и закрытый ключ const privateKey = fs.readFileSync('./bin/crypto/ssl.key'); const certificate = fs.readFileSync('./bin/crypto/ssl.crt');  const options = { key: privateKey, cert: certificate };  // создаем HTTPS сервер const app = https.createServer(options, server);  // запускаем на порту, который указали в .env файле app.listen(PORT, () => {     console.info(`Listening on https://localhost:${PORT}`);     console.info(`Open https://localhost:${PORT}/api-docs for documentation`); });

Основная информация взята из этих статей:

https://losikov.medium.com/part-2-express-open-api-3-0-634385c97a4e

https://medium.com/swlh/everything-you-need-to-know-about-the-passport-jwt-passport-js-strategy-8b69f39014b0

Спасибо за внимание, надеюсь кому-то этот лонгрид поможет .

ссылка на оригинал статьи https://habr.com/ru/post/559136/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *