Part 7— Implement the post author feature by connecting user and post data

Loi Le
9 min readNov 17, 2023

--

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.

Index | < Pre | Next >

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.

Index | < Pre | Next >

--

--