SpaceRocket |> Blog

Think, Learn, Share

Michael Chavez
Michael Chavez

Elixir Phoenix - Display Pow User as an Author in a Blog Post

Posted by Michael Chavez on November 19, 2019

Create a new project

mix phx.new author_example
  1. mix phx.new author_example
  2. cd author_example && mix ecto.create
  3. git init && git add --all && git commit -m "initial-commit

Adding Pow

Pow is a robust, modular, and extendable authentication and user management solution for Phoenix and Plug-based apps.

Features

  • User registration
  • Session based authorization
  • Per Endpoint/Plug configuration
  • API token authorization
  • Mnesia cache with automatic cluster healing
  • Multitenancy
  • User roles
  • Extendable
  • I18n
  • And more
def deps do
  [
    # ...
    {:pow, "~> 1.0.14"}
    # ...
  ]
end
mix deps.get
mix pow.install

There are three files you’ll need to configure first before you can use Pow.

First, append this to config/config.exs:

config :author_example, :pow,
  user: AuthorExample.Users.User,
  repo: AuthorExample.Repo

Next, add Pow.Plug.Session plug to lib/author_example_web/endpoint.ex:

lib/author_example_web/endpoint.ex

defmodule AuthorExampleWeb.Endpoint do
  use Phoenix.Endpoint, otp_app: :author_example

  # ...

  plug Plug.Session,
    store: :cookie,
    key: "dsrzYLq4ra73L6X7AfEoWa0EoFmwCsY8A0fts66FQPnj4KHvE7YE6gkTQ3M77l9E",
    signing_salt: "B+kmSoOSUGp/GxG2mvE0duO6LXVhPg1/T0dBQBxonOiZjyM3wUIOis0iHhkQBX6k"

  plug Pow.Plug.Session, otp_app: :author_example

  # ...
end

You can use mix phx.gen.secret to generate keys

Last, update lib/author_example_web/router.ex with the Pow routes:

lib/author_example_web/router.ex

defmodule AuthorExampleWeb.Router do
  use AuthorExampleWeb, :router
  use Pow.Phoenix.Router

  # ... pipelines

  pipeline :protected do
    plug Pow.Plug.RequireAuthenticated,
      error_handler: Pow.Phoenix.PlugErrorHandler
  end
  
  scope "/" do
    pipe_through :browser

    pow_routes()
  end 

  scope "/", AuthorExampleWeb do
    pipe_through [:browser, :protected]
    get "/", PageController, :index
  end 

  # ... routes
end

That’s it! Run mix ecto.setup and you can now visit http://localhost:4000/registration/new, and create a new user.

You see the newly exposed user routes by typing mix phx.routes

Modifying templates

mix pow.phoenix.gen.templates

config/config.exs

config :author_example, :pow,
  ...
  web_module: AuthorExampleWeb

Lets try it out what we have so far:

mix phx.server

http://localhost:4000/users/new

Create PostTypes Context with Post Schema

We use —web PostTypes so that in the future we can have a controller named “PageController” that won’t conflict with Phoenix’s default PageController. For now we are just having “Post” “PostType”, but in the future we might also have a “Page” “PostType”.

mix phx.gen.html PostTypes Post posts title:unique body:text --web PostTypes

author_example/lib/author_example_web/router.ex

defmodule AuthorExampleWeb.Router do
  ...

  scope "/blog", AuthorExampleWeb.PostTypes, as: :post_types do
    pipe_through [:browser, :protected]

    resources "/posts", PostController
  end  

  ...
end
mix ecto.migrate

Add Author Schema to PostTypes Context

mix phx.gen.html PostTypes Author authors \
first_name:string \
last_name:string \
username:string \
bio:text \
role:string \
user_id:references:users:unique \
--web PostTypes

author_example/priv/repo/migrations/20191116082958_create_authors.exs

defmodule AuthorExample.Repo.Migrations.CreateAuthors do
  ...

  def change do
    create table(:authors) do
      ...
      # add :user_id, references(:users, on_delete: :nothing)
      add :user_id, references(:users, on_delete: :delete_all), null: false

      ...
    end

    ...
  end
end

Add Author ID Migration

mix ecto.gen.migration add_author_id_to_posts

author_example/priv/repo/migrations/20191116085843_add_author_id_to_posts.exs

defmodule AuthorExample.Repo.Migrations.AddAuthorIdToPosts do
  ...

  def change do
    alter table(:posts) do
        add :author_id, references(:authors, on_delete: :delete_all), null: false
    end

    create index(:posts, [:author_id])
  end
end

Add Author Association to Post Schema and Vis Versa

author_example/lib/author_example/post_types/post.ex

defmodule AuthorExample.PostTypes.Post do
  alias AuthorExample.PostTypes.Author

  schema "posts" do
    ...
    belongs_to :author, Author
    ...
  end

  ...
end

author_example/lib/author_example/post_types/author.ex

defmodule AuthorExample.PostTypes.Author do
  ...
  alias AuthorExample.PostTypes.Post
  alias AuthorExample.Users.User

  schema "authors" do
    ...
    # field :user_id, :id
    has_many :posts, Post
    belongs_to :user, User
    ...
  end

  ...
end

Require Author When Creating Post

author_example/lib/author_example/post_types.ex

defmodule AuthorExample.PostTypes do
  ...
  # alias AuthorExample.PostTypes.Post
  alias AuthorExample.PostTypes.{Post, Author}

  ...
  # def list_posts do
  #   Repo.all(Post)
  # end
  def list_posts do
    Post
    |> Repo.all()
    |> Repo.preload(author: [:user])
  end
  ...
  # def get_post!(id), do: Repo.get!(Post, id)
  def get_post!(id) do
    Post
    |> Repo.get!(id)
    |> Repo.preload(author: [:user])
  end
  ...
  # def get_author!(id), do: Repo.get!(Author, id)
  def get_author!(id) do
    Author
    |> Repo.get!(id)
    |> Repo.preload(:user)
  end
  ...
