はじめに
ひとりLiveView Advent Calendar の6日目の記事です
この記事はElixir Conf US 2021の発表したシステムの構築と関連技術の解説を目的とした記事です
キーボードで入力が困難なデバイスまたはemail,passwordを保存したくないデバイス(Apple Watch, raspberry pi等)で認証トークン発行するために再発行が容易なパスコードを使用できるようにします。
今回は以下の3つを実装します
- Userにpasscodeカラム追加
- passcode発行画面
- passcode認証
Userにpasscodeカラム追加
カラムを追加する場合は以下のコマンドで空のmigrationファイルを作成します
mix ecto.gen.migration add_passcode_to_users
カラムを追加する場合はalter table(table名) doで囲って追加するカラムを記述します
defmodule LiveLogger.Repo.Migrations.AddPasscodeToUsers do
use Ecto.Migration
def change do
alter table("users") do
add :passcode, :string
end
end
end
DBに反映させます
mix ecto.migrate
field追加
defmodule LiveLogger.Accounts.User do
use Ecto.Schema
import Ecto.Changeset
schema "users" do
field :email, :string
field :password, :string, virtual: true, redact: true
field :hashed_password, :string, redact: true
field :confirmed_at, :naive_datetime
field :passcode, :string # 追加
timestamps()
end
...
end
これでpasscodeカラムが追加できました
passcode発行画面
次にパスコード発行ボタンをusers/settingsに追加します
passcodeを生成するだけのchangesetを作成します
15桁の数値を保存しようとすると桁数が多すぎると怒られるため、string型にしています
defmodule LiveLogger.Accounts.User do
...
def passcode_changeset(user) do
user
|> cast(%{}, [:passcode])
|> put_change(
:passcode,
Enum.random(100_000_000_000_000..999_999_999_999_999)
|> to_string()
)
end
...
end
Accountsでpasscodeをupdateする関数を追加
defmodule LiveLogger.Accounts do
...
def generate_passcode(%User{} = user) do
user
|> User.passcode_changeset()
|> Repo.update()
end
...
end
衝突しないようにpasscodeをuniqueにすればエラーが出るかもしれませんが、今回はしないので正常系のみ実装
defmodule LiveLoggerWeb.UserSettingsController do
...
def generate_passcode(conn, _params) do
case Accounts.generate_passcode(conn.assigns.current_user) do
{:ok, _ } ->
conn
|> put_flash(:info, "Passcode generate successfully.")
|> redirect(to: Routes.user_settings_path(conn, :edit))
end
end
...
end
テンプレートがheexになってform_forが使えないので、linkをmethod postにしてformの代替にしています
<h1>Settings</h1>
<h3>Generate Passcode</h3>
<%= link "Generate Passcode",
to: Routes.user_settings_path(@conn, :generate_passcode),
method: :post,
class: "button is-info"
%>
<%= if @conn.assigns.current_user.passcode do %>
<p><%= "passcode:" <> @conn.assigns.current_user.passcode %></p>
<% end %>
...
routerに追加
defmodule LiveLoggerWeb.Router do
...
scope "/", LiveLoggerWeb do
pipe_through [:browser, :require_authenticated_user]
get "/users/settings", UserSettingsController, :edit
put "/users/settings", UserSettingsController, :update
get "/users/settings/confirm_email/:token", UserSettingsController, :confirm_email
post "/users/settings/generate_passcode", UserSettingsController, :generate_passcode # 追加
live "/maps", MapLive.Index, :index
live "/maps/new", MapLive.Index, :new
live "/maps/:id/edit", MapLive.Index, :edit
live "/maps/:id", MapLive.Show, :show
live "/maps/:id/show/edit", MapLive.Show, :edit
end
...
end
動作確認をしていきます
ボタンを押してpasscodeが発行されるのを確認できました
passcode認証
最後に認証部分を実装します
passcodeでuserを取得するクエリーを追加します
defmodule LiveLogger.Accounts do
...
def get_user_by_passcode(passcode) when is_binary(passcode) do
Repo.get_by(User, passcode: passcode)
end
...
end
paramsの部分の引数のパターンマッチを変えているので、同名の関数でも処理を分けて実行することができます
defmodule LiveLoggerWeb.UserApiSessionController do
...
def create(conn, %{"email" => email, "password" => password}) do
with user when is_struct(user) <- Accounts.get_user_by_email_and_password(email, password),
token <- user |> Accounts.generate_user_session_token() |> Base.encode64() do
render(conn, "token.json", token: token)
else
_error -> {:error, :unauthorized}
end
end
# 以下追加
def create(conn, %{"passcode" => passcode}) do
with user when is_struct(user) <- Accounts.get_user_by_passcode(passcode),
token <- user |> Accounts.generate_user_session_token() |> Base.encode64() do
render(conn, "token.json", token: token)
else
_error -> {:error, :unauthorized}
end
end
...
end
動作確認をしていきます
先程発行したpasscodeで認証トークンを取得することができました
最後に
これでemail, passwordで認証トークンが発行できないデバイスをパスコードを使用して認証トークンを発行できるようになりました
本記事は以上になります
次はLiveViewでGoogleMapを表示する部分を実装します
code