-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: live view and plug integrations
- Loading branch information
Showing
8 changed files
with
302 additions
and
57 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.