Quantcast
Channel: fukuoka.ex Elixir/Phoenix Advent Calendarの記事 - Qiita
Viewing all articles
Browse latest Browse all 25

【Phoenix1.4】(前編)パスワード認証&リレーションあり「ブログチュートリアル」

$
0
0

(この記事は、fukuoka.ex Elixir/Phoenix Advent Calendar 2018の2日目です)
昨日はzacky1972さんの「ZEAM開発ログ2018年総集編その1: Elixir 研究構想についてふりかえる(前編)」です!こちらもぜひぜひ!

学生エンジニアKoyoです!
紅葉の季節ですね~

今までCakePHP, RailsからGoやVueまで色んな言語(フレームワーク)を触ってきました
フルスタックのフレームワークには大体「ブログチュートリアル」がありますよね

ElixirのフレームワークであるPhoenixは、公式ドキュメントにはいくつかチュートリアルはあるのですが、日本語の情報が少ないです><
(結構苦労して笑)先日パスワード認証&リレーションありの「ブログチュートリアル」を自分でやってみたので、その過程をシェアしたいと思います

|> 後編はこちら
【Phoenix1.4】(後編)パスワード認証&リレーションあり「ブログチュートリアル」

|> 対象読者

|> Elixir, Phoenixをインストールして少し動かしてみたけど、もうすこしステップアップしたい方

特にfukuoka.ex代表のpiacereさんのコラムは入門に最適なので、まだやってない方は是非全6回やってみてください!

|> Excelから関数型言語マスター1回目:行の「並べ替え」と「絞り込み」

また、今回はつい先日リリースされたPhoenix 1.4でやってみようかなと思うので、以下のコラムも見てみるといいと思います!(1.3の場合はRoutes.user_path(...)という記述のRoutes.をすべて除けば動作すると思われます)

|> Phoenix 1.4正式版① インストール編

|> ブログチュートリアル

|> 筆者の実行環境

  • Windows10
  • WSL(Windows Subsystem for Linux)
  • Erlang/OTP 21
  • Elixir 1.7.2 (compiled with Erlang/OTP 20)
  • Phoenix1.4

動作未確認ですが他の環境でも(Macとか)でもおそらく大丈夫だと思います!

|> プロジェクトを作成

