Skip to content

Commit

Permalink
scope phx.gen.auth
Browse files Browse the repository at this point in the history
  • Loading branch information
SteffenDE committed Feb 20, 2025
1 parent 33fbb4c commit 72f5137
Show file tree
Hide file tree
Showing 17 changed files with 227 additions and 107 deletions.
2 changes: 1 addition & 1 deletion lib/mix/phoenix/schema.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ defmodule Mix.Phoenix.Schema do
repo_alias = if String.ends_with?(Atom.to_string(repo), ".Repo"), do: "", else: ", as: Repo"
file = Mix.Phoenix.context_lib_path(ctx_app, basename <> ".ex")
table = opts[:table] || schema_plural
scope = Mix.Phoenix.Scope.scope_from_opts(opts[:scope], opts[:no_scope])
scope = Mix.Phoenix.Scope.scope_from_opts(otp_app, opts[:scope], opts[:no_scope])
{cli_attrs, uniques, redacts} = extract_attr_flags(cli_attrs)
{assocs, attrs} = partition_attrs_and_assocs(module, attrs(cli_attrs))
types = types(attrs)
Expand Down
24 changes: 12 additions & 12 deletions lib/mix/phoenix/scope.ex
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,17 @@ defmodule Mix.Phoenix.Scope do
@doc """
Returns a `%{name: scope}` map of configured scopes.
"""
def scopes_from_config do
scopes = Application.get_env(:phoenix, :scopes, [])
def scopes_from_config(otp_app) do
scopes = Application.get_env(otp_app, :scopes, [])

Map.new(scopes, fn {name, opts} -> {name, new!(name, opts)} end)
end

@doc """
Returns the default scope.
"""
def default_scope do
with {_, scope} <- Enum.find(scopes_from_config(), fn {_, scope} -> scope.default end) do
def default_scope(otp_app) do
with {_, scope} <- Enum.find(scopes_from_config(otp_app), fn {_, scope} -> scope.default end) do
scope
end
end
Expand All @@ -44,20 +44,20 @@ defmodule Mix.Phoenix.Scope do
Returns `nil` for `--no-scope` and raises if a specific scope is not configured.
"""
def scope_from_opts(bin, false) when is_binary(bin) do
def scope_from_opts(_otp_app, bin, false) when is_binary(bin) do
raise "--scope and --no-scope must not be used together"
end

def scope_from_opts(_, true), do: nil
def scope_from_opts(__otp_app, _name, true), do: nil

def scope_from_opts(nil, _) do
default_scope() || raise """
def scope_from_opts(otp_app, nil, _) do
default_scope(otp_app) || raise """
no default scope configured!
Either run the generator with --no-scope to skip scoping, specify a scope with --scope,
or configure a default scope in your application's config:
config :phoenix, :scopes, [
config :#{otp_app}, :scopes, [
user: [
default: true,
...
Expand All @@ -66,16 +66,16 @@ defmodule Mix.Phoenix.Scope do
"""
end

