Клон Trello на Phoenix и React. Части 1-3

в 16:41, , рубрики: Elixir, elixir-lang, phoenix framework, React, redux, ruby on rails, Trello, Программирование, Разработка веб-сайтов, функциональное программирование
image

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

Оглавление

  1. Введение и выбор стека технологий
  2. Начальная настройка проекта Phoenix Framework
  3. Модель User и JWT-аутентификация
  4. Front-end для регистрации на React и Redux
  5. Начальное заполнение базы данных и контроллер для регистрации
  6. Аутентификация на front-end на React и Redux
  7. Настраиваем сокеты и каналы
  8. Выводим список досок и создаём новые
  9. Добавляем новых пользователей досок
  10. Отслеживаем подключённых пользователей досок
  11. Добавляем списки и карточки
  12. Выкладываем проект на 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. А тем временем не забудьте взглянуть на живое демо и исходный код конечного результата.

Автор: heathen

Источник

Поделиться новостью

* - обязательные к заполнению поля