Содержание

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

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

Компоненты Login и Sign-up React

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

Компонент входа

Создайте файлsrc/components/Login.js со следующим содержимым (мы вернемся немного позже, чтобы реализовать функцию входа в систему и обернуть компонент необходимыми контейнерами):

Это даст нам потрясающе выглядящий компонент входа в систему (серьезно, мне нужно сделать немного CSS и улучшить дизайн - рекомендации более чем приветствуются!):

Аналогичным образом займемся компонентом регистрации.

Компонент регистрации

Регистрация выглядит примерно так же, как и вход:

Подключение компонентов

Чтобы отобразить эти два новых компонента, нам нужно немного изменить AppRouter, чтобы указать, что всякий раз, когда кто-то запрашивает путь /login или /signup, мы будем отображать соответствующие компоненты. После того, как вы импортировали компоненты Login и Signup в AppRouter.js, добавьте следующие два маршрута под маршрутом компонента Home:

<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />

Сначала пряжа (yarn start) и перейдите к /login и /signup, чтобы увидеть рендеринг компонентов.

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

Добавить поддержку аутентификации в сервис Graphcool

Graphcool поддерживает шаблоны, которые позволяют вам использовать различные функции в ваших проектах - есть целый репозиторий GitHub официально поддерживаемых шаблонов, которые включают, например, интеграцию Twilio, интеграцию Mailgun (отправка текстовых сообщений и электронных писем как часть подписки , в функции распознавателя и т. д.) и (хорошие новости для нас) аутентификация по электронной почте / паролю. Другими интересными шаблонами, связанными с аутентификацией, являются Facebook auth, Google auth, Github и т. Д.

Мы собираемся использовать шаблон аутентификации email-пароля, который состоит из 3-х функций, которые более подробно описаны ниже.

Регистрация

Для регистрации мы определяем новую мутацию с именем signupUser, имеющую эту сигнатуру:

signupUser(email: String!, password: String!): SignupUserPayload

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

  1. Проверяет адрес электронной почты
  2. Проверяет, существует ли пользователь с таким адресом электронной почты
  3. Хеширует пароль
  4. Создает нового пользователя
  5. Создает токен, который мы используем для идентификации пользователя

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

Аутентифицировать

Authenticate - это еще одна новая мутация, которую мы определяем, и она очень похожа по сигнатуре на мутацию регистрации:

authenticateUser(email: String!, password: String!): AuthenticateUserPayload

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

Авторизованный пользователь

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

Давайте добавим шаблон в нашу службу, выполнив эту команду из подпапки graphcool/:

graphcool add-template graphcool/templates/auth/email-password

Вышеупомянутая команда извлечет файлы .graphql и .ts, которые представляют мутации и функции, которые мы объяснили выше. Нам все еще нужно вручную обновить файлы types.graphql и graphcool.yml, чтобы включить их в нашу службу. Если вы откроете любой из этих двух файлов, вы увидите, что к нему были добавлены закомментированные объявления - это интерфейс командной строки Graphcool, помогающий нам определять функции и / или модели.

Раскомментируйте функции в graphcool.yml, а также модель User в файле types.graphql.

Примечание. мы вскоре вернемся к файлу types.graphql и добавим связь между моделями User и Link, так что пока не беспокойтесь об этом.

Давайте развернем изменения и убедимся, что все в порядке - запустите graphcool deploy, и через пару секунд вы должны получить список функций и типов преобразователя, которые были созданы.

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

Запишите меня!

Как объяснялось выше, нам нужно вызвать мутацию signupUser, чтобы подписать пользователя и сохранить токен, который мы получили после мутации.

Это мутация, которую мы собираемся использовать в компоненте Registration.js:

const SIGNUP_USER_MUTATION = gql`
  mutation SignupUser($email: String!, $password: String!) {
    signupUser(email: $email, password: $password) {
      id
      token
    }
  }`;

Нам также нужно обернуть компонент graphql:

export default graphql(SIGNUP_USER_MUTATION, { name: 'signupUserMutation' })(
  Signup,
);

Наконец, нам нужно реализовать функцию signup. Вот полная реализация:

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

Примечание: вы, вероятно, заметили отсутствие надлежащей обработки ошибок - слепая запись ошибок в консоль - это не очень хорошо - убедитесь, что вы всегда анализируете ошибки и показываете очищенные сообщение об ошибке вместо полных ошибок, которые могут включать стек вызовов и, возможно, информацию, которая никогда не должна передаваться. Чтобы исправить это, вы можете добавить свойство ошибки в состояние и обновить его общей ошибкой, например Не удалось зарегистрироваться - попробуйте еще раз. Я добавил это как проблему в репозиторий GitHub.

