8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ElixirAdvent Calendar 2024

Day 7

ElixirDesktopで作るスマホアプリ Part 3 セッション管理周りの改修

Last updated at Posted at 2024-12-02

はじめに

この記事はElixirアドベントカレンダー2024のシリーズ2、7日目の記事です

本記事では、前回実装した認証機能に関して、セッション周りの改修を行っていきます

今回の作業ブランチを作成します

git checkout -b feature/session_for_desktop

ログイン有無でのリダイレクト処理

アプリを起動した時に、ログインしていたら設定ページ、していなかったらログインページにリダイレクトするようにします。

lib/trarecord_web/controllers/page_controller.ex
defmodule TrarecordWeb.PageController do
  use TrarecordWeb, :controller
+ alias Trarecord.Accounts.User

  def home(conn, _params) do
    # The home page is often custom made,
    # so skip the default app layout.
    render(conn, :home, layout: false)
  end

+ def index(conn, _params) do
+   redirect_to(conn, conn.assigns.current_user)
+ end

+ defp redirect_to(conn, %User{}) do
+   redirect(conn, to: ~p"/users/settings")
+ end

+ defp redirect_to(conn, nil) do
+   redirect(conn, to: ~p"/users/log_in")
+ end
end

トップページで開くパスを変更します。

lib/trarecord_web/router.ex:L20
  scope "/", TrarecordWeb do
    pipe_through :browser

-   get "/", PageController, :home
+   get "/", PageController, :index
  end

セッション保持

ElixirDesktopはセッション情報をetsというインメモリDBで管理するのでアプリを終了するとセッション情報は消えてしまい、起動毎にログインを要求されるので、セッション情報を保持するようにします。

トークンのパスは頻繁に使うので関数化しておきます

lib/trarecord.ex:L4
  @env Mix.env()
  def config_dir() do
    case(@env) do
      :test ->
        "tmp"

      _ ->
        {path, _} =
          Code.eval_string("Path.join([Desktop.OS.home(), dir, app])",
            dir: ".config",
            app: "trarecord"
          )

        path
    end
  end

+ def token_path() do
+   config_dir() <> "/token"
+ end

トークンが生成された時に端末にもテキストデータとして保存するようにします

lib/trarecord_web/user_auth.ex:L28
  def log_in_user(conn, user, params \\ %{}) do
    token = Accounts.generate_user_session_token(user)
+   File.write!(Trarecord.token_path(), Base.encode64(token))

    user_return_to = get_session(conn, :user_return_to)

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

セッション復活

起動時にセッションを復活できるようにします。

user_auth.exでページを表示する前に実行されている、fetch_current_user/2関数内のensure_user_token/1関数をセッションにトークンがなかった場合はCookieを見に行っているのを、トークンファイルを見に行くように変更します。

lib/trarecord_web/user_auth.ex:L98
  defp ensure_user_token(conn) do
    if token = get_session(conn, :user_token) do
      {token, conn}
    else
-     conn = fetch_cookies(conn, signed: [@remember_me_cookie])
-
-     if token = conn.cookies[@remember_me_cookie] do
-       {token, put_token_in_session(conn, token)}
-     else
-       {nil, conn}
+     case File.read(Trarecord.token_path()) do
+       {:ok, token} ->
+         {token, put_token_in_session(conn, Base.decode64!(token))}
+ 
+       {:error, :enoent} ->
+         {nil, conn}
      end      
    end
  end

これで起動時にセッションを復活できるようになりました。

ログアウト時にトークンを削除する

ログアウトをしてもトークンファイルが残り続けて、起動の度にログインされるので、ログアウト時にトークンファイルを削除するようにします。

lib/trarecord_web/user_auth.ex:L77
  def log_out_user(conn) do
    user_token = get_session(conn, :user_token)
    user_token && Accounts.delete_user_session_token(user_token)
+   File.rm(Trarecord.token_path())

    if live_socket_id = get_session(conn, :live_socket_id) do
      TrarecordWeb.Endpoint.broadcast(live_socket_id, "disconnect", %{})
    end

    conn
    |> renew_session()
    |> delete_resp_cookie(@remember_me_cookie)
    |> redirect(to: ~p"/")
  end

実装が完了したのでコミットしておきます

git add .
git commit -m 'update session logic'

動作確認

以下の動作確認を行います

  • 新規作成
  • 再起動時のログイン保持
  • ログアウト
  • ログアウト後の再起動でログインされない

b03fd6b91e1a559dd3d91f44004a5830.gif

テストの修正

セッション周りの改修でテストが通らなくなっているので修正していきます

トークンファイルのパス

テスト実行前にtmpディレクトリを掘る処理追加

lib/trarecord/application.ex:L9
  def start(_type, _args) do
+   File.mkdir_p!(Trarecord.config_dir())
    ...
  end

page_controller_test

未ログイン時はログインページにリダイレクトされるようになったので変更

test/trarecord_web/controllers/page_controller_test.exs
defmodule TrarecordWeb.PageControllerTest do
  use TrarecordWeb.ConnCase

  test "GET /", %{conn: conn} do
    conn = get(conn, ~p"/")
