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

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: 'one',
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
Версія: 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 in action [: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. А тим часом не забудьте поглянути на живе демо і вихідний код кінцевого результату.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.