LiveView and Phx.Gen.Auth: How to have an unauthenticated liveview pass to an authenticated one

I’m posting this for developers who are totally new to Phoenix like myself. This is all probably obvious to more skilled Phoenix developers, but I spent a lot of time figuring this out so thought I might save others time by posting the solution.

SOLUTION SUMMARY: This explains how to have an unauthenticated liveview hand off to a set of authenticated liveviews. Authorization is being handled by phx.gen.auth.

APP GOAL: I want to present the user with all of the available groups. If they like a group, I want them to “Save” it to their own repository of saved groups called My Groups. This means that the “Index” and “Show” pages will be unauthenticated and allow anyone to view them. But if the user clicks “Save”, then they need to be authenticated and passed to the apply_action “save” functionality.

WHAT DID NOT WORK: I had autogenerated liveviews for “groups” using phx.gen.live. I also had installed phx.gen.auth and had authentication completely working. To achieve my goal, I thought that the only thing I needed to do was change the router like this:

scope "/", MyApp do
    pipe_through :browser

    live "/groups", GroupLive.Index, :index
    live "/groups/:id", GroupLive.Show, :show

end

scope "/", MyApp do
    pipe_through [:browser, :require_authenticated_user]

    live "/groups/:id/edit", GroupLive.Index, :edit
    live "/groups/:id/save", GroupLive.Index, :save
    live "/groups/new", GroupLive.Index, :new
    live "/groups/:id/show/edit", GroupLive.Show, :edit

end

Side note: If I ran all of the “group” liveviews as UNauthenticated, then it worked fine. And if I ran all of the “group” liveviews as Authenticated, then it ran fine. It was when I tried to split the functionality that things got tricky.

First, I struggled with how to make the “mount” function in Index only request the authenticated user IF the user had asked to save the “group.” I posted on Elixir Forum and @kokolegorille pointed me in the right direction. He explained that it was better to keep the unauthenticated functionality in one set of liveviews and the authenticated functionality in another set.

Once I separated the functionality, my router looked like this:

scope "/", MyApp do
    pipe_through :browser

    live "/groups", GroupLive.Index, :index
    live "/groups/:id", GroupLive.Show, :show

end

scope "/", MyApp do
    pipe_through [:browser, :require_authenticated_user]

    live "/mygroups", GroupLive.MyGroups, :index
    live “/mygroups/:id/edit", GroupLive.MyGroups, :edit
    live “/mygroups/:id/save", GroupLive.MyGroups, :save
    live “/mygroups/new", GroupLive.MyGroups, :new
    live “/mygroups/:id/show/edit", GroupLive.Show, :edit

end

I added two new files to my group_live directory:

my_groups.ex
my_groups.html.leex

** See note at bottom about big naming convention mistake I made that cost me hours.

THE HAND OFF

The code inside GroupLive.Index remained relatively the same, although I deleted all of the functionality for everything except :index because it was no longer needed.

The tricky part came with “index.html.leex.” First I removed the modal code that was autogenerated. It is for “new” and “edit.” That functionality would not happen in an unauthenticated page, so I deleted that from the top of the html.leex. Next, I needed to add the functionality for a “Save” link.

I needed to call a Router helper to redirect to the MyGroups liveview, but I could not figure out how to do that. I read in hexdocs that “Phoenix automatically generates a module Helpers inside your router which contains named helpers to help developers generate and keep their routes up to date.” I looked EVERYWHERE for that module and could not find it, so I couldn’t figure out where that functionality was coming from. Again, @kokolegorille helped out and pointed me to the tool for figuring out the routes. You want to run the following command: mix phx.routes. When I did that, I found all of the routes for my app:

group_index_path  GET     /groups                                    Phoenix.LiveView.Plug :index
group_show_path  GET     /groups/:id                               Phoenix.LiveView.Plug :show
group_my_groups_path  GET     /mygroups                      Phoenix.LiveView.Plug :index
group_my_groups_path  GET     /mygroups/:id/edit          Phoenix.LiveView.Plug :edit
group_my_groups_path  GET     /mygroups/:id/save        Phoenix.LiveView.Plug :save
group_my_groups_path  GET     /mygroups/new              Phoenix.LiveView.Plug :new
group_show_path  GET     /mygroups/:id/show/edit          Phoenix.LiveView.Plug :edit

Here are links to useful documentation:
Router: Phoenix.Router — Phoenix v1.5.8
Routing: Routing — Phoenix v1.5.8
LiveView.Helpers: Phoenix.LiveView.Helpers — Phoenix LiveView v0.15.5

Now that I had the route, I could redirect to my authenticated pages. I changed the code in index.html.leex to this:

<td>
          <span><%= live_redirect "Show", to: Routes.group_show_path(@socket, :show, group) %></span>
          <span><%= live_redirect "Save", to: Routes.group_my_groups_path(@socket, :save, group) %></span>
</td>

NOTE: you need to use live_redirect NOT live_patch because you are passing functionality to a new liveview.

Inside GroupLive.MyGroups, I added the authentication functionality to mount so that it grabbed the current_user. A lot of the other code was just copied over from Index.ex (the new, edit, delete) functionality.

NAMING CONVENTION MISHAP

Sadly, things still didn’t work. I ran into one more problem that revolved around naming conventions. I named this new liveview “MyGroups” and the file names were originally “mygroups.ex” and “mygroups.html.leex.” NOTE the camel case in the liveview name. I didn’t think anything of it, but it wreaked some havoc. What I didn’t realize is that the liveview name (with a camel case) required that I name the files my_groups.ex and my_groups.html.leex (note the underbar between my_groups). I ran into a series of errors as a result of my files being originally named mygroups.ex and mygroups.html.leex. I finally fixed the file names, BUT I forgot to fix my routes in the Index file. I kept getting the following error:

function MyApp.Router.Helpers.group_my_groups_path/3 is undefined or private
Called with 3 arguments 1. Socket ..., 2. :edit and 3. %Group{...}

I went crazy trying to figure out why it thought the function was undefined. Many thanks to @axelson who caught the typo in my code. My original index.html.leex had the following router path: Routes.group_mygroups_path(@socket, :save, group) BUT it needed to be
Routes.group_my_groups_path(@socket, :save, group). It was the underbar between my_groups that cost me hours of reading documentation and trying to figure out the problem.

Moral of the story: Be really really careful about the naming conventions.

8 Likes

You might use better tooling to avoid spending hours on such typo. Elixir LS catch those kind of mistakes.

Your next step is metaprogramming, it makes the code harder, but remove tons of duplicate code. It’s also a good way to understand how Phoenix work.

Thank you for that Elixir LS tip! I’m going to definitely plug that into Visual Studio.

I had a good laugh over the metaprogramming. I am sooooooooooo not ready for that. I’d be happy if I could just get my app to work! ha ha ha But I’ll keep that in mind if I ever get to the point where I’m not chasing down typos, bad routes, loop errors and about a million compilation errors. :smiley: