本記事では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 :live_map, LiveMap.Guardian,
issuer: "live_map",
secret_key: "add generated secret by mix guardian.gen.secret"
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
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
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を追加
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]では認証が必要なものを記述していきます
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で上書きしてワンタイムパスワードとして扱うと良さそうです
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
defmodule LiveMap.Accounts do
...
def generate_iot_token(%User{} = user) do
user
|> User.gen_token_changeset()
|> Repo.update()
end
end
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
<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のいいところですね
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
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
defmodule LiveMapWeb.Api.UserView do
use LiveMapWeb, :view
def render("jwt.json", %{token: token}) do
%{token: token}
end
end
defmodule LiveMapWeb.Router do
scope "/api", LiveMapWeb do
pipe_through :api
post "/sign_in", Api.UserController, :sign_in # add this
end
end
tokenの生成と生成したトークンでJWT認証できることが確認できました
Map API Index Show Create
次はMapの作成、一覧、詳細のAPIを実装します
alias LievMap.Loggers.MapまでしてしまうとElixirのMapとかぶってしまうので、Loggers.Mapとしています
ModelはMapじゃなくてRouteあたりにしておけばよかったかもしれません・・・
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の中身を返しています
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
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
Point API Create
最後にPoint(座標)を作成するAPIを実装していきます
create1つしか関数を使用しないのでLoggers contextに記述します
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の値を追加します
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
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
次はリアルタイム更新と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