技術メモ

プログラミングとか電子工作とか

Phoenix入門 (第13章 Contexts その3)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
Contextsの3回目です。なんか3回じゃ終わらないかも・・・(´゚'ω゚`)

前回はContext内でリレーションを作りました。
今回はContextに機能を追加していくところになるかと。

Adding Account functions

とりあえず前回まででユーザー情報(IDとパスワード)とメール情報のCRUDはできました。(メールはユーザー情報経由ですが。)
ですが、基本的な機能だけで認証機能とかはまだありません。
お次は認証情報(今回はメールアドレス)を使ってセッションを作成したりするお話っぽいです。

想定としては、メールアドレスとパスワードを入力してユーザー情報を取得する、という流れです。
なので

> user = Accounts.authenticate_by_email_password(email, password)

という形でユーザー情報が得られ、この情報をセッションに渡せば良さそうです。(ユーザー情報のやりとりなんかはChannelの章あたりが参考になるかもしれません。)

Contextに関数を追加

この関数をContextのlib/hello/accounts.exに追記します。

...
  def authenticate_by_email_password(email, _password) do
    query =
      from u in User,
        inner_join: c in assoc(u, :credential),
        where: c.email == ^email
  
    case Repo.one(query) do
      %User{} = user -> {:ok, user}
      nil -> {:error, :unauthorized}
    end
  end
end

一番最後に追記しました。

ここで、^emailとなっていて、「ピンオペレータ?」と思ったんですが右辺だしなんやねんこれ、と思っていたんですがどうやらEctoのクエリとして外部の変数(クエリ内で出てこない、Elixir上の変数)を利用するときはハット(^)を変数の名前の前に付けるそうです。

hexdocs.pm

もうちょっとEctoと仲良くなる必要があるかもしれません。

とりあえずこの章ではパスワードは破棄してますが、もしパスワード認証を利用したい場合はGuardianComeoninというトークンを発行したりハッシュ化したりするElixirのライブラリがあるそうなので、それらを使うのが良いそうです。
今回はメールアドレスがあるかないかだけ判定します。

Webレイヤーを実装

それでは先ほどの関数を利用して認証ページのようなものを作成していきます。
なにはともあれまずはコントローラです。lib/hello_web/controllers/session_controller.exを作成します。




大枠は普通のコントローラと同じです。createでセッションを生成してPlug.ConnにユーザーIDを渡しています。(本来であればトークンの方が望ましい?)
configure_sessionあたりはセッションID固定化攻撃(Session Fixation)対策だそうです。

次に、ルーターを編集します。lib/hello_web/router.exです。

...
  scope "/", HelloWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController
    resources "/sessions", SessionController, only: [:new, :create, :delete], singleton: true
  end
...

resources "/sessions"の行を追記しました。
コントローラには:new, :create, :deleteの3つしか実装していないので、resourcesの3つを利用するよう:onlyオプションを利用しています。
また、singletonオプションはURIにリソースのIDを作成しない設定だそうです。今回はセッションの作成用のURIなので/sessions/12345のようなIDは不要です。(セッション情報は接続情報に載っていくので)

あとは認証確認をPlugにしてルーター内で認証機能が必要なときに接続情報のパイプラインに乗せれるようにしておきます。同様にrouter.exの最後にでも書いときます。

...
  defp authenticate_user(conn, _) do
    case get_session(conn, :user_id) do
      nil ->
        conn
        |> Phoenix.Controller.put_flash(:error, "Login required")
        |> Phoenix.Controller.redirect(to: "/")
        |> halt()
      user_id ->
        assign(conn, :current_user, Hello.Accounts.get_user!(user_id))
    end
  end
end

use HelloWeb, :routerがあるのでrouter.exではimport Plug.Connが既になされています。なのでPlug.Conn.get_session/2get_session/2として利用できます。
この処理で:user_idがセッション情報内にあるかチェックしています。無ければput_flashでメッセージを送信してルートページへリダイレクトの後、Plug処理を停止という流れです。
あればセッション情報内に:current_userというキーでユーザーidを登録します。

このプラグは今は使いませんが、後々使うそうです。

ウェブレイヤー実装の最後にセッション生成用のビューを作成します。
lib/hello_web/views/session_view.exを作成して記載していきます。

defmodule HelloWeb.SessionView do
    use HelloWeb, :view
end

特に処理は不要なのでいたってシンプル。
テンプレートを作成します。lib/hello_web/templates/session/new.html.eex

<h1>Sign in</h1>

<%= form_for @conn, Routes.session_path(@conn, :create), [method: :post, as: :user], fn f -> %>
  <div class="form-group">
    <%= text_input f, :email, placeholder: "Email" %>
  </div>

  <div class="form-group">
    <%= password_input f, :password, placeholder: "Password" %>
  </div>

  <div class="form-group">
    <%= submit "Login" %>
  </div>
<% end %>

<%= form_for @conn, Routes.session_path(@conn, :delete), [method: :delete, as: :user], fn _ -> %>
  <div class="form-group">
    <%= submit "logout" %>
  </div>
<% end %>

シンプルなフォームです。
ここまで来ると、ようやくウェブ上で確認できます。
http://localhost:4000/sessions/newにアクセスします。

f:id:ysmn_deus:20190402105043p:plain

とりあえず表示は問題無さそうです。
試しに空送信してみると、きちんとエラーが表示されます。
パスワードは無視していますが、メールアドレスはデータベースを参照しています。試しに前回作成したメールアドレスでログインできるか入力します。

f:id:ysmn_deus:20190402105258p:plain

メールアドレスがあっていればルートページにリダイレクトされ、「Welcome back!」のメッセージが着いていると思います。

きりが良いのでココで終わらせたいのですが、逆にキリが悪い方が学習効率は上がる(ツァイガルニク効果)筈なのであとちょっとだけ進めます。

Cross-context dependencies

ユーザー認証はもう良さそうです(ホントはトークン発行にしたりなどありますが)。
お次はCMSたる所以のページをマネージする機能を実装していきたいと思います。

想定としては承認されたユーザー(管理者的な)はページを作成したり修正したりできるといったものです。現状ではAccountsというContextが存在してますが、CMSの機能はアカウント管理機能とは分離されていないと機能を実装したり拡張したりするときに非常にヤヤコシイことになります。ですのでContextを分けます。
ただし、タイトル的にはContext同士で依存関係をかくことにはなると思います。(100%分離するのは無理)

ということでCMSのContextを作成していくのですが、CMSの仕様を考えておきましょう。

  1. ページを作成したりアップデートしたりする
  2. ページは作成者に従属し、作成者に変更する権限がある
  3. 作成者の情報がページ上にあり、その役割("editior"や"writer"など)の情報もある

といったところでしょうか。
仕様やからページのリソースが必要なのは明確ですが、あとは「作成者」をどうするかです。
Accountsを拡張して役割を付与するのも手ですが、関係性がかなり複雑になるのが目に見えています。
ここではCMSのContextにAuthorというスキーマを作成し、このAuthorAccountsUserを関連付けるのが良さそうです。
基本的にユーザー情報と紐付けてなんかする、なんてのは山のようにあると思いますので、この辺の設計はこの章の設計を踏襲するのがよさそうです。というか、Accountsの設計はなんにでも流用できそうです。

さて、次回はmix phx.gen.htmlCMSのContextを作成して行きます。