4
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LiveViewとGoogleMapAPIで作る GPS Logger 2

Last updated at Posted at 2021-06-29

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

LiveViewとGoogleMapAPIで作る GPS Logger 1
LiveViewとGoogleMapAPIで作る GPS Logger 2
LiveViewとGoogleMapAPIで作る GPS Logger 3
LiveViewとGoogleMapAPIで作る GPS Logger short ver
LiveViewとGoogleMapAPIで作る GPS Logger Client watchOS App + SwiftUI

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での2種類になります
新たなアクションを作成しなくても引数で挙動を変えれるのは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
スクリーンショット 2021-06-27 13.45.43.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は1つだけ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

スクリーンショット 2021-06-27 23.58.22.png

スクリーンショット 2021-06-27 23.59.11.png

Point API Create

最後にPoint(座標)を作成するAPIを実装していきます
create1つしか関数を使用しないので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

次はリアルタイム更新と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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?