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 — платный хостинг
+7 993 930-19-90 suport@cyberhost.biz

Оглавление (текущий материал выделен)

  • Введение и выбор стека технологий
  • Начальная настройка проекта Phoenix Framework
  • Модель User и JWT-аутентификация
  • Front-end для регистрации на React и Redux
  • Начальное заполнение базы данных и контроллер для входа в приложение
  • Аутентификация на front-end на React и Redux
  • Настраиваем сокеты и каналы
  • Выводим список и создаём новые доски
  • Добавляем новых пользователей досок
  • Отслеживаем подключённых пользователей досок
  • Добавляем списки и карточки
  • Выкладываем проект на Heroku

  • Front-end для регистрации на React и Redux

    Оригинал

    Предыдущую публикацию мы закончили созданием модели 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, давайте взглянем на него:

    // web/static/js/containers/authenticated.js
    import React from ‘react’;
    import { connect } from ‘react-redux’;
    import { routeActions } from ‘redux-simple-router’;
    class AuthenticatedContainer extends React.Component {
    componentDidMount() {
    const { dispatch, currentUser } = this.props;
    if (localStorage.getItem(‘phoenixAuthToken’)) {
    dispatch(Actions.currentUser());
    } else {
    dispatch(routeActions.push(‘/sign_up’));
    }
    }
    render() {
    // …
    }
    }
    const mapStateToProps = (state) => ({
    currentUser: state.session.currentUser,
    });
    export default connect(mapStateToProps)(AuthenticatedContainer);

    Вкратце, что мы тут делаем: проверяем при подключении компонента, присутствует ли jwt-токен в локальном хранилище браузера. Позже мы разберёмся, как этот токен сохранить, но пока давайте представим, что токен не существует; в результате благодаря библиотеке redux-simple-route перенаправим пользователя на страницу регистрации.

    Компонент представления (view component) для регистрации

    Это то, что мы будем показывать пользователю, если обнаружим, что он не аутентифицирован:

    // web/static/js/views/registrations/new.js
    import React, {PropTypes} from ‘react’;
    import { connect } from ‘react-redux’;
    import { Link } from ‘react-router’;
    import { setDocumentTitle, renderErrorsFor } from ‘../../utils’;
    import Actions from ‘../../actions/registrations’;
    class RegistrationsNew extends React.Component {
    componentDidMount() {
    setDocumentTitle(‘Sign up’);
    }
    _handleSubmit(e) {
    e.preventDefault();
    const { dispatch } = this.props;
    const data = {
    first_name: this.refs.firstName.value,
    last_name: this.refs.lastName.value,
    email: this.refs.email.value,
    password: this.refs.password.value,
    password_confirmation: this.refs.passwordConfirmation.value,
    };
    dispatch(Actions.signUp(data));
    }
    render() {
    const { errors } = this.props;
    return (
    <div className="view-container registrations new">
    <main>
    <header>
    <div className="logo" />
    </header>
    <form onSubmit={::this._handleSubmit}>
    <div className="field">
    <input ref="firstName" type="text" placeholder="First name" required={true} />
    {renderErrorsFor(errors, ‘first_name’)}
    </div>
    <div className="field">
    <input ref="lastName" type="text" placeholder="Last name" required={true} />
    {renderErrorsFor(errors, ‘last_name’)}
    </div>
    <div className="field">
    <input ref="email" type="email" placeholder="Email" required={true} />
    {renderErrorsFor(errors, ’email’)}
    </div>
    <div className="field">
    <input ref="password" type="password" placeholder="Password" required={true} />
    {renderErrorsFor(errors, ‘password’)}
    </div>
    <div className="field">
    <input ref="passwordConfirmation" type="password" placeholder="Confirm password" required={true} />
    {renderErrorsFor(errors, ‘password_confirmation’)}
    </div>
    <button type="submit">Sign up</button>
    </form>
    <Link to="/sign_in">Sign in</Link>
    </main>
    </div>
    );
    }
    }
    const mapStateToProps = (state) => ({
    errors: state.registration.errors,
    });
    export default connect(mapStateToProps)(RegistrationsNew);

    Не особо много можно рассказать об этом компоненте… он изменяет заголовок документа при подключении, выводит форму регистрации и перенаправляет результат конструктора действия (action creator) регистрации singUp.

    Конструктор действия (action creator)

    Когда предыдущая форма отправлена, нам нужно переслать данные на сервер, где они будут обработаны:

    // web/static/js/actions/registrations.js
    import { pushPath } from ‘redux-simple-router’;
    import Constants from ‘../constants’;
    import { httpPost } from ‘../utils’;
    const Actions = {};
    Actions.signUp = (data) => {
    return dispatch => {
    httpPost(‘/api/v1/registrations’, {user: data})
    .then((data) => {
    localStorage.setItem(‘phoenixAuthToken’, data.jwt);
    dispatch({
    type: Constants.CURRENT_USER,
    currentUser: data.user,
    });
    dispatch(pushPath(‘/’));
    })
    .catch((error) => {
    error.response.json()
    .then((errorJSON) => {
    dispatch({
    type: Constants.REGISTRATIONS_ERROR,
    errors: errorJSON.errors,
    });
    });
    });
    };
    };
    export default Actions;

    Когда компонент 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:

    // web/static/js/reducers/session.js
    import Constants from ‘../constants’;
    const initialState = {
    currentUser: null,
    };
    export default function reducer(state = initialState, action = {}) {
    switch (action.type) {
    case Constants.CURRENT_USER:
    return { …state, currentUser: action.currentUser };
    default:
    return state;
    }
    }

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

    // web/static/js/reducers/registration.js
    import Constants from ‘../constants’;
    const initialState = {
    errors: null,
    };
    export default function reducer(state = initialState, action = {}) {
    switch (action.type) {
    case Constants.REGISTRATIONS_ERROR:
    return {…state, errors: action.errors};
    default:
    return state;
    }
    }

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

    // web/static/js/utils/index.js
    // …
    export function renderErrorsFor(errors, ref) {
    if (!errors) return false;
    return errors.map((error, i) => {
    if (error[ref]) {
    return (
    <div key={i} className="error">
    {error[ref]}
    </div>
    );
    }
    });
    }

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


    Начальное заполнение базы данных и контроллер для входа в приложение

    Оригинал

    Вход пользователя в приложение

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

    Начальное заполнение базы данных

    Если у вас есть опыт работы с Rails, вы увидите, что первоначальное заполнение базы данных в Phoenix выглядит очень похоже. Всё, что нам нужно для этого — наличие файла seeds.exs:

    # priv/repo/seeds.exs
    alias PhoenixTrello.{Repo, User}
    [
    %{
    first_name: "John",
    last_name: "Doe",
    email: "john@phoenix-trello.com",
    password: "12345678"
    },
    ]
    |> Enum.map(&User.changeset(%User{}, &1))
    |> Enum.each(&Repo.insert!(&1))

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

    $ 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