end

Persist Authors Upon Post Create or Edit

author_example/lib/author_example/post_types.ex

defmodule AuthorExample.PostTypes do
  alias AuthorExample.Users.User
  ...
  # def create_post(attrs \\ %{}) do
  #   %Post{}
  #   |> Post.changeset(attrs)
  #   |> Repo.insert()
  # end
  def create_post(%Author{} = author, attrs \\ %{}) do
    %Post{}
    |> Post.changeset(attrs)
    |> Ecto.Changeset.put_change(:author_id, author.id)
    |> Repo.insert()
  end

  def ensure_author_exists(%User{} = user) do
    %Author{user_id: user.id}
    |> Ecto.Changeset.change()
    |> Ecto.Changeset.unique_constraint(:user_id)
    |> Repo.insert()
    |> handle_existing_author()
  end

  defp handle_existing_author({:ok, author}), do: author
  defp handle_existing_author({:error, changeset}) do
    Repo.get_by!(Author, user_id: changeset.data.user_id)
  end

  ...
end

Update Post Controller

author_example/lib/author_example_web/controllers/post_types/post_controller.ex

defmodule AuthorExampleWeb.PostTypes.PostController do
  ...
  plug :require_existing_author
  plug :authorize_post when action in [:edit, :update, :delete]

  ...

  defp require_existing_author(conn, _) do
    author = PostTypes.ensure_author_exists(conn.assigns.current_user)
    assign(conn, :current_author, author)
  end

  defp authorize_post(conn, _) do
    post = PostTypes.get_post!(conn.params["id"])

    if conn.assigns.current_author.id == post.author_id do
      assign(conn, :post, post)  
    else
      conn
      |> put_flash(:error, "You can't modify that post")
      |> redirect(to: Routes.post_types_post_path(conn, :index))
      |> halt()
    end
  end
end

author_example/lib/author_example_web/controllers/post_types/post_controller.ex

defmodule AuthorExampleWeb.PostTypes.PostController do
  ...
  # def edit(conn, %{"id" => id}) do
  #   post = PostTypes.get_post!(id)
  #   changeset = PostTypes.change_post(post)
  #   render(conn, "edit.html", post: post, changeset: changeset)
  # end

  def edit(conn, _) do
    changeset = PostTypes.change_post(conn.assigns.post)
    render(conn, "edit.html", changeset: changeset)
  end
  ...
  # def create(conn, %{"post" => post_params}) do
  #   case PostTypes.create_post(post_params) do
  #     {:ok, post} ->
  #       conn
  #       |> put_flash(:info, "Post created successfully.")
  #       |> redirect(to: Routes.post_types_post_path(conn, :show, post))

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

  def create(conn, %{"post" => post_params}) do
    case PostTypes.create_post(conn.assigns.current_author, post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post created successfully.")
        |> redirect(to: Routes.post_types_post_path(conn, :show, post))

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

  ...

  # def update(conn, %{"id" => id, "post" => post_params}) do
  #   post = PostTypes.get_post!(id)

  #   case PostTypes.update_post(post, post_params) do
  #     {:ok, post} ->
  #       conn
  #       |> put_flash(:info, "Post updated successfully.")
  #       |> redirect(to: Routes.post_types_post_path(conn, :show, post))

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

  def update(conn, %{"post" => post_params}) do

    case PostTypes.update_post(conn.assigns.post, post_params) do
      {:ok, post} ->
        conn
        |> put_flash(:info, "Post updated successfully.")
        |> redirect(to: Routes.post_types_post_path(conn, :show, post))

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

  ...

  # def delete(conn, %{"id" => id}) do
  #   post = PostTypes.get_post!(id)
  #   {:ok, _post} = PostTypes.delete_post(post)

  #   conn
  #   |> put_flash(:info, "Post deleted successfully.")
  #   |> redirect(to: Routes.post_types_post_path(conn, :index))
  # end

  def delete(conn, _) do

    {:ok, _post} = PostTypes.delete_post(conn.assigns.post)

    conn
    |> put_flash(:info, "Post deleted successfully.")
    |> redirect(to: Routes.post_types_post_path(conn, :index))
  end

end

Update Post View

author_example/lib/author_example_web/views/post_types/post_view.ex

defmodule AuthorExampleWeb.PostTypes.PostView do
  ...

  alias AuthorExample.PostTypes

  def author_email(%PostTypes.Post{author: author}) do
    author.user.email
  end
end

Update Post Templates

Finally, lets display the author’s email in the post!

author_example/lib/author_example_web/templates/post_types/post/show.html.eex

...
  <li>
    <strong>Author Email:</strong>
    <%= author_email(@post) %>
  </li>
...

Check it out, we can finally add data from the User model in our blog post with just the author_id that is associated with the user_id:

mix phx.server
iex -S mix

Customize iex

.iex.exs

import_if_available Ecto.Query

alias AuthorExample.{
    Repo,
    PostTypes,
    PostTypes.Post,
    PostTypes.Author
}
posts = Repo.all(from p in Post, preload: :author)
authors = Repo.all(from a in Author, preload: [:user])
users = Repo.all(from u in User)

Neat!

Bonus: Add Sign Out Link

<%= if Pow.Plug.current_user(@conn) do %>
  <span><%= link "Sign out", to: Routes.pow_session_path(@conn, :delete), method: :delete %></span>
<% else %>
  <span><%= link "Register", to: Routes.pow_registration_path(@conn, :new) %></span>
  <span><%= link "Sign in", to: Routes.pow_session_path(@conn, :new) %></span>
<% end %>