mix phx.new ex_blog でex_blogという名前でプロジェクトを作成しましょう
※今回はwebpack使わないからといって--no-webpackつけたりすると、deleteメソッドが動かない罠にハマるので気を付けましょう(筆者はハマった笑

以下のプロンプトが出たらYで依存関係をインストールしましょう
image.png

以下の指示に従ってDBを生成しましょう

cd ex_blog
mix ecto.create

image.png

iex -S mix phx.serverとして以下が表示されればOKです!
image.png

ここで上手くいかない場合はPhoenixのインストールやDBの設定が上手くいっていない場合が考えられるので、以下を参考に見直してみましょう
Excelから関数型言語マスター3回目:WebにDBデータ表示【PostgreSQL or MySQL編】

※ちなみに筆者はPostgreSQL起動し忘れていたのでsudo service postgresql startしました笑

|> Userリソースの追加

Accountsという名前でコンテキストを生成し、その中にUserリソースを追加します
Userはname, email, passwordカラムを持ち、emailは一意である(ユニーク制約)としましょう
mix phx.gen.html Accounts User users name:string email:string:unique password:string

ちなみにRails触っていた人はコンテキストの概念がよく分からないと思いますが、モデルのロジック相当と捉えるとしっくりくるかと思います(合ってるか分かりませんが筆者はそう解釈しています)
似たようなロジックは同じコンテキスト内にまとめていくとよいでしょう

以下のようにファイルがたくさんできます
image.png

指示に従ってrouter.exresources "/users", UserControllerを追加

router.ex
defmodule ExBlogWeb.Router do
  use ExBlogWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

  scope "/", ExBlogWeb do
    pipe_through :browser

    get "/", PageController, :index
    resources "/users", UserController # ここ追加!
  end

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

これで以下のようにRESTfulなルーティングが追加されますbb
image.png

|> Articleリソースの追加

title, content属性を持つArticleリソースを作成しましょう
リレーションはこんな感じ

  • User has many Articles
  • Article belongs to user

以下のように書くとuser_idを外部キーとしてBlogコンテキストのArticleリソースを作成できます!
mix phx.gen.html Blog Article articles user_id:references:users title:string content:string

今回Articleリソースは、userリソースが属するAccountコンテキストと同じグループではないと考えられるので、新しくBlogコンテキストを作ってその中にいれてみました
例えばArticleに対するコメント機能などを作成する場合は、commentリソースはBlogコンテキストに属すると思います

resources "/articles", ArticleControllerをroute.exに追加しましょう
その後mix ecto.migrateしてみましょう
※エラーになる場合はmix ecto.resetしてみましょう

|> DB周りの設定

|> Migrationファイルをいじる

priv/repo 以下にタイムスタンプつきのcreate_users.exsファイルができていると思います

20181130....._create_users.exs
defmodule ExBlog.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string
      add :email, :string
      add :password, :string

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

DBレベルでnullを許容したくないので以下のように書き換えます

defmodule ExBlog.Repo.Migrations.CreateUsers do
  use Ecto.Migration

  def change do
    create table(:users) do
      add :name, :string, null: false # ここ追加
      add :email, :string, null: false # ここ追加
      add :password, :string, null: false # ここ追加

      timestamps()
    end

    create unique_index(:users, [:email])
  end
end

同様にarticleの方も

20181130..._create_articles.exs
defmodule ExBlog.Repo.Migrations.CreateArticles do
  use Ecto.Migration

  def change do
    create table(:articles) do
      add :title, :string
      add :content, :string
      add :user_id, references(:users, on_delete: :nothing)

      timestamps()
    end

    create index(:articles, [:user_id])
  end
end

userリソースが削除されたら関連するArticleも削除してほしいので、以下のように書き換えます

defmodule ExBlog.Repo.Migrations.CreateArticles do
  use Ecto.Migration

   def change do
    create table(:articles) do
      add :title, :string, null: false ## ここ変更
      add :content, :string
      add :user_id, references(:users, on_delete: :delete_all), null: false ## ここ変更

      timestamps()
    end

    create index(:articles, [:user_id])
  end
end

このあたりの設定は、Phoenixが依存しているEctoSQLに詳しく載っているので、カスタマイズしてみたい方は試してみてください!

最後にmix ecto.resetしてmigrationを再設定しましょう

|> Schemaファイルをいじる

自動生成されたSchemaファイルは以下のようになっていると思います

ex_blog/lib/ex_blog/accounts/user.ex
defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset


  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
  end
end

Ecto(PhoexnixのORM, Railsで言えばActionRecord)でリレーションたどれるように設定していきます

defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Blog.Article # ここ追加

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    has_many :articles, Article # ここ追加
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
  end
end

Articleスキーマも同様に

ex_blog/lib/ex_blog/blog/article.ex
defmodule ExBlog.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset


  schema "articles" do
    field :content, :string
    field :title, :string
    field :user_id, :id

    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :content])
    |> validate_required([:title, :content])
  end
end

以下のように変更します

defmodule ExBlog.Blog.Article do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Accounts.User # ここ追加

  schema "articles" do
    field :content, :string
    field :title, :string

    belongs_to :user, User # ここ追加
    timestamps()
  end

  @doc false
  def changeset(article, attrs) do
    article
    |> cast(attrs, [:title, :content])
    |> validate_required([:title, :content])
  end
end

|> ここまでで画面の確認

さて、ここまででリレーションの土台ができました
これでArticleを投稿したときにログインしたユーザのuser_idをカラムに追加するように設定したりできます

iex -S mix phx.serverしてlocalhost:4000にアクセスしてみましょう
localhost:4000/users
image.png

localhost:4000/users/new
image.png

