Graphcool Prisma. Добавляем авторизацию.

В предыдущей статье мы разворачивали Graphcool Prisma с нуля. На выходе кроме всего прочего мы получили веб-интерфейс, через который можно было добавлять новые топики и публиковать их.

Alt text

Но это была простейшая система без каких-либо пользователей. База данных содержала одну основную таблицу - Post (топики)

Alt text

Сегодня мы добавим пользователей, а так же связь Пользователь-Топики и проверку на владельца записей, чтобы публиковать черновики могли только их владельцы.

Добавим модель пользователя.

Прежде чем связать добавить связь топик-пользователь, просто добавим самостоятельную модель User (Пользователь). Для этого откроем файл server/database/datamodel.graphql

Сейчас там прописана только модель Post (Топик).

type Post {
  id: ID! @unique
  isPublished: Boolean!
  title: String!
  text: String!
}

Допишем ниже:

type User {
  id: ID! @unique
  email: String! @unique
  password: String!
  name: String!
}

Схема описана, теперь надо ее задеплоить, чтобы призма обновила структуру базы данных и сгенерировала необходимые методы для API и работы с базой данных. Для этого перейдем в папку server/ и выполним prisma deploy

Результат выполнения:

prisma deploy
Deploying service `hello-world` to stage `dev` on cluster `local` 52ms

Changes:

  User (Type)
  + Created type `User`
  + Created field `id` of type `GraphQLID!`
  + Created field `email` of type `String!`
  + Created field `password` of type `String!`
  + Created field `name` of type `String!`
  + Created field `updatedAt` of type `DateTime!`
  + Created field `createdAt` of type `DateTime!`

Applying changes 1.1s

Hooks:

Writing database schema to `src/generated/prisma.graphql`  97ms

Теперь у нас в базе данных появилась таблица User

Alt text

Запустим веб-консоль, чтобы посмотреть какие методы API у нас теперь имеются.

yarn start
yarn run v1.3.2
warning package.json: No license field
$ node src/index.js
Server is running on http://localhost:4000

Теперь у нас есть есть схема User. Но по-прежнему нет ни запросов (Queries), ни мутаций (Mutations).

Alt text

query - это обычный запрос, то есть на получение данных. mutation - это запрос на изменение данных. На самом деле это все очень условно, так как GraphQL особо ничего не знает о выполняемых эти запросы/мутации резолверах (функции, обрабатывающие запросы), резолверы на query могут выполнять запросы на обновления, а мутации просто выборки данных. Но есть одна важная деталь: query за один раз можно выполнить сразу несколько и параллельно, в то время как mutation выполняется только поштучно и последовательно.

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

Откроем файл server/src/schema.graphql и допишем в нем в Query users: [User!]! и в Mutation createUser(name: String!, email: String!, password: String!): User

итого получится

# import Post from "./generated/prisma.graphql"

type Query {
  feed: [Post!]!
  drafts: [Post!]!
  post(id: ID!): Post
  users: [User!]
}

type Mutation {
  createDraft(title: String!, text: String): Post
  deletePost(id: ID!): Post
  publish(id: ID!): Post
  createUser(name: String!, email: String!, password: String!): User
}

Обратите внимание на # import Post from "./generated/prisma.graphql"

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

Перезапустим сервер приложения, чтобы вступила в силу новая схема (нажмем Ctrl+C и опять выполним yarn start) и обновим страницу.

Вот теперь у нас появились новые методы в схеме

Alt text

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

Alt text

Это происходит потому что хотя у нас схема описана, не прописан резолвер на обработку этого запроса. То есть граф обрабатывает запрос, схема вся валидная, выполнение разрешено, но данных нет и не прописана функция на обработку запроса (возврат данных). В таком случае он просто возвращает пустое значение. А вот если бы мы прописали в Query users: [User!]! вместо users: [User!], то тут бы мы получили ошибку от графа, так как знак ! сигнализирует о запрете нулевого значения, то есть обязан быть ненулевой список, и как следует из указания User!, список этот не должен содержать нулевые значения пользователей.

Здесь на всякий случай еще раз объясню структуру запроса

mutation {
  createUser(
    name:"Test"
    email: "test@local.host"
    password:"123123"
  ){
    id
    name
    email
    password
  }
}

mutation, логично, указывает на то, что это именно запрос из мутаций выполняется, а не просто query. Это в графе разные группы запросов.

createUser - это название конкретной операции, мы так назвали ее в схеме выше.

Все что в круглых скобках - это передаваемые параметры в запрос.

В фигурных - структура возвращаемых данных. То есть в результате выполнения, если будет создан пользователь, мы сразу получим в ответ указанные поля из данных этого пользователя.

Итак, допишем мутацию на создание пользователя. Для этого открываем файл server/src/index.js и в Mutation дописываем наш обработчик createUser.


    async createUser(parent, { name, email, password }, ctx, info){

      password = await bcrypt.hash(password, 10);

      return ctx.db.mutation.createUser(
        { data: { name, email, password } },
        info,
      )
    },

bcrypt я подключил в этом файле выше через const bcrypt = require('bcryptjs')

Так как в этой версии приложения пакет bcryptjs не был установлен, устанавливаем его через команду yarn add bcryptjs и после этого опять запускаем сервер.

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

Alt text

Остается только дописать запрос на получение пользователей в Query.

users(parent, args, ctx, info) {
   return ctx.db.query.users({}, info)
},

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

Alt text

