Phoenix1.3のUser AccountsとSession

　Phoenix1.3にはCookieを使ったSession機能が最初から提供されています。ですからPhoenixでマルチユーザのアプリを作るにはSessionを使うのが基本でしょう。前に[「Phoenix1.3+Guardian1.0でJWT - Qiita」](https://qiita.com/sand/items/393827194e687efafd4f)という記事を書きましたが、順番が逆でした。以下、簡単にマルチユーザシステムのためにSession機能を使って、ユーザ登録やログイン画面を作ってみたいと思います。


#１．キャプチャー画面

　これから行う設定作業のイメージを明確にするために、出来上がった画面のキャプチャーを貼っておきます。


 １－１．User登録画面
パス = /users/new
User登録画面です。(**yamada** userを登録します)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/21daf648-e24c-a346-b055-ad4ed005085b.png)



１－２．ログイン画面（未ログイン）
パス = /login
まだログインしていない状態でログイン画面を表示しています。(**yamada** userでログインします)
サーバ側でログインに成功するとput_sessionでログインuserを記憶します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/18e999eb-b7b4-606a-d514-cd522fa6d22b.png)


１－３．ログイン画面（ログイン済み）
パス = /login
ログイン画面を表示する前に、サーバ側でget_sessionで記憶したuserを呼び出し、画面に表示します。(**yamada** userが表示されます)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/8f952166-0db4-f3ee-4630-a6fc5d3df236.png)



#２．プロジェクト作成

　まずプロジェクトを作成します。

```
mix phx.new multi_users
cd multi_users
mix ecto.create
```

