Клон Trello на Phoenix і React. Частини 4-5



Зміст (поточний виділений матеріал)
  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


Front-end для реєстрації на React і Redux
Оригінал
Попередню публікацію ми закінчили створенням моделі
User
з перевіркою коректності і необхідними для створення зашифрованого пароля трансформаціями набору змін (changeset); так само ми оновили файл маршрутизатора і створили контролер
RegistrationController
, який обробляє запит на створення нового користувача і повертає дані користувача і його jwt-токен для аутентифікації майбутніх запитів в форматі JSON. Тепер рушимо далі — до front-end.
Підготовка маршрутизатора React
Основна мета — мати два публічних маршруту,
/sign_in
та
/sign_up
, за яким зможе пройти будь-який відвідувач, щоб увійти в додаток або зареєструвати новий обліковий запис.
Крім цього нам знадобиться
/
як кореневої маршрут, щоб показати всі дошки, що відносяться до користувача, і, нарешті, маршрут
/board/:id
для виводу вмісту вибраної користувачем дошки. Для доступу до останніх двох маршрутах користувач повинен бути аутентифікований, в іншому випадку ми направимо його на екран реєстрації.
Оновимо файл
routes
для react-router, щоб відобразити те, що ми хочемо зробити:
// web/static/js/routes/index.js

import { IndexRoute, Route } from 'react-router';
import React from 'react';
import MainLayout from '../layouts/main';
import AuthenticatedContainer from '../containers/authenticated';
import HomeIndexView from '../views/home';
import RegistrationsNew from '../views/registrations/new';
import SessionsNew from '../views/sessions/new';
import BoardsShowView from '../views/boards/show';

export default (
<Route component={MainLayout}>
<Route path="/sign_up" component={RegistrationsNew} />
<Route path="/sign_in" component={SessionsNew} />

<Route path="/" component={AuthenticatedContainer}>
<IndexRoute component={HomeIndexView} />

<Route path="/boards/:id" component={BoardsShowView} />
</Route>
</Route>
);

Хитрий момент —
AuthenticatedContainer
, давайте поглянемо на нього:
// web/static/js/containers/authenticated.js

import React from 'react';
import { connect } from 'react-redux';
import { routeActions } from 'redux-simple-router';

class AuthenticatedContainer extends React.Component {
componentDidMount() {
const { dispatch, currentUser } = this.props;

if (localStorage.getItem('phoenixAuthToken')) {
dispatch(Actions.currentUser());
} else {
dispatch(routeActions.push('/sign_up'));
}
}

render() {
// ...
}
}

const mapStateToProps = (state) => ({
currentUser: state.session.currentUser,
});

export default connect(mapStateToProps)(AuthenticatedContainer);

Коротенько, що ми тут робимо: перевіряємо при підключенні компонента, присутній jwt-токен в локальному сховищі браузера. Пізніше ми розберемося, як цей токен зберегти, але поки давайте уявимо, що токен не існує; в результаті завдяки бібліотеці redux-simple-route перенаправимо користувача на сторінку реєстрації.
Компонент представлення (view component) для реєстрації
Це те, що ми будемо показувати користувачу, якщо виявимо, що він не аутентифікований:
// web/static/js/views/registrations/new.js

import React, {PropTypes} from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';

import { setDocumentTitle, renderErrorsFor } from '../../utils';
import Actions from '../../actions/registrations';

class RegistrationsNew extends React.Component {
componentDidMount() {
setDocumentTitle('Sign up');
}

_handleSubmit(e) {
e.preventDefault();

const { dispatch } = this.props;

const data = {
first_name: this.refs.firstName.value,
last_name: this.refs.lastName.value,
email: this.refs.email.value,
password: this.refs.password.value,
password_confirmation: this.refs.passwordConfirmation.value,
};

dispatch(Actions.signUp(data));
}

render() {
const { errors } = this.props;

return (
<div className="view-container registrations new">
<main>
<header>
<div className="logo" />
</header>
<form onSubmit={::this._handleSubmit}>
<div className="field">
<input ref="firstName" type="text" placeholder="First name" required={true} />
{renderErrorsFor(errors, 'first_name')}
</div>
<div className="field">
<input ref="lastName" type="text" placeholder="Last name" required={true} />
{renderErrorsFor(errors, 'last_name')}
</div>
<div className="field">
<input ref="email" type="email" placeholder="Email" required={true} />
{renderErrorsFor(errors, 'email')}
</div>
<div className="field">
<input ref="password" type="password" placeholder="Password" required={true} />
{renderErrorsFor(errors, 'password')}
</div>
<div className="field">
<input ref="passwordConfirmation" type="password" placeholder="Confirm password" required={true} />
{renderErrorsFor(errors, 'password_confirmation')}
</div>
<button type="submit">Sign up</button>
</form>
<Link to="/sign_in">Sign in</Link>
</main>
</div>
);
}
}