ここでパスワード入力する際にパスワードがガッツリ表示されています
また、indexやshowにパスワードが表示されるようになっています
これはあまり好ましくないので以下を参考に修正します

Phoenixのphx.gen.htmlでpasswordフィールドを生成するときは忘れずにtemplateを変更しよう!

lib/ex_blog_web/templates/user/index.html.eex
<h1>Listing Users</h1>

<table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
-      <th>Password</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for user <- @users do %>
    <tr>
      <td><%= user.name %></td>
      <td><%= user.email %></td>
-      <td><%= user.password %></td>

      <td>
        <%= link "Show", to: Routes.user_path(@conn, :show, user) %>
        <%= link "Edit", to: Routes.user_path(@conn, :edit, user) %>
        <%= link "Delete", to: Routes.user_path(@conn, :delete, user), method: :delete, data: [confirm: "Are you sure?"] %>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New User", to: Routes.user_path(@conn, :new) %></span>

lib/ex_blog_web/templates/user/show.html.eex
<h1>Show User</h1>

<ul>

  <li>
    <strong>Name:</strong>
    <%= @user.name %>
  </li>

  <li>
    <strong>Email:</strong>
    <%= @user.email %>
  </li>

-  <li>
-    <strong>Password:</strong>
-    <%= @user.password %>
-  </li>

</ul>

<span><%= link "Edit", to: Routes.user_path(@conn, :edit, @user) %></span>
<span><%= link "Back", to: Routes.user_path(@conn, :index) %></span>

lib/ex_blog_web/templates/user/form.html.eex
<%= form_for @changeset, @action, fn f -> %>
  <%= if @changeset.action do %>
    <div class="alert alert-danger">
      <p>Oops, something went wrong! Please check the errors below.</p>
    </div>
  <% end %>

  <%= label f, :name %>
  <%= text_input f, :name %>
  <%= error_tag f, :name %>

  <%= label f, :email %>
  <%= text_input f, :email %>
  <%= error_tag f, :email %>

  <%= label f, :password %>
-  <%= text_input f, :password %>
+  <%= password_input f, :password %>
  <%= error_tag f, :password %>

  <div>
    <%= submit "Save" %>
  </div>
<% end %>

これで以下のように安全にパスワード入力ができます!
image.png

次は実際にリレーションを反映するためにログイン機能を実装しましょう

|> Guardianでパスワード認証の実装

パスワード認証などを行うにはセッションを扱う必要があります
以下の分かりやすい記事のようにPhoenixのSession機能を使って実装する方法もありますが今回はGuardianというライブラリを使ってみます

Guardianについては公式ドキュメントのほかに以下が参考になりました

|> Guardianと必要なライブラリの導入

以下をmix.exsに書いて、mix deps.getします

mix.exs
      {:guardian, "~> 1.1"},
      {:comeonin, "~> 4.1"},
      {:bcrypt_elixir, "~> 1.1"}

次にmix guardian.gen.secretを実行して出てきた文字列を使って以下のように設定します

config/config.exs
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
#
# This configuration file is loaded before any dependency and
# is restricted to this project.

# General application configuration
use Mix.Config

config :ex_blog,
  ecto_repos: [ExBlog.Repo]

# Configures the endpoint
config :ex_blog, ExBlogWeb.Endpoint,
...

+ config :ex_blog, ExBlog.Accounts.Guardian,
+   issuer: "ex_blog",
+   secret_key: # mix guardian.gen.secret の結果を貼り付け

# Use Jason for JSON parsing in Phoenix
config :phoenix, :json_library, Jason

# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.eznv()}.exs"

|>ちょっと余談

ちなみに学習用は構いませんが本番環境でsecret_keyを直書きするのは良くないので、.bashrcexport EX_BLOG_GUARDIAN_KEY=.....などで環境変数をセットしておいて、以下のように書く方法もあります

secret_key: "${EX_BLOG_GUARDIAN_KEY}"

