LoginSignup
3
0

More than 1 year has passed since last update.

PhoenixLiveViewを使用したmix phx.gen_authを詳しく見てみた【ログイン編】③

Posted at

はじめに

この記事ではPhoenixのバージョン1.7.0を使用しています。

$ mix phx.new -v
Phoenix installer v1.7.0

前回の記事に引き続き、今回はアカウントのログインを見ていきます。

以下はこのシリーズの目次です。

目次

生成されたファイルの確認

前回はアカウントの認証メールを再送信するための機能を確認しました。
今回はログインをするまでの生成されたファイルを確認します。

ログイン

ログインをするためのurlは以下です。
http://localhost:4000/accounts/log_in

ログインに該当するソースコードは以下です。

auth_sample/lib/auth_sample_web/live/account_login_live.ex
defmodule AuthSampleWeb.AccountLoginLive do
  use AuthSampleWeb, :live_view

  def render(assigns) do
    ~H"""
    <div class="mx-auto max-w-sm">
      <.header class="text-center">
        Sign in to account
        <:subtitle>
          Don't have an account?
          <.link navigate={~p"/accounts/register"} class="font-semibold text-brand hover:underline">
            Sign up
          </.link>
          for an account now.
        </:subtitle>
      </.header>

      <.simple_form for={@form} id="login_form" action={~p"/accounts/log_in"} phx-update="ignore">
        <.input field={@form[:email]} type="email" label="Email" required />
        <.input field={@form[:password]} type="password" label="Password" required />

        <:actions>
          <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
          <.link href={~p"/accounts/reset_password"} class="text-sm font-semibold">
            Forgot your password?
          </.link>
        </:actions>
        <:actions>
          <.button phx-disable-with="Signing in..." class="w-full">
            Sign in <span aria-hidden="true">→</span>
          </.button>
        </:actions>
      </.simple_form>
    </div>
    """
  end

  def mount(_params, _session, socket) do
    email = live_flash(socket.assigns.flash, :email)
    form = to_form(%{email: email}, as: "account")
    {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
  end
end

まず、mountを確認します。

auth_sample/lib/auth_sample_web/live/account_login_live.ex
def mount(_params, _session, socket) do
  email = live_flash(socket.assigns.flash, :email)
  form = to_form(%{email: email}, as: "account")
  {:ok, assign(socket, form: form), temporary_assigns: [form: form]}
end

まずlive_flash/2flashにアサインされている:emailキーの値を取得します。
続いて取得したemailを使用したPhoenix.HTML.Form構造体を作成します。
そして作成したPhoenix.HTML.Form構造体をアサインしています。
mountの返り値で指定しているキーワードtemporary_assignsはレンダリングの度にその値をリセットする必要がある、アサインされているキーを指定します。値がリセットされると、明示的に割り当てられるまで再レンダリングはしません。

ソケットにformキーでアサインされている値は以下です。

%Phoenix.HTML.Form{
  source: %{email: nil},
  impl: Phoenix.HTML.FormData.Map,
  id: "account",
  name: "account",
  data: %{},
  hidden: [],
  params: %{email: nil},
  errors: [],
  options: [],
  index: nil,
  action: nil
}

次にrenderを確認します。

auth_sample/lib/auth_sample_web/live/account_login_live.ex
def render(assigns) do
  ~H"""
  <div class="mx-auto max-w-sm">
    <.header class="text-center">
      Sign in to account
      <:subtitle>
        Don't have an account?
        <.link navigate={~p"/accounts/register"} class="font-semibold text-brand hover:underline">
          Sign up
        </.link>
        for an account now.
      </:subtitle>
    </.header>

    <.simple_form for={@form} id="login_form" action={~p"/accounts/log_in"} phx-update="ignore">
      <.input field={@form[:email]} type="email" label="Email" required />
      <.input field={@form[:password]} type="password" label="Password" required />

      <:actions>
        <.input field={@form[:remember_me]} type="checkbox" label="Keep me logged in" />
        <.link href={~p"/accounts/reset_password"} class="text-sm font-semibold">
          Forgot your password?
        </.link>
      </:actions>
      <:actions>
        <.button phx-disable-with="Signing in..." class="w-full">
          Sign in <span aria-hidden="true">→</span>
        </.button>
      </:actions>
    </.simple_form>
  </div>
  """
end

ここで確認すべきはフォームの送信先は/accounts/log_inということです。今回はphx-submitが指定されていないのでAccountLoginLiveのイベントが呼ばれないということです。ではどこに送信するかというと、AccountSessionControllercreateアクションです。

ではAccountSessionControllercreateアクションを確認します。

auth_sample/lib/auth_sample_web/controllers/account_session_controller.ex
defmodule AuthSampleWeb.AccountSessionController do
  use AuthSampleWeb, :controller

  alias AuthSample.Accounts
  alias AuthSampleWeb.AccountAuth

  def create(conn, %{"_action" => "registered"} = params) do
    create(conn, params, "Account created successfully!")
  end

  def create(conn, %{"_action" => "password_updated"} = params) do
    conn
    |> put_session(:account_return_to, ~p"/accounts/settings")
    |> create(params, "Password updated successfully!")
  end

  def create(conn, params) do
    create(conn, params, "Welcome back!")
  end

  defp create(conn, %{"account" => account_params}, info) do
    %{"email" => email, "password" => password} = account_params

    if account = Accounts.get_account_by_email_and_password(email, password) do
      conn
      |> put_flash(:info, info)
      |> AccountAuth.log_in_account(account, account_params)
    else
      # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
      conn
      |> put_flash(:error, "Invalid email or password")
      |> put_flash(:email, String.slice(email, 0, 160))
      |> redirect(to: ~p"/accounts/log_in")
    end
  end

  def delete(conn, _params) do
    conn
    |> put_flash(:info, "Logged out successfully.")
    |> AccountAuth.log_out_account()
  end
end

createアクションの関数が3つありますが、今回呼ばれるのは以下です。

auth_sample/lib/auth_sample_web/controllers/account_session_controller.ex
def create(conn, params) do
  create(conn, params, "Welcome back!")
end

では、create/3関数を呼び出しているのでこちらも確認します。

auth_sample/lib/auth_sample_web/controllers/account_session_controller.ex
defp create(conn, %{"account" => account_params}, info) do
  %{"email" => email, "password" => password} = account_params

  if account = Accounts.get_account_by_email_and_password(email, password) do
    conn
    |> put_flash(:info, info)
    |> AccountAuth.log_in_account(account, account_params)
  else
    # In order to prevent user enumeration attacks, don't disclose whether the email is registered.
    conn
    |> put_flash(:error, "Invalid email or password")
    |> put_flash(:email, String.slice(email, 0, 160))
    |> redirect(to: ~p"/accounts/log_in")
  end
end

まず、Accounts.get_account_by_email_and_password/2関数にメールアドレスとパスワードを渡してアカウントを取得します。
ここで取得できなかった場合はput_flash/3でエラー時のメッセージとフォームから受け取ったメールアドレスを保持させます。その後/accounts/log_inAccountLoginLivemountを呼びます)にリダイレクトします。
ではアカウントを取得できた時はput_flash/3で引数で受け取ったメッセージを保持させます。その後、AccountAuth.log_in_account/3を呼びアカウントのログイン処理をします。