const mapStateToProps = (state) => ({
errors: state.registration.errors,
});

export default connect(mapStateToProps)(RegistrationsNew);

Не особливо багато можна розповісти про цьому компоненті… він змінює заголовок документа при підключенні, виводить форму реєстрації і перенаправляє результат конструктора дії (action creator) реєстрації
singUp
.
Конструктор дії (action creator)
Коли попередня форма відправлена нам потрібно переслати дані на сервер, де вони будуть оброблені:
// web/static/js/actions/registrations.js

import { pushPath } from 'redux-simple-router';
import Constants from '../constants';
import { httpPost } from '../utils';

const Actions = {};

Actions.signUp = (data) => {
return dispatch => {
httpPost('/api/v1/registrations', {user: data})
.then((data) => {
localStorage.setItem('phoenixAuthToken', data.jwt);

dispatch({
type: Constants.CURRENT_USER,
currentUser: data.user,
});

dispatch(pushPath('/'));
})
.catch((error) => {
error.response.json()
.then((errorJSON) => {
dispatch({
type: Constants.REGISTRATIONS_ERROR,
errors: errorJSON.errors,
});
});
});
};
};

export default Actions;

Коли компонент
RegistrationsNew
викликає конструктор дії, передаючи йому дані форми на сервер відправляється новий POST-запит. Запит фільтрується маршрутизатором Phoenix і обробляється контролером
RegistrationController
, який ми створили попередній публікації. У разі успіху отриманий з сервера jwt-токен зберігається в
localStorage
, дані створеного користувача передаються дії
CURRENT_USER
і, нарешті, користувач переадресується на кореневий шлях. Навпаки, якщо присутні будь-які помилки, пов'язані з реєстраційними даними, буде викликано дію
REGISTRATIONS_ERROR
з помилками в параметрах, так що ми зможемо показати їх користувачеві у формі.
Для роботи з http-запитами ми збираємося покластися на пакет isomorphic-fetch, викликається з допоміжного файлу, який для цих цілей включає кілька методів:
// web/static/js/utils/index.js

import React from 'react';
import fetch from 'isomorphic-fetch';
import { polyfill } from 'es6-promise';

export function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response;
} else {
var error = new Error(response.statusText);
error.response = response;
throw error;
}
}

export function parseJSON(response) {
return response.json();
}

export function httpPost(url, data) {
const headers = {
Authorization: localStorage.getItem('phoenixAuthToken'),
Accept: 'application/json',
'Content-Type': 'application/json',
}

const body = JSON.stringify(data);

return fetch(url {
method: 'post',
headers: headers,
body: body
})
.then(checkStatus)
.then(parseJSON);
}

// ...

Перетворювачі (reducers)
Останній крок — обробка цих результатів дій з допомогою перетворювачів, в результаті чого ми зможемо створити нове дерево стану, необхідний нашому додатком. По-перше, поглянемо на перетворювач
session
, в якому буде зберігатися
currentUser
:
// web/static/js/reducers/session.js

import Constants from '../constants';

const initialState = {
currentUser: null,
};

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.CURRENT_USER:
return { ...state, currentUser: action.currentUser };

default:
return state;
}
}

У разі наявності помилок реєстрації будь-якого типу необхідно додати їх до нового стану, щоб ми могли показати їх користувачеві. Додамо їх до перетворювача
registration
:
// web/static/js/reducers/registration.js

import Constants from '../constants';

const initialState = {
errors: null,
};

export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case Constants.REGISTRATIONS_ERROR:
return {...state, errors: action.errors};

default:
return state;
}
}

Зверніть увагу, що для виводу помилок ми звертаємося до функції
renderErrorsFor
з цього допоміжного файлу:
// web/static/js/utils/index.js

// ...

export function renderErrorsFor(errors, ref) {
if (!errors) return false;

return errors.map((error, i) => {
if (error[ref]) {
return (
<div key={i} className="error">
{error[ref]}
</div>
);
}
});
}

У цілому це все, що потрібно для процесу реєстрації. Далі ми побачимо, як існуючий користувач може аутентифицироваться в додатку і отримати доступ до власного змісту.

Початкове заповнення бази даних і контролер для входу в додаток
Оригінал
Вхід користувача в додаток
Раніше ми підготували все для того, щоб відвідувачі могли реєструватися і створювати нові користувальницькі акаунти. У цій частині ми збираємося реалізувати функціональність, необхідну, щоб дозволити відвідувачам аутентифицироваться в додаток, використовуючи e-mail і пароль. В кінці ми створимо механізм для отримання даних користувача за допомогою їх токенів аутентифікації.
Початкове заповнення бази даних
Якщо у вас є досвід роботи з Rails, ви побачите, що первинне заповнення бази даних в Phoenix виглядає дуже схоже. Все, що нам потрібно для цього — наявність файлу
seeds.exs
:
# priv/repo/seeds.exs

alias PhoenixTrello.{Repo, User}

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

По суті, в цьому файлі ми просто додаємо в базу даних всі дані, які хотіли б надати нашому додатку в якості початкових. Якщо ви хочете зареєструвати будь-якого іншого користувача — просто додайте його в список і запустіть заповнення бази:
$ mix run priv/repo/seeds.exs

Контролер для входу в додаток
До того, як створити контролер, необхідно внести деякі зміни в файл
router.ex
:
# web/router.ex

defmodule PhoenixTrello.Router do
use PhoenixTrello.Web, :router

#...

pipeline :api do
# ...

plug Guardian.Plug.VerifyHeader
plug Guardian.Plug.LoadResource
end

scope "/api", PhoenixTrello do
pipe_through :api

scope "/v1" do
# ...

post "/sessions", SessionController, :create
delete "/sessions", SessionController, :delete

# ...
end
end

#...
end

Перша добавка, яку потрібно зробити — додати в ланцюжок
api
дві вставити plugs, далі буде оригінальний термін використовуватися — plug, — оскільки слово "вставка" хоч і відображає букву суті, але не передає, як мені здається, повного сенсу; але якщо я не прав, буду радий нормальному російській терміну. Також має сенс для розуміння почитати переказний матеріал plug і plug pipeline — прим. перекладача):
  • VerifyHeader: цей plug просто перевіряє наявність сертифіката в заголовку
    Authorization
    (насправді, він крім цього намагається розшифрувати його, попутно перевіряючи на коректність, і створює структуру з вмістом токена — прим. перекладача)
  • LoadResource: якщо токен є, то робить поточний ресурс (в даному випадку — конретную запис з моделі
    User
    — прим. перекладача
    ) доступним як результат виклику
    Guardian.Plug.current_resource(conn)
Також потрібно додати в область
/api/v1
ще два маршрути для створення і видалення сесії користувача, обидва оброблювані контролером
SessionController
. Почнемо з обробника
:create
:
# web/controllers/api/v1/session_controller.ex

defmodule PhoenixTrello.SessionController do
use PhoenixTrello.Web, :controller

plug :scrub_params, "session" when in action [:create]

def create(conn, %{"session" => session_params}) do
case PhoenixTrello.Session.authenticate(session_params) do
{:ok, user} ->
{:ok, jwt, _full_claims} = user |> Guardian.encode_and_sign(:token)

conn
|> put_status(:created)
|> render("show.json", jwt jwt, user: user)

:error ->
conn
|> put_status(:unprocessable_entity)
|> render("error.json")
end
end

# ...
end

Щоб аутентифікувати користувача з отриманими параметрами, ми скористаємося допоміжним модулем
PhoenixTrello.Session
. Якщо все
:ok
, то ми зашифруем ідентифікатор користувача і впустимо його (encode and sign in — кілька вільний, але більш зрозумілий переклад — прим. перекладача). Це дасть нам jwt-маркер, який ми зможемо повернути разом з записом
user
у вигляді JSON. Перш, ніж продовжити, давайте поглянемо на допоміжний модуль
Session
:
# web/helpers/session.ex

