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.0000 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.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0 [Перевод] Клон Trello на Phoenix и React. Части 1-3 | Хостинг за 90 р. от cyberhost.biz — платный хостинг
+7 993 930-19-90 suport@cyberhost.biz

Trello — одно из самых моих любимых приложений. Я пользуюсь им с момента появления, и мне очень нравится то, как оно работает, его простота и гибкость. Каждый раз, начиная изучать новую технологию, я предпочитаю создать полноценное приложение, в котором смогу применить на практике всё, что изучил, для решения реальных проблем, и проверить эти решения. Так что начав изучать Elixir и его Phoenix Framework я понял: я должен на практике использовать весь этот потрясающий материал, с которым познакомился, и поделиться им в виде руководства о том, как реализовать простое, но функциональное посвящение Trello.


Оглавление

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

    В начале года, решив познакомиться с Elixir и Phoenix Framework, я наткнулся в Сети на интересный цикл статей, посвященный реализации клона Trello с помощью Elixir, Phoenix и React. Он показался мне довольно интересным, русского перевода я не нашёл, но поделиться захотелось. Наконец-то руки дошли до перевода.

    Должен отметить, что с экосистемой React я совершенно незнаком, эта часть будет приведена как есть; к тому же, некоторые моменты в Elixir/Phoenix за это время изменились — проекты на месте не стоят. Так же надеюсь найти время в будущем на то, чтобы реализовать front-end с помощью Angular2 и опубликовать статью об этом, благо как раз занимаюсь связкой Angular2 <-> Phoenix Channels <-> Elixir/Phoenix Framework.

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

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

    *И прошу прощения за дублирование вступления — даже под спойлером не получилось до ката разместить и примечание, и введение от автора. Решил, что введение важнее.

    Введение и выбор стека технологий

    Оригинал

    Trello — одно из самых моих любимых приложений. Я пользуюсь им с момента появления, и мне очень нравится то, как оно работает, его простота и гибкость. Каждый раз, начиная изучать новую технологию, я предпочитаю создать полноценное приложение, в котором смогу применить на практике всё, что изучил, для решения реальных проблем, и проверить эти решения. Так что начав изучать Elixir и его Phoenix Framework я понял: я должен на практике использовать весь этот потрясающий материал, с которым познакомился, и поделиться им в виде руководства о том, как реализовать простое, но функциональное посвящение Trello.

    Что мы собираемся сделать

    По сути, мы создадим одностраничное приложение, в котором существующие пользователи смогут авторизоваться, создать несколько досок, поделиться ими с другими пользователями и добавить на них списки и карточки. Подключенные пользователи будут показаны при просмотре доски, а любые изменения автоматически немедленно — в стиле Trello — будут отражаться в браузере каждого такого пользователя.

    Текущий стек технологий

    Phoenix управляет статическими ресурсами с помощью npm и собирает их, прямо "из коробки" используя Branch или Webpack, так что довольно просто по-настоящему разделить front-end и back-end, при этом сохраняя единую кодовую базу. Так, для back-end мы воспользуемся:

    • Elixir
    • Phoenix Framework
    • Ecto
    • PostgreSQL

    А чтобы создать одностраничное приложение для front-end:

    • Webpack
    • Sass для таблиц стилей
    • React
    • React router
    • Redux
    • ES6/ES7 JavaScript

    Мы используем несколько большим количеством зависимостей Elixir’а и пакетов npm, но я расскажу о них позднее, в процессе использования.

    Почему этот стек?

    Elixir — очень быстрый и мощный функциональный язык, базирующийся на Erlang и имеющий дружелюбный синтаксис, весьма похожий на Ruby. Он очень надёжен и специализируется на параллельности, и благодаря виртуальной машине Erlang (Erlang VM, BEAM — прим. переводчика) может справиться с тысячами параллельных процессов. Я новичок в Elixir, так что мне всё ещё предстоит изучить немало, но исходя из уже изученного могу сказать, что это очень впечатляюще.

    Мы будем использовать Phoenix — на текущий момент наиболее популярный веб-фреймворк для Elixir, который не только реализует некоторые моменты и стандарты, привнесённые в веб-разработку Rails, но и предлагает много других клёвых возможностей вроде способа управления статическими ресурсами, который я упомянул выше, и, самое важное для меня, встроенной realtime функциональности с помощью websockets без каких-либо сложностей и дополнительных внешних зависимостей (и поверьте мне — это работает как часы).

    В то же время мы воспользуемся React, react-router и Redux, потому что я просто обожаю использовать это сочетание для создания одностраничных приложений и управления их состоянием. Вместо того, чтобы как обычно использовать CoffieScript, в новом году (статья была написана в начале января 2016 года — прим. переводчика) я хочу поработать с ES6 и ES7, так что это отличная возможность начать и втянуться.

    Конечный результат

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

    Главный экран будет содержать список собственных досок пользователя и досок, к которым он был подключён другими пользователями:

    И, наконец, представление доски, где все пользователи смогут видеть, кто к ней подключён, а так же управлять списками и карточками:

    Но довольно разговоров. Остановимся здесь, чтобы я мог начать подготовку второй части, в которой мы увидим, как создать новый проект Phoenix, что необходимо изменить, чтобы возпользоваться Webpack вместо Branch и как настроить основу для front-end.

    Начальная настройка проекта Phoenix Framework

    Оригинал

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

    Статические ресурсы с помощью Webpack

    В отличие от Ruby on Rails Phoenix не имеет собственного конвейера обработки ресурсов (asset pipeline, некоторые русскоязычные Rails-ресурсы переводят термин как "файлопровод" — прим. переводчика), вместо этого используется Branch как средство для сборки ресурсов, что лично я считаю более современным и гибким. Прикольно, что нет необходимости использовать и Branch, если вы этого не хотите, можно воспользоваться Webpack. Я никогда не имел дела с Branch, поэтому вместо него мы применим Webpack.

    Phoenix включает node.js как опциональную зависимость, поскольку она требуется для Branch, но так как Webpack тоже нуждается в node.js, удостоверьтесь, что последняя у вас установлена.

    Создадим новый проект Phoenix без Branch:

    $ mix phoenix.new —no-brunch phoenix_trello



    $ cd phoenix_trello

    Хорошо, теперь у нас есть новый проект без средств сборки ресурсов. Создадим новый файл package.json и установим Webpack как зависимость для разработки (dev dependency — прим. переводчика):

    $ npm init
    … (Можно просто нажать Enter в ответ на вопрос об установке значений по умолчанию)


    $ npm i webpack —save-dev

    Теперь наш package.json должен выглядеть примерно так:

    {
    "name": "phoenix_trello",
    "devDependencies": {
    "webpack": "^1.12.9"
    },
    "dependencies": {
    },
    }

    Для проекта нам понадобится куча зависимостей, так что вместо того, чтобы листать их все тут, пожалуйста, загляните в исходный файл в репозитории проекта и скопируйте их оттуда в свой package.json. Теперь необходимо запустить следующую команду, чтобы установить все пакеты:

    $ npm install

    Нам так же нужно добавить конфигурационный файл webpack.json, чтобы подсказать Webpack, как собирать ресурсы:

    ‘use strict’;
    var path = require(‘path’);
    var ExtractTextPlugin = require(‘extract-text-webpack-plugin’);
    var webpack = require(‘webpack’);
    function join(dest) { return path.resolve(__dirname, dest); }
    function web(dest) { return join(‘web/static/’ + dest); }
    var config = module.exports = {
    entry: {
    application: [
    web(‘css/application.sass’),
    web(‘js/application.js’),
    ],
    },
    output: {
    path: join(‘priv/static’),
    filename: ‘js/application.js’,
    },
    resolve: {
    extesions: [», ‘.js’, ‘.sass’],
    modulesDirectories: [‘node_modules’],
    },
    module: {
    noParse: /vendor/phoenix/,
    loaders: [
    {
    test: /.js$/,
    exclude: /node_modules/,
    loader: ‘babel’,
    query: {
    cacheDirectory: true,
    plugins: [‘transform-decorators-legacy’],
    presets: [‘react’, ‘es2015’, ‘stage-2’, ‘stage-0’],
    },
    },
    {
    test: /.sass$/,
    loader: ExtractTextPlugin.extract(‘style’, ‘css!sass?indentedSyntax&includePaths[]=’ + __dirname + ‘/node_modules’),
    },
    ],
    },
    plugins: [
    new ExtractTextPlugin(‘css/application.css’),
    ],
    };
    if (process.env.NODE_ENV === ‘production’) {
    config.plugins.push(
    new webpack.optimize.DedupePlugin(),
    new webpack.optimize.UglifyJsPlugin({ minimize: true })
    );
    }

    Здесь мы указываем, что потребуется две точки входа webpack, одна для JavaScript и вторая — для таблиц стилей, обе расположены в директории web/static. Выходные файлы будут созданы в priv/static. Так как мы собираемся воспользоваться некоторыми возможностями ES6/7 и JSX, то будем использовать Babel с некоторыми предустановками, созданными для этих целей.

    Последний шаг — указать Phoenix стартовать Webpack каждый раз при запуске сервера разработки, чтобы Webpack отслеживал изменения в процессе разработки и генерировал соответствующие файлы ресурсов, на которые ссылается представление front-end’а. Для этого необходимо добавить описание ‘наблюдателя’ в файл config/dev.exs:

    config :phoenix_trello, PhoenixTrello.Endpoint,
    http: [port: 4000],
    debug_errors: true,
    code_reloader: true,
    cache_static_lookup: false,
    check_origin: false,
    watchers: [
    node: ["node_modules/webpack/bin/webpack.js", "—watch", "—color"]
    ]

    Если мы теперь запустим наш сервер разработки, то сможем увидеть, что Webpack так же работает и отслеживает изменения:

    $ mix phoenix.server
    [info] Running PhoenixTrello.Endpoint with Cowboy using http on port 4000
    Hash: 93bc1d4743159d9afc35
    Version: webpack 1.12.10
    Time: 6488ms
    Asset Size Chunks Chunk Names
    js/application.js 1.28 MB 0 [emitted] application
    css/application.css 49.3 kB 0 [emitted] application
    [0] multi application 40 bytes {0} [built]
    + 397 hidden modules
    Child extract-text-webpack-plugin:
    + 2 hidden modules

    Ещё одна вещь, которую нужно сделать. Если мы заглянем в директорию priv/static/js, то обнаружим файл phoenix.js. Этот файл содержит всё, что нам понадобится для использования websocket и channels, так что давайте переместим его в нашу базовую директорию с исходниками web/static/js, чтобы мы могли подключить его в момент, когда это понадобится.

    Основная структура front-end

    Теперь у нас есть всё, чтобы начать программировать, начнём с создания структуры приложения front-end, которому, среди прочих, понадобятся следующие пакеты:

    • bourbon и bourbon-neat, моя самая любимая библиотека включений (mixin) для Sass
    • history для управления историей из JavaScript
    • react и react-dom
    • redux и react-redux для управления состоянием (state)
    • react-router в качестве библиотеки для маршрутизации (роутинга)
    • redux-simple-router для сохранения изменений маршрутов в состоянии (state)

    Я не собираюсь терять время на обсуждении таблиц стилей, поскольку всё ещё правлю их, но хотел бы отметить, что для создания подходящей структуры моих Sass-файлов обычно использую css-buritto, который, по моему личному мнению, весьма полезен.

    Нам нужно настроить хранилище Redux (redux store), так что создадим следующий файл:

    //web/static/js/store/index.js
    import { createStore, applyMiddleware } from ‘redux’;
    import createLogger from ‘redux-logger’;
    import thunkMiddleware from ‘redux-thunk’;
    import { syncHistory } from ‘react-router-redux’;
    import reducers from ‘../reducers’;
    const loggerMiddleware = createLogger({
    level: ‘info’,
    collapsed: true,
    });
    export default function configureStore(browserHistory) {
    const reduxRouterMiddleware = syncHistory(browserHistory);
    const createStoreWithMiddleware = applyMiddleware(reduxRouterMiddleware, thunkMiddleware, loggerMiddleware)(createStore);
    return createStoreWithMiddleware(reducers);
    }

    Фактически, мы настраиваем хранилище (Store) с тремя промежуточными слоями (middleware):

    • reduxRouterMiddleware для передачи действий маршрутизатора к хранилищу
    • redux-thunk для передачи асинхронный действий
    • redux-logger для логирования любых действий и изменений состояния в консоль браузера

    Нам также нужно передать комбинацию преобразователей состояния (state reducers), так что создадим базовую версию этого файла:

    //web/static/js/reducers/index.js
    import { combineReducers } from ‘redux’;
    import { routeReducer } from ‘redux-simple-router’;
    import session from ‘./session’;
    export default combineReducers({
    routing: routeReducer,
    session: session,
    });

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

    //web/static/js/reducers/session.js
    const initialState = {
    currentUser: null,
    socket: null,
    error: null,
    };
    export default function reducer(state = initialState, action = {}) {
    return state;
    }

    Изначальное состояние последнего будет содержать объекты currentUser, который мы передадим после аутентификации посетителей, socket, которым мы воспользуемся для подключения к каналам (channels), и error для отслеживания любых проблем во время аутентификации пользователя.

    Закончив с этим, мы можем перейти к нашему основному файлу application.js и отрисовать компонент Root:

    //web/static/js/application.js
    import React from ‘react’;
    import ReactDOM from ‘react-dom’;
    import { browserHistory } from ‘react-router’;
    import configureStore from ‘./store’;
    import Root from ‘./containers/root’;
    const store = configureStore(browserHistory);
    const target = document.getElementById(‘main_container’);
    const node = <Root routerHistory={browserHistory} store={store}/>;
    ReactDOM.render(node, target);

    Мы создаём объект, содержащий историю браузера, настраиваем хранилища, и, наконец, отрисовываем в основном шаблоне приложения компонент Root, который будет Redux-адаптером (wrapper) Provider для routes:

    //web/static/js/containers/root.js
    import React from ‘react’;
    import { Provider } from ‘react-redux’;
    import { Router } from ‘react-router’;
    import invariant from ‘invariant’;
    import routes from ‘../routes’;
    export default class Root extends React.Component {
    _renderRouter() {
    invariant(
    this.props.routerHistory,
    ‘<Root /> needs either a routingContext or routerHistory to render.’
    );
    return (
    <Router history={this.props.routerHistory}>
    {routes}
    </Router>
    );
    }
    render() {
    return (
    <Provider store={this.props.store}>
    {this._renderRouter()}
    </Provider>
    );
    }
    }

    Теперь давайте опишем очень простой файл маршрутов:

    //web/static/js/routes/index.js
    import { IndexRoute, Route } from ‘react-router’;
    import React from ‘react’;
    import MainLayout from ‘../layouts/main’;
    import RegistrationsNew from ‘../views/registrations/new’;
    export default (
    <Route component={MainLayout}>
    <Route path="/" component={RegistrationsNew} />
    </Route>
    );

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

    В завершении нам необходимо добавить html-контейнер, в котором мы будем отрисовывать компонент Root в основном шаблоне приложения Phoenix:

    <!— web/templates/layout/app.html.eex —>
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="ricardo@codeloveandboards.com">
    <title>Phoenix Trello</title>
    <link rel="stylesheet" href="<%= static_path(@conn, "/css/application.css") %>">
    </head>
    <body>
    <main id="main_container" role="main"></main>
    <script src="<%= static_path(@conn, "/js/application.js") %>"></script>
    </body>
    </html>

    Обратите внимание, что теги link и script ссылаются на статические ресурсы, сгенерированные Webpack.

    Так как мы собираемся управлять маршрутизацией на front-end, необходимо сказать Phoenix отправлять любые http-запросы на действие index контроллера PageController, который будет только отрисовывать основной шаблон и компонент Root:

    # master/web/router.ex
    defmodule PhoenixTrello.Router do
    use PhoenixTrello.Web, :router
    pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
    end
    scope "/", PhoenixTrello do
    pipe_through :browser # Use the default browser stack
    get "*path", PageController, :index
    end
    end

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

    Модель User и JWT-аутентификация

    Оригинал

    Регистрация пользователя

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

    Модель и миграция User

    Phoenix использует Ecto как посредник при любом взаимодействии с базой данных. В случае с Rails можно сказать, что Ecto был бы чем-то, похожим на ActiveRecords, хотя он и делит похожую функциональность по разным модулям.

    Прежде, чем продолжить, необходимо создать базу данных (но перед этим необходимо настроить параметры подключения к базе данных в config/dev.exs — прим. переводчика):

    $ mix ecto.create

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

    $ mix phoenix.gen.model User users first_name:string last_name:string email:string encrypted_password:string

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

    # priv/repo/migrations/20151224075404_create_user.exs
    defmodule PhoenixTrello.Repo.Migrations.CreateUser do
    use Ecto.Migration
    def change do
    create table(:users) do
    add :first_name, :string, null: false
    add :last_name, :string, null: false
    add :email, :string, null: false
    add :crypted_password, :string, null: false
    timestamps
    end
    create unique_index(:users, [:email])
    end
    end

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

    Теперь давайте создадим в базе данных таблицу users:

    $ mix ecto.migrate

    Настало время посмотреть на модель User поближе:

    # web/models/user.ex
    defmodule PhoenixTrello.User do
    use Ecto.Schema
    import Ecto.Changeset
    schema "users" do
    field :first_name, :string
    field :last_name, :string
    field :email, :string
    field :encrypted_password, :string
    timestamps
    end
    @required_fields ~w(first_name last_name email)
    @optional_fields ~w(encrypted_password)
    def changeset(model, params \ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    end
    end

    В ней можно увидеть два основных раздела:

    • Блок схемы (schema), в котором расположены все метаданные, относящиеся к полям таблицы
    • Функция changeset, в которое можно определить все проверки и трансформации, применяемые к данным до того, как они будут готовы к использованию в нашем приложении.

    Прим. переводчика:
    В последние версии Ecto были внесены некоторые изменения. Например, атом :empty помечен как нерекомендуемый (deprecated), вместо него необходимо использовать пустой ассоциативный массив (map) %{}, а функцию cast/4 рекомендуется заменить на связку cast/3 и validate_required/3. Естественно, генератор последних версий Phoenix этим рекомендациям следует.

    Проверки и трансформации набора изменений (changeset)

    Итак, когда пользователь регистрируется, мы хотели бы дополнительно ввести некоторые проверки, поскольку ранее добавили запрет на использование null в качестве значения полей и ввели требование уникальности email. Мы обязаны отразить это в модели User, чтобы обработать возможные ошибки, вызванные некорректными данными. Так же хотелось бы зашифровать поле encrypted_field так, чтобы даже несмотря на использование простой строки в качестве пароля записан он был в защищённом виде.

    Давайте обновим модель и для начала добавим некоторые проверки:

    # web/models/user.ex
    defmodule PhoenixTrello.User do
    # …
    schema "users" do
    # …
    field :password, :string, virtual: true
    # …
    end
    @required_fields ~w(first_name last_name email password)
    @optional_fields ~w(encrypted_password)
    def changeset(model, params \ :empty) do
    model
    |> cast(params, @required_fields, @optional_fields)
    |> validate_format(:email, ~r/@/)
    |> validate_length(:password, min: 5)
    |> validate_confirmation(:password, message: "Password does not match")
    |> unique_constraint(:email, message: "Email already taken")
    end
    end

    В основном, мы сделали следующие модификации:

    • добавили новое виртуальное поле password, которое не будет записано в базу данных, но может использоваться как любое другое поле для любых иных целей. В нашем случае мы будем его заполнять из формы регистрации
    • сделали поле password обязательным
    • добавили проверку формата поля email
    • добавили проверку пароля, требуя его длины минимум в 5 символов; также будет проверяться массив параметров на предмет идентичности пароля с полем password_confirmation
    • добавили ограничение уникальности для проверки на наличие уже существующего email

    Этими изменениями мы покрыли все требуемые проверки. Однако до записи данных также необходимо заполнить поле encrypted_password. Для этого воспользуемся библиотекой хэширования паролей comeonin, добавив её в mix.exs как приложение и зависимость:

    # mix.exs
    defmodule PhoenixTrello.Mixfile do
    use Mix.Project
    # …
    def application do
    [mod: {PhoenixTrello, []},
    applications: [
    # …
    :comeonin
    ]
    ]
    end
    #…
    defp deps do
    [
    # …
    {:comeonin, "~> 2.0"},
    # …
    ]
    end
    end

    Не забудьте установить библиотеку командой:

    $ mix deps.get

    После установки comeonin давайте вернёмся к модели User и для генерации encrypted_password добавим новый шаг к цепочке changeset:

    # web/models/user.ex
    defmodule PhoenixTrello.User do
    # …
    def changeset(model, params \ :empty) do
    model
    # … другие проверки и ограничения
    |> generate_encrypted_password
    end
    defp generate_encrypted_password(current_changeset) do
    case current_changeset do
    %Ecto.Changeset{valid?: true, changes: %{password: password}} ->
    put_change(current_changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password))
    _ ->
    current_changeset
    end
    end
    end

    В этом новом методе мы сначала проверяем, корректны ли изменения в наборе и изменился ли пароль. Если да, мы шифруем пароль с помощью comeonin и помещаем результат в поле encrypted_password нашего набора, в противном случае возвращаем набор как есть.

    Маршрутизатор

    Теперь, когда модель User готова, продолжим реализацию процесса регистрации, добавив в файл router.ex цепочку :api и наш первый маршрут:

    # web/router.ex
    defmodule PhoenixTrello.Router do
    use PhoenixTrello.Web, :router
    #…
    pipeline :api do
    plug :accepts, ["json"]
    end
    scope "/api", PhoenixTrello do
    pipe_through :api
    scope "/v1" do
    post "/registrations", RegistrationController, :create
    end
    end
    #…
    end

    Так, любой запрос POST к /api/v1/registrations будет обработан действием :create контроллера RegistrationController, принимающего данные в формате json… в целом, всё довольно очевидно 🙂

    Контроллер

    До начала реализации контроллера давайте подумаем, что же нам нужно. Посетитель зайдёт на страницу регистрации, заполнит форму и отправит её. Если данные, полученные контроллером, корректны, нам потребуется добавить нового пользователя в базу данных, ввести его в систему и вернуть front-end’у в формате json данные вместе токеном аутентификации jwt в качестве результата входа в систему. Этот токен — то, что потребуется не только для отправки с каждым запросом для аутентификации пользователя, но и для доступа пользователя к защищённым экранам приложения.

    Чтобы реализовать аутентификацию и генерацию jwt, воспользуемся библиотекой Guardian, которая очень неплохо справляется с этой задачей. Просто добавьте следующее в mix.exs:

    # mix.exs
    defmodule PhoenixTrello.Mixfile do
    use Mix.Project
    #…
    defp deps do
    [
    # …
    {:guardian, "~> 0.9.0"},
    # …
    ]
    end
    end

    После запуска mix deps.get потребуется внести настройки библиотеки в config.exs:

    # config/confg.exs
    #…
    config :guardian, Guardian,
    issuer: "PhoenixTrello",
    ttl: { 3, :days },
    verify_issuer: true,
    secret_key: <your guardian secret key>,
    serializer: PhoenixTrello.GuardianSerializer

    Так же понадобится создать GuardianSerializer, который подскажет Guardian, как кодировать и декодировать информацию о пользователе в и из токена:

    # lib/phoenix_trello/guardian_serializer.ex
    defmodule PhoenixTrello.GuardianSerializer do
    @behaviour Guardian.Serializer
    alias PhoenixTrello.{Repo, User}
    def for_token(user = %User{}), do: { :ok, "User:#{user.id}" }
    def for_token(_), do: { :error, "Unknown resource type" }
    def from_token("User:" <> id), do: { :ok, Repo.get(User, String.to_integer(id)) }
    def from_token(_), do: { :error, "Unknown resource type" }
    end

    Теперь готово всё, чтобы реализовать RegistrationController:

    # web/controllers/api/v1/registration_controller.ex
    defmodule PhoenixTrello.RegistrationController do
    use PhoenixTrello.Web, :controller
    alias PhoenixTrello.{Repo, User}
    plug :scrub_params, "user" when action in [:create]
    def create(conn, %{"user" => user_params}) do
    changeset = User.changeset(%User{}, user_params)
    case Repo.insert(changeset) do
    {:ok, user} ->
    {:ok, jwt, _full_claims} = Guardian.encode_and_sign(user, :token)
    conn
    |> put_status(:created)
    |> render(PhoenixTrello.SessionView, "show.json", jwt: jwt, user: user)
    {:error, changeset} ->
    conn
    |> put_status(:unprocessable_entity)
    |> render(PhoenixTrello.RegistrationView, "error.json", changeset: changeset)
    end
    end
    end

    Благодаря механизму сопоставления с шаблоном (pattern matching), действие create ожидает в параметрах ключ "user". С этими параметрами мы создадим набор User и добавим его в базу данных. Если всё будет хорошо, мы воспользуемся Guardian для кодирования и подписи (метод encode_and_sign) данных нового пользователя, получив токен jwt и преобразуя его вместе с данными о пользователе в json. В противном случае, если набор данных некорректен, мы отобразим ошибки в виде json так, что сможем показать их пользователю в форме регистрации.

    Сериализация JSON

    Phoenix в качестве библиотеки JSON по-умолчанию использует Poison. Так как это одна из зависимостей Phoenix, для её установки нам не потребуется делать что-то особенное. Что же действительно нужно сделать — так это обновить модель User и указать, какие поля необходимо сериализовать:

    # web/models/user.ex
    defmodule PhoenixTrello.User do
    use PhoenixTrello.Web, :model
    # …
    @derive {Poison.Encoder, only: [:id, :first_name, :last_name, :email]}
    # …
    end

    С этого момента когда мы будем конвертировать данные о пользователе или список пользователей в ответ на действие в контроллере или канале (channel), библиотека просто вернёт указанные поля. Проще паренной репы!

    Получив back-end, готовый к регистрации новых пользователей, в следующей публикации мы переместимся к front-end, и чтобы завершить процесс регистрации, запрограммируем несколько прикольных штук на React и Redux. А тем временем не забудьте взглянуть на живое демо и исходный код конечного результата.

    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.0000 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.0000 356272 1. {main}() /volume1/web/cyberhost.biz/index.php:0