Николай Ланец
19 янв. 2018 г., 22:32

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

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

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

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

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

Прежде чем связать добавить связь топик-пользователь, просто добавим самостоятельную модель 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

Запустим веб-консоль, чтобы посмотреть какие методы 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).

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) и обновим страницу.
Вот теперь у нас появились новые методы в схеме

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

Это происходит потому что хотя у нас схема описана, не прописан резолвер на обработку этого запроса. То есть граф обрабатывает запрос, схема вся валидная, выполнение разрешено, но данных нет и не прописана функция на обработку запроса (возврат данных). В таком случае он просто возвращает пустое значение. А вот если бы мы прописали в 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 и после этого опять запускаем сервер.

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

Остается только дописать запрос на получение пользователей в Query.
users(parent, args, ctx, info) { return ctx.db.query.users({}, info) },
Перезапускаем сервер, выполняем запрос и видим результат.

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

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

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

В схему в Mutation допишем login(email: String!, password: String!): User
и там же ниже допишем еще одну модель.
type AuthPayload { token: String! user: User! }
Это чтобы в ответ мы получали не только объект пользователя, но и токен.

И допишем в резолверы.
async createUser(parent, { name, email, password }, ctx, info){ password = await bcrypt.hash(password, 10); return ctx.db.mutation.createUser( { data: { name, email, password } }, info, ) },
Перезапускаем сервер, выполняем запрос на авторизацию и получаем ошибку.

Это потому что для работы авторизации требуется объявление произвольного секретного ключа.
Остановим сервер и запустим вот так: APP_SECRET="wefewfwefwef" yarn start

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

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

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

Допишем в схему 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. Если все ОК, мы получим пользователя.

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

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

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

Для удобства получение 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 этого пользователя. При чем я специально оставил возможность передачи пустого объекта пользователя, чтобы оставить возможность публикации топиков и анонимно.

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

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

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

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

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


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

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

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

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