defmodule PhoenixTrello.Session do
alias PhoenixTrello.{Repo, User}

def authenticate(%{"email" => email, "password" => password}) do
user = Repo.get_by(User email: String.downcase(email))

case check_password(user, password) do
true -> {:ok, user}
_ -> :error
end
end

defp check_password(user, password) do
case user do
nil -> false
_ -> Comeonin.Bcrypt.checkpw(password, user.encrypted_password)
end
end
end

Він намагається знайти користувача по e-mail і перевіряє, чи відповідає прийшов пароль зашифрованим паролю користувача. Якщо користувач існує і пароль правильний, повертається кортеж, що містить
{:ok, user}
. В іншому випадку, якщо користувач не знайдений або пароль невірний, повертається атом
:error
.
Повертаючись до контролера
SessionController
зверніть увагу, що він інтерпретує шаблон
error.json
, якщо результат аутентифікації користувача — згаданий раніше атом
:error
. Нарешті, необхідно створити модуль
SessionView
для відображення обох результатів:
# web/views/session_view.ex

defmodule PhoenixTrello.SessionView do
use PhoenixTrello.Web, :view

def render("show.json", %{jwt jwt, user: user}) do
%{
jwt jwt,
user: user
}
end

def render("error.json", _) do
%{error: "Invalid email or password"}
end
end

Користувачі, вже авторизовавшиеся в додатку
Інша причина повертати подання користувача JSON при аутентифікації в додатку полягає в тому, що ці дані можуть нам знадобитися для різних цілей; наприклад, щоб показати ім'я користувача в шапці програми. Це відповідає тому, що ми вже зробили. Але що, якщо користувач оновить сторінку браузера, перебуваючи на першому екрані? Все просто: стан додаток, кероване Redux, буде обнулити, а отримана раніше інформація зникне, що може призвести до небажаних помилок. А це не те, чого ми хочемо, тому що для запобігання такій ситуації ми можемо створити новий контролер, який відповідає за повернення при необхідності даних аутентифицированного користувача.
Додамо в файл
router.ex
новий маршрут:
# web/router.ex

defmodule PhoenixTrello.Router do
use PhoenixTrello.Web, :router

#...

scope "/api", PhoenixTrello do
pipe_through :api

scope "/v1" do
# ...

get "/current_user", CurrentUserController, :show

# ...
end
end

#...
end

Тепер нам потрібен контролер
CurrentUserController
, який виглядає так:
# web/controllers/api/v1/current_user_controller.ex

defmodule PhoenixTrello.CurrentUserController do
use PhoenixTrello.Web, :controller

plug Guardian.Plug.EnsureAuthenticated, handler: PhoenixTrello.SessionController

def show(conn, _) do
user = Guardian.Plug.current_resource(conn)

conn
|> put_status(:ok)
|> render("show.json", user: user)
end
end

Guardian.Plug.EnsureAuthenticated
перевіряє наявність раніше перевіреного сертифіката, і при його відсутності перенаправляє запит на функцію
:unauthenticated
контролера
SessionController
. Таким способом ми захистимо приватні контролери, так що якщо з'явиться бажання певні маршрути зробити доступними тільки аутентифікованим користувачам, все, що знадобиться — додати цей plug у відповідні контролери. Інша функціональність досить проста: після підтвердження наявності аутентифицированного токена буде трансльований
current_resource
, яким у нашому випадку є дані користувача.
Нарешті, потрібно в контролер
SessionController
додати обробник
unauthenticated
:
# web/controllers/api/v1/session_controller.ex

defmodule PhoenixTrello.SessionController do
use PhoenixTrello.Web, :controller

# ...

def unauthenticated(conn, _params) do
conn
|> put_status(:forbidden)
|> render(PhoenixTrello.SessionView, "forbidden.json", error: "Not Authenticated")
end
end

Він поверне код 403 — Forbidden разом з простим текстовим описом помилки JSON. На цьому ми закінчили з функціональність back-end, що відноситься до входу в додаток і подальшої аутентифікації. У наступній публікації ми розкриємо, як впоратися з цим під front-end і як підключитися до
UserSocket
, серця всіх смачненького режиму реального часу. А поки не забудьте поглянути на живе демо і вихідний код кінцевого результату.
Джерело: Хабрахабр

0 коментарів

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