Skip to content

Commit

Permalink
feat: live view and plug integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
zoedsoupe committed Apr 18, 2024
1 parent 87ec8ea commit 86c9b36
Show file tree
Hide file tree
Showing 8 changed files with 302 additions and 57 deletions.
2 changes: 2 additions & 0 deletions .env.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export SUPABASE_URL=http://127.0.0.1:54321
export SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImV4cCI6MTk4MzgxMjk5Nn0.EGIM96RAZx35lJzdJsyH-qQwv8Hdp7fsn3W0YpN81IU
4 changes: 4 additions & 0 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,8 @@ if config_env() == :dev do
config :supabase_potion,
supabase_base_url: System.get_env("SUPABASE_URL"),
supabase_api_key: System.get_env("SUPABASE_KEY")

config :supabase_gotrue,
authentication_client: :pescarte,
endpoint: nil
end
129 changes: 129 additions & 0 deletions lib/supabase/go_true/live_view.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
defmodule Supabase.GoTrue.LiveView do
@moduledoc """
Provides LiveView integrations for the Supabase GoTrue authentication in Elixir applications.
This module enables the seamless integration of authentication flows within Phoenix LiveView applications by leveraging the Supabase GoTrue SDK. It supports operations such as mounting current users, handling authenticated and unauthenticated states, and logging out users.
## Configuration
The module requires some application environment variables to be set:
- `authentication_client`: The Supabase client used for authentication.
- `endpoint`: Your web app endpoint, used internally for broadcasting user disconnection events.
- `signed_in_path`: The route to where socket should be redirected to after authentication
You can set up these config in your `config.exs`:
```
config :supabase_gotrue,
endpoint: YourApp.Endpoint,
signed_in_path: "/dashboard",
authentication_client: :my_supabase_potion_client_name
```
## Usage
Typically, this module is used in your `YourAppWeb.Router`, to handle user authentication states through a series of `on_mount` callbacks, which ensure that user authentication logic is processed during the LiveView lifecycle.
Check `on_mount/4` for more detailed usage instructions on LiveViews
"""

import Phoenix.Component, only: [assign_new: 3]

alias Phoenix.LiveView.Socket
alias Supabase.GoTrue
alias Supabase.GoTrue.Admin
alias Supabase.GoTrue.Session
alias Supabase.GoTrue.User

@client Application.compile_env!(:supabase_gotrue, :authentication_client)
@endpoint Application.compile_env!(:supabase_gotrue, :endpoint)
@signed_in_path Application.compile_env(:supabase_gotrue, :signed_in_path)

@doc """
Logs out the user from the session and broadcasts a disconnect event.
## Parameters
- `socket`: The `Phoenix.LiveView.Socket` representing the current LiveView state.
- `scope`: An optional scope parameter for the logout request. Check `Supabase.GoTrue.Admin.sign_out/3` for more detailed information.
## Examples
iex> log_out_user(socket, :local)
# Broadcasts 'disconnect' and removes the user session
"""
def log_out_user(%Socket{} = socket, scope) do
user = socket.assigns.current_user
user_token = socket.assigns[:user_token]
session = %Session{access_token: user_token}
user_token && Admin.sign_out(@client, session, scope)
@endpoint.broadcast_from(self(), socket.id, "disconnect", %{user: user})
end