Мы можем опробовать регистрацию сейчас и посмотреть, работает ли она - перейдите к /signup и попробуйте ввести адрес электронной почты и образец пароля - если все в порядке, вы должны быть перенаправлены на домашнюю страницу. Вы также можете проверить, хранилась ли информация о пользователе в сервисе Graphcool (совет: запустите graphcool playground и нажмите «Данные»).

Войти

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

Вот мутация, которую мы используем:

const AUTHENTICATE_USER_MUTATION = gql`
  mutation AuthUser($email: String!, $password: String!) {
    authenticateUser(email: $email, password: $password) {
      id
      token
    }
  }`;

Оберните компонент контейнером graphql:

export default graphql(AUTHENTICATE_USER_MUTATION, {
  name: 'authenticateUserMutation',
})(Login);

И реализуйте функцию входа в систему, которая в значительной степени идентична функции регистрации (разница заключается в мутации, которую мы вызываем):

Попробуйте это, перейдя на страницу / login и используя тот же адрес электронной почты / пароль, который вы использовали при регистрации. Если вас перенаправили на домашнюю страницу, это сработало!

Следующая проблема, которую мы должны решить, заключается в том, что независимо от того, вошли вы в систему или нет, при переходе на главную страницу вы всегда будете получать список ссылок и возможность создавать ссылки. Мы должны это изменить, чтобы, если вы не вошли в систему, мы отображаем что-то, что дает вам ссылки на страницу входа и регистрации, а если вы вошли в систему, мы будем показывать вам ссылки.

Никаких ссылок для вас!

Помните третий запрос, который мы добавили ранее, запрос loggedInUser? Это то, что мы собираемся использовать. Но сначала нам нужно внести изменение, которое будет связывать токен пользователя из локального хранилища с каждым запросом GraphQL, который мы делаем.

В Apollo есть шаблон, который позволяет нам делать именно это - с ApolloLink мы можем вводить токен авторизации в каждый запрос. Сначала установите его:

$ yarn add apollo-link

После того, как вы импортировали ApolloLink из apollo-link, мы можем создать его новый экземпляр в файле index.js:

Мы определяем apolloLinkWithToken в приведенном выше коде (строки 1–10), затем читаем наш SHORTLY_TOKEN из локального хранилища, создаем заголовок auth с токеном (или null, если токена нет) и устанавливаем его в качестве заголовка авторизации. Наконец, мы вызываем функцию forward и передаем обновленную операцию (с заголовком авторизации) - это продолжается цепочкой вызовов, и в конечном итоге запрос отправляется на сервер. Наконец, нам нужно обновить ссылку, которую мы используем в строке 20, чтобы она была httpLinkWithToken, которую мы определили в строке 12.

На этом этапе, если мы вошли в систему и попытаемся вызвать запрос loggedInUser, мы вернем идентификатор зарегистрированного пользователя. Давайте добавим эту функцию в компонент Home.js. Во-первых, это запрос, который мы собираемся использовать:

const LOGGED_IN_USER_QUERY = gql`
  query CurrentUser {
    loggedInUser {
      id
    }
  }`;

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

Обернем компонент:

export default graphql(LOGGED_IN_USER_QUERY, { name: 'currentUser' })(Home);

И обновите метод рендеринга для рендеринга другого пользовательского интерфейса в зависимости от того, вошел ли пользователь в систему или нет, а также реализуйте функцию выхода из системы. Вот полный файл Home.js со всем этим:

В методе рендеринга мы ждем выполнения запроса, а затем пытаемся получить userId из запроса (строка 27). Наконец, мы используем простой оператор if, чтобы проверить, получили ли мы идентификатор пользователя, затем визуализируем компонент с кнопкой выхода и списком ссылок, в противном случае мы визуализируем div со ссылками на страницу входа и регистрации. Посмотрите скриншоты ниже для обоих:

Да, это выглядит намного лучше (с точки зрения функциональности, а не с точки зрения дизайна :)). Последнее, что осталось, - это добавить связь между моделями User и Link и немного изменить запросы, чтобы мы отображали только ссылки от авторизованных пользователей.

Связывая вещи вместе

Давайте откроем файл types.graphql и соединим модели Link и User, добавив линии, выделенные жирным шрифтом:

type Link @model {
  ...
  createdBy: User @relation(name: "UserLinks")  
  ...
}
type User @model {
  ...
  links: [Link!]! @relation(name: "UserLinks")  
  ...  
}