-   assert html_response(conn, 200) =~ "Peace of mind from prototype to production"    
+   assert html_response(conn, 302) =~ "/users/log_in"
  end
end

user_session_controller_test

ログイン状態で/にアクセスすると設定ページにリダイレクトされるようになったので変更

test/trarecord_web/controllers/user_session_controller_test.exs
    test "logs the user in", %{conn: conn, user: user} do
      conn =
        post(conn, ~p"/users/log_in", %{
          "user" => %{"email" => user.email, "password" => valid_user_password()}
        })

      assert get_session(conn, :user_token)
      assert redirected_to(conn) == ~p"/"

      # Now do a logged in request and assert on the menu
      conn = get(conn, ~p"/")
-     response = html_response(conn, 200)
-     assert response =~ user.email
-     assert response =~ ~p"/users/settings"
-     assert response =~ ~p"/users/log_out"      
+     response = html_response(conn, 302)
+     assert response =~ "/users/settings"
    end

user_confirmation_instructions_live_test

トークンファイルが残っていてテストがコケる場合があるので初期化処理追加

test/trarecord_web/live/user_confirmation_instructions_live_test.exs:L9
  setup do
+   File.rm(Trarecord.token_path())
    %{user: insert(:user)}
  end

user_registration_live_test.exs

ログイン状態で/にアクセスすると設定ページにリダイレクトされるようになったので変更

test/trarecord_web/live/user_registration_live_test.exs:L39
   test "creates account and logs the user in", %{conn: conn} do
      {:ok, lv, _html} = live(conn, ~p"/users/register")

      email = unique_user_email()
      form = form(lv, "#registration_form", user: params_for(:user_form_data, email: email))
      render_submit(form)
      conn = follow_trigger_action(form, conn)

      assert redirected_to(conn) == ~p"/"

      # Now do a logged in request and assert on the menu
      conn = get(conn, "/")
-     response = html_response(conn, 200)
-     assert response =~ email
-     assert response =~ "Settings"
-     assert response =~ "Log out"
+     response = html_response(conn, 302)
+     assert response =~ "/users/settings"
    end

user_reset_password_live_test

トークンファイルが残っていてテストがコケる場合があるので初期化処理追加

test/trarecord_web/live/user_reset_password_live_test.exs
  setup do
+   File.rm(Trarecord.token_path())
    user = insert(:user)

    token =
      extract_user_token(fn url ->
        Accounts.deliver_user_reset_password_instructions(user, url)
      end)

    %{token: token, user: user}
  end

user_auth_test

トークンファイルが残っていてテストがコケる場合があるので初期化処理追加

test/trarecord_web/user_auth_test.exs:L10
  setup %{conn: conn} do
    conn =
      conn
      |> Map.replace!(:secret_key_base, TrarecordWeb.Endpoint.config(:secret_key_base))
      |> init_test_session(%{})

+   File.rm(Trarecord.token_path())
    %{user: insert(:user), conn: conn}
  end

クッキー周りを使用したテストを削除します

test/trarecord_web/user_auth_test.exs:L86
  describe "fetch_current_user/2" do
    test "authenticates user from session", %{conn: conn, user: user} do
      user_token = Accounts.generate_user_session_token(user)
      conn = conn |> put_session(:user_token, user_token) |> UserAuth.fetch_current_user([])
      assert conn.assigns.current_user.id == user.id
    end

-   test "authenticates user from cookies", %{conn: conn, user: user} do
-     logged_in_conn =
-       conn |> fetch_cookies() |> UserAuth.log_in_user(user, %{"remember_me" => "true"})
-
-     user_token = logged_in_conn.cookies[@remember_me_cookie]
-     %{value: signed_token} = logged_in_conn.resp_cookies[@remember_me_cookie]
-
-     conn =
-       conn
-       |> put_req_cookie(@remember_me_cookie, signed_token)
-       |> UserAuth.fetch_current_user([])
-
-     assert conn.assigns.current_user.id == user.id
-     assert get_session(conn, :user_token) == user_token
-
-     assert get_session(conn, :live_socket_id) ==
-              "users_sessions:#{Base.url_encode64(user_token)}"
-   end
-
-   test "does not authenticate if data is missing", %{conn: conn, user: user} do
-     _ = Accounts.generate_user_session_token(user)
-     conn = UserAuth.fetch_current_user(conn, [])
-     refute get_session(conn, :user_token)
-     refute conn.assigns.current_user
-   end
- end

テストの修正が完了したのでコミットします

git add .
git commit -m 'fix test'

CI通過 & Merge

pushしてPRを作成します

git push origin feature/session_for_desktop

スクリーンショット 2024-12-02 23.49.21.png

CIが無事通ったのでマージして完了です

最後に

セッション周りを改修して、アプリに近い挙動をするようになりました
次は起動時に開くWelcomeページとオンボーディングを実装していきます

本記事は以上になりますありがとうございました

8
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
8
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?