Nodejs GraphQl Аутентификация с JWT, Apollo-server, MySql и Sequelize ORM.

В последние несколько лет мы наблюдаем рост микросервисной архитектуры на совершенно другом уровне. Она фокусируется на разработке программных систем, которые пытаются сосредоточиться на создании однофункциональных модулей с четко определенными интерфейсами и операциями. Наряду с этим мы также наблюдаем массовый рост Agile, Devops и API. До нескольких лет назад REST API были отраслевым стандартом и горячей темой, но в 2015 году Facebook представил GraphQL, а в 2018 году они выпустили первую стабильную версию.

Github Repo — Аутентификация на GraphQL

В этой статье мы сосредоточимся на локальной аутентификации с помощью токена JWT. Для базы данных вы можете использовать любую базу данных MySql. Apollo-server — это сервер GraphQL с открытым исходным кодом, совместимый с любым клиентом GraphQL. Я буду использовать apollo для раскрытия API вместо express.

Мы сделаем простую аутентификацию, в которой пользователь будет иметь имя, фамилию, электронную почту, пароль, компанию и уникальный идентификатор сотрудника. Компания будет храниться в другой таблице, чтобы мы могли исследовать получение ассоциаций с помощью GraphQL. Сначала установим необходимые пакеты:

npm i apollo-server bcrpytjs dotenv jsonwebtoken sequelize mysql2 graphql

npm i -D sequelize-cli nodemon