このときSystem.get_envを使う方法と${}を使う方法2通りあるんですが、System.get_envだと(後述するGigalixirなどで必要になる)Distilleryなどでコンパイルするときに、コンパイル時の実行時の環境変数で固定されてしまうので後から環境変数を注入できず不便です
${}だと現在の環境変数読んでくれるみたいなのでこっちがいいかもです

https://twitter.com/KoyoMiyamura/status/1056952578745892864
https://twitter.com/KoyoMiyamura/status/1056948597957156864

|> Guardianを使ったログイン機能の実装

|> ルーティング

まず最終的なルーティングを以下のようにします
2つのパイプライン:authと:ensure_authを定義します
:authはセッションを取得したりする機能を使うために必要で、これはログインしているユーザを判別するために必要なので全ページに適用します
:ensure_authはログイン済みかどうかを判定してくれて、userのcreate/new(ユーザ登録)以外のuserリソースとarticleリソースはログイン済みのみ閲覧可能にしたいので以下のように適用させます
またログイン/ログアウト用にSessionコントローラを新しく定義します

lib/ex_blog_web/router.ex
defmodule ExBlogWeb.Router do
  use ExBlogWeb, :router

  pipeline :browser do
    plug :accepts, ["html"]
    plug :fetch_session
    plug :fetch_flash
    plug :protect_from_forgery
    plug :put_secure_browser_headers
  end

  pipeline :api do
    plug :accepts, ["json"]
  end

+ pipeline :auth do
+   plug ExBlog.Accounts.Pipeline
+ end
+ pipeline :ensure_auth do
+   plug Guardian.Plug.EnsureAuthenticated
+ end

  scope "/", ExBlogWeb do
-    pipe_through :browser
+    pipe_through [:browser, :auth]

+     get "/", PageController, :index
+     get "/signin", UserController, :new
+     post "/signin", UserController, :create
+     get "/login", SessionController, :new
+     post "/login", SessionController, :create
+     delete "/logout", SessionController, :delete
+   end
-    resources "/users", UserController
+   scope "/", ExBlogWeb do
+     pipe_through [:browser, :auth, :ensure_auth]
+     resources "/users", UserController, except: [:new, :create]
    resources "/articles", ArticleController
  end

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

|> パイプラインの実装

先ほど紹介した参考記事やGuardianの公式を参考に実装していきます

lib/ex_blog/accounts/guardian.ex
defmodule ExBlog.Accounts.Guardian do
  use Guardian, otp_app: :ex_blog
  alias ExBlog.Accounts
  def subject_for_token(user, _claims) do
    {:ok, to_string(user.id)}
  end
  def resource_from_claims(claims) do
    user = claims["sub"]
    |> Accounts.get_user!
    {:ok, user}
    # If something goes wrong here return {:error, reason}
  end
end
lib/ex_blog/accounts/pipeline.ex
defmodule ExBlog.Accounts.Pipeline do
  use Guardian.Plug.Pipeline,
    otp_app: :ex_blog,
    error_handler: ExBlog.Accounts.ErrorHandler,
    module: ExBlog.Accounts.Guardian
  # If there is a session token, validate it
  plug Guardian.Plug.VerifySession, claims: %{"typ" => "access"}
  # If there is an authorization header, validate it
  plug Guardian.Plug.VerifyHeader, claims: %{"typ" => "access"}
  # Load the user if either of the verifications worked
  plug Guardian.Plug.LoadResource, allow_blank: true
end
lib/ex_blog/accounts/error_handler.ex
defmodule ExBlog.Accounts.ErrorHandler do
  import Plug.Conn
  def auth_error(conn, {type, _reason}, _opts) do
    body = to_string(type)
    conn
    |> put_resp_content_type("text/plain")
    |> send_resp(401, body)
  end
end

|> Accountsリソースにログイン機能の実装

コントローラで使いたいので、コンテキストにログイン機能を追加します
authenticate_userはログインemailとパスワードを受け取ってログイン可能かを判定します

