annad

annad

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.

First Post!

kokolegorille

kokolegorille

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.

Where Next?

Popular in Guides/Tuts Top

pel_daniel
Hi! I created some animations to explain what happens internally with map &amp; reduce functions. The code is open source. Any feedback...
New
voltone
The EEF’s Security WG has released the first public draft of the Secure Coding and Deployment Hardening Guidelines for BEAM languages. “...
New
njwest
Greetings: I just wrote a step-by-step guide on building a Phoenix 1.3 JWT Auth API with Guardian JWTs and Comeonin password hashing. I ...
New
malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
smpallen99
Some advice for Elixir programmers. I was reviewing someone's Elixir Code yesterday and found a deadlock condition bug in a GenServer i...
New
TwistingTwists
This is a thread to note down things/best practices encountered in LiveBeats App as I explore the source code. Found this usage of r...
New
wfgilman
I'm writing up this quick "How to" because what I thought was going to be an easy implementation of a Plug to validate a webhook request ...
New
ben-pr-p
Hey all! I put together a starter-pack / instructions to set up Phoenix with the new parcel-bundler instead of brunch (or webpack). It’s...
New
bitli
In case this is handy for other people, here is how you can run Elixir on Android: Install https://termux.com/ apt update; apt upgrad...
New
anuragg
We finally have a Mix clustering guide to go with Phoenix deployment with Mix Releases. Deploy an Elixir Cluster with Mix Releases and l...
New

Other popular topics Top

Qqwy
Update: How to use the Blogs &amp; Podcasts section You can post links to your blog posts or podcasts either in one of the Official Blog...
3268 119930 1237
New
sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
yawaramin
In the Dialyzer docs ( http://erlang.org/doc/man/dialyzer.html#requesting-or-suppressing-warnings-in-source-files ), there is a way to tu...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 record...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod -- where is this set? Thanks.
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
beno
I will often find my self writing things similar to: case some_value do nil -&gt; something() "" -&gt; something() _ -&gt; someth...
New

We're in Beta

About us Mission Statement