スポンサーリンク

2015年7月15日

[Elixir+Phoenix]Processing after the login

Goal

ログイン後における、リンクとレイアウトの変更を実装する。
登録と同時にログイン状態にする。
サインアウト機能を実装する。

Dev-Environment

OS: Windows8.1
Erlang: Eshell V6.4, OTP-Version 17.5
Elixir: v1.0.4
Phoenix Framework: v0.13.1
PostgreSQL: postgres (PostgreSQL) 9.4.4

Wait a minute

当初、ログアウトしない限りログイン状態を保持する内容を実施しようと考えた。
しかし、(暫定)タスクに分割してみたところ以下のようになった。
  • アカウント設定の保存
    • カラム項目を追加(マイグレーション)
    • Base64によるトークン生成(SecureRandom)
    • トークンの暗号化(SHA1)
    • before_insert機能による登録時のトークン追加
    • ログイン時にトークンの更新をする
    • トークンから現在のユーザを参照できるようにする
    • セッションハイジャックについて
つまり中々大変。
なので、上記の実装は最後の項目とする。
今回は、それ以外の未実装機能を実施する。
具体的な内容は以下の項目です。
  • レイアウトの変更
  • リンクの変更(Sign-inからSign-outへリンクと表示の変更、プロファイルページへのリンクを表示)
  • 登録と同時にサインイン
  • サインアウト

Index

  1. Change links and layout
  2. Sign-out
  3. After registration, sign-in
  4. Extra
ログイン後、リンクとレイアウトを変更しましょう!
レイアウトのヘッダーに他のページへのリンクを表示しています。
しかし、ログインしたのにLoginまたはSign-inのリンクが表示されているのは変です。
また、現在はログインユーザ自身の情報を表示するページへのリンクがありません。
ここでは、上記二つの実装を行います。
ログインしているか否かの判断はどうしましょうか?
前回、現在のログインユーザを取得する機能を実装したのを覚えていますか?
def current_user(conn) do
  conn.assigns[:current_user]
end
ログインしていればユーザを返す。していなければnilが返ってきます。
Elixirでは、falseとnilは真偽値で偽と判断されます。
ここまで言えば分かりますね?
そうです!
EExテンプレートでif文を利用して処理を分ければいいんです。
つまりこんな形に記述したいわけです。
<%= if current_user(@conn) do %>
  ログインしている時の処理...
<% else %>
  ログインしていない時の処理...
<% end %>
ファイル: web/templates/layout/header.html.eex
<header class="navbar navbar-inverse">
  <div class="navbar-inner">
    <div class="container">
      <a class="logo" href="<%= page_path(@conn, :index) %>"></a>
      <ul class="nav nav-pills pull-right">
        <li><%= link "Home", to: static_pages_path(@conn, :home) %></li>
        <li><%= link "Help", to: static_pages_path(@conn, :help) %></li>
        <li><%= link "About", to: static_pages_path(@conn, :about) %></li>
        <li><%= link "Contact", to: static_pages_path(@conn, :contact) %></li>
      <%= if current_user(@conn) do %>
        <li><%= link "Sign out", to: session_path(@conn, :delete), method: :delete %></li>
        <li><%= link "Profile", to: user_path(@conn, :show, current_user(@conn).id) %><li>
      <% else %>
        <li><%= link "Sign-in", to: session_path(@conn, :new) %></li>
      <% end %>
        <li><a href="http://www.phoenixframework.org/docs">Phoenix Get Started</a></li>
      </ul>
    </div> <!-- container -->
  </div> <!-- navbar-inner -->
</header>
<h2>
  <p class="alert alert-info" role="alert" style="text-align: center;"><%= get_flash(@conn, :info) %></p>
  <p class="alert alert-danger" role="alert" style="text-align: center;"><%= get_flash(@conn, :error) %></p>
</h2>
Sign-outリンクが浮いてデザインが崩れてる・・・
修正は後に行います。
今は早々にログアウトを実装してしまいましょう。

2. Sign-out

ログアウト機能を実装します。
当初セッションのルーティングを作成する際に、
deleteアクションをログアウトに利用することを想定していましたね。
セッションコントローラのdeleteアクションにログアウトを実装します。
ファイル: web/controllers/session_controller.ex
def delete(conn, _params) do
  conn
  |> put_flash(:info, "Logout now! See you again!!")
  |> delete_session(:user_id)
  |> redirect(to: static_pages_path(conn, :home))
end
特に難しい部分はありませんね。
それぞれ行っていることは・・・
  • ログアウトをしたメッセージの表示
  • セッションのデータを削除
  • リダイレクトで/homeのページを表示
上記を行っているだけです。

3. After registration, sign-in

今回の記事で最後の機能、ユーザ登録後のログイン処理を実装します。
ファイル: web/controllers/user_controller.ex
createアクションを以下のように編集して下さい。
def create(conn, %{"user" => user_params}) do
  changeset = SampleApp.User.changeset(%SampleApp.User{}, user_params)

  if changeset.valid? do
    Repo.insert(changeset)

    user = Ecto.Changeset.get_field(changeset, :email)
        |> SampleApp.User.find_user_from_email

    conn
    |> put_flash(:info, "User registration is success!!")
    |> put_session(:user_id, user.id)
    |> redirect(to: static_pages_path(conn, :home))
  else
    render(conn, "new.html", changeset: changeset)
  end
end
Description:
少々冗長な処理をしていますが・・・
changesetからemailの値を取得し、
そのemailを利用して先ほど挿入したユーザのデータを取得しています。
何故こんな面倒な手順を踏んでいるのかと言いますと。
一度、DBにデータを挿入しないとidの値が生成されないからです。
簡単に順番を記述します。
  1. changeset(id値なし)
  2. DB insert(id値生成)
  3. データ取得(id値あり)
そのため先ほどのような処理になっています。

4. Extra

実装途中でこんな記述をしたのですが・・・
<%= link "Sign-out", to: session_path(@conn, :delete, current_user(@conn)), method: :delete %>
エラーが出てしまいました。
(Protocol.UndefinedError) protocol Enumerable not implemented for...
参考にした記事を見てみると・・・
<%= link "Remove", to: user_path(@conn, :delete, @user), method: :delete %>
対応コントローラのアクション
def delete(conn, %{"id" => id}) do
何が問題なのでしょうか?
デザイン崩れも含めて、ちょっと後で検証してみます。
これができないのは困る。
渡し方の問題のようです。
(コントローラ側は変わりません)
<%= link "Sign-out", to: session_path(@conn, :delete, id: current_user(@conn)), method: :delete %>
デザインの問題はCSSではない方法で解決しました。
deleteで渡していましたが、
別にパラメータを渡すこともないのでgetで通信を行うように変更しました。
(アクション名の変更はしてません)
ファイル: web/router.ex
delete "/signout", SessionController, :delete
get "/signout", SessionController, :delete

Speaking to oneself

第八章も終わりが見えてきましたね。
分割してしまえば、そんなに難しい内容でもなかった気がします。
最後に「アカウント設定の保存」を実装すれば、
第八章もまとめ記事を作成して終わりです。
アクセス制御についてなのですが、
今回の章で実施するかと思ったのですが、
第九章で実装する内容だったようです。
それでは、次の記事も待っていてくれると嬉しいです。

Bibliography

人気の投稿