check_passwordの定義がすごくElixirらしいですね!
第一引数がnilの場合とそうでない場合で処理を分けるためにパターンマッチを使って分けています
これにより一つの関数は小さく保ちつつ複雑な処理を記述することを可能にします

lib/ex_blog/accounts/accounts.ex
  alias ExBlog.Accounts.User
+  alias Comeonin.Bcrypt
...

+  def authenticate_user(email, plain_text_password) do
+    query = from u in User, where: u.email == ^email
+    Repo.one(query)
+    |> check_password(plain_text_password)
+  end
+  defp check_password(nil, _), do: {:error, "Incorrect username or password"}
+  defp check_password(user, plain_text_password) do
+    case Bcrypt.checkpw(plain_text_password, user.password) do
+      true -> {:ok, user}
+      false -> {:error, "Incorrect username or password"}
+    end
+  end
+ end   
end

|> userスキーマに保存時にパスワードのハッシュ化と簡単なバリデーションの実装

userスキーマにパスワード保存時のハッシュ化処理を追加します
同名関数を定義して、パターンマッチするかどうかで処理を分岐させるやりかたはいかにもElixirらしいスタイルですね!

lib/ex_blog/accounts/user.ex
defmodule ExBlog.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset
  alias ExBlog.Blog.Article
+  alias Comeonin.Bcrypt

  schema "users" do
    field :email, :string
    field :name, :string
    field :password, :string

    has_many :articles, Article # ここ追加
    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email, :password])
    |> validate_required([:name, :email, :password])
    |> unique_constraint(:email)