auth_sample/lib/auth_sample_web/account_auth.ex
defmodule AuthSampleWeb.AccountAuth do
# ・・・(省略)

  @max_age 60 * 60 * 24 * 60
  @remember_me_cookie "_auth_sample_web_account_remember_me"
  @remember_me_options [sign: true, max_age: @max_age, same_site: "Lax"]

# ・・・(省略)

  def log_in_account(conn, account, params \\ %{}) do
    token = Accounts.generate_account_session_token(account)
    account_return_to = get_session(conn, :account_return_to)

    conn
    |> renew_session()
    |> put_token_in_session(token)
    |> maybe_write_remember_me_cookie(token, params)
    |> redirect(to: account_return_to || signed_in_path(conn))
  end

  defp maybe_write_remember_me_cookie(conn, token, %{"remember_me" => "true"}) do
    put_resp_cookie(conn, @remember_me_cookie, token, @remember_me_options)
  end

  defp maybe_write_remember_me_cookie(conn, _token, _params) do
    conn
  end

  defp renew_session(conn) do
    conn
    |> configure_session(renew: true)
    |> clear_session()
  end

# ・・・(省略)

  defp put_token_in_session(conn, token) do
    conn
    |> put_session(:account_token, token)
    |> put_session(:live_socket_id, "accounts_sessions:#{Base.url_encode64(token)}")
  end

# ・・・(省略)

  defp signed_in_path(_conn), do: ~p"/"
end

まず、Accounts.generate_account_session_tokenでログイン用のトークンを作成します。
続いて、renew_session/1で一度セッションをクリアしています。
次に、put_token_in_session/1でセッションに:account_tokenで生成したトークン、:live_socket_idで生成したトークンをBase64でエンコードした値を保持させています。
次にmaybe_write_remeber_me_cookie/3でログイン状態を保つかをチェックします。
ログイン状態を保つ場合は、put_resp_cookie/4でCookieに値を証明します。
ログイン状態を保たない場合は何もしません。
最後にリダイレクトをします。
リダイレクト先はセッションにログイン後のurlの情報があればそのurlにリダイレクトします。情報がない場合は/にリダイレクトします。

終わり

今回もここまで読んで頂きありがとうございました。

次回はパスワードリセットの部分について見ていきます。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0