Постановка задачи
Необходимо собрать базовый шаблон 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.
Логика такая:
При успешной аутентификации клиент получает JWT access токен и защищённый http-only куки с refresh токеном.
При каждом запросе клиент проверяет, не истек ли срок жизни токена доступа (JWT access token), если все ок, вставляет его в заголовок «Authorization», если токен истек, то сначала запрашивает новый токен доступа. Ниже более подробно про наш бэкенд.
Регистрация пользователя
На Backend передаем в открытом виде(но только по HTTPS) e-mail, пароль, какие-то дополнительные данные, которые вам нужны (nickname для примера на диаграмме)
Генерируем уникальную соль и хэшируем пароль с этой солью, после чего записываем в базу
Аутентификация
Клиент передает по HTTPS email и пароль
Пытаемся получить пользователя из базы
Получаем либо пользователя, либо undefined
Если undefined возвращаем сообщение об ошибке, и не говорим неверна почта или пароль
Берем из базы соль пользователя, хэшируем с этой солью введенный пользователем пароль и сверяем с сохраненным в базе хэшем. Если пароль введен неверно, возвращаем сообщение об ошибке, аналогично пункту 4
Если пароль введен верно, генерируем JWT токен доступа, с коротким сроком жизни и Refresh токен, который нужен для получения нового токена доступа, с более длительным сроком жизни. Это не показано на диаграмме, но refresh токен записывается в базу как одно из полей пользователя.
Возвращаем пользователю JWT токен доступа и устанавливаем HTTP-only cookie (secure, т.к. у нас HTTPS).
Обновление JWT токена доступа
Обращаемся на маршрут обновления токена доступа
Получаем из HTTP-only cookie refresh токен
Если refresh токена нет – возвращаем ошибку
Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.
Ищем пользователя по refresh токену.
Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.
Если пользователь найден, то генерируем новую пару JWT токена доступа и refresh токена. Записываем refresh в базу
И как в предыдущем разделе передаем токен доступа и записываем refresh токен в cookie
Выход из системы
Переход на маршрут выхода из системы. Внимание на диаграмме ошибка, маршрут что-то типа /user/logout
Получаем из HTTP-only cookie refresh токен
Если refresh токена нет – возвращаем ошибку
Если токен есть, то проверяем его валидность. Если токен не валидный, возвращаем ошибку.
Ищем пользователя по refresh токену.
Если пользователь не найден, то считаем токен более не валидным и возвращаем ошибку.
Удаляем у пользователя из базы refresh токен
Сбрасываем 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
Спасибо за внимание, надеюсь кому-то этот лонгрид поможет .
yarkov
Всё хорошо, но я бы на TypeScript писал. Как можно начинать сейчас проект на чистом JS - я не понимаю.
Не навязываю никому, просто ИМХО.
UrbanRider Автор
В целом соглашусь. Опять же тот, кто умеет в TypeScript скорее всего легко сможет перевести данный шаблон на ts, не так уже и сложно(настроить tsconfig, прописать типы. Ну можно переделать в ООП, написать классы и интерфейсы, для библиотек, которые используются в проекте описания типов есть почти для всех точно)
Этот гайд настроен больше на тех, кто осваивает JS, а без нормального знания JS, лезть в TS не стоит, имхо.