LiveViewとGoogleMapAPIで作る GPS Logger 2

本記事ではPhoenixのLiveViewでGoogleMapAPIを使用してGPS Loggerを作成していきます
今回は認証周りとMapとPoint(座標)を作成するAPIを実装していきます

[LiveViewとGoogleMapAPIで作る GPS Logger 1](https://qiita.com/the_haigo/items/c1eaf7fde3f88eadb4be)
[LiveViewとGoogleMapAPIで作る GPS Logger 2](https://qiita.com/the_haigo/items/addc4e0e5c4ad398ca5c)
[LiveViewとGoogleMapAPIで作る GPS Logger 3](https://qiita.com/the_haigo/items/bae2203ffdb7f6963f72)
[LiveViewとGoogleMapAPIで作る GPS Logger short ver](https://qiita.com/the_haigo/items/9cb8d6446492c50d690c)
[LiveViewとGoogleMapAPIで作る GPS Logger Client watchOS App + SwiftUI](https://qiita.com/the_haigo/items/6a8ddf84434cfa47a129)

# APIのJWT認証
API側の認証にGuradianを使用するのでそちらをセットアップしていきます


```
mix guardian.gen.secret
```

```config/config.exs
config :live_map, LiveMap.Guardian,
  issuer: "live_map",
  secret_key: "add generated secret by mix guardian.gen.secret"

```

```lib/live_map/guardian.ex
defmodule LiveMap.Guardian do
  use Guardian, otp_app: :live_map

  def subject_for_token(user, _claims) do
    sub = to_string(user.id)
    {:ok, sub}
  end

  def resource_form_claims(claims) do
    id = claims["sub"]
    resource = LiveMap.Accounts.get_user!(id)
    {:ok, resource}
  end
end
```

```lib/live_map_web/api_auth_pipeline.ex
defmodule LiveMapWeb.ApiAuthPipeline do
  use Guardian.Plug.Pipeline, otp_app: :live_map,
    module: LiveMap.Guardian,
    error_handler: LiveMapWeb.ApiAuthErrorHandler

  plug Guardian.Plug.VerifyHeader, realm: "Bearer"
  plug Guardian.Plug.EnsureAuthenticated
  plug Guardian.Plug.LoadResource
end
```

```lib/live_map_web/api_auth_error_handler.ex
defmodule LiveMapWeb.ApiAuthErrorHandler do
  import Plug.Conn

  def auth_error(conn, { type, _reason}, _opts) do
    body = Jason.encode!(%{ error: to_string(type) })
    send_resp(conn, 401, body)
  end
end
```
認証成功時にuser_idをconnに入れるPlugを追加

```lib/live_map_web/auth_helpler.ex
defmodule LiveMapWeb.AuthHelper do
  import Guardian.Plug

  def init(opts), do: opts

  def call(conn, _opts) do
    Map.put(conn, :user_id, current_resource(conn).id)
  end
end

```
routerにpipelineを追加していきます
pipelineとは指定したスコープ内でアクションが実行される前に実行される処理群です
今回はjwt_authenticatedでGuardianでのJWT認証と認証後にconnにuser_idを入れる処理を実行するようにしています
pipe_through :apiではsign_inなど認証が必要ないもの
pipe_through [:api, :jwt_authenticated]では認証が必要なものを記述していきます

```lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
...
  alias LiveMapWeb.ApiAuthPipeline
  alias LiveMapWeb.AuthHelper

  pipeline :jwt_authenticated do
    plug ApiAuthPipeline
    plug AuthHelper
  end

  # Other scopes may use custom stacks.
  scope "/api", LiveMapWeb do
    pipe_through :api
  end

  scope "/api", LiveMapWeb do
    pipe_through [:api, :jwt_authenticated]
  end
...
end
```
# 認証トークン生成
IoTデバイスのようなフルキーボードでemailとpasswordを入力するのが難しいデバイス用に16桁の数字の認証キー生成します
実運用する際は認証後にJWTを発行後キーをnilで上書きしてワンタイムパスワードとして扱うと良さそうです

```lib/live_map/accounts/user.ex
defmodule LiveMap.Accounts.User do
...
  @doc """
  Generate Auth Token for IoT Device 
  """
  def gen_token_changeset(user) do
    user
    |> cast(%{}, [:token])
    |> put_change(:token, Enum.random(100000000000000..999999999999999) |> Integer.to_string())
  end
...
end
```

```lib/live_map/accounts.ex
defmodule LiveMap.Accounts do
  ...
  def generate_iot_token(%User{} = user) do
    user
    |> User.gen_token_changeset()
    |> Repo.update()
  end
end
```

```lib/live_map_web/controllers/user_settings_controller.ex
defmodule LiveMapWeb.UserSettingsController do
...
  def gen_token(conn, _params) do
    case Accounts.generate_iot_token(conn.assigns.current_user) do
      {:ok, _} ->
        conn
        |> put_flash(:info, "Token generate successfully.")
        |> redirect(to: Routes.user_settings_path(conn, :edit))

      {:error, _} ->
        conn
        |> put_flash(:error, "Token generate failed!")
        |> redirect(to: Routes.user_settings_path(conn, :edit))
    end
  end
...
end
```

```[lib/live_map_web/templates/user_settings/edit.html.eex].html
<h1>Settings</h1>

<h3>Generate Token for IoT</h3>
<%= form_for @conn, Routes.user_settings_path(@conn, :gen_token), fn f -> %>
  <%= submit "Generate Token" %>
<% end %>

<%= if @conn.assigns.current_user.token do %>
  <p><%= "token:" <> @conn.assigns.current_user.token %></p>
<% end %>
...
```
認証はemailとpasswordとtokenでの２種類になります
新たなアクションを作成しなくても引数で挙動を変えれるのはElixirのいいところですね

```lib/live_map/accounts.ex
defmodule LiveMap.Accounts do
...
  alias LiveMap.Guardian
  def token_sign_in(email, password) do
    user = get_user_by_email_and_password(email, password)
    if user do
      Guardian.encode_and_sign(user)
    else
      { :error, :unauthorized }
    end
  end

  def one_time_signin(token) do
    if user = Repo.get_by(User, token: token) do
      Guardian.encode_and_sign(user)
    else
      {:error, :unauthorized}
    end
  end
...
end
```

```lib/live_map_web/controllers/api/user_contoller.ex
defmodule LiveMapWeb.Api.UserController do
  use LiveMapWeb, :controller

  alias LiveMap.Accounts

  def sign_in(conn, %{"email" => email, "password" => password }) do
    with { :ok, token, _claims } <- Accounts.token_sign_in(email, password) do
      conn |> render("jwt.json", token: token)
    end
  end

  def sign_in(conn, %{ "token" => token}) do
    with {:ok, jwt, _claims } <- Accounts.one_time_signin(token) do
      conn |> render("jwt.json", token: jwt)
    end
  end
end
```

```lib/live_map_web/views/api/user_view.ex
defmodule LiveMapWeb.Api.UserView do
  use LiveMapWeb, :view

  def render("jwt.json", %{token: token}) do
    %{token: token}
  end
end

```

```lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
  scope "/api", LiveMapWeb do
    pipe_through :api

    post "/sign_in", Api.UserController, :sign_in  # add this
  end
end
```
tokenの生成と生成したトークンでJWT認証できることが確認できました

![スクリーンショット 2021-06-27 13.44.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/a63b6b25-5323-af38-ba39-03170b82e782.png)
![スクリーンショット 2021-06-27 13.45.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/3e3cad5c-b2c9-ff7a-1fd0-75ba7dc63489.png)

# Map API Index Show Create
次はMapの作成、一覧、詳細のAPIを実装します
alias LievMap.Loggers.MapまでしてしまうとElixirのMapとかぶってしまうので、Loggers.Mapとしています
ModelはMapじゃなくてRouteあたりにしておけばよかったかもしれません・・・

```lib/live_map_web/controllers/api/map_controller.ex
defmodule LiveMapWeb.Api.MapController do
  use LiveMapWeb, :controller

  alias LiveMap.Loggers

  def index(conn, _params) do
    render(conn, "index.json", maps: Loggers.list_maps())
  end

  def create(conn, map_params \\ %{}) do
    with {:ok, %Loggers.Map{} = map } <- Loggers.create_map(
    Map.put(map_params, "user_id", conn.user_id)
    ) do
      conn |> render("show.json", map: map)
    end
  end

  def show(conn, %{ "id" => id }) do
    render(conn, "show.json", map: Loggers.get_map!(id))
  end
end
```
Viewではクライアント側に返す値を記述しています
render_manyで複数,render_oneは１つだけmap.jsonの中身を返しています

```lib/live_map_web/views/api/map_view.ex
defmodule LiveMapWeb.Api.MapView do
  use LiveMapWeb, :view
  alias LiveMapWeb.Api.MapView

  def render("index.json", %{maps: maps}) do
    %{data: render_many(maps, MapView, "map.json")}
  end

  def render("show.json", %{map: map}) do
    render_one(map, MapView, "map.json")
  end

  def render("map.json", %{map: map}) do
    %{
      id: map.id,
      name: map.name,
      description: map.description
    }
  end
end
```

```lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
  scope "/api", LiveMapWeb do
    pipe_through [:api, :jwt_authenticated]

    resources "maps", Api.MapController, only: [:index, :show, :create] # add this
  end
end
```
![スクリーンショット 2021-06-27 23.59.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/f2066931-2073-6f07-121a-f9ef06dfe974.png)


![スクリーンショット 2021-06-27 23.58.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/178e2ec8-dac5-2d28-0871-b5ce8c186c69.png)

![スクリーンショット 2021-06-27 23.59.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/22a30d0e-8256-399b-2b0a-7d2d3d9297cb.png)

# Point API Create
最後にPoint(座標)を作成するAPIを実装していきます
create１つしか関数を使用しないのでLoggers contextに記述します

```lib/live_map/loggers.ex
defmodule LiveMap.Loggers do
  alias LiveMap.Loggers.Point
...  
  def create_point(attrs \\ %{}) do
    %Point{}
    |> Point.changeset(attrs)
    |> Repo.insert()
  end
...
end
```
認証時にconnにuser_idを入れているので、作成時にMap.putでuser_idの値を追加します

```lib/live_map_web/controllers/api/point_controller.ex
defmodule LiveMapWeb.Api.PointController do
  use LiveMapWeb, :controller

  alias LiveMap.Loggers
  alias LiveMap.Loggers.Point

  def create(conn, point_params) do
    with {:ok, %Point{}} <- Loggers.create_point(
      Map.put(point_params, "user_id", conn.user_id)
    ) do
      send_resp(conn, 200, "ok")
    end
  end
end

```

```lib/live_map_web/router.ex
defmodule LiveMapWeb.Router do
...
  scope "/api", LiveMapWeb do
    pipe_through [:api, :jwt_authenticated]

    resources "maps", Api.MapController, only: [:index, :show, :create]
    resources "points", Api.PointController, only: [:create] # add this
  end
...
end
```
問題なく座標が保存されていることが確認できました
![スクリーンショット 2021-06-29 22.52.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/64198/d39626f7-ed7d-ed26-8b7d-f513dd73bd54.png)

次はリアルタイム更新とLiveViewによるGoogleMapのマーカーの追加を実装していきます

# 今回のコード
https://github.com/thehaigo/live_map/tree/qiita
https://github.com/thehaigo/live_map/tree/with_authentication

# 参考ページ
https://github.com/ueberauth/guardian#installation
https://elixirschool.com/ja/lessons/libraries/guardian/
https://qiita.com/the_haigo/items/e01e671ca117ed2b8648