　password hashingライブラリのComeoninをインストールします。前のバージョンでは不要だったと思いますが、Comeonin 4 からはbcrypt_elixirを別途インストールする必要があります。
[riverrun/comeonin - github]
(https://github.com/riverrun/comeonin/wiki)


```elixir:mix.exs
#
  defp deps do
    [
      {:phoenix, "~> 1.3.0"},
      {:phoenix_pubsub, "~> 1.0"},
      {:phoenix_ecto, "~> 3.2"},
      {:postgrex, ">= 0.0.0"},
      {:phoenix_html, "~> 2.10"},
      {:phoenix_live_reload, "~> 1.0", only: :dev},
      {:gettext, "~> 0.11"},
      {:cowboy, "~> 1.0"},
      {:comeonin, "~> 4.0.3"},      # 追加
      {:bcrypt_elixir, "~> 1.0.4"}  # 追加
    ]
#
```

　インストールします。

```
mix do deps.get, compile
```



#３．Sessionの初期設定

　Phoenixでsessionがどのように初期設定されているかを確認します。確認だけです。

　endpoint.exにはcookieを使うように設定されています。

```elixir:lib/multi_users_web/endpoint.ex
#
  # The session will be stored in the cookie and signed,
  # this means its contents can be read but not tampered with.
  # Set :encryption_salt if you would also like to encrypt it.
  plug Plug.Session,
    store: :cookie,
    key: "_multi_users_key",
    signing_salt: "JdJp4tZU"
#
```

　routerをみると必ずfetch_sessionを通り、常にsessionが復元されていることが確認できます。

```elixir:lib/multi_users_web/router.ex
defmodule MultiUsersWeb.Router do
  use MultiUsersWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session      # これ
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end
#
```



#４．User Accounts(User)

　次にUserアカウントに必要なリソースを生成します。phx.gen.html Generatorで自動生成します。今回の作業には不要なものも生成されますが無視しておきます。どのようなファイルが生成されるかは以下の過去記事に説明してあります。
[「Phoenix1.3の基本的な仕組み - Qiita」](https://qiita.com/sand/items/f42dc35c47589eab687e)


```
mix phx.gen.html Accounts User users username:string email:string encrypted_password:string
```

routerに /users パスを追加します。

```elixir:lib/multi_users_web/router.ex
#
  scope "/", MultiUsersWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/users", UserController   # 追加
  end
#
end
```


　User Schemaを修正します。passwordとpassword_confirmationを追加します。それらは入力フォームで入力される必要がありますが、テーブルに反映されないので、virtual: true を指定します。passwordとpassword_confirmationはハッシュ化されてencrypted_passwordとなり、encrypted_passwordがテーブルに保存されることになります。

```elixir:lib/multi_users/accounts/user.ex
defmodule MultiUsers.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset


  schema "users" do
    field :username, :string
    field :email, :string
    field :encrypted_password, :string

    field :password, :string, virtual: true               # 追加
    field :password_confirmation, :string, virtual: true  # 追加

    timestamps()
  end


  def changeset(%User{}=user, attrs) do
    user
    ### 入力にはpasswordとpassword_confirmationが必要です。
    |> cast(attrs, [:username, :email, :password, :password_confirmation])
    ### password==password_confirmationをチェックを行います。
    |> validate_confirmation(:password, message: "does not match password!")
    ### passwordをハッシュ化してencrypted_passwordとしてchangesetに保存します。
    |> encrypt_password()
    ### changesetに必要な項目がそろっていることを確認します。
    |> validate_required([:username, :email, :encrypted_password])
  end

  ### changeからpasswordをgetし、ハッシュ化しencrypted_passwordにputする。
  def encrypt_password(changeset) do
    with password when not is_nil(password) <- get_change(changeset, :password) do
      ### 入力されたpasswordをハッシュ化して保存
      put_change(changeset, :encrypted_password, Comeonin.Bcrypt.hashpwsalt(password))
    else
      _ -> changeset
    end
  end
end
```

　ここでvalidate_confirmationはEcto.Changesetの関数で、xxxxxとxxxxx_confirmationが等しいことをチェックする関数です。xxxxx_confirmationはconfirmationパラメータと呼ばれます。
https://hexdocs.pm/ecto/Ecto.Changeset.html




　form.html.eexを修正して、encrypted_passwordの入力欄を削除して、代わりにpasswordとpassword_confirmationを入力するようにします。


```html:lib/multi_users_web/templates/user/form.html.eex
<%= form_for @changeset, @action, 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 class="form-group">
    <%= label f, :username, class: "control-label" %>
    <%= text_input f, :username, class: "form-control" %>
    <%= error_tag f, :username %>
  </div>

  <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>

  <div class="form-group">
    <%= label f, :password, class: "control-label" %>
    <%= text_input f, :password, class: "form-control" %>
    <%= error_tag f, :password %>
  </div>

  <div class="form-group">
    <%= label f, :password_confirmation, class: "control-label" %>
    <%= text_input f, :password_confirmation, class: "form-control" %>
    <%= error_tag f, :password_confirmation %>
  </div>

  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>
```



#５． ログイン画面(Session）

　ログイン画面の機能を追加するために、SessionControllerを作成し、以下の３つのパスをrouterに追加する必要があります。

```elixir:lib/multi_users_web/router.ex
#
  scope "/", MultiUsersWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    resources "/users", UserController

    resources "/sessions", SessionController, only: [:create]   # 追加

    get "/login", SessionController, :new     # 追加
    get "/logout", SessionController, :delete # 追加
  end
#
 ```

　session_controller.exを作ります。

```elixir:lib/multi_users_web/controllers/session_controller.ex
defmodule MultiUsersWeb.SessionController do
  use MultiUsersWeb, :controller

  alias MultiUsers.Accounts

  def new(conn, _) do
    current = get_session(conn, :user)  # (1) sessionから取得
    render(conn, "new.html", current: current)
  end

  def delete(conn, _) do
    conn
    |> delete_session(:user) # (2) sessionを削除
    |> put_flash(:info, "Logged out successfully!")
    |> redirect(to: "/")
  end

  def create(conn, %{"username" => username, "password" => password}) do
    with user <- Accounts.get_user_by_username(username),
         {:ok, login_user} <- login(user, password)
    do
      conn
      |> put_flash(:info, "Logged in successfully!")
        # (3) sessionに保存
      |> put_session(:user, %{ id: login_user.id, username: login_user.username, email: login_user.email })
      |> redirect(to: "/")
    else
      {:error, _} ->
        conn
        |> put_flash(:error, "Invalid username/password!")
        |> render("new.html")
    end
  end

  def login(user, password) do
    ### 入力されたpasswordが保存されているハッシュ値に等しいかをチェック
    Comeonin.Bcrypt.check_pass(user, password)
  end
end
```

　context moduleに新関数get_user_by_usernameを追加します。

```lib/multi_users/accounts/accounts.ex
#
  def get_user_by_username(username) do
    Repo.get_by(User, username: username)
  end
#
```

　ログイン画面の定義new.html.eexです。controllerでget_sessionで取得したcurrentがnullでない場合は、すでにログイン済みと判断しusernameを表示します。

```html:lib/multi_users_web/templates/session/new.html.eex
<div>
  <%= if @current do %>
    <div class="alert alert-danger">
      <p> Already loggined ! : <%= @current.username %> </p>
    </div>
  <% end %>
  <%= form_tag session_path(@conn, :create) do %>
    <label>
      Username:<br />
      <input type="text" name="username">
    </label>
    <br />
    <label>
      Password:<br />
      <input type="password" name="password">
    </label>
    <br />
    <%= submit "Login" %>
  <% end %>
</div>
```


#６． アクセス制限


　さて現状では、ログインユーザでも非ログイン状態でも、/users パスにアクセスすると、ユーザ一覧が表示されます。これをログインユーザのみに制限したいと思います。

##６－１． Plug Module

　アクセス制限を行うためのPlug moduleを以下のように定義します。Plug moduleとして実現しておけば、いつでもどこからでも呼び出すことができます。
https://hexdocs.pm/phoenix/plug.html


```elixir:lib/multi_users_web/auth_plug.ex
defmodule MultiUsersWeb.AuthPlug do
  import Plug.Conn, only: [get_session: 2, halt: 1]
  import Phoenix.Controller, only: [put_flash: 3, redirect: 2]

  def init(opts), do: opts

  def call(conn, _opts) do
    case get_session(conn, :user) do
      nil ->
        conn
        |> put_flash(:error, "まだログインしていません。ログインする必要がありま
す。")
        |> redirect(to: "/")
        |> halt()
      _ -> conn
    end
  end
end
```

　ユーザ一覧表示時に上で定義したPlug module (AuthPlug)を呼び出すように、UserControllerに以下の一行を追加します。

```
defmodule MultiUsersWeb.UserController do
  use MultiUsersWeb, :controller

  alias MultiUsers.Accounts
  alias MultiUsers.Accounts.User
                                                                                  
  plug MultiUsersWeb.AuthPlug when action in [:index]  # これを追加

  def index(conn, _params) do
    users = Accounts.list_users()
    render(conn, "index.html", users: users)
  end
#
```

##６－２．未ログインで/usersにアクセス

　アクセス制限されて、トップページにredirectされます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/39799aa4-ce67-2e47-7acc-fd96a398c3a1.png)



##６－３．ログイン済で/usersにアクセス

　アクセス制限をパスして、ユーザ一覧が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/da858ae4-ec37-5c60-06a8-4bcf7a3bfbe6.png)




以上です。


**■ Elixir/Phoenixの基礎についてまとめた過去記事**
[Elixir Ecto チュートリアル - Qiita](https://qiita.com/sand/items/0861ad7a16f7ac612f41)
[Elixir Ecto のまとめ - Qiita](https://qiita.com/sand/items/71d0b35d74a4781f3564)
[Elixir Ecto Association - Qiita]
(https://qiita.com/sand/items/5581497972473e308f05)
[Phoenix1.3の基本的な仕組み - Qiita](https://qiita.com/sand/items/f42dc35c47589eab687e)
[Phoenixのログイン管理のためのSessionの使い方 - Qiita](https://qiita.com/sand/items/a88f1c39f3a6f7a01f99)
[Phoenix1.3のUserアカウントとSession - Qiita](https://qiita.com/sand/items/8e541f8a950ae6c63931)
[Phoenix1.3+Guardian1.0でJWT - Qiita](https://qiita.com/sand/items/393827194e687efafd4f)


**■JWTを使った認証アプリ例**
[マルチユーザ対応geolocationアプリ - Elm + Phoenix -Qiita](https://qiita.com/sand/items/4d70fd944921e213ed64)