Phoenixで作るGPS Logging System 6 APIの認証2

# はじめに
[ひとりLiveView Advent Calendar](https://qiita.com/advent-calendar/2021/liveview_alone) の6日目の記事です

この記事は[Elixir Conf US 2021](https://www.elixirconf.com/)の[発表](https://www.youtube.com/watch?v=vHNqZmLLTj4)したシステムの構築と関連技術の解説を目的とした記事です

キーボードで入力が困難なデバイスまたは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で囲って追加するカラムを記述します

```priv/repo/migrations/20211208151908_add_passcode_to_users.exs
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追加

```lib/live_logger/accounts/user.ex
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型にしています

```lib/live_logger/accounts/user.ex
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する関数を追加

```lib/live_logger/accounts.ex
defmodule LiveLogger.Accounts do
  ...
  def generate_passcode(%User{} = user) do
    user
    |> User.passcode_changeset()
    |> Repo.update()
  end
  ...
end
```
衝突しないようにpasscodeをuniqueにすればエラーが出るかもしれませんが、今回はしないので正常系のみ実装

```lib/live_logger_web/controllers/user_settings_controller.ex
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の代替にしています

```leex:lib/live_logger_web/templates/user_settings/edit.html.heex
<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に追加

```lib/live_logger_web/router.ex
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
```
動作確認をしていきます

![スクリーンショット 2021-12-09 1.53.42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/a4f90d9c-84ee-590f-0f23-bbd93a1e60cf.png)
ボタンを押してpasscodeが発行されるのを確認できました

![スクリーンショット 2021-12-09 1.58.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/f4dc8c57-a5b1-c0be-f791-e19fe97d0598.png)

# passcode認証
最後に認証部分を実装します

passcodeでuserを取得するクエリーを追加します

```lib/live_logger/accounts.ex
defmodule LiveLogger.Accounts do
  ...
  def get_user_by_passcode(passcode) when is_binary(passcode) do
    Repo.get_by(User, passcode: passcode)
  end
  ...
end
```
paramsの部分の引数のパターンマッチを変えているので、同名の関数でも処理を分けて実行することができます


```lib/live_logger_web/controllers/user_api_session_controller.ex
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
```
動作確認をしていきます

![スクリーンショット 2021-12-09 2.15.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/f9ebaf7c-6463-d7ed-5c9f-cfc25cfcd067.png)
先程発行したpasscodeで認証トークンを取得することができました

# 最後に
これでemail, passwordで認証トークンが発行できないデバイスをパスコードを使用して認証トークンを発行できるようになりました
本記事は以上になります

次はLiveViewでGoogleMapを表示する部分を実装します

# code
https://github.com/thehaigo/live_logger/tree/day6