どうも、靖宗です。
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上の変数)を利用するときはハット(^
)を変数の名前の前に付けるそうです。
もうちょっとEctoと仲良くなる必要があるかもしれません。
とりあえずこの章ではパスワードは破棄してますが、もしパスワード認証を利用したい場合はGuardian
やComeonin
というトークンを発行したりハッシュ化したりする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/2
がget_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
にアクセスします。
とりあえず表示は問題無さそうです。
試しに空送信してみると、きちんとエラーが表示されます。
パスワードは無視していますが、メールアドレスはデータベースを参照しています。試しに前回作成したメールアドレスでログインできるか入力します。
メールアドレスがあっていればルートページにリダイレクトされ、「Welcome back!」のメッセージが着いていると思います。
きりが良いのでココで終わらせたいのですが、逆にキリが悪い方が学習効率は上がる(ツァイガルニク効果)筈なのであとちょっとだけ進めます。
Cross-context dependencies
ユーザー認証はもう良さそうです(ホントはトークン発行にしたりなどありますが)。
お次はCMSたる所以のページをマネージする機能を実装していきたいと思います。
想定としては承認されたユーザー(管理者的な)はページを作成したり修正したりできるといったものです。現状ではAccounts
というContextが存在してますが、CMSの機能はアカウント管理機能とは分離されていないと機能を実装したり拡張したりするときに非常にヤヤコシイことになります。ですのでContextを分けます。
ただし、タイトル的にはContext同士で依存関係をかくことにはなると思います。(100%分離するのは無理)
ということでCMSのContextを作成していくのですが、CMSの仕様を考えておきましょう。
- ページを作成したりアップデートしたりする
- ページは作成者に従属し、作成者に変更する権限がある
- 作成者の情報がページ上にあり、その役割("editior"や"writer"など)の情報もある
といったところでしょうか。
仕様やからページのリソースが必要なのは明確ですが、あとは「作成者」をどうするかです。
Accountsを拡張して役割を付与するのも手ですが、関係性がかなり複雑になるのが目に見えています。
ここではCMSのContextにAuthor
というスキーマを作成し、このAuthor
とAccounts
のUser
を関連付けるのが良さそうです。
基本的にユーザー情報と紐付けてなんかする、なんてのは山のようにあると思いますので、この辺の設計はこの章の設計を踏襲するのがよさそうです。というか、Accountsの設計はなんにでも流用できそうです。
さて、次回はmix phx.gen.html
でCMSのContextを作成して行きます。