LiveViewで認証付きのチャットアプリを構築する(準備編)

はじめに

Phoenix LiveView についてまた取り上げます。
この機能は最近活発に開発が行われていまして、サーバーサードレンダリングの新しい潮流として期待が持てます。
今度はより具体的な例として、認証機能付きチャットアプリを作成します。
これから作成するアプリには、以下の要素が含まれています。

  • ユーザー認証
  • 発言の投稿
  • LiveViewを使用したチャットの表示
  • phx-hookを使用した最終発言へのスクロール
  • プレゼンスを使用した接続ユーザー表示
  • 発言入力時のステータス表示

本テーマについて

本テーマは7記事構成になっています。
7記事それぞれで取り扱う内容は以下です。

  1. プロジェクト作成からログイン処理まで(本記事)
  2. 認証機能の作成
  3. チャットボードのコンテキスト周り作成
  4. チャットボードの画面周り作成
  5. Phoenix.PubSubを使用したリアルタイム更新
  6. Phoenix.Presenceを使用したアクティブユーザー表示
  7. 入力ステータスの表示

本テーマは以下の方針で記述します。

  • 機能実装は本テーマに沿った必要最小限のみ
  • UIもほぼそのまま
  • 機能へのアクセスはURLで直に指定を想定

完成版の画面イメージは以下のようになっています。

lv_chat

プロジェクトの準備

まずはプロジェクトの準備を行います。

バージョン情報

記事公開時点でのそれぞれのバージョンは以下です。

項目バージョン
erlang22.1.4
elixir1.9.2
Phoenix1.4.10
phoenixliveview0.3.1

手順

本記事では、以下の手順にしたがって進めてゆきます。

  • プロジェクトの作成
  • LiveViewのセットアップ
  • ユーザー登録機能の作成

プロジェクトの作成

lv_chat というプロジェクトを作成します。

$ mix phx.new lv_chat

DBの作成

プロジェクトのディレクトリに移動し、DBを作成します。

$ cd lv_chat
$ mix ecto.create

パッケージの取得

プロジェクトに必要なパッケージを追加します。

追加パッケージの記述

以下のパッケージを追加します。

パッケージ用途
phoenixliveviewLiveViewの機能実装に使用
pbkdf2パスワードのハッシュに使用
# mix.exs
  defp deps do
    [
        ...
      {:phoenix_live_view, "~> 0.3.0"},      {:pbkdf2_elixir, "~> 1.0"},        ...
    ]
  end

mixコマンドによるパッケージの取得

パッケージを取得します。

$ mix deps.get

LiveViewのセットアップ

LiveViewのセットアップを行います。
以前公開した以下のブログを参考にしてください。

Phoenix LiveViewによる動的サーバーサイドレンダリング1
ただし、LiveViewは現在も開発中なので、この手順が変わる可能性があります。
おかしいなと思ったら本家を参照してください。

ユーザー登録機能の作成

アクセスするユーザーの区別を行えるようにするため、ユーザー登録機能を作成します。

定義について

ユーザー・アカウントは以下のように定義します。

メンバー備考
namestring
  • 指定を必須とする
  • 重複しない
  • 1文字以上20文字以下
passwordstring
  • DBに保存しない
  • 3文字以上20文字以下
password_hashstring
  • パスワードのハッシュ値

コンテキストの作成

上記定義をもとに、以下のコマンドでコンテキストを作成します。

$ mix phx.gen.context Accounts User users name:string password_hash:string password:string

ecto.migrate を行う前に、自動生成されたファイルを意図したものになるように少々変更します。

Userモジュールの変更

変更点は以下です。

項目変更点
共通
  • :name 以外は別の changeset
:password
  • virtual 指定を追加
:name
  • 必須指定
  • 文字数制限の指定
  • 重複禁止

上記変更を以下のように記述します。

# /lib/lv_chat/accounts/user.ex
defmodule LvChat.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  schema "users" do
    field :name, :string
    field :password, :string, virtual: true    field :password_hash, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name])    |> validate_required([:name])    |> validate_length(:name, min: 1, max: 20)    |> unique_constraint(:name)  end
end

マイグレーションファイルの変更

マイグレーションファイルは、phx.gen.context を行った際に自動的に生成されます。
場所は以下にあります。