Обратите внимание, что мы не писали никаких запросов на непосредственную работу с базой данных. За нас все необходимые запросы создала призма при деплое новой схемы (когда выполняли prisma deploy). Напомню, что запросы эти пишутся в файл server/src/generated/prisma.graphql

А если еще более правильно выражаться, то там не запросы, а API-схемы для еще более низкого слоя всей этой системы - API-сервера призмы, что крутится на порту 4466. То есть получается, что наш проект крутится в своей папке, для него есть своя схема, через которую запросы транслируются на сервер призмы, которая в свою очередь работает с базой данных. При этом в призму мы деплоим изменения только в типах объектов схемы, запросы мы туда не деплоим, это уже наш локальный вопрос.

Авторизация пользователей.

Ну а теперь добавим непосредственно авторизацию пользователей. Зачем нам пользователи без этого?

В схему в Mutation допишем login(email: String!, password: String!): User и там же ниже допишем еще одну модель.

type AuthPayload {
  token: String!
  user: User!
}

Это чтобы в ответ мы получали не только объект пользователя, но и токен.

И допишем в резолверы.

Перезапускаем сервер, выполняем запрос на авторизацию и получаем ошибку.

Alt text

Это потому что для работы авторизации требуется объявление произвольного секретного ключа.

Остановим сервер и запустим вот так: APP_SECRET="wefewfwefwef" yarn start

Вот теперь авторизация прошла успешно и мы получили не только объект пользователя, но и токен:

Alt text

Запрос на получение текущего пользователя.

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

Допишем в схему Query me: User

И резолвер

    me(parent, args, ctx, info) {
      
      let id;

      const Authorization = ctx.request.get('Authorization');

      if (Authorization) {

        const token = Authorization.replace('Bearer ', '')
        const { userId } = jwt.verify(token, process.env.APP_SECRET);

        id = userId;

      }
      else {
        throw "Не был получен токен";
      }

      return ctx.db.query.user({ where: { id } }, info)
    },

Теперь токен, полученный при авторизации укажем в заголовок запроса Authorization. Если все ОК, мы получим пользователя.

Alt text

Обратите внимание на приставку Bearer, ее необходимо указывать.

Вот теперь у нас есть не только создание пользователей, но и авторизация.

Связываем пользователей и топики.

Ну и последний штрих: настроим связи Топик-Пользователь и научимся получать топики конкретных пользователей и авторов топиков. Для этого нам надо подправить наши схемы пользователя и топика.

Допишем в модель Post author: User @relation(name: "UserPosts"), а в User posts: [Post!]! @relation(name: "UserPosts"). Получится

type Post {
  id: ID! @unique
  isPublished: Boolean!
  title: String!
  text: String!
  author: User @relation(name: "UserPosts")
}

type User {
  id: ID! @unique
  email: String! @unique
  password: String!
  name: String!
  posts: [Post!]! @relation(name: "UserPosts")
}

Задеплоим нашу новую схему prisma deploy.

prisma deploy
Deploying service `hello-world` to stage `dev` on cluster `local` 128ms

Changes:

  Post (Type)
  + Created field `author` of type `Relation`

  User (Type)
  + Created field `posts` of type `[Relation!]!`

  UserPosts (Relation)
  + Created relation between Post and User

Applying changes 1.0s

Hooks:

Writing database schema to `src/generated/prisma.graphql`  119ms

Что примечательно, призма не просто создала новую таблицу для хранения записей Топик-Пользователь, но даже настроила первичные-вторичные ключи.

Alt text

Перезапустим сервер, обновим страницу и у нас уже есть возможность прописывать в запрос пользователя получение топиков.

Alt text

Сейчас у нас список пустой, потому что мы не создавали еще топики от имени пользователя.

Для удобства получение ID текущего пользователя вынесем в отдельный метод.

function getUserId(ctx) {
  
  const Authorization = ctx.request.get('Authorization')
  
  if (Authorization) {
    const token = Authorization.replace('Bearer ', '')
    const { userId } = jwt.verify(token, process.env.APP_SECRET)
    return userId
  }

  return null;
}

И с его использованием чуть перепишем мутацию создания топика.

    createDraft(parent, { title, text }, ctx, info) {
      
      const userId = getUserId(ctx)

      const author = userId && {
        connect: { id: userId },
      } || undefined;

      return ctx.db.mutation.createPost(
        { data: { 
          title, 
          text, 
          isPublished: false,
          author,
        } },
        info,
      )
    }

Здесь мы просто дописали получение ID текущего пользователя и если был получен, то передаем в запрос создания топика объект с ID этого пользователя. При чем я специально оставил возможность передачи пустого объекта пользователя, чтобы оставить возможность публикации топиков и анонимно.

Вот теперь при создании топика, если пользователь авторизован, прописывается автор в топик.

Alt text

А в списке топиков теперь выводятся авторы, если указаны.

Alt text

А в списке пользователей видны теперь топики пользователей.

Alt text

И совсем не сложно теперь в списке топиков у авторов получить все топики этих авторов.

Alt text

Я уж не буду говорить, что сейчас доступны методы и на редактирование/удаление этих сущностей.

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

Я не буду сейчас расписывать программирование фронта под все это (чтобы веб-морда для авторизации была и т.п.), это будет в следующем уроке. Скажу только что материал освоен и там не менее интересно, чем этот урок.

Кому интересно, поиграться можно здесь: http://prismagraphql.ru:4000/

Исходники проекта лежат под тегом Lesson2: https://github.com/MODX-Club/prismagraphql-demo/tree/Lesson2