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. А тем временем не забудьте взглянуть на живое демо и исходный код конечного результата.
Автор: heathen