@doc """
Handles mounting and authenticating the current_user in LiveViews.
## `on_mount` arguments
* `:mount_current_user` - Assigns current_user
to socket assigns based on user_token, or nil if
there's no user_token or no matching user.
* `:ensure_authenticated` - Authenticates the user from the session,
and assigns the current_user to socket assigns based
on user_token.
Redirects to login page if there's no logged user.
* `:redirect_if_user_is_authenticated` - Authenticates the user from the session.
Redirects to signed_in_path if there's a logged user.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_user:
defmodule PescarteWeb.PageLive do
use PescarteWeb, :live_view
on_mount {PescarteWeb.Authentication, :mount_current_user}
...
end
Or use the `live_session` of your router to invoke the on_mount callback:
live_session :authenticated, on_mount: [{PescarteWeb.UserAuth, :ensure_authenticated}] do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_user, _params, session, socket) do
{:cont, mount_current_user(session, socket)}
end

def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)

if socket.assigns.current_user do
{:cont, socket}
else
{:halt, Phoenix.LiveView.redirect(socket, to: @signed_in_path)}
end
end

def on_mount(:redirect_if_user_is_authenticated, _params, session, socket) do
socket = mount_current_user(session, socket)

if socket.assigns.current_user do
{:halt, Phoenix.LiveView.redirect(socket, to: @signed_in_path)}
else
{:cont, socket}
end
end

def mount_current_user(session, socket) do
case session do
%{"user_token" => user_token} ->
socket
|> assign_new(:current_user, fn ->
session = %Session{access_token: user_token}
case GoTrue.get_user(@client, session) do
{:ok, %User{} = user} -> user
_ -> nil
end
end)
|> assign_new(:user_token, fn -> user_token end)

%{} ->
assign_new(socket, :current_user, fn -> nil end)
end
end
end
189 changes: 158 additions & 31 deletions lib/supabase/go_true/plug.ex
Original file line number Diff line number Diff line change
@@ -1,61 +1,188 @@
defmodule Supabase.GoTrue.Plug do
@moduledoc false
@moduledoc """
Provides Plug-based authentication support for the Supabase GoTrue authentication in Elixir applications.
This module offers a series of functions to manage user authentication through HTTP requests in Phoenix applications. It facilitates operations like logging in with a password, logging out users, fetching the current user from a session, and handling route protections based on authentication state.
## Configuration
The module requires some application environment variables to be set:
- `authentication_client`: The Supabase client used for authentication.
- `signed_in_path`: The route to where conn should be redirected to after authentication
- `not_authenticated_path `: The route to where conn should be redirect to if user isn't authenticated
You can set up these config in your `config.exs`:
```
config :supabase_gotrue,
endpoint: YourApp.Endpoint,
signed_in_path: "/dashboard",
authentication_client: :my_supabase_potion_client_name
```
## Authentication Flow
It handles session management, cookie operations, and redirects based on user authentication status, providing a seamless integration for user sessions within Phoenix routes.
"""

import Plug.Conn
import Supabase.Client, only: [is_client: 1]
import Phoenix.Controller

alias Plug.Conn
alias Supabase.GoTrue
alias Supabase.GoTrue.Admin
alias Supabase.GoTrue.Session
alias Supabase.GoTrue.User

@key "supabase_gotrue_token"
@session_cookie "_supabase_go_true_session_cookie"
@session_cookie_options [sign: true, same_site: "Lax"]

def session_active?(%Conn{} = conn) do
key = :second |> System.os_time() |> to_string()
get_session(conn, key) == nil
rescue
ArgumentError -> false
end
@client Application.compile_env!(:supabase_gotrue, :authentication_client)
@signed_in_path Application.compile_env(:supabase_gotrue, :signed_in_path)
@not_authenticated_path Application.compile_env(:supabase_gotrue, :not_authenticated_path, "/")

def authenticated?(%Conn{} = conn) do
not is_nil(conn.private[@key])
end
@doc """
Logs in a user using a username and password. Stores the user token in the session and a cookie, if a `"remember_me"` key is present inside `params`.
For more information on how Supabase login with email and password works, check `Supabase.GoTrue.sign_in_with_password/2`
"""
def log_in_with_password(conn, params \\ %{}) do
{:ok, session} = GoTrue.sign_in_with_password(@client, params)
user_return_to = get_session(conn, :user_return_to)

def put_current_token(%Conn{} = conn, token) do
put_private(conn, @key, token)
conn
|> renew_session()
|> put_token_in_session(session.access_token)
|> maybe_write_session_cookie(session, params)
|> redirect(to: user_return_to || @signed_in_path)
end

def put_session_token(%Conn{} = conn, token) do
defp renew_session(conn) do
conn
|> put_session(@key, token)
|> configure_session(renew: true)
|> clear_session()
end

def sign_in(%Conn{} = conn, client, attrs) when is_client(client) do
case maybe_sign_in(conn, client, attrs) do
{:ok, session} -> put_session_token(conn, session.access_token)
defp maybe_write_session_cookie(conn, %Session{} = session, params) do
case params do
%{"remember_me" => "true"} ->
token = session.access_token
opts = Keyword.put(@session_cookie_options, :max_age, session.expires_in)
put_resp_cookie(conn, @session_cookie, token, opts)
_ -> conn
end
end

defp maybe_sign_in(conn, client, credentials) do
if session_active?(conn) do
Supabase.GoTrue.sign_in_with_password(client, credentials)
@doc """
Logs out the user from the application, clearing session data
"""
def log_out_user(%Plug.Conn{} = conn, scope) do
user_token = get_session(conn, :user_token)
session = %Session{access_token: user_token}
user_token && Admin.sign_out(@client, session, scope)

conn
|> renew_session()
|> redirect(to: @not_authenticated_path )
end


@doc """
Retrieves the current user from the session or a signed cookie, assigning it to the connection's assigns.
Can be easily used as a plug, for example inside a Phoenix web app
pipeline in your `YourAppWeb.Router`, you can do something like:
```
import Supabase.GoTrue.Plug
pipeline :browser do
plug :fetch_session # comes from Plug.Conn
plug :fetch_current_user
# rest of plug chain...
end
```
"""
def fetch_current_user(conn, _opts) do
{user_token, conn} = ensure_user_token(conn)
user = user_token && fetch_user_from_session_token(user_token)
assign(conn, :current_user, user)
end

defp fetch_user_from_session_token(user_token) do
case GoTrue.get_user(@client, %Session{access_token: user_token}) do
{:ok, %User{} = user} -> user
_ -> nil
end
end

def sign_out(%Conn{} = conn) do
if session_active?(conn) do
delete_session(conn, @key)
defp ensure_user_token(conn) do
if user_token = get_session(conn, :user_token) do
{user_token, conn}
else
conn = fetch_cookies(conn, signed: [@session_cookie])

if user_token = conn.cookies[@session_cookie] do
{user_token, put_token_in_session(conn, user_token)}
else
{nil, conn}
end
end
end

@doc """
Redirects an user to the configured `signed_in_path` if it is authenticated, if not, just halts the connection.
Generaly you wan to use it inside your scopes routes inside `YourAppWeb.Router`:
```
scope "/" do
pipe_trough [:browser, :redirect_if_user_is_authenticated]
get "/login", LoginController, :login
end
```
"""
def redirect_if_user_is_authenticated(conn, _opts) do
if conn.assigns[:current_user] do
conn
|> redirect(to: @signed_in_path)
|> halt()
else
conn
end
end

@doc """
Ensures an user is authenticated before executing the rest of Plugs chain.
Generaly you wan to use it inside your scopes routes inside `YourAppWeb.Router`:
```
scope "/" do
pipe_trough [:browser, :require_authenticated_user]
get "/super-secret", SuperSecretController, :secret
end
```
"""
def require_authenticated_user(conn, _opts) do
if conn.assigns[:current_user] do
conn
else
conn
|> maybe_store_return_to()
|> redirect(to: @signed_in_path)
|> halt()
end
end

def fetch_token_from_cookies(%Conn{} = conn) do
token = conn.req_cookies[@key] || conn.req_cookies[to_string(@key)]
if token, do: {:ok, token}, else: {:error, :not_found}
defp maybe_store_return_to(%{method: "GET"} = conn) do
put_session(conn, :user_return_to, current_path(conn))
end

def current_token(%Conn{} = conn) do
conn.private[@key]
defp maybe_store_return_to(conn), do: conn

defp put_token_in_session(conn, token) do
base64_token = Base.url_encode64(token)

conn
|> put_session(:user_token, token)
|> put_session(:live_socket_id, "users_session:#{base64_token}")
end
end
24 changes: 0 additions & 24 deletions lib/supabase/go_true/plug/verify_header.ex

This file was deleted.

Loading

0 comments on commit 86c9b36

Please sign in to comment.