priv/repo/migrations/*_create_users.ex

※ ファイル名の*には日付と時刻が入ります

生成されたマイグレーションファイルに対して以下の変更を行います。

生成された項目変更点
:password
  • 削除
:name
  • null: false 追加
  • ユニークインデックスの作成

最終的に以下のようになります。

# /priv/repo/migrations/*_create_users.ex
defmodule Chat.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string, null: false      add :password_hash, :string

      timestamps()
    end
    create unique_index(:users, [:name])
  end
end

マイグレーションの実行

必要な作業が完了したので、以下のコマンドによりマイグレーションを行います。

$ mix ecto.migrate

ユーザー登録機能の実装

生成されたいくつかのファイルに実際の機能を追加していきます。

Userモジュールの変更

user.ex に対して、さらにユーザー登録用関数を追加します。
その関数では、以下の事を行っています。

項目変更点
追加処理
  • もともとの changeset の呼び出し
  • ハッシュ値の計算
:password
  • 必須指定
  • 文字数制限

上記変更を以下のように記述します。

# /lib/chat/accounts/user.ex
def registration_changeset(user, params) do
  user
  |> changeset(params)
  |> cast(params, [:password])
  |> validate_required([:password])
  |> validate_length(:password, min: 3, max: 100)
  |> put_pass_hash()
end

def put_pass_hash(changeset) do
  case changeset do
    %Ecto.Changeset{valid?: true, changes: %{password: pass}} ->
      put_change(changeset, :password_hash, Pbkdf2.hash_pwd_salt(pass))
    _ ->
      changeset
  end
end

Accountsモジュールの変更

コントローラーから呼び出すための関数を追加します。
それぞれ create_user/1change_user/1 のユーザー登録版です。

# /lib/chat/accounts.ex
def register_user(attrs \\ %{}) do
  %User{}
  |> User.registration_changeset(attrs)
  |> Repo.insert()
end

def change_registration(%User{} = user, params) do
  User.registration_changeset(user, params)
end

テスト用データの作成

ユーザーが登録できるようになったので、開発の便宜上ダミーデータをDBに登録しておきます。

ダミーデータの定義

seeds.exs にダミーデータを定義し、register_user/1 を呼び出してユーザー登録を行います。

# /priv/repo/seeds.exs
alias LvChat.Accounts

%{name: "freddie", password: "mercury"} |> Accounts.register_user
%{name: "brian", password: "may"}       |> Accounts.register_user
%{name: "john", password: "deacon"}     |> Accounts.register_user
%{name: "roger", password: "taylor"}    |> Accounts.register_user

因みに、arg |> funcfunc( arg ) とも書けます。

DBへ反映

以下のコマンドを実行して、データを追加します。

$ mix run priv/repo/seeds.exs

これで4人のメンバーが揃いました。

ログイン処理

ユーザー登録ページを作成する前に、そこで必要となるログイン処理の機能を作成します。 lib/lv_chat_web/controllers/auth.ex というファイルを作成し、そこにログイン処理関連の実装を行っていきます。

# /lib/lv_chat_web/controllers/auth.ex
defmodule LvChatWeb.Auth do
  import Plug.Conn

  def login(conn, user) do
    conn
    |> put_current_user(user)
    |> put_session(:user_id, user.id)
    |> configure_session(renew: true)
  end

  def logout(conn) do
    configure_session(conn, drop: true)
  end

  defp put_current_user(conn, user) do
    conn
    |> assign(:current_user, user)
  end

end

ユーザー登録用ページ

以下の手順でユーザー登録機能を作っていきます。

  1. RouterへUser操作用APIの追加
  2. UserControllerの作成
  3. Viewの作成
  4. Templateの作成

RouterへUser操作用APIの追加

とりあえず登録のところのみ作成します。

# /lib/lv_chat_web/router.ex
scope "/", LvChatWeb do
  pipe_through [:browser]
  ...
  resources "/users", UserController, only: [:new, :create]  ...
end

UserControllerの作成

/lib/lv_chat_web/controllers/user_controller.ex というファイルを作成し、Routerで記述したnew/2create/2 を作ります。
それぞれの関数は以下のタイミングで呼ばれます。

関数urlmethodタイミング
new/users/newGET新規登録ページの取得時
create/users/POSTsubmitボタン押下時
# /lib/lv_chat_web/controllers/user_controller.ex
use LvChatWeb, :controller

alias LvChat.Accounts
alias LvChat.Accounts.User

def new(conn, _params)do
  changeset = Accounts.change_registration(%User{}, %{})
  render(conn, "new.html", changeset: changeset)
end

def create(conn, %{"user" => user_params}) do
  case Accounts.register_user(user_params) do
    {:ok, user} ->      conn
      |> LvChatWeb.Auth.login(user)
      |> put_flash(:info, "#{user.name} が作成されました!")
      |> redirect(to: Routes.page_path(conn, :index))
    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

create/2 の結果により、以下の2パターンへリダイレクトされます。

register_userの戻り処理
成功ログイン処理後所定のページにリダイレクト
失敗もう一度登録ページへ

Viewの作成

lib/lv_chat_web/views/user_view.ex というファイルを作成しそこに実装します。
といっても現時点ではほとんど記述がありません。

# /lib/lv_chat_web/views/user_view.ex
defmodule LvChatWeb.UserView do
  @moduledoc false
  use LvChatWeb, :view
end

Templateの作成

/lib/lv_chat_web/templates/user というディレクトリを作成し、 そこに new.html.eex というファイルを作成します。

# /lib/lv_chat_web/templates/user/new.html.eex
<h1>New User</h1>

<%= form_for @changeset, Routes.user_path(@conn, :create), fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops something went wrong! Please check the errors below.</p>
    </div>
  <% end %>
  <div>
    <%= text_input f, :name, placeholder: "Name" %>
    <%= error_tag f, :name %>
  </div>
  <div>
    <%= password_input f, :password, placeholder: "Password" %>
    <%= error_tag f, :password %>
  </div>
  <%= submit "Create User" %>
<% end %>

これでユーザー登録ページの作成が完了しました。

動作の確認

以下のコマンドによりサーバーを起動します。

$ mix phx.server

起動したら以下ブラウザで以下へアクセスし、動作を確認します。

http://localhost:4000/users/new

おわりに

以上で、ユーザー登録機能まで作成できました。
次回は認証機能の作成を行います。