ElixirDesktop クラサバ構成 APIサーバー構築

# はじめに
この記事は Elixirアドベントカレンダーのシリーズ４の13日目の記事です

PhoenixのAPIサーバーとElixirDesktopをクライアントとしたクラサバ構成の構築と認証について解説します
今回はAPIサーバーの構築とログインAPIを作成します

# APIサーバーの構築

```
mix phx.new blog
cd blog
mix ecto.create
```

# API関連ファイルを作成
認証周りを実装する前にAPIのエラーレスポンスを返す処理群がほしいので先にAPIを作成します

```
mix phx.gen.json Posts Post posts title:string body:string
mix ecto.migrate
```
上記のコマンドで以下のファイルが作成されます
これを使用することでAPIでエラーがあった時にエラー内容に応じでレスポンスを返してくれます
ここに認証エラーを足していきます

```diff_elixir:lib/blog_web/controllers/fallback_controller.ex
defmodule BlogWeb.FallbackController do
  @moduledoc """
  Translates controller action results into valid `Plug.Conn` responses.

  See `Phoenix.Controller.action_fallback/1` for more details.
  """
  use BlogWeb, :controller

  # This clause handles errors returned by Ecto's insert/update/delete.
  def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
    conn
    |> put_status(:unprocessable_entity)
    |> put_view(json: BlogWeb.ChangesetJSON)
    |> render(:error, changeset: changeset)
  end

  # This clause is an example of how to handle resources that cannot be found.
  def call(conn, {:error, :not_found}) do
    conn
    |> put_status(:not_found)
    |> put_view(html: BlogWeb.ErrorHTML, json: BlogWeb.ErrorJSON)
    |> render(:"404")
  end

+ def call(conn, {:error, :unauthorized}) do
+   conn
+   |> put_status(:unauthorized)
+   |> put_view(html: BlogWeb.ErrorHTML, json: BlogWeb.ErrorJSON)
+   |> render(:"401")
+ end
end

```


# 通常の認証機能を追加
以下のコマンドで認証周りのファイルを作成します
```
mix phx.gen.auth Accounts User users
mix deps.get
mix ecto.migrate
```

# ログインAPI実装
ログインAPIを実装します

生成されたトークンはBinaryなので、レスポンスで返す用に Base64でエンコードします
また検証用にstatus APIも作成します

先程作成した fallbackを以下のように指定すると、関数の返り値が {:error,xx}となるものがfallbackで定義した関数パターンマッチで対応するレスポンスを返します

`action_fallback BlogWeb.FallbackController`


```lib/blog_web/controllers/user_api_session_controller.ex
defmodule BlogWeb.UserController do
  use BlogWeb, :controller

  alias Blog.Accounts

  action_fallback BlogWeb.FallbackController

  def register(conn, %{"user" => user_params}) do
    case Accounts.register_user(user_params) do
      {:ok, user} ->
        render(conn, :token,%{token: gen_token(user)})

      {:error, changeset} ->
        {:error, changeset}
    end
  end

  def login(conn, params) do
    if user = Accounts.get_user_by_email_and_password(params["email"], params["password"]) do
      render(conn, %{token: gen_token(user)})
    else
      {:error, :unauthorized}
    end
  end

  def login(_, _) do
    {:error, :unauthorized}
  end

  def status(conn, _params) do
    user = conn.assigns.current_user
    render(conn, :status, user)
  end

  defp gen_token(user) do
    user
    |> Accounts.generate_user_session_token()
    |> Base.encode64()
  end
end
```

# レスポンステンプレートファイルを作成
1.7以前はDSLでしたが1.7以降は単純なMapをJSONにエンコードします


```lib/blog_web/controllers/user_json.ex
defmodule BlogWeb.UserJSON do
  def token(%{token: token}) do
    %{token: token}
  end

  def status(user) do
    %{id: user.id, email: email}
  end
end

```

# header Authorizationをチェック

213行目のrequire_authenticated_userの下辺りにpipelineを追加
authorization headerは Bearer tokenとなっているのでBearerのあとのスペースを忘れないこと

tokenはbase64でDBのtokenはBinaryなのでデコードが必要です
ユーザーが取得したらconnにアサインして各APIから参照できるようにしておきます

```elixir:lib/blog_web/user_auth.ex:L214
  def require_verify_header(conn, _opts) do
    with ["Bearer " <> token] <- get_req_header(conn, "authorization"),
         user when is_struct(user) <-
           token |> Base.decode64!() |> Accounts.get_user_by_session_token() do
      assign(conn, :current_user, user)
    else
      _error ->
        conn
        |> put_status(:unauthorized)
        |> put_view(BlogWeb.ErrorJSON)
        |> render(:"401")
        |> halt()
    end
  end

```

# 認証あり、認証なしのスコープを作成

```diff_elixir:lib/blog_web/router.ex
-  # Other scopes may use custom stacks.
-  # scope "/api", BlogWeb do
-  #   pipe_through :api
-  # end
+  scope "/api", BlogWeb do
+    pipe_through :api
+
+    post "/register", UserController, :register
+    post "/login", UserController, :login
+  end

+  scope "/api", BlogWeb do
+    pipe_through [:api, :require_verify_header]
+
+    get "/status", UserController, :status
+    resources "/posts", PostController, except: [:new, :edit]
+  end
```

# 動作確認

register

![スクリーンショット 2023-12-14 14.35.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/9cc76665-77ea-4329-a89f-f3bbbbc007f1.png)

login

![スクリーンショット 2023-12-14 14.35.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/c0b7b0f5-7919-0f73-ef7e-d05fd34fc4bc.png)

status
valid token 
![スクリーンショット 2023-12-14 14.35.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/53d8cc8d-183d-ea0e-e5e9-2c9166f0175c.png)

invalid token 

![スクリーンショット 2023-12-14 14.44.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/de146de2-99b0-b377-2bf4-70e97b63e48c.png)

# 最後に
今回はクラサバ構成のアプリを作るための下準備として認証機能をもたせたAPIサーバーをPhoenixで構築しました
fallback controllerを使うとエラーの共通処理が簡単にかけて便利です

次はクライアント側のElixirDesktopの方を実装していきます
本記事は以上になりますありがとうごうざいました