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

By mchavez

Hi, I am Michael Chavez, a web developer based in San Francisco, California. I started Space-Rocket in 2012 to make custom content management systems that are tailored to business needs.

Leave a comment

Your email address will not be published. Required fields are marked *