Вышеупомянутые изменения добавляют поле createdBy к каждой ссылке, указывающей на пользователя, создавшего ссылку, и поле в модели пользователя, которое дает нам массив всех ссылок, созданных пользователем.

Поскольку изменения схемы связаны с новыми отношениями с необходимыми значениями, я предлагаю вам удалить все ссылки и пользователей, которых вы создали до сих пор для тестирования, а затем запустить graphcool deploy, чтобы развернуть обновленную схему.

Надеюсь, развертывание прошло без проблем (если этого не произошло, обязательно проверьте, в чем заключается ошибка, и обычно удаление старых данных устраняет любые проблемы).

Теперь мы можем приступить к обновлению запросов. Начнем с CreateShortLink.js и CREATE_SHORT_LINK_MUTATION, включив в запрос поле createdBy:

const CREATE_SHORT_LINK_MUTATION = gql`
  mutation CreateLinkMutation(
    $url: String!
    $description: String!
    $createdById: ID!) {
      createLink(
        url: $url
        description: $description
        createdById: $createdById) {
          id
        }
      }`;

И передаем идентификатор пользователя, который мы читаем из локального хранилища, в мутацию в функции createShortLink:

await this.props.createShortLinkMutation({
  variables: {
    url,
    description,
    createdById : localStorage.getItem('SHORTLY_ID')
  },
});

Примечание: мы также могли бы добавить к компоненту запрос loggedInUser и использовать идентификатор пользователя из этого запроса для создания ссылки. Еще лучшим решением, вероятно, было бы создать контейнер с именем LoggedInContainer и использовать его для обертывания всех компонентов, которые мы хотим использовать, когда пользователь вошел в систему. Таким образом, мы выполняем запрос loggedInUser на уровне контейнера, а не отдельно в каждом компоненте.

Далее идут запросы ALL_LINKS_QUERY и LINKS_SUBSRIPTION в LinkList.js файле.

В ALL_LINKS_QUERY нам нужно отфильтровать все ссылки только на те, которые принадлежат зарегистрированному пользователю (изменения выделены жирным шрифтом, а остальная часть запроса опущена):

const ALL_LINKS_QUERY = gql`
  query AllLinksQuery($createdById: ID!) {
    allLinks(filter: { createdBy:  { id: $createdById } }) {
      ...
    }
  }`;

Поскольку мы добавили поле createdById в фильтр, нам нужно обновить оператор экспорта, чтобы передать идентификатор из локального хранилища в ALL_LINKS_QUERY следующим образом:

export default graphql(ALL_LINKS_QUERY, {
  name: 'allLinksQuery',
  options: props => ({
    variables: {
      createdById: localStorage.getItem('SHORTLY_ID'),
    },
  }),
})(LinkList);

Кроме того, нам нужно обновить фильтр в нашей подписке, чтобы он срабатывал только тогда, когда пользователь, в данный момент вошедший в систему, создает или обновляет ссылку. Вот переменная LINKS_SUBSCRIPTION, обновленная жирным шрифтом:

subscription NewLinkCreatedSubscription($createdById: ID!) {
  Link(
    filter: {
      mutation_in: [CREATED, UPDATED]
      node: { createdBy: { id: $createdById } }
  })
...

Нам также необходимо передать переменную createdById в подписку, когда мы вызываем функцию subscribeToMore в componentDidMount. Вот строка, которую нужно добавить после свойства документа:

variables: { createdById: localStorage.getItem('SHORTLY_ID') }

Как и в случае с предыдущими запросами, мы считываем идентификатор пользователя из локального хранилища и передаем его как переменную createdById в документ (запрос GraphQL).

И МЫ СДЕЛАНО

На этом этапе вы можете создать несколько пользователей, создать ссылки для каждого из них и убедиться, что вы видите ссылки только от вошедшего в систему пользователя.

Заключение

Если вы зашли так далеко - спасибо! Я надеюсь, что смог предоставить вам некоторую полезную информацию в этой серии. Я хотел бы услышать ваши отзывы - вы можете прокомментировать историю ниже или написать мне в Твиттере или на GitHub.

Если вас интересует окончательный код, проверьте репозиторий GitHub здесь - не стесняйтесь форкнуть репо и поиграть с кодом. Я также внесу в репозиторий некоторые проблемы, которые я хотел бы исправить в ближайшие недели, но если что-то бросится в глаза, я буду более чем счастлив просмотреть любые запросы на включение!