const getUser = token => {
    try {
        if (token) {
            return jwt.verify(token, JWT_SECRET)
        }
        return null
    } catch (error) {
        return null
    }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Это первая строка после импорта, так мы определили промежуточное ПО JWT, которое будет проверять, действителен ли наш токен JWT.

const server = new ApolloServer({
    typeDefs,
    resolvers,
    context: ({ req }) => {
        const token = req.get('Authorization') || ''
        return { user: getUser(token.replace('Bearer', ''))}
    },
    introspection: true,
    playground


: true
})
server.listen({ port: PORT || 8080 }).then(({ url }) => {
    console.log(`🚀 Server ready at ${url}`);
  });
Вход в полноэкранный режим Выйти из полноэкранного режима

После этого мы определяем наш сервер Apollo, которому мы должны передать объект, содержащий:

  1. typeDefs: это схема для API graphQL, она определяет запросы и мутации, которые мы можем вызывать на API.

  2. resolvers: это функции, которые отвечают за возврат результата для соответствующих вызовов API.

  3. контекст: это объект, общий для всех резольверов конкретного выполнения. Здесь мы получаем токен JWT из заголовка и запускаем функцию getUser, которую мы определили ранее, чтобы проверить, действителен ли он, и сохраняем результат в переменной user, к которой может получить доступ любой резольвер.

  4. Интроспекция: определяет, можем ли мы запрашивать схему для получения информации о том, какие запросы она поддерживает и какова их структура (обычно false в продакшене).

  5. playground: это графическая, интерактивная, внутрибраузерная GraphQL IDE, которую мы можем использовать для выполнения запросов.

Давайте проверим наш типDefs или схему.

const typeDefs = gql`
    input Pagination {
        page: Int!
        items: Int!
    }
    input UserFilter {
        employeeId: Int
        firstName: String
        lastName: String
    }
    type User {
        employeeId: Int!
        firstName: String!
        lastName: String!
        password: String!
        email: String!
        company: String!
    }
    type AuthPayload {
        token: String!
        user: User!
    }
    type Query {
        getUserList(search:UserFilter, pagination:Pagination, sort:String): [User]
    }
    type Mutation {
        registerUser(firstName: String!, lastName: String!, employeeId: Int!, email: String!, password: String!, company: String!): AuthPayload!
        login (email: String!, password: String!): AuthPayload!
    }
`
Войти в полноэкранный режим Выход из полноэкранного режима

Шаблонный литеральный тег gql можно использовать для краткого написания запроса GraphQL, который разбирается в стандартный GraphQL AST. type определяет объект с его параметрами. Метка ! означает, что параметры являются обязательными и не могут быть неопределенными или нулевыми. Существует два различных типа, запрос и мутация. Проще говоря, запрос — это оператор SELECT, а мутация — операция INSERT.

Помимо скалярных типов String, Int, Float, Boolean и ID, которые мы можем непосредственно назначить в качестве типа аргумента или параметра, мы можем иметь собственные определенные сложные типы в качестве входных данных. Для этого мы используем тег input. Вход UserFilter — это пользовательский вход, который передается для получения запроса списка пользователей. [User] означает, что будет возвращен массив типа Users.

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

//User.js
module.exports = (sequelize, DataTypes) => {
    const User = sequelize.define('User', {
        firstName: { type: DataTypes.STRING, allowNull: true },
        lastName: { type: DataTypes.STRING, allowNull: true },
        email: { type: DataTypes.STRING, allowNull: false, unique: true },
        password: {type: DataTypes.STRING,allowNull: false},
        employeeId:{ type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
    }, {timestamps: false,
        hooks: {
            beforeCreate: async (user) => {
             if (user.password) {
              const salt = await bcrypt.genSaltSync(10, 'a');
              user.password = bcrypt.hashSync(user.password, salt);
             }
            },
            beforeUpdate:async (user) => {
             if (user.password) {
              const salt = await bcrypt.genSaltSync(10, 'a');
              user.password = bcrypt.hashSync(user.password, salt);
             }
            }
           }
    });
    User.associate = function (models) {
        User.hasOne(models.Company, { foreignKey: "employeeId" });
      };
    User.validPassword = async (password, hash) => {
        return await bcrypt.compareSync(password, hash);
       }
    return User;
  };
//Company.js
module.exports = (sequelize, DataTypes) => {
    const Company = sequelize.define('Company', {
        company: {type: DataTypes.STRING,allowNull: false},
        employeeId:{ type: DataTypes.INTEGER, allowNull: false, primaryKey: true, unique: true },
    }, {
      timestamps: false,
      freezeTableName: true,
    });
    Company.associate = function (models) {
        Company.belongsTo(models.User, { foreignKey: "employeeId" });
      };
    return Company;
  };
Вход в полноэкранный режим Выход из полноэкранного режима

beforeCreate — это хук, который вызывается при вызове запроса create. Хук содержит логику хэширования пароля с солью, чтобы мы не хранили незашифрованный пароль в базе данных. beforeUpdate этот хук вызывается, когда вызывается запрос update для таблицы user. Как и раньше, он хэширует обновленный пароль. User.validPassword — это класс Methods, который использует bcrypt для сравнения хэша, хранящегося в базе данных, со строкой, чтобы проверить, одинаковы ли они. User.associate — это ассоциация один-к-одному с таблицей company с внешним ключом employeeId. Timestamp:false по умолчанию sequelize включает createdAt и updateAt записи в SQL таблице, но это устанавливает значение false. freezeTableName по умолчанию sequelize делает имя таблицы множественным, что приводит к ошибкам, если мы не установили их такими по умолчанию. Поскольку я этого не делаю, freezeTableName помогает мне сохранить имена таблиц именно такими, какими я их определил, и не менять User на Users или Company на Companies. Index.js — это просто файлы seqelize по умолчанию для подключения к базе данных. Он также берет все модели, определенные в папке models, и применяет их к объекту «db».

const resolvers = {
    Query: {
        async getUserList(root, args, { user }) {
            try {
                if(!user) throw new Error('You are not authenticated!')
                const {search,pagination,sort} =args;
                var query={
                    offset:0,
                    limit:5,
                    raw: true,
                    //this is done to flaten out the join command
                    attributes: ['firstName','lastName','email','employeeId','Company.company',],
                    include: [{ model: models.Company,attributes:[]}]
                    }
                    //by defaults query is paginated to limit 5 items
                if(pagination){
                    query.limit=pagination.items;
                    query.offset=pagination.items*(pagination.page-1)
                }
                if(search){
                    query.where={
                        [Op.or]: [
                            search.firstName?{ firstName: search.firstName }:null,
                            search.lastName?{ lastName: search.lastName}:null,
                            search.employeeId?{ employeeId: search.employeeId}:null
                        ] 
                    }
                }
                if(sort){
                    query.order= [
                        [sort, 'ASC'],
                    ];
                }
                return await models.User.findAll(query);
            } catch (error) {
                throw new Error(error.message)
            }
        }
    },

    Mutation: {
        async registerUser(root, { firstName, lastName, email, password, employeeId,company }) {
            try {
                const userCheck = await models.User.findOne({ 
                    where: { 
                        [Op.or]: [
                            { email: email },
                            { employeeId: employeeId }
                    ] 
                }})
                if (userCheck) {
                    throw new Error('Email or Employee id already exists')
                }
                const user = await models.User.create({
                    firstName,
                    lastName,
                    employeeId,
                    email,
                    password
                })
                const companyModel = await models.Company.create({
                    employeeId,
                    company
                })
                const token = jsonwebtoken.sign(
                    { employeeId: user.employeeId, email: user.email},
                    process.env.JWT_SECRET,
                    { expiresIn: '1y' }
                )
                let createdUser={
                    company:companyModel.company,
                    employeeId: user.employeeId,
                    firstName: user.firstName, 
                    lastName: user.lastName, 
                    email: user.email
                }

                return {
                    token, user:createdUser, message: "Registration succesfull"
                }
            } catch (error) {
                throw new Error(error.message)
            }
        },

        async login(_, { email, password }) {
            try {
                const user = await models.User.findOne({ where: { email }})

                if (!user) {
                    throw new Error('No user with that email')
                }
                const isValid = await models.User.validPassword(password, user.password)
                if (!isValid) {
                    throw new Error('Incorrect password')
                }

                // return jwt
                const token = jsonwebtoken.sign(
                    { employeeId: user.employeeId, email: user.email},
                    process.env.JWT_SECRET,
                    { expiresIn: '1d'}
                )

                return {
                   token, user
                }
            } catch (error) {
                throw new Error(error.message)
            }
        }

    },
}
Вход в полноэкранный режим Выход из полноэкранного режима

Резольверы содержат функции, которые вызываются для соответствующего запроса и мутации. Они принимают 4 аргумента

Объект query в getUserList — это динамический объект, который изменяет значения в зависимости от аргументов, переданных в запрос. Все аргументы являются необязательными. Для всех запросов требуется заголовок Authorization с действительным jwt-токеном. Это проверяется

Переменные пользователя извлекаются из контекста, который мы передали ранее в server.js. Если мы не хотим, чтобы маршрут проходил аутентификацию, нам просто нужно избавиться от этой строки. Давайте объясним основной запрос. offset и limit — это параметры пагинации. raw используется для возврата объекта JSON вместо объекта sequelize, чтобы его было легче разобрать. Атрибуты позволяют нам определить, какие столбцы мы хотим вернуть из SQL. Например, мы применяем соединение между таблицами Company и User, чтобы не получить название компании для конкретного пользователя. Вы заметили, что мы установили атрибуты include как пустые. Это означает, что хотя они будут возвращены в запросе, они не будут отображаться. Они будут выглядеть примерно так, если вернуть {Company.company: "name",Company.employeeId:2}, и это вызовет ошибку, когда мы попытаемся разобрать это с помощью схемы graphQL, поскольку там мы определили, что у пользователя должен быть ключ company, а не Company.company в качестве ключа. Таким образом, чтобы решить эту проблему, мы выбираем 'Company.company' в качестве атрибута пользователя, который сопоставляется с компанией.

{
“Authorization”:”eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbXBsb3llZUlkIjoyLCJlbWFpbCI6ImJAZ21haWwuY29tIiwiaWF0IjoxNjIyNTMwNTAyLCJleHAiOjE2MjI2MTY5MDJ9.3qQOHPzhKOjM6r5JNRWoMsvyt2IzwX8aa7Bj7s1zEZw”
}
Вход в полноэкранный режим Выход из полноэкранного режима

Оцените статью
devanswers.ru
Добавить комментарий