This is a part of the Tutorial to build a blog platform with Elixir Phoenix and Next.js that help you to develop a web application from scratch using modern technologies such as Elixir Phoenix, Next.js, and more.
In the previous part, we added the authentication feature for the post creation API. However, we still have an issue: we cannot identify the author of the posts, nor can the user retrieve the posts that he/she created. In this section, we will solve this problem by adding the author field to our backend.
We have two schemas in our project:
lib/lani_blog/blog/post.ex
defmodule LaniBlog.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :description, :string
field :content, :string
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :description, :content])
|> validate_required([:title, :content])
end
end
lib/lani_blog/users/user.ex
defmodule LaniBlog.Users.User do
use Ecto.Schema
use Pow.Ecto.Schema
schema "users" do
pow_user_fields()
timestamps()
end
end
We want to create a relationship between them, so that each post belongs to a user. To do this, we need to modify the LaniBlog.Blog.Post
schema and add a reference to the LaniBlog.Users.User
schema. Here is the updated code for lib/lani_blog/blog/post.ex
defmodule LaniBlog.Blog.Post do
use Ecto.Schema
import Ecto.Changeset
schema "posts" do
field :title, :string
field :description, :string
field :content, :string
belongs_to :user, LaniBlog.Users.User
timestamps()
end
def changeset(post, attrs) do
post
|> cast(attrs, [:title, :description, :content])
|> validate_required([:title, :content])
end
end
We added the line:
belongs_to :user, LaniBlog.Users.User
This establishes the relationship between the post and the user. Next, we need to update the database to reflect this change. We can generate a migration file with this command:
mix ecto.gen.migration add_user_id_to_post
...
* creating priv/repo/migrations/20231008035150_add_user_id_to_post.exs
Then we need to edit the file and add the following code:
defmodule LaniBlog.Repo.Migrations.AddUserIdToPost do
use Ecto.Migration
def change do
alter table(:posts) do
add :user_id, references(:users, on_delete: :nothing)
end
create index(:posts, [:user_id])
end
end
This adds the user_id
field to the posts
table and creates an index on it. We can run the migration with this command:
$ mix ecto.migrate
11:00:52.249 [info] == Running 20231008035150 LaniBlog.Repo.Migrations.AddUserIdToPost.change/0 forward
11:00:52.252 [info] alter table posts
11:00:52.291 [info] create index posts_user_id_index
11:00:52.307 [info] == Migrated 20231008035150 in 0.0s
Now the schema and the database are in sync. The last step is to update the create_post
function in the lib\lani_blog\blog.ex
context. We need to pass the user as a parameter and associate it with the post. Here is the updated code:
def create_post!(%LaniBlog.Users.User{} = user, attrs \\ %{}) do
%Post{}
|> Post.changeset(attrs)
|> Ecto.Changeset.put_assoc(:user, user)
|> Repo.insert!()
|> Repo.preload([:user])
end
We added the user parameter and used Ecto.Changeset.put_assoc
to associate the user with the post. We also used Repo.preload
to include the user information in the response.
Next, we need to modify the create
function in lani_blog_web\controllers\post_controller.ex
to pass the user
params from the current_user
we obtain from pow
to the context.
def create(conn, %{"data" => post_params}) do
post =
conn
|> Pow.Plug.current_user()
|> Blog.create_post!(post_params)
render(conn, "show.json", post: post)
end
This way, we can associate the author with the post by creating a relationship between the user
and post
schemas. To verify that the post we have created belongs to the user we have logged in, we can check the user_id
field in the posts
table in the database. Alternatively, we can implement a new api to get the posts from the current user. If this api returns the post we have created, it means our create post api works correctly. To do this, we need to import Ecto.Query and add a new function list_posts
to the lib\lani_blog\blog.ex
context:
...
import Ecto.Query
...
def list_posts(%LaniBlog.Users.User{id: user_id}) do
Repo.all(from p in Post, where: p.user_id == ^user_id, preload: [:user], order_by: [desc: :inserted_at])
end
Then we add a new action get_my_posts
in the lib/lani_blog_web/controllers/post_controller.ex
def get_my_posts(conn, _params) do
posts =
conn
|> Pow.Plug.current_user()
|> Blog.list_posts()
render(conn, "index.json", posts: posts)
end
Finally, we can update the lib/lani_blog_web/router.ex
scope "/api", LaniBlogWeb do
pipe_through [:api, :api_protected]
resources "/posts", PostController, only: [:create]
get "/my-posts", PostController, :get_my_posts
end
Let’s test our implementation. First, we need to log in
curl --location --request POST 'http://localhost:4000/api/session' \
--header 'Content-Type: application/json' \
--data-raw '{
"user": {
"email": "john.doe@test.com",
"password": "letmein@123"
}
}'
We should get a successful response
{
"data": {
"user": { "email": "john.doe@test.com" },
"access_token": "SFMyNTY.Yzk4YzQ0MjctNmY3Mi00YjE5LWE3MzktZTQ5YzAzZjQ0NThl.m0kG-xqoc90c6Xfn4mb1aEsHrOgKkcPfLcDc98Z7wXI",
"renewal_token": "SFMyNTY.MmFhMThmYTUtNWIwMS00NzZiLThkNWItNDY2ODY2ZTliZDg2.6Fi3bz_8_iYteExzWzewu4dKcur0LCJKLnglSyxJsGc",
"expired_at": "2023-11-13T10:34:08.856958Z"
}
}
We use the access_token
to request the create
API:
curl --location --request POST 'http://localhost:4000/api/posts' \
--header 'Authorization: SFMyNTY.Yzk4YzQ0MjctNmY3Mi00YjE5LWE3MzktZTQ5YzAzZjQ0NThl.m0kG-xqoc90c6Xfn4mb1aEsHrOgKkcPfLcDc98Z7wXI' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"title": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"description": "Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit",
"content": "Adipiscing enim eu turpis **pretium** aenean pharetra magna ac. Ornare suspendisse sed nisi lacus sed. Ultrices sagittis orci a scelerisque purus semper eget duis.\n\n- Orci sagittis eu volutpat odio facilisis. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.\n- Nisl nisi scelerisque eu ultrices vitae auctor eu augue ut.\n- Consectetur a erat nam at lectus urna duis convallis.\n\n Tincidunt lobortis feugiat vivamus at augue eget arcu. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada."
}
}'
And we should also get a successful response:
{
"data": {
"id": 4,
"description": "Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit",
"title": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"content": "Adipiscing enim eu turpis **pretium** aenean pharetra magna ac. Ornare suspendisse sed nisi lacus sed. Ultrices sagittis orci a scelerisque purus semper eget duis.\n\n- Orci sagittis eu volutpat odio facilisis. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.\n- Nisl nisi scelerisque eu ultrices vitae auctor eu augue ut.\n- Consectetur a erat nam at lectus urna duis convallis.\n\n Tincidunt lobortis feugiat vivamus at augue eget arcu. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada.",
"created_at": "2023-11-13T10:09:23"
}
}
Now we can test the get my post list API
curl --location --request GET 'http://localhost:4000/api/my-posts' \
--header 'Authorization: SFMyNTY.Yzk4YzQ0MjctNmY3Mi00YjE5LWE3MzktZTQ5YzAzZjQ0NThl.m0kG-xqoc90c6Xfn4mb1aEsHrOgKkcPfLcDc98Z7wXI'
We should get a successful result now.
{
"data": [
{
"id": 4,
"description": "Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit",
"title": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"content": "Adipiscing enim eu turpis **pretium** aenean pharetra magna ac. Ornare suspendisse sed nisi lacus sed. Ultrices sagittis orci a scelerisque purus semper eget duis.\n\n- Orci sagittis eu volutpat odio facilisis. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.\n- Nisl nisi scelerisque eu ultrices vitae auctor eu augue ut.\n- Consectetur a erat nam at lectus urna duis convallis.\n\n Tincidunt lobortis feugiat vivamus at augue eget arcu. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada.",
"created_at": "2023-11-13T10:09:23"
}
]
}
It seems OK now. But I noticed a problem. Currently, we have an action get_my_posts
in lib/lani_blog_web/controllers/post_controller.ex
. In Phoenix, there are some conventional action names such as: index
, show
, update
, delete
, … They correspond to the HTTP verbs in Restful API. So, if we have a strange name like get_my_posts
, it might indicate a flaw in the design. Let’s think about it. We have the post_controller
that is the API endpoint for post
resource. But if we think carefully, we have two kinds of post
resource here. The first one is the post that anyone can get and does not need to log in. The other one is the post that user has to log in to get its information. If so, we can use the current post_controller.ex
for the first one. For the second one — the post resource that requires user authentication, we can introduce a new controller for it: lib/lani_blog_web/controllers/my_post_controller.ex
defmodule LaniBlogWeb.MyPostController do
use LaniBlogWeb, :controller
alias LaniBlog.Blog
action_fallback LaniBlogWeb.FallbackController
def index(conn, _params) do
posts =
conn
|> Pow.Plug.current_user()
|> Blog.list_posts()
conn
|> put_view(LaniBlogWeb.PostView)
|> render("index.json", posts: posts)
end
end
We have moved the get_my_posts
action from lib/lani_blog_web/controllers/post_controller.ex
to the index
action of the lib/lani_blog_web/controllers/my_post_controller.ex
. And because of the naming convention, we have to put_view
(LaniBlogWeb.PostView
) so that we can use the PostView
to render the list of posts. By thinking this way, we can see that the create action from post_controller
also belongs to the my_post_controller
. So, we have to move it to the right place. Then, our new controller lib/lani_blog_web/controllers/my_post_controller.ex
should look like this:
defmodule LaniBlogWeb.MyPostController do
use LaniBlogWeb, :controller
alias LaniBlog.Blog
action_fallback LaniBlogWeb.FallbackController
def index(conn, _params) do
posts =
conn
|> Pow.Plug.current_user()
|> Blog.list_posts()
conn
|> put_view(LaniBlogWeb.PostView)
|> render("index.json", posts: posts)
end
def create(conn, %{"data" => post_params}) do
post =
conn
|> Pow.Plug.current_user()
|> Blog.create_post!(post_params)
conn
|> put_view(LaniBlogWeb.PostView)
|> render("show.json", post: post)
end
end
After moving these actions, the lib/lani_blog_web/controllers/post_controller.ex
should look like this:
defmodule LaniBlogWeb.PostController do
use LaniBlogWeb, :controller
alias LaniBlog.Blog
action_fallback LaniBlogWeb.FallbackController
def index(conn, _params) do
posts = Blog.list_posts()
render(conn, "index.json", posts: posts)
end
def show(conn, %{"id" => id}) do
with {:ok, post} <- Blog.get_post(id) do
render(conn, "show.json", post: post)
end
end
end
Finally, we have to update the lib/lani_blog_web/router.ex
...
scope "/api", LaniBlogWeb do
pipe_through [:api, :api_protected]
resources "/my-posts", MyPostController, only: [:index, :create]
end
scope "/api", LaniBlogWeb do
pipe_through :api
resources "/registration", RegistrationController, singleton: true, only: [:create]
resources "/session", SessionController, singleton: true, only: [:create, :delete]
post "/session/renew", SessionController, :renew
resources "/posts", PostController, only: [:index, :show]
end
...
Now we can check our create post API again with the new resource URL:
curl --location --request POST 'http://localhost:4000/api/my-posts' \
--header 'Authorization: SFMyNTY.Yzk4YzQ0MjctNmY3Mi00YjE5LWE3MzktZTQ5YzAzZjQ0NThl.m0kG-xqoc90c6Xfn4mb1aEsHrOgKkcPfLcDc98Z7wXI' \
--header 'Content-Type: application/json' \
--data-raw '{
"data": {
"title": "Parturient montes nascetur ridiculus mus mauris vitae ultricies leo integer",
"description": "Ultrices sagittis orci a scelerisque purus semper eget",
"content": "Luctus venenatis lectus magna fringilla urna. Metus vulputate eu scelerisque felis imperdiet proin fermentum..\n\n- Pharetra magna ac placerat vestibulum.\n- Convallis aenean et tortor at. Scelerisque purus semper eget duis at tellus at.\n- Mauris vitae ultricies leo integer malesuada nunc vel risus. Tristique nulla aliquet enim tortor at auctor urna.\n\n Phasellus faucibus scelerisque eleifend donec pretium vulputate. Varius duis at consectetur lorem donec massa sapien. Tellus molestie nunc non blandit massa enim."
}
}'
We should get a successful response now:
{
"data": {
"id": 5,
"description": "Ultrices sagittis orci a scelerisque purus semper eget",
"title": "Parturient montes nascetur ridiculus mus mauris vitae ultricies leo integer",
"content": "Luctus venenatis lectus magna fringilla urna. Metus vulputate eu scelerisque felis imperdiet proin fermentum..\n\n- Pharetra magna ac placerat vestibulum.\n- Convallis aenean et tortor at. Scelerisque purus semper eget duis at tellus at.\n- Mauris vitae ultricies leo integer malesuada nunc vel risus. Tristique nulla aliquet enim tortor at auctor urna.\n\n Phasellus faucibus scelerisque eleifend donec pretium vulputate. Varius duis at consectetur lorem donec massa sapien. Tellus molestie nunc non blandit massa enim.",
"created_at": "2023-11-13T10:18:58"
}
}
Next, let’s test the get my posts API:
curl --location --request GET 'http://localhost:4000/api/my-posts' \
--header 'Authorization: SFMyNTY.Yzk4YzQ0MjctNmY3Mi00YjE5LWE3MzktZTQ5YzAzZjQ0NThl.m0kG-xqoc90c6Xfn4mb1aEsHrOgKkcPfLcDc98Z7wXI'
We should also get the response
{
"data": [
{
"id": 5,
"description": "Ultrices sagittis orci a scelerisque purus semper eget",
"title": "Parturient montes nascetur ridiculus mus mauris vitae ultricies leo integer",
"content": "Luctus venenatis lectus magna fringilla urna. Metus vulputate eu scelerisque felis imperdiet proin fermentum..\n\n- Pharetra magna ac placerat vestibulum.\n- Convallis aenean et tortor at. Scelerisque purus semper eget duis at tellus at.\n- Mauris vitae ultricies leo integer malesuada nunc vel risus. Tristique nulla aliquet enim tortor at auctor urna.\n\n Phasellus faucibus scelerisque eleifend donec pretium vulputate. Varius duis at consectetur lorem donec massa sapien. Tellus molestie nunc non blandit massa enim.",
"created_at": "2023-11-13T10:18:58"
},
{
"id": 4,
"description": "Amet consectetur adipiscing elit duis tristique sollicitudin nibh sit",
"title": "Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua",
"content": "Adipiscing enim eu turpis **pretium** aenean pharetra magna ac. Ornare suspendisse sed nisi lacus sed. Ultrices sagittis orci a scelerisque purus semper eget duis.\n\n- Orci sagittis eu volutpat odio facilisis. Ut tristique et egestas quis ipsum suspendisse ultrices gravida.\n- Nisl nisi scelerisque eu ultrices vitae auctor eu augue ut.\n- Consectetur a erat nam at lectus urna duis convallis.\n\n Tincidunt lobortis feugiat vivamus at augue eget arcu. Eleifend donec pretium vulputate sapien nec sagittis aliquam malesuada.",
"created_at": "2023-11-13T10:09:23"
}
]
}
You have successfully completed the mission. You can now create your own posts and become the author. You can also view your list of posts. However, the create post feature is not secure in the front end. We need to improve our front-end side to protect it. We will do that in the next part.