はじめに
この記事はElixirアドベントカレンダー2024のシリーズ2、7日目の記事です
本記事では、前回実装した認証機能に関して、セッション周りの改修を行っていきます
今回の作業ブランチを作成します
git checkout -b feature/session_for_desktop
ログイン有無でのリダイレクト処理
アプリを起動した時に、ログインしていたら設定ページ、していなかったらログインページにリダイレクトするようにします。
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
トップページで開くパスを変更します。
scope "/", TrarecordWeb do
pipe_through :browser
- get "/", PageController, :home
+ get "/", PageController, :index
end
セッション保持
ElixirDesktop
はセッション情報をets
というインメモリDBで管理するのでアプリを終了するとセッション情報は消えてしまい、起動毎にログインを要求されるので、セッション情報を保持するようにします。
トークンのパスは頻繁に使うので関数化しておきます
@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
トークンが生成された時に端末にもテキストデータとして保存するようにします
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を見に行っているのを、トークンファイルを見に行くように変更します。
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
これで起動時にセッションを復活できるようになりました。
ログアウト時にトークンを削除する
ログアウトをしてもトークンファイルが残り続けて、起動の度にログインされるので、ログアウト時にトークンファイルを削除するようにします。
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'
動作確認
以下の動作確認を行います
- 新規作成
- 再起動時のログイン保持
- ログアウト
- ログアウト後の再起動でログインされない
テストの修正
セッション周りの改修でテストが通らなくなっているので修正していきます
トークンファイルのパス
テスト実行前にtmpディレクトリを掘る処理追加
def start(_type, _args) do
+ File.mkdir_p!(Trarecord.config_dir())
...
end
page_controller_test
未ログイン時はログインページにリダイレクトされるようになったので変更
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 "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
トークンファイルが残っていてテストがコケる場合があるので初期化処理追加
setup do
+ File.rm(Trarecord.token_path())
%{user: insert(:user)}
end
user_registration_live_test.exs
ログイン状態で/
にアクセスすると設定ページにリダイレクトされるようになったので変更
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
トークンファイルが残っていてテストがコケる場合があるので初期化処理追加
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
トークンファイルが残っていてテストがコケる場合があるので初期化処理追加
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
クッキー周りを使用したテストを削除します
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
CIが無事通ったのでマージして完了です
最後に
セッション周りを改修して、アプリに近い挙動をするようになりました
次は起動時に開くWelcomeページとオンボーディングを実装していきます
本記事は以上になりますありがとうございました