def scope_from_opts(name, _) do
def scope_from_opts(otp_app, name, _) do
key = String.to_atom(name)
scopes = scopes_from_config()
scopes = scopes_from_config(otp_app)
Map.get_lazy(scopes, key, fn ->
raise """
scope :#{key} not configured!
Ensure that the scope :#{key} is configured in your application's config:
config :phoenix, :scopes, [
config :#{otp_app}, :scopes, [
#{key}: [
...
]
Expand Down
62 changes: 59 additions & 3 deletions lib/mix/tasks/phx.gen.auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
context
|> copy_new_files(binding, paths)
|> inject_conn_case_helpers(paths, binding)
|> inject_config(hashing_library)
|> inject_hashing_config(hashing_library)
|> inject_scope_config()
|> maybe_inject_mix_dependency(hashing_library)
|> inject_routes(paths, binding)
|> maybe_inject_router_import(binding)
Expand Down Expand Up @@ -289,6 +290,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
"schema_token.ex": [context.dir, "#{singular}_token.ex"],
"auth.ex": [web_pre, web_path, "#{singular}_auth.ex"],
"auth_test.exs": [web_test_pre, web_path, "#{singular}_auth_test.exs"],
"scope.ex": [context.dir, "#{singular}_scope.ex"],
"session_controller.ex": [controller_pre, "#{singular}_session_controller.ex"],
"session_controller_test.exs": [
web_test_pre,
Expand Down Expand Up @@ -622,7 +624,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
end
end

defp inject_config(context, %HashingLibrary{} = hashing_library) do
defp inject_hashing_config(context, %HashingLibrary{} = hashing_library) do
file_path =
if Mix.Phoenix.in_umbrella?(File.cwd!()) do
Path.expand("../../")
Expand All @@ -634,7 +636,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
file =
case read_file(file_path) do
{:ok, file} -> file
{:error, {:file_read_error, _}} -> "use Mix.Config\n"
{:error, {:file_read_error, _}} -> "import Config\n"
end

case Injector.test_config_inject(file, hashing_library) do
Expand All @@ -657,6 +659,60 @@ defmodule Mix.Tasks.Phx.Gen.Auth do
context
end

defp inject_scope_config(%Context{} = context) do
file_path =
if Mix.Phoenix.in_umbrella?(File.cwd!()) do
Path.expand("../../")
else
File.cwd!()
end
|> Path.join("config/config.exs")

file =
case read_file(file_path) do
{:ok, file} -> file
{:error, {:file_read_error, _}} -> "import Config\n"
end

existing_scopes = Application.get_env(context.context_app, :scopes, [])
scope_name = context.schema.singular
if Enum.find(existing_scopes, fn {k, _} -> k == scope_name end) do
raise "scope #{scope_name} is already configured"
end

scope_config = """
config :#{context.context_app}, :scopes,
#{context.schema.singular}: [
default: true,
module: #{inspect(context.module)}.#{inspect(context.schema.alias)}Scope,
assign_key: :current_scope,
access_path: [:#{context.schema.singular}, :#{context.schema.opts[:primary_key] || :id}],
schema_key: :#{context.schema.singular}_#{context.schema.opts[:primary_key] || :id},
schema_type: :#{if(context.schema.binary_id, do: :binary_id, else: :id)},
schema_table: :#{context.schema.table}
]
"""
|> String.trim()

case Injector.config_inject(file, scope_config) do
{:ok, new_file} ->
print_injecting(file_path)
File.write!(file_path, new_file)

:already_injected ->
:ok

{:error, :unable_to_inject} ->
Mix.shell().info("""
Add the following to #{Path.relative_to_cwd(file_path)}:
#{scope_config}
""")
end

context
end

defp print_shell_instructions(%Context{} = context) do
Mix.shell().info("""
Expand Down
35 changes: 21 additions & 14 deletions lib/mix/tasks/phx.gen.auth/injector.ex
Original file line number Diff line number Diff line change
Expand Up @@ -40,16 +40,9 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
end

@doc """
Injects configuration for test environment into `file`.
Injects configuration into `file`.
"""
@spec test_config_inject(String.t(), HashingLibrary.t()) ::
{:ok, String.t()} | :already_injected | {:error, :unable_to_inject}
def test_config_inject(file, %HashingLibrary{} = hashing_library) when is_binary(file) do
code_to_inject =
hashing_library
|> test_config_code()
|> normalize_line_endings_to_file(file)

def config_inject(file, code_to_inject) when is_binary(file) and is_binary(code_to_inject) do
inject_unless_contains(
file,
code_to_inject,
Expand All @@ -65,6 +58,20 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
)
end

@doc """
Injects configuration for test environment into `file`.
"""
@spec test_config_inject(String.t(), HashingLibrary.t()) ::
{:ok, String.t()} | :already_injected | {:error, :unable_to_inject}
def test_config_inject(file, %HashingLibrary{} = hashing_library) when is_binary(file) do
code_to_inject =
hashing_library
|> test_config_code()
|> normalize_line_endings_to_file(file)

config_inject(file, code_to_inject)
end

@doc """
Instructions to provide the user when `test_config_inject/2` fails.
"""
Expand All @@ -87,7 +94,7 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
@router_plug_anchor_line "plug :put_secure_browser_headers"

@doc """
Injects the fetch_current_<schema> plug into router's browser pipeline
Injects the fetch_current_scope plug into router's browser pipeline
"""
@spec router_plug_inject(String.t(), context) ::
{:ok, String.t()} | :already_injected | {:error, :unable_to_inject}
Expand Down Expand Up @@ -128,8 +135,8 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do
"plug " <> router_plug_name(schema)
end

defp router_plug_name(%Schema{} = schema) do
":fetch_current_#{schema.singular}"
defp router_plug_name(_schema) do
":fetch_current_scope"
end

@doc """
Expand Down Expand Up @@ -168,9 +175,9 @@ defmodule Mix.Tasks.Phx.Gen.Auth.Injector do

template = """
<ul class="relative z-10 flex items-center gap-4 px-4 sm:px-6 lg:px-8 justify-end">
<%= if @current_#{schema.singular} do %>
<%= if @current_scope.#{schema.singular} do %>
<li class="#{base_tailwind_classes}">
{@current_#{schema.singular}.email}
{@current_scope.#{schema.singular}.email}
</li>
<li>
<.link
Expand Down
48 changes: 26 additions & 22 deletions priv/templates/phx.gen.auth/auth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ defmodule <%= inspect auth_module %> do
import Phoenix.Controller

alias <%= inspect context.module %>
alias <%= inspect context.module %>.<%= inspect schema.alias %>Scope

# Make the remember me cookie valid for 60 days.
# If you want bump or reduce this value, also change
Expand Down Expand Up @@ -100,10 +101,10 @@ defmodule <%= inspect auth_module %> do
Authenticates the <%= schema.singular %> by looking into the session
and remember me token.
"""
def fetch_current_<%= schema.singular %>(conn, _opts) do
def fetch_current_scope(conn, _opts) do
{<%= schema.singular %>_token, conn} = ensure_<%= schema.singular %>_token(conn)
<%= schema.singular %> = <%= schema.singular %>_token && <%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
assign(conn, :current_<%= schema.singular %>, <%= schema.singular %>)
assign(conn, :current_scope, <%= inspect schema.alias %>Scope.for_<%= schema.singular %>(<%= schema.singular %>))
end

defp ensure_<%= schema.singular %>_token(conn) do
Expand All @@ -121,28 +122,28 @@ defmodule <%= inspect auth_module %> do
end

<%= if live? do %>@doc """
Handles mounting and authenticating the current_<%= schema.singular %> in LiveViews.
Handles mounting and authenticating the current_scope in LiveViews.
## `on_mount` arguments
* `:mount_current_<%= schema.singular %>` - Assigns current_<%= schema.singular %>
* `:mount_current_scope` - Assigns current_scope
to socket assigns based on <%= schema.singular %>_token, or nil if
there's no <%= schema.singular %>_token or no matching <%= schema.singular %>.
* `:ensure_authenticated` - Authenticates the <%= schema.singular %> from the session,
and assigns the current_<%= schema.singular %> to socket assigns based
and assigns the current_scope to socket assigns based
on <%= schema.singular %>_token.
Redirects to login page if there's no logged <%= schema.singular %>.
## Examples
Use the `on_mount` lifecycle macro in LiveViews to mount or authenticate
the current_<%= schema.singular %>:
the current_scope:
defmodule <%= inspect context.web_module %>.PageLive do
use <%= inspect context.web_module %>, :live_view
on_mount {<%= inspect auth_module %>, :mount_current_<%= schema.singular %>}
on_mount {<%= inspect auth_module %>, :mount_current_scope}
...
end
Expand All @@ -152,14 +153,14 @@ defmodule <%= inspect auth_module %> do
live "/profile", ProfileLive, :index
end
"""
def on_mount(:mount_current_<%= schema.singular %>, _params, session, socket) do
{:cont, mount_current_<%= schema.singular %>(socket, session)}
def on_mount(:mount_current_scope, _params, session, socket) do
{:cont, mount_current_scope(socket, session)}
end

def on_mount(:ensure_authenticated, _params, session, socket) do
socket = mount_current_<%= schema.singular %>(socket, session)
socket = mount_current_scope(socket, session)

if socket.assigns.current_<%= schema.singular %> do
if socket.assigns.current_scope.<%= schema.singular %> do
{:cont, socket}
else
socket =
Expand All @@ -172,9 +173,9 @@ defmodule <%= inspect auth_module %> do
end

def on_mount(:ensure_sudo_mode, _params, session, socket) do
socket = mount_current_<%= schema.singular %>(socket, session)
socket = mount_current_scope(socket, session)

if <%= inspect context.alias %>.sudo_mode?(socket.assigns.current_<%= schema.singular %>, -10) do
if <%= inspect context.alias %>.sudo_mode?(socket.assigns.current_scope.user, -10) do
{:cont, socket}
else
socket =
Expand All @@ -186,19 +187,22 @@ defmodule <%= inspect auth_module %> do
end
end

defp mount_current_<%= schema.singular %>(socket, session) do
Phoenix.Component.assign_new(socket, :current_<%= schema.singular %>, fn ->
if <%= schema.singular %>_token = session["<%= schema.singular %>_token"] do
<%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
end
defp mount_current_scope(socket, session) do
Phoenix.Component.assign_new(socket, :current_scope, fn ->
<%= schema.singular %> =
if <%= schema.singular %>_token = session["<%= schema.singular %>_token"] do
<%= inspect context.alias %>.get_<%= schema.singular %>_by_session_token(<%= schema.singular %>_token)
end

<%= inspect schema.alias %>Scope.for_<%= schema.singular %>(<%= schema.singular %>)
end)
end

<% else %>@doc """
Used for routes that require sudo mode.
"""
def require_sudo_mode(conn, _opts) do
if <%= inspect context.alias %>.sudo_mode?(conn.assigns.current_<%= schema.singular %>, -10) do
if <%= inspect context.alias %>.sudo_mode?(conn.assigns.current_scope.user, -10) do
conn
else
conn
Expand All @@ -213,7 +217,7 @@ defmodule <%= inspect auth_module %> do
Used for routes that require the <%= schema.singular %> to not be authenticated.
"""
def redirect_if_<%= schema.singular %>_is_authenticated(conn, _opts) do
if conn.assigns[:current_<%= schema.singular %>] do
if conn.assigns.current_scope.<%= schema.singular %> do
conn
|> redirect(to: signed_in_path(conn))
|> halt()
Expand All @@ -229,7 +233,7 @@ defmodule <%= inspect auth_module %> do
they use the application at all, here would be a good place.
"""
def require_authenticated_<%= schema.singular %>(conn, _opts) do
if conn.assigns[:current_<%= schema.singular %>] do
if conn.assigns.current_scope.<%= schema.singular %> do
conn
else
conn
Expand Down Expand Up @@ -269,7 +273,7 @@ defmodule <%= inspect auth_module %> do

<%= if live? do %>@doc "Returns the path to redirect to after log in."
# the <%= schema.singular %> was already logged in, redirect to settings
def signed_in_path(%Plug.Conn{assigns: %{current_<%= schema.singular %>: %<%= inspect context.alias %>.<%= inspect schema.alias %>{}}}) do
def signed_in_path(%Plug.Conn{assigns: %{current_scope: %<%= inspect schema.alias %>Scope{user: %<%= inspect context.alias %>.<%= inspect schema.alias %>{}}}}) do
~p"<%= schema.route_prefix %>/settings"
end

Expand Down
Loading

0 comments on commit 72f5137

Please sign in to comment.