Scope LiveView resource to user_id?

I am a beginner in Elixir/Phoenix/LiveView and have finished a few courses + exercises in the last few weeks. I am now trying to write a simple app to implement the concepts, but am stuck when it comes to scoping a resource to a logged-in user.

The app should allow users to log in and manage their own tasks. These are the commands I used to create the app:

mix phx.new test_app
cd test_app
mix ecto.create
mix phx.gen.auth Accounts User users
mix deps.get
mix phx.gen.live Tasks Task tasks name completed:boolean user_id:references:users
mix ecto.migrate

I have been able to scope the :index functionality to the user by going through the following steps:

  1. Created an assign_current_user helper under lib/test_app/web/live/live_helpers.ex:
  def assign_current_user(socket, session) do
    assign_new(
      socket,
      :current_user,
      fn ->
      Accounts.get_user_by_session_token(session["user_token"])
      end
    )
  end
  1. Added the helper to the TestAppWeb entry point (lib/test_app_web.ex) by adding the following line in the live_view function:
import TestAppWeb.LiveHelpers
  1. Assigned the current user to the socket by modifying the mount function in lib/test_app_web/live/task_live/index.ex like this:
  def mount(_params, session, socket) do
    socket = assign_current_user(socket, session)
    {:ok, assign(socket, :tasks, list_tasks(socket.assigns.current_user.id))}
  end
  1. Created a list_tasks/1 function scoped to the user_id in lib/test_app/tasks.ex:
  def list_tasks(user_id) do
    query = from(t in Task, where: t.user_id == ^user_id)
    Repo.all(query)
  end

Now when I visit localhost:4000/tasks, I only see the tasks for the currently logged in user.

However, I have been unable to do this when it comes to creating, editing and viewing a specific task.

The first approach I tried was to add the user_id as a hidden field in lib/test_app_web/live/task_live/form_component.html.heex. However, that would allow the user to edit the value, whereas this scoping functionality should ideally be performed on the server. Furthermore, the user information is not accessible on the socket anymore when form_component.ex is evaluated, and I was unable to figure out how to add it.

The second approach I tried was to go through the Ecto guide for Multi tenancy with foreign keys, but using user_id instead of org_id. This resulted in various errors for me, so I concluded that for now I would just do it manually for the /tasks CRUD operations and worry about doing this globally later on.

I now think that the best approach would be to modify the TestApp.Tasks module so it adds the user_id before performing the rest of the CRUD operations. However, most of these are called from lib/test_app_web/live/task_live/form_component.ex, which doesn’t have a mount() function to add the current user to the socket, so I am unable to figure out how to do this.

Could someone here advise me on the best way to achieve this? I.e., securely scope the tasks to the currently logged-in user, so each user can only view, create, edit and delete their own tasks?

Here is a repo with the full application code for reference: https://github.com/bysja/test-app

1 Like

Furthermore, the user information is not accessible on the socket anymore when form_component.ex is evaluated, and I was unable to figure out how to add it.

You can pass the :current_user assign to the component:

<.live_component
  module={TestAppWeb.TaskLive.FormComponent}
  ...
  current_user={@current_user}
/>

and then in your handle_event for the form submit, you’ll use socket.assigns.current_user or pattern matching to get the user.

2 Likes

Thanks! This allowed me to implement the saving functionality as follows:

  1. Passed the :current_user to the component
  2. Added a line to assign the user_id to the task before saving it in lib/test_app_web/live/task_live/form_component.ex:
  def handle_event("save", %{"task" => task_params}, socket) do
    task_params = Map.put(task_params, "user_id", socket.assigns.current_user.id)
    save_task(socket, socket.assigns.action, task_params)
  end

However, a user can still log in and view other users’ tasks by typing in their URLs. For example, if task #1 was created by user 1, all other logged-in users can still view it by going to localhost:4000/tasks/1.

There is also nothing to prevent a user from editing or deleting tasks which belong to other users.

Is there a recommended way to prevent this? Or maybe an example repository for a multi tenancy app, where common patterns to scope resources to users can be seen?

It looks like TaskLive.Show is missing the logic to check if the task belongs to the current user. Here’s a very basic example:

def handle_params(%{"id" => id}, _, socket) do
  task = Tasks.get_task!(id)

  if task.user_id == socket.assigns.current_user.id do
    {:noreply,
     socket
     |> assign(:page_title, page_title(socket.assigns.live_action))
     |> assign(:task, task)}
  else
    # Redirect to error page, index, or whatever your desired functionality is
  end
end

See also: Security considerations of the LiveView model — Phoenix LiveView v0.17.5

3 Likes

Dear bysja,

I cannot express the gratefulness about your post and you producing your example app. I was banging my head against the wall for too many hours and your post here, the answers of the other friendly people and your extra mile for publishing your example app.

Thanks again!

Best,
Volker

2 Likes