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

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
mix.exs

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/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 https://wp-api.space-rocket.com:4000/registration/new, and create a new user.

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


Modifying Templates

Run the following to generate Pow templates:

terminal

mix pow.phoenix.gen.templates

Modifying templates

mix pow.phoenix.gen.templates

mix pow.phoenix.gen.templates
config/config.exs

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

Let's try out what we have so far:

mix phx.server

mix phx.server

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 and 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

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

mix ecto.migrate

Add Author Schema to PostTypes Context

mix phx.gen.html PostTypes Author authors

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

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 Vice 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(%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

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, let’s 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

mix phx.server

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!

author_example/lib/author_example_web/templates/layout/sign_out.html.eex

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

Launch Your Project

Get your project off the ground with Space-Rocket! Fill out the form below to get started.

Space-Rocket pin icon