Create a new project
mix phx.new author_example
mix phx.new author_example
cd author_example && mix ecto.create
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 %>