LoginSignup
10
1

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

Last updated at Posted at 2023-12-14

はじめに

この記事は Elixirアドベントカレンダーのシリーズ4の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でエラーがあった時にエラー内容に応じでレスポンスを返してくれます
ここに認証エラーを足していきます

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から参照できるようにしておきます

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

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

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

login

スクリーンショット 2023-12-14 14.35.28.png

status
valid token
スクリーンショット 2023-12-14 14.35.50.png

invalid token

スクリーンショット 2023-12-14 14.44.53.png

最後に

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

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

10
1
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
10
1