+    |> validate_format(:email, ~r/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
+    |> validate_length(:password, min: 5)
+    |> put_pass_hash()
  end
+
+  defp put_pass_hash(%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset) do
+    change(changeset, password: Bcrypt.hashpwsalt(password))
+  end
+  defp put_pass_hash(changeset), do: changeset
end

layoutのViewファイルを変更して、eex中でcurrent_userという関数で現在ログイン中のユーザを取得できるようにします

lib/ex_blog_web/views/layout_view.ex
defmodule ExBlogWeb.LayoutView do
  use ExBlogWeb, :view
+  alias ExBlog.Accounts.Guardian
+  def current_user(conn) do
+    Guardian.Plug.current_resource(conn)
+  end
end

|> 細かい部分の変更

user登録完了後にuser/showではなくpage/indexに飛ぶようにします(好みなので必須では無いです)

lib/ex_blog_web/controllers/user_controller.ex
  def create(conn, %{"user" => user_params}) do
    case Accounts.create_user(user_params) do
-      {:ok, user} ->
+      {:ok, _user} ->
        conn
        |> put_flash(:info, "User created successfully.")
-        |> redirect(to: Routes.user_path(conn, :show, user))
+        |> redirect(to: Routes.page_path(conn, :index))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

|> sessionコントローラ/ビューの実装

lib/ex_blog_web/controllers/session_controller.ex
defmodule ExBlogWeb.SessionController do
  use ExBlogWeb, :controller
  alias ExBlog.Accounts
  alias ExBlog.Accounts.User
  alias ExBlog.Accounts.Guardian
   def new(conn, _params) do
    changeset = Accounts.change_user(%User{})
    render(conn, "new.html", changeset: changeset)
  end
   def create(conn, %{"user" => %{"email" => email, "password" => password}}) do
    Accounts.authenticate_user(email, password)
    |> login_reply(conn)
  end
   defp login_reply({:error, error}, conn) do
    conn
    |> put_flash(:error, error)
    |> redirect(to: Routes.session_path(conn, :new))
  end
  defp login_reply({:ok, user}, conn) do
    conn
    |> put_flash(:info, "Welcome back!")
    |> Guardian.Plug.sign_in(user)
    |> redirect(to: "/")
  end
   def delete(conn, _) do
    conn
    |> Guardian.Plug.sign_out()
    |> put_flash(:info, "Logout successfully.")
    |> redirect(to: Routes.page_path(conn, :index))
  end
end

Contorllerには必ず対応するViewファイルが必要なので作成しましょう(ないとエラーになる)

Railsやったことある人はViewってerbテンプレートの置き場所だと感じるのですが、Phoenixの場合はコントローラから渡ってきた変数をビュー用に整形したり、表示用のhelper関数を定義する場所として使われるようです
Railsでいうhelperの役割と、コントローラ/テンプレート間の仲立ちをする層というイメージみたいです

ちなみにJSON APIを作る場合はここでレスポンスデータの定義をします

lib/ex_blog_web/views/session_view.ex
defmodule ExBlogWeb.SessionView do
  use ExBlogWeb, :view
end

ログイン画面の実装

lib/ex_blog_web/templates/session/new.html.eex
<h2>Login Page</h2>
<%= form_for @changeset, Routes.session_path(@conn, :create), fn f -> %>
   <div class="form-group">
    <%= label f, :email, class: "control-label" %>
    <%= text_input f, :email, class: "form-control" %>
    <%= error_tag f, :email %>
  </div>
  <div class="form-group">
    <%= label f, :password, class: "control-label" %>
    <%= password_input f, :password, class: "form-control" %>
    <%= error_tag f, :password %>
  </div>
  <div class="form-group">
    <%= submit "Submit", class: "btn btn-primary" %>
  </div>
<% end %>

ヘッダー部分にLogoutリンクを追加し、userがログインしている場合のみ表示するようにしてみましょう

lib/ex_blog_web/templates/layout/app.html.eex
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>ExBlog · Phoenix Framework</title>
    <link rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
  </head>
  <body>
    <header>
      <section class="container">
        <nav role="navigation">
          <ul>
-            <li><a href="https://hexdocs.pm/phoenix/overview.html">Get Started</a></li>
+            <%= if current_user(@conn) do %>
+              <li><%= link "Logout", to: Routes.session_path(@conn, :delete), method: :delete %></li>
+            <% end %>
          </ul>
        </nav>
        <a href="http://phoenixframework.org/" class="phx-logo">
          <img src="<%= Routes.static_path(@conn, "/images/phoenix.png") %>" alt="Phoenix Framework Logo"/>
        </a>
      </section>
    </header>
    <main role="main" class="container">
      <p class="alert alert-info" role="alert"><%= get_flash(@conn, :info) %></p>
      <p class="alert alert-danger" role="alert"><%= get_flash(@conn, :error) %></p>
      <%= render @view_module, @view_template, assigns %>
    </main>
    <script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
  </body>
</html>

|> ここまでで画面表示

さて、iex -S mix phx.serverしてlocalhost:4000を開いてみましょう
ログインしていない場合に、ログインが必要なリソースを表示させると以下のようにエラーページが表示されます
image.png

localhost:4000/signinにアクセスし、適当なユーザを作成してみましょう
image.png

image.png

今回はユーザ登録完了後にログインを自動で行う実装ではないので、ログインを行う必要があります
(UX的にはユーザ登録後にログインした方がよさげ)

image.png

ログインするとヘッダー部分にLogoutのリンクができていますね!
image.png

localhost:4000/usersにアクセスするとログイン前は見ることが出来なかったページが見れるようになっています
image.png

localhost:4000/articles
image.png

ヘッダのLogoutリンクをクリックすると
image.png

フラッシュが表示されます
もう一度localhost:4000/userslocalhost:4000/articlesにアクセスしてみましょう
image.png

ちゃんとアクセスできなくなっています!すごい!笑

|> ※続きはElixir Advent Calendarで!

若干睡眠不足で執筆していたので、今見返したらめちゃ長いですねこの記事・・・!
後半は分割・修正してElixir Advent Calandar の方にしようと思います

|> 後編はこちら
【Phoenix1.4】(後編)パスワード認証&リレーションあり「ブログチュートリアル」

|> 次回

次回はsym_numさんのはじめての並行処理 -Queensパズルを題材にして-です!こちらも是非どうぞ~


Viewing all articles
Browse latest Browse all 25

Trending Articles