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
:
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:
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:
mix pow.phoenix.gen.templates
Modifying templates
mix pow.phoenix.gen.templates
config :author_example, :pow,
...
web_module: AuthorExampleWeb
Let's try out what we have so far:
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
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
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
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
defmodule AuthorExample.PostTypes.Post do
alias AuthorExample.PostTypes.Author
schema "posts" do
...
belongs_to :author, Author
...
end
...
end
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
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
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
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
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!
...
<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
Customize iex
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 %>
Launch Your Project
Get your project off the ground
with Space-Rocket!
Fill out the form below to get started.