[JavaScript, Программирование, Node.JS] RESTful backend приложение. Базовый шаблон

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Май-2021 15:34

Постановка задачиНеобходимо собрать базовый шаблон 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-634385c97a4ehttps://medium.com/swlh/everything-you-need-to-know-about-the-passport-jwt-passport-js-strategy-8b69f39014b0Спасибо за внимание, надеюсь кому-то этот лонгрид поможет .
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_javascript, #_programmirovanie (Программирование), #_node.js, #_restful, #_express, #_passport.js, #_jwt, #_node, #_openapi, #_javascript, #_programmirovanie (
Программирование
)
, #_node.js
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 16-Ноя 09:22
Часовой пояс: UTC + 5