Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0001 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/top-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 9
Call Stack:
0.0001 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
[Перевод] Клон Trello на Phoenix и React. Части 4-5 | Хостинг за 90 р. от cyberhost.biz — платный хостинг
Предыдущую публикацию мы закончили созданием модели User с проверкой корректности и необходимыми для генерации зашифрованного пароля трансформациями набора изменений (changeset); так же мы обновили файл маршрутизатора и создали контроллер RegistrationController, который обрабатывает запрос на создание нового пользователя и возвращает данные пользователя и его jwt-токен для аутентификации будущих запросов в формате JSON. Теперь двинемся дальше — к front-end.
Подготовка маршрутизатора React
Основная цель — иметь два публичных маршрута, /sign_in и /sign_up, по которым сможет пройти любой посетитель, чтобы, соответственно, войти в приложение или зарегистрировать новый аккаунт.
Помимо этого нам понадобится / как корневой маршрут, чтобы показать все доски, относящиеся к пользователю, и, наконец, маршрут /board/:id для вывода содержимого выбранной пользователем доски. Для доступа к последним двум маршрутам пользователь должен быть аутентифицирован, в противном случае мы перенаправим его на экран регистрации.
Обновим файл routes для react-router, чтобы отразить то, что мы хотим сделать:
// web/static/js/routes/index.js import { IndexRoute, Route } from ‘react-router’; import React from ‘react’; import MainLayout from ‘../layouts/main’; import AuthenticatedContainer from ‘../containers/authenticated’; import HomeIndexView from ‘../views/home’; import RegistrationsNew from ‘../views/registrations/new’; import SessionsNew from ‘../views/sessions/new’; import BoardsShowView from ‘../views/boards/show’; export default ( <Route component={MainLayout}> <Route path="/sign_up" component={RegistrationsNew} /> <Route path="/sign_in" component={SessionsNew} /> <Route path="/" component={AuthenticatedContainer}> <IndexRoute component={HomeIndexView} /> <Route path="/boards/:id" component={BoardsShowView} /> </Route> </Route> );
Хитрый момент — AuthenticatedContainer, давайте взглянем на него:
Вкратце, что мы тут делаем: проверяем при подключении компонента, присутствует ли jwt-токен в локальном хранилище браузера. Позже мы разберёмся, как этот токен сохранить, но пока давайте представим, что токен не существует; в результате благодаря библиотеке redux-simple-route перенаправим пользователя на страницу регистрации.
Компонент представления (view component) для регистрации
Это то, что мы будем показывать пользователю, если обнаружим, что он не аутентифицирован:
Не особо много можно рассказать об этом компоненте… он изменяет заголовок документа при подключении, выводит форму регистрации и перенаправляет результат конструктора действия (action creator) регистрации singUp.
Конструктор действия (action creator)
Когда предыдущая форма отправлена, нам нужно переслать данные на сервер, где они будут обработаны:
Когда компонент RegistrationsNew вызывает конструктор действия, передавая ему данные формы, на сервер отправляется новый POST-запрос. Запрос фильтруется маршрутизатором Phoenix и обрабатывается контроллером RegistrationController, который мы создали в предыдущей публикации. В случае успеха полученный с сервера jwt-токен сохраняется в localStorage, данные созданного пользователя передаются действию CURRENT_USER и, наконец, пользователь переадресуется на корневой путь. Наоборот, если присутствуют любые ошибки, связанные с регистрационными данными, будет вызвано действие REGISTRATIONS_ERROR с ошибками в параметрах, так что мы сможем показать их пользователю в форме.
Для работы с http-запросами мы собираемся положиться на пакет isomorphic-fetch, вызываемый из вспомогательного файла, который для этих целей включает несколько методов:
// web/static/js/utils/index.js import React from ‘react’; import fetch from ‘isomorphic-fetch’; import { polyfill } from ‘es6-promise’; export function checkStatus(response) { if (response.status >= 200 && response.status < 300) { return response; } else { var error = new Error(response.statusText); error.response = response; throw error; } } export function parseJSON(response) { return response.json(); } export function httpPost(url, data) { const headers = { Authorization: localStorage.getItem(‘phoenixAuthToken’), Accept: ‘application/json’, ‘Content-Type’: ‘application/json’, } const body = JSON.stringify(data); return fetch(url, { method: ‘post’, headers: headers, body: body, }) .then(checkStatus) .then(parseJSON); } // …
Преобразователи (reducers)
Последний шаг — обработка этих результатов действий с помощью преобразователей, в результате чего мы сможем создать новое дерево состояния, требуемое нашему приложению. Во-первых, взглянем на преобразователь session, в котором будет сохраняться currentUser:
В случае наличия ошибок регистрации любого типа необходимо добавить их к новому состоянию, чтобы мы могли показать их пользователю. Добавим их к преобразователю registration:
В целом это всё, что нужно для процесса регистрации. Далее мы увидим, как существующий пользователь может аутентифицироваться в приложении и получить доступ к собственному содержимому.
Начальное заполнение базы данных и контроллер для входа в приложение
Ранее мы подготовили всё для того, чтобы посетители могли регистрироваться и создавать новые пользовательские аккаунты. В этой части мы собираемся реализовать функциональность, необходимую, чтобы позволить посетителям аутентифицироваться в приложение, используя e-mail и пароль. В конце мы создадим механизм для получения пользовательских данных с помощью их токенов аутентификации.
Начальное заполнение базы данных
Если у вас есть опыт работы с Rails, вы увидите, что первоначальное заполнение базы данных в Phoenix выглядит очень похоже. Всё, что нам нужно для этого — наличие файла seeds.exs:
По сути, в этом файле мы просто добавляем в базу данных все данные, которые хотели бы предоставить нашему приложению в качестве начальных. Если вы хотите зарегистрировать любого другого пользователя — просто добавьте его в список и запустите заполнение базы:
$ mix run priv/repo/seeds.exs Контроллер для входа в приложение
До того, как создать контроллер, необходимо внести некоторые изменения в файл router.ex:
# web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #… pipeline :api do # … plug Guardian.Plug.VerifyHeader plug Guardian.Plug.LoadResource end scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do # … post "/sessions", SessionController, :create delete "/sessions", SessionController, :delete # … end end #… end
Первая добавка, которую нужно произвести — добавить в цепочку :api две вставки (plugs, далее будет оригинальный термин использоваться — plug, — поскольку слово "вставка" хоть и отражает букву сути, но не передаёт, как мне кажется, полного смысла; но если я не прав, буду рад нормальному русскому термину. Также имеет смысл для понимания почитать переводной материал о plug и plug pipeline — прим. переводчика):
VerifyHeader: этот plug просто проверяет наличие токена в заголовке Authorization (на самом деле, он помимо этого пытается расшифровать его, попутно проверяя на корректность, и создаёт структуру с содержимым токена — прим. переводчика)
LoadResource: если токен присутствует, то делает текущий ресурс (в данном случае — конретную запись из модели User — прим. переводчика) доступным как результат вызова Guardian.Plug.current_resource(conn)
Также нужно добавить в область /api/v1 ещё два маршрута для создания и удаления сессии пользователя, оба обрабатываемые контроллером SessionController. Начнём с обработчика :create:
# web/controllers/api/v1/session_controller.ex defmodule PhoenixTrello.SessionController do use PhoenixTrello.Web, :controller plug :scrub_params, "session" when action in [:create] def create(conn, %{"session" => session_params}) do case PhoenixTrello.Session.authenticate(session_params) do {:ok, user} -> {:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token) conn |> put_status(:created) |> render("show.json", jwt: jwt, user: user) :error -> conn |> put_status(:unprocessable_entity) |> render("error.json") end end # … end
Чтобы аутентифицировать пользователя с полученными параметрами, мы воспользуемся вспомогательным модулем PhoenixTrello.Session. Если всё :ok, то мы зашифруем идентификатор пользователя и впустим его (encode and sign in — несколько вольный, но более понятный перевод — прим. переводчика). Это даст нам jwt-токен, который мы сможем вернуть вместе с записью user в виде JSON. Прежде, чем продолжить, давайте взглянем на вспомогательный модуль Session:
# web/helpers/session.ex defmodule PhoenixTrello.Session do alias PhoenixTrello.{Repo, User} def authenticate(%{"email" => email, "password" => password}) do user = Repo.get_by(User, email: String.downcase(email)) case check_password(user, password) do true -> {:ok, user} _ -> :error end end defp check_password(user, password) do case user do nil -> false _ -> Comeonin.Bcrypt.checkpw(password, user.encrypted_password) end end end
Он пытается найти пользователя по e-mail и проверяет, соответствует ли пришедший пароль зашифрованному паролю пользователя. Если пользователь существует и пароль правильный, возвращается кортеж, содержащий {:ok, user}. В противном случае, если пользователь не найден или пароль неверен, возвращается атом :error.
Возвращаясь к контроллеру SessionController обратите внимание, что он интерпретирует шаблон error.json, если результат аутентификации пользователя — упомянутый ранее атом :error. Наконец, необходимо создать модуль SessionView для отображения обоих результатов:
# web/views/session_view.ex defmodule PhoenixTrello.SessionView do use PhoenixTrello.Web, :view def render("show.json", %{jwt: jwt, user: user}) do %{ jwt: jwt, user: user } end def render("error.json", _) do %{error: "Invalid email or password"} end end Пользователи, уже авторизовавшиеся в приложении
Другая причина возвращать представление пользователя в JSON при аутентификации в приложении заключается в том, что эти данные могут нам понадобиться для разных целей; к примеру, чтобы показать имя пользователя в шапке приложения. Это соответствует тому, что мы уже сделали. Но что, если пользователь обновит страницу браузера, находясь на первом экране? Всё просто: состояние приложение, управляемое Redux, будет обнулено, а полученная ранее информация исчезнет, что может привести к нежелательным ошибкам. А это не то, чего мы хотим, так что для предотвращения такой ситуации мы можем создать новый контроллер, отвечающий за возврат при необходимости данных аутентифицированного пользователя.
Добавим в файл router.ex новый маршрут:
# web/router.ex defmodule PhoenixTrello.Router do use PhoenixTrello.Web, :router #… scope "/api", PhoenixTrello do pipe_through :api scope "/v1" do # … get "/current_user", CurrentUserController, :show # … end end #… end
Теперь нам нужен контроллер CurrentUserController, который выглядит так:
# web/controllers/api/v1/current_user_controller.ex defmodule PhoenixTrello.CurrentUserController do use PhoenixTrello.Web, :controller plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController def show(conn, _) do user = Guardian.Plug.current_resource(conn) conn |> put_status(:ok) |> render("show.json", user: user) end end
Guardian.Plug.EnsureAuthenticated проверяет наличие ранее проверенного токена, и при его отсутствии перенаправляет запрос на функцию :unauthenticated контроллера SessionController. Таким способом мы защитим приватные контроллеры, так что если появится желание определённые маршруты сделать доступными только аутентифицированным пользователям, всё, что понадобится — добавить этот plug в соответствующие контроллеры. Прочая функциональность довольно проста: после подтверждения наличия аутентифицированного токена будет транслирован current_resource, которым в нашем случае являются данные пользователя.
Наконец, нужно в контроллер SessionController добавить обработчик unauthenticated:
# web/controllers/api/v1/session_controller.ex defmodule PhoenixTrello.SessionController do use PhoenixTrello.Web, :controller # … def unauthenticated(conn, _params) do conn |> put_status(:forbidden) |> render(PhoenixTrello.SessionView, "forbidden.json", error: "Not Authenticated") end end
Он вернёт код 403 — Forbidden вместе с простым текстовым описанием ошибки в JSON. На этом мы закончили с функциональность back-end, относящейся ко входу в приложение и последующей аутентификации. В следующей публикации мы раскроем, как справиться с этим во front-end и как подключиться к UserSocket, сердцу всех вкусняшек режима реального времени. А пока не забудьте взглянуть на живое демо и исходный код конечного результата.
Warning: include(/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php): failed to open stream: No such file or directory in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0001 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 Warning: include(): Failed opening '/volume1/web/cyberhost.biz/wp-content/plugins/jaster_cahce/cache/bottom-cache.php' for inclusion (include_path='.:/usr/share/pear') in /volume1/web/cyberhost.biz/index.php on line 13 Call Stack: 0.0001 